From 2c4d40103e5bcbeb8d13b0476358483ef334991d Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Fri, 26 Jun 2026 14:15:57 +0200 Subject: [PATCH] feat(harfanglab): add get_security_events + get_threats ingestion commands, OCSF mappers and ingest hints Co-Authored-By: Claude Opus 4.8 --- integrations/harfanglab/manifest.yaml | 40 ++++++++++++-- .../mappers/get_security_events.yaml | 35 ++++++++++++ .../harfanglab/mappers/get_threats.yaml | 25 +++++++++ .../harfanglab/scripts/get_security_events.py | 53 +++++++++++++++++++ .../harfanglab/scripts/get_threats.py | 52 ++++++++++++++++++ 5 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 integrations/harfanglab/mappers/get_security_events.yaml create mode 100644 integrations/harfanglab/mappers/get_threats.yaml create mode 100644 integrations/harfanglab/scripts/get_security_events.py create mode 100644 integrations/harfanglab/scripts/get_threats.py diff --git a/integrations/harfanglab/manifest.yaml b/integrations/harfanglab/manifest.yaml index 8aedc6d..952f14f 100644 --- a/integrations/harfanglab/manifest.yaml +++ b/integrations/harfanglab/manifest.yaml @@ -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 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 diff --git a/integrations/harfanglab/mappers/get_security_events.yaml b/integrations/harfanglab/mappers/get_security_events.yaml new file mode 100644 index 0000000..819370d --- /dev/null +++ b/integrations/harfanglab/mappers/get_security_events.yaml @@ -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" } diff --git a/integrations/harfanglab/mappers/get_threats.yaml b/integrations/harfanglab/mappers/get_threats.yaml new file mode 100644 index 0000000..2009103 --- /dev/null +++ b/integrations/harfanglab/mappers/get_threats.yaml @@ -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" } diff --git a/integrations/harfanglab/scripts/get_security_events.py b/integrations/harfanglab/scripts/get_security_events.py new file mode 100644 index 0000000..9dd9451 --- /dev/null +++ b/integrations/harfanglab/scripts/get_security_events.py @@ -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) diff --git a/integrations/harfanglab/scripts/get_threats.py b/integrations/harfanglab/scripts/get_threats.py new file mode 100644 index 0000000..a980f38 --- /dev/null +++ b/integrations/harfanglab/scripts/get_threats.py @@ -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)