From d831d0973aa85365f0e271821e57f2114e64ca1d Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Wed, 7 Oct 2026 07:35:31 +0000 Subject: [PATCH] feat(harfanglab): fetch a threat's security events (1.7.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since HarfangLab merged alerts and security events, a threat's events are the alerts whose threat_key is the threat id — the filter the vendor's XSOAR integration uses. get_alerts could filter on it but stops at one page of 100; the new threat_security_events command pages through up to max_results and returns the threat's total with a truncated flag. threat_status, threat_level and alert_tag offer their status and level as drop-downs, and threat_status now reads as the place to close a threat or mark it a false positive, HarfangLab's only verdict. Co-Authored-By: Claude Opus 5.5 --- integrations/harfanglab/manifest.yaml | 35 +++++++-- .../scripts/threat_security_events.py | 76 +++++++++++++++++++ 2 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 integrations/harfanglab/scripts/threat_security_events.py diff --git a/integrations/harfanglab/manifest.yaml b/integrations/harfanglab/manifest.yaml index 666f0dc..2bc99f3 100644 --- a/integrations/harfanglab/manifest.yaml +++ b/integrations/harfanglab/manifest.yaml @@ -1,8 +1,8 @@ id: harfanglab name: HarfangLab EDR -version: 1.6.0 +version: 1.7.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.6.0 — New endpoint_count command: counts agents matching filters without listing them. The HarfangLab agent collection is DRF-paginated, so its `count` field already carries the filtered total; endpoint_count asks for limit=1 and reads it, instead of pulling up to 10000 agent records back through the playbook the way endpoint_search does. Filters cover the documented agent query parameters (hostname, os, version, status, isolation_state, tenant, network), add lastseen/firstseen bounds, and leave a raw `filters` escape hatch for the rest. The API silently drops a parameter it does not recognise as a filter — which turns a typo into a plausible-looking fleet-wide total — so the fleet `total` is returned next to `count` to make that case visible. | 1.5.0 — Declare the risk tier on the 71 commands that carried none: 39 read, 24 safe_write, 8 destructive. An unset risk is read as destructive by the agent approval gate, so the 63 that only read or only annotate sat behind the same prompt as the ones that change the estate. deisolate_from_threat and unquarantine_file move from safe_write to destructive in the same pass: undoing a containment mid-incident is its own decision, and is tiered like the containment it lifts. | 1.4.1 — get_alerts now maps image_name, the one process path present on every alert subtype and the only one on those carrying neither a process object nor an event log (yara, binary, ioc). HarfangLab prefixes it with the matched threat name in brackets, so both halves are split out: the clean path feeds actor.process.file.path and its basename actor.process.name, and the threat name feeds malware.name — which now surfaces on every subtype rather than only on Defender event-log detections. Each half is guarded on the bracket being present so an unprefixed path is never written into malware.name. 1.4.0 — Mapper coverage from real payloads: get_alerts now maps the Windows event-log payload sigma alerts carry (alert_subtype = eventlog), which was entirely unmapped — Defender 1116/1117 yields the malware name, classification, severity and provider, the detected file, the process that touched it and the acting user, plus the log provider and event code. Also added the agent IP, DNS domain, device type, agent version and groups, the tenant, the alert subtype and threat type, the string severity and the event time. get_threats gained the threat level (previously never reaching the OCSF document at all), the event time and the top agent's reachability. 1.3.1 — Flagged get_alerts as ingestion-ready (ingest hints: results_path/dedup/incremental on from_date) so it appears in the recommended ingestion commands when creating an instance. 1.3.0 — Response & triage expansion: kill/dump process, on-demand AV/YARA scans, file/service/scheduled-task deletion, quarantine management, threat-level isolation, job cancel/relaunch/wait; alert search (get_alerts) and full alert/threat triage (tag, comments, notes, status, level, history, details); exhaustive alert OCSF mappers; reworked get_threats mapper (fixed MITRE tactics map handling, full top-agent/user/rule and counts coverage, rule-based incident title/description). 1.2.2 — Documentation wording cleanup. 1.2.1 — Corrected the get_threats OCSF mapper to native threat fields (top_agents/top_impacted_users/top_rules/mitre_tactics) instead of enriched-only arrays; added agent.osversion to get_security_events. 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+." +changelog: "1.7.0 — New threat_security_events command: returns every security event of a threat. Since HarfangLab merged alerts and security events, a threat's events are the alerts whose threat_key is the threat id — the filter the vendor's own XSOAR integration uses. get_alerts could already filter on it but stops at one page of 100; threat_security_events pages through up to max_results (default 1000) and returns the threat's `total` with a `truncated` flag, so a cut-off list never passes for the whole threat. threat_status, threat_level and alert_tag now offer their status and level as drop-downs, and threat_status says it is where a threat is closed or marked a false positive — HarfangLab has no separate verdict field and no assignment on a threat. | 1.6.0 — New endpoint_count command: counts agents matching filters without listing them. The HarfangLab agent collection is DRF-paginated, so its `count` field already carries the filtered total; endpoint_count asks for limit=1 and reads it, instead of pulling up to 10000 agent records back through the playbook the way endpoint_search does. Filters cover the documented agent query parameters (hostname, os, version, status, isolation_state, tenant, network), add lastseen/firstseen bounds, and leave a raw `filters` escape hatch for the rest. The API silently drops a parameter it does not recognise as a filter — which turns a typo into a plausible-looking fleet-wide total — so the fleet `total` is returned next to `count` to make that case visible. | 1.5.0 — Declare the risk tier on the 71 commands that carried none: 39 read, 24 safe_write, 8 destructive. An unset risk is read as destructive by the agent approval gate, so the 63 that only read or only annotate sat behind the same prompt as the ones that change the estate. deisolate_from_threat and unquarantine_file move from safe_write to destructive in the same pass: undoing a containment mid-incident is its own decision, and is tiered like the containment it lifts. | 1.4.1 — get_alerts now maps image_name, the one process path present on every alert subtype and the only one on those carrying neither a process object nor an event log (yara, binary, ioc). HarfangLab prefixes it with the matched threat name in brackets, so both halves are split out: the clean path feeds actor.process.file.path and its basename actor.process.name, and the threat name feeds malware.name — which now surfaces on every subtype rather than only on Defender event-log detections. Each half is guarded on the bracket being present so an unprefixed path is never written into malware.name. 1.4.0 — Mapper coverage from real payloads: get_alerts now maps the Windows event-log payload sigma alerts carry (alert_subtype = eventlog), which was entirely unmapped — Defender 1116/1117 yields the malware name, classification, severity and provider, the detected file, the process that touched it and the acting user, plus the log provider and event code. Also added the agent IP, DNS domain, device type, agent version and groups, the tenant, the alert subtype and threat type, the string severity and the event time. get_threats gained the threat level (previously never reaching the OCSF document at all), the event time and the top agent's reachability. 1.3.1 — Flagged get_alerts as ingestion-ready (ingest hints: results_path/dedup/incremental on from_date) so it appears in the recommended ingestion commands when creating an instance. 1.3.0 — Response & triage expansion: kill/dump process, on-demand AV/YARA scans, file/service/scheduled-task deletion, quarantine management, threat-level isolation, job cancel/relaunch/wait; alert search (get_alerts) and full alert/threat triage (tag, comments, notes, status, level, history, details); exhaustive alert OCSF mappers; reworked get_threats mapper (fixed MITRE tactics map handling, full top-agent/user/rule and counts coverage, rule-based incident title/description). 1.2.2 — Documentation wording cleanup. 1.2.1 — Corrected the get_threats OCSF mapper to native threat fields (top_agents/top_impacted_users/top_rules/mitre_tactics) instead of enriched-only arrays; added agent.osversion to get_security_events. 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 as the API base and call /api/... paths. @@ -1015,7 +1015,7 @@ commands: alert_ids: { type: string, description: "Comma-separated alert IDs (fetched individually; other filters ignored)" } hostname: { type: string, description: "Endpoint hostname" } agent_id: { type: string, description: "Agent unique identifier" } - threat_id: { type: string, description: "Threat key — alerts aggregated under this threat" } + threat_id: { type: string, description: "Threat id — alerts aggregated under this threat (one page; threat_security_events fetches them all)" } rule_name: { type: string, description: "Detection rule name" } level: { type: string, description: "Comma-separated severities (low, medium, high, critical)" } status: { type: string, description: "Comma-separated statuses (new, investigating, closed, false_positive)" } @@ -1039,7 +1039,10 @@ commands: inputs_schema: properties: alert_ids: { type: string, description: "Comma-separated alert IDs" } - new_status: { type: string, description: "new | investigating | false_positive | closed" } + new_status: + type: string + description: "New status of the alerts." + enum: [new, investigating, false_positive, closed] new_comment: { type: string, description: "Optional comment attached to the status change" } required: [alert_ids, new_status] outputs_schema: { properties: {} } @@ -1081,14 +1084,29 @@ commands: threat_id: { type: string, description: "Threat identifier" } required: [threat_id] outputs_schema: { properties: {} } + - id: threat_security_events + name: harfanglab-get-threat-security-events + description: "Fetch every security event (alert) of a threat, paging through them. Returns {threat_id, count, total, truncated, results}." + risk: read + inputs_schema: + properties: + threat_id: { type: string, description: "Threat identifier" } + status: { type: string, description: "Comma-separated statuses to keep (new, probable_false_positive, investigating, false_positive, closed); all when empty" } + level: { type: string, description: "Comma-separated severities to keep (low, medium, high, critical); all when empty" } + max_results: { type: number, description: "Maximum number of events to return (default 1000); truncated is true when the threat has more" } + required: [threat_id] + outputs_schema: { properties: {} } - id: threat_status name: harfanglab-threat-status - description: "Change the status of one or more threats (new, investigating, false_positive, closed), optionally propagating to their security events." + description: "Update a threat's status — take it into investigation, close it, or mark it a false positive (HarfangLab's only verdict) — optionally propagating to its security events." risk: safe_write inputs_schema: properties: threat_ids: { type: string, description: "Comma-separated threat IDs" } - new_status: { type: string, description: "new | investigating | false_positive | closed" } + new_status: + type: string + description: "New status. false_positive is the verdict HarfangLab records; closed ends the threat as handled." + enum: [new, investigating, false_positive, closed] tag_security_events: { type: boolean, description: "Also apply the status to the threat's security events (default false)" } required: [threat_ids, new_status] outputs_schema: { properties: {} } @@ -1099,7 +1117,10 @@ commands: inputs_schema: properties: threat_ids: { type: string, description: "Comma-separated threat IDs" } - new_level: { type: string, description: "low | medium | high | critical" } + new_level: + type: string + description: "New severity level." + enum: [low, medium, high, critical] required: [threat_ids, new_level] outputs_schema: { properties: {} } - id: threat_comment diff --git a/integrations/harfanglab/scripts/threat_security_events.py b/integrations/harfanglab/scripts/threat_security_events.py new file mode 100644 index 0000000..3ae42da --- /dev/null +++ b/integrations/harfanglab/scripts/threat_security_events.py @@ -0,0 +1,76 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + +PAGE = 500 + + +def request(method, url, headers, body=None): + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) 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 csv(v): + return [x.strip() for x in str(v or "").split(",") if x.strip()] + + +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", + } + threat_id = str(inputs.get("threat_id") or "").strip() + if not threat_id: + raise ValueError("threat_id is required") + max_results = int(inputs.get("max_results") or 1000) + # Since HarfangLab merged alerts and security events, a threat's events are + # the alerts whose threat_key is the threat id (same filter as the + # vendor's own XSOAR integration). + params = { + "threat_key": threat_id, + "ordering": "-alert_time", + "status": ",".join(csv(inputs.get("status"))) or None, + "level": ",".join(csv(inputs.get("level"))) or None, + "offset": 0, + } + results, total = [], 0 + while len(results) < max_results: + params["limit"] = min(PAGE, max_results - len(results)) + page = request("GET", base + "/api/data/alert/alert/Alert/" + qs(params), headers) + batch = page.get("results") or [] + total = page.get("count", total) + results.extend(batch) + if not batch or not page.get("next"): + break + params["offset"] += len(batch) + print(json.dumps({ + "threat_id": threat_id, + "count": len(results), + "total": total, + "truncated": len(results) < total, + "results": results, + })) + + +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)