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:
@@ -1,8 +1,8 @@
|
|||||||
id: harfanglab
|
id: harfanglab
|
||||||
name: HarfangLab EDR
|
name: HarfangLab EDR
|
||||||
version: 1.1.1
|
version: 1.2.0
|
||||||
description: "HarfangLab EDR — endpoint detection & response: endpoint enrichment, isolation, threat-intelligence (IOC/whitelist), telemetry hunting and forensic collection jobs."
|
description: "HarfangLab EDR — endpoint detection & response: alert/threat ingestion, 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+."
|
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
|
category: endpoint
|
||||||
|
|
||||||
# Per-instance configuration. Scripts use <url> as the API base and call /api/... paths.
|
# Per-instance configuration. Scripts use <url> as the API base and call /api/... paths.
|
||||||
@@ -30,6 +30,40 @@ auth:
|
|||||||
secret_field: api_token
|
secret_field: api_token
|
||||||
|
|
||||||
commands:
|
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 ────────────────────────────────────────────────────
|
# ── Endpoints / agents ────────────────────────────────────────────────────
|
||||||
- id: get_endpoint_info
|
- id: get_endpoint_info
|
||||||
name: harfanglab-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)
|
||||||
Reference in New Issue
Block a user