feat(opencti): new OpenCTI threat-intelligence integration
Add a marketplace integration for the OpenCTI platform (GraphQL API, compatible with OpenCTI 5.x/6.x), built on the Python pycti client and run from a remote engine. 26 commands: observables (list/create/delete/field update/add/remove), indicators (list/create/update/field add/remove, types), incidents (list/create/delete, types) with an OCSF ingestion mapper, relationships (list/create/delete), and reference data (organizations, labels, marking definitions, external references). - Ingestion: get_incidents to an OCSF finding mapper + an OpenCTI Incident type. - Auth: user API key (Bearer) via pycti; requires pip install pycti on the engine host. - Scripts are self-contained (INTEGRATION_SECRETS/INTEGRATION_INPUTS in, JSON out) following the established marketplace pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def as_bool(v):
|
||||
return v if isinstance(v, bool) else str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def as_int(v, default=None):
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def as_list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
if v in (None, ""):
|
||||
return []
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
try:
|
||||
from pycti import OpenCTIApiClient
|
||||
except ImportError as e:
|
||||
fail("The 'pycti' Python library is required for the OpenCTI integration. "
|
||||
"Install it on the execution host (engine): pip install pycti", detail=str(e))
|
||||
|
||||
|
||||
def client():
|
||||
base = str(S.get("base_url", "")).strip().rstrip("/")
|
||||
api_key = S.get("api_key") or (S.get("credentials") or {}).get("password")
|
||||
return OpenCTIApiClient(base, api_key, ssl_verify=not as_bool(S.get("insecure")), log_level="error")
|
||||
|
||||
|
||||
TYPES_TO_OPENCTI = {
|
||||
"account": "User-Account", "domain": "Domain-Name", "email": "Email-Addr",
|
||||
"file-md5": "StixFile", "file-sha1": "StixFile", "file-sha256": "StixFile", "file": "StixFile",
|
||||
"host": "X-OpenCTI-Hostname", "ip": "IPv4-Addr", "ipv6": "IPv6-Addr",
|
||||
"registry key": "Windows-Registry-Key", "url": "Url",
|
||||
}
|
||||
OBSERVABLE_TYPE_TO_STIX_PATTERN = {
|
||||
"IPv4-Addr": "[ipv4-addr:value = '{{indicator}}']",
|
||||
"IPv6-Addr": "[ipv6-addr:value = '{{indicator}}']",
|
||||
"Domain-Name": "[domain-name:value = '{{indicator}}']",
|
||||
"Url": "[url:value = '{{indicator}}']",
|
||||
"Email-Addr": "[email-addr:value = '{{indicator}}']",
|
||||
"StixFile": "[file:hashes.'SHA-256' = '{{indicator}}']",
|
||||
"Process": "[process:pid = '{{indicator}}']",
|
||||
"User-Account": "[user-account:user_id = '{{indicator}}']",
|
||||
"Windows-Registry-Key": "[windows-registry-key:key = '{{indicator}}']",
|
||||
}
|
||||
|
||||
|
||||
def build_stix_pattern(indicator, observable_type):
|
||||
if observable_type not in OBSERVABLE_TYPE_TO_STIX_PATTERN:
|
||||
fail(f"Invalid observable type: {observable_type}")
|
||||
return OBSERVABLE_TYPE_TO_STIX_PATTERN[observable_type].replace("{{indicator}}", indicator)
|
||||
|
||||
|
||||
name = I.get("name")
|
||||
indicator = I.get("indicator")
|
||||
main_observable_type = TYPES_TO_OPENCTI.get(str(I.get("main_observable_type", "")).lower(), I.get("main_observable_type"))
|
||||
description = I.get("description")
|
||||
valid_from = I.get("valid_from")
|
||||
valid_until = I.get("valid_until")
|
||||
created_by = I.get("created_by")
|
||||
label_id = I.get("label_id")
|
||||
marking_id = I.get("marking_id")
|
||||
external_references_id = I.get("external_references_id")
|
||||
|
||||
pattern = build_stix_pattern(indicator, main_observable_type)
|
||||
|
||||
try:
|
||||
result = client().indicator.create(
|
||||
name=name,
|
||||
description=description,
|
||||
pattern=pattern,
|
||||
pattern_type="stix",
|
||||
x_opencti_main_observable_type=main_observable_type,
|
||||
indicator_types=as_list(I.get("indicator_types")),
|
||||
confidence=as_int(I.get("confidence"), 50),
|
||||
x_opencti_score=as_int(I.get("score"), 50),
|
||||
valid_from=valid_from,
|
||||
valid_until=valid_until,
|
||||
createdBy=created_by,
|
||||
objectLabel=label_id,
|
||||
objectMarking=marking_id,
|
||||
externalReferences=external_references_id,
|
||||
x_opencti_create_observables=as_bool(I.get("create_observables")),
|
||||
)
|
||||
except Exception as e:
|
||||
fail("Can't create indicator in OpenCTI.", detail=str(e))
|
||||
|
||||
out({"id": result.get("id")})
|
||||
Reference in New Issue
Block a user