feat(harfanglab): add get_security_events + get_threats ingestion commands, OCSF mappers and ingest hints

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guillaume BOURGEOIS
2026-06-26 14:15:57 +02:00
parent be7d4a8b7c
commit 2c4d40103e
5 changed files with 202 additions and 3 deletions
+37 -3
View File
@@ -1,8 +1,8 @@
id: harfanglab
name: HarfangLab EDR
version: 1.1.1
description: "HarfangLab EDR — endpoint detection & response: endpoint enrichment, isolation, threat-intelligence (IOC/whitelist), telemetry hunting and forensic collection jobs."
changelog: "1.1.1 — Added test_connection for the instance Test button. 1.1.0 — Command names prefixed with 'harfanglab-' (e.g. harfanglab-isolate-endpoint) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: endpoint/agent management, isolation, policy assignment, IOC & whitelist management, security-event triage, telemetry hunting (processes, network, DNS, authentications, binaries, event logs), threat hunting by hash, and forensic collection jobs (pipes, prefetch, run keys, scheduled tasks, drivers, services, processes, network, sessions, WMI, IOC scan, artifacts, RAM dump) with their result retrieval commands. Compatible with HarfangLab EDR 2.13.7+."
version: 1.2.0
description: "HarfangLab EDR — endpoint detection & response: alert/threat ingestion, endpoint enrichment, isolation, threat-intelligence (IOC/whitelist), telemetry hunting and forensic collection jobs."
changelog: "1.2.0 — Added ingestion commands get_security_events (alerts) and get_threats with bundled OCSF mappers and ingest hints (results_path/dedup/incremental). 1.1.1 — Added test_connection for the instance Test button. 1.1.0 — Command names prefixed with 'harfanglab-' (e.g. harfanglab-isolate-endpoint) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: endpoint/agent management, isolation, policy assignment, IOC & whitelist management, security-event triage, telemetry hunting (processes, network, DNS, authentications, binaries, event logs), threat hunting by hash, and forensic collection jobs (pipes, prefetch, run keys, scheduled tasks, drivers, services, processes, network, sessions, WMI, IOC scan, artifacts, RAM dump) with their result retrieval commands. Compatible with HarfangLab EDR 2.13.7+."
category: endpoint
# Per-instance configuration. Scripts use <url> as the API base and call /api/... paths.
@@ -30,6 +30,40 @@ auth:
secret_field: api_token
commands:
# ── Alerts & threats (ingestion) ──────────────────────────────────────────
- id: get_security_events
name: harfanglab-get-security-events
description: "Fetch HarfangLab EDR security events (alerts) for ingestion. Returns {results: [...]} — use results as the alert rule results path."
risk: read
inputs_schema:
properties:
min_severity: { type: string, description: "Minimum severity to fetch: low, medium, high, critical" }
status: { type: string, description: "Comma-separated statuses (new, investigating, closed, false_positive)" }
alert_type: { type: string, description: "Comma-separated alert types (sigma, yara, hlai, vt, ransom, ioc, glimps, orion...)" }
created_after: { type: string, description: "ISO8601 lower bound on alert_time (incremental fetch watermark)" }
limit: { type: number, description: "Maximum number of events to fetch" }
required: []
outputs_schema: { properties: {} }
ingest:
results_path: results
dedup_key: id
incremental_field: created_after
- id: get_threats
name: harfanglab-get-threats
description: "Fetch HarfangLab EDR threats (aggregated security events) for ingestion. Returns {results: [...]} enriched with agents/impacted_users/rules."
risk: read
inputs_schema:
properties:
min_severity: { type: string, description: "Minimum severity to fetch: low, medium, high, critical" }
status: { type: string, description: "Comma-separated statuses (new, investigating, closed, false_positive)" }
created_after: { type: string, description: "ISO8601 lower bound on creation_date (incremental fetch watermark)" }
limit: { type: number, description: "Maximum number of threats to fetch" }
required: []
outputs_schema: { properties: {} }
ingest:
results_path: results
dedup_key: id
incremental_field: created_after
# ── Endpoints / agents ────────────────────────────────────────────────────
- id: get_endpoint_info
name: harfanglab-get-endpoint-info
@@ -0,0 +1,35 @@
name: "HarfangLab Security Events → OCSF"
description: "Maps a HarfangLab EDR security event/alert (/api/data/alert/alert/Alert/, results_path = results) to OCSF. The matched process is the actor (actor.*). Process fields are best-effort; confirm against a live sample via the builder Test."
field_mappings:
title: "rule_name"
severity: "level = 'critical' ? 5 : (level = 'high' ? 4 : (level = 'medium' ? 3 : 2))"
description: "alert_type"
# results_path = results; source_path is JSONata over ONE alert object.
# Paths absent from a given alert are skipped at ingestion, so extra entries are safe.
ocsf:
# ── Endpoint ──────────────────────────────────────────────────────
- { source_path: "agent.hostname", ocsf_field: "src_endpoint.hostname" }
- { source_path: "agent.domainname", ocsf_field: "src_endpoint.domain" }
- { source_path: "agent.osproducttype", ocsf_field: "src_endpoint.os.name" }
- { source_path: "agent.ostype", ocsf_field: "src_endpoint.os.type" }
- { source_path: "agent.agentid", ocsf_field: "device.uid" }
# ── Finding ───────────────────────────────────────────────────────
- { source_path: "id", ocsf_field: "finding_info.uid" }
- { source_path: "rule_name", ocsf_field: "finding_info.title" }
- { source_path: "alert_time", ocsf_field: "finding_info.created_time" }
- { source_path: "last_update", ocsf_field: "finding_info.modified_time" }
- { source_path: "alert_type", ocsf_field: "finding_info.analytic.type" }
- { source_path: "status", ocsf_field: "status" }
- { source_path: "maturity", ocsf_field: "status_detail" }
# ── Matched process — the actor ───────────────────────────────────
- { source_path: "process.process_name", ocsf_field: "actor.process.name" }
- { source_path: "process.image_name", ocsf_field: "actor.process.file.path" }
- { source_path: "process.commandline", ocsf_field: "actor.process.cmd_line" }
- { source_path: "process.username", ocsf_field: "actor.user.name" }
- { source_path: "process.integrity_level", ocsf_field: "actor.process.integrity" }
- { source_path: "process.pid", ocsf_field: "actor.process.pid" }
- { source_path: "process.hashes.sha256", ocsf_field: "actor.process.file.hashes.sha256" }
- { source_path: "process.hashes.sha1", ocsf_field: "actor.process.file.hashes.sha1" }
- { source_path: "process.hashes.md5", ocsf_field: "actor.process.file.hashes.md5" }
- { source_path: "process.parent_image", ocsf_field: "actor.process.parent_process.name" }
- { source_path: "process.parent_commandline", ocsf_field: "actor.process.parent_process.cmd_line" }
@@ -0,0 +1,25 @@
name: "HarfangLab Threats → OCSF"
description: "Maps a HarfangLab EDR threat (/api/data/alert/alert/Threat/, results_path = results, enriched with agents/impacted_users/rules) to OCSF."
field_mappings:
title: "slug"
severity: "level = 'critical' ? 5 : (level = 'high' ? 4 : (level = 'medium' ? 3 : 2))"
description: "status"
# results_path = results; source_path is JSONata over ONE threat object.
# Paths absent from a given threat are skipped at ingestion, so extra entries are safe.
ocsf:
# ── Finding ───────────────────────────────────────────────────────
- { source_path: "id", ocsf_field: "finding_info.uid" }
- { source_path: "slug", ocsf_field: "finding_info.title" }
- { source_path: "creation_date", ocsf_field: "finding_info.created_time" }
- { source_path: "first_seen", ocsf_field: "finding_info.first_seen_time" }
- { source_path: "last_seen", ocsf_field: "finding_info.last_seen_time" }
- { source_path: "last_update", ocsf_field: "finding_info.modified_time" }
- { source_path: "status", ocsf_field: "status" }
# ── Impacted endpoint (first enriched agent) ──────────────────────
- { source_path: "agents[0].hostname", ocsf_field: "src_endpoint.hostname" }
- { source_path: "agents[0].domainname", ocsf_field: "src_endpoint.domain" }
- { source_path: "agents[0].osproducttype", ocsf_field: "src_endpoint.os.name" }
- { source_path: "agents[0].ostype", ocsf_field: "src_endpoint.os.type" }
# ── Impacted user / rule ──────────────────────────────────────────
- { source_path: "impacted_users[0].full_name", ocsf_field: "user.name" }
- { source_path: "rules[0].rule_name", ocsf_field: "rule.name" }
@@ -0,0 +1,53 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
SEVERITIES = ["low", "medium", "high", "critical"]
def request(method, url, headers):
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as resp:
raw = resp.read()
return json.loads(raw) if raw else {}
def qs(params):
clean = {}
for k, v in params.items():
if v is None or v == "" or v == []:
continue
clean[k] = v
return ("?" + urllib.parse.urlencode(clean)) if clean else ""
def main():
secrets = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
base = secrets.get("url", "").rstrip("/")
headers = {
"Authorization": "Token " + secrets.get("api_token", ""),
"Accept": "application/json",
"Content-Type": "application/json",
}
min_sev = str(inputs.get("min_severity") or "low").lower()
level = ",".join(SEVERITIES[SEVERITIES.index(min_sev):]) if min_sev in SEVERITIES else ",".join(SEVERITIES)
params = {
"ordering": "-alert_time",
"level": level,
"limit": int(inputs.get("limit") or 100),
"offset": 0,
"status": inputs.get("status"),
"alert_type": inputs.get("alert_type"),
"alert_time__gte": inputs.get("created_after"),
}
url = base + "/api/data/alert/alert/Alert/" + qs(params)
print(json.dumps(request("GET", url, headers)))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,52 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
SEVERITIES = ["low", "medium", "high", "critical"]
def request(method, url, headers):
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as resp:
raw = resp.read()
return json.loads(raw) if raw else {}
def qs(params):
clean = {}
for k, v in params.items():
if v is None or v == "" or v == []:
continue
clean[k] = v
return ("?" + urllib.parse.urlencode(clean)) if clean else ""
def main():
secrets = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
base = secrets.get("url", "").rstrip("/")
headers = {
"Authorization": "Token " + secrets.get("api_token", ""),
"Accept": "application/json",
"Content-Type": "application/json",
}
min_sev = str(inputs.get("min_severity") or "low").lower()
level = ",".join(SEVERITIES[SEVERITIES.index(min_sev):]) if min_sev in SEVERITIES else ",".join(SEVERITIES)
params = {
"ordering": "-last_seen",
"level": level,
"limit": int(inputs.get("limit") or 100),
"offset": 0,
"status": inputs.get("status"),
"creation_date__gte": inputs.get("created_after"),
}
url = base + "/api/data/alert/alert/Threat/" + qs(params)
print(json.dumps(request("GET", url, headers)))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)