From c406edb5e7339342ee1e8288a2dec3e3341f6d78 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Mon, 29 Jun 2026 23:24:57 +0200 Subject: [PATCH] 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 --- .../incident-types/opencti-incident.yaml | 3 + integrations/opencti/manifest.yaml | 378 ++++++++++++++++++ .../opencti/mappers/get_incidents.yaml | 18 + .../scripts/external_reference_create.py | 61 +++ integrations/opencti/scripts/get_incidents.py | 120 ++++++ .../opencti/scripts/get_indicators.py | 125 ++++++ .../opencti/scripts/get_observables.py | 137 +++++++ .../opencti/scripts/incident_create.py | 72 ++++ .../opencti/scripts/incident_delete.py | 55 +++ .../opencti/scripts/incident_types_list.py | 64 +++ .../opencti/scripts/indicator_create.py | 109 +++++ .../opencti/scripts/indicator_field_add.py | 64 +++ .../opencti/scripts/indicator_field_remove.py | 64 +++ .../opencti/scripts/indicator_types_list.py | 64 +++ .../opencti/scripts/indicator_update.py | 114 ++++++ integrations/opencti/scripts/label_create.py | 58 +++ integrations/opencti/scripts/label_list.py | 62 +++ .../scripts/marking_definition_list.py | 62 +++ .../opencti/scripts/observable_create.py | 97 +++++ .../opencti/scripts/observable_delete.py | 55 +++ .../opencti/scripts/observable_field_add.py | 69 ++++ .../scripts/observable_field_remove.py | 66 +++ .../scripts/observable_field_update.py | 63 +++ .../opencti/scripts/organization_create.py | 64 +++ .../opencti/scripts/organization_list.py | 63 +++ .../opencti/scripts/relationship_create.py | 64 +++ .../opencti/scripts/relationship_delete.py | 55 +++ .../opencti/scripts/relationship_list.py | 72 ++++ .../opencti/scripts/test_connection.py | 55 +++ 29 files changed, 2353 insertions(+) create mode 100644 integrations/opencti/incident-types/opencti-incident.yaml create mode 100644 integrations/opencti/manifest.yaml create mode 100644 integrations/opencti/mappers/get_incidents.yaml create mode 100644 integrations/opencti/scripts/external_reference_create.py create mode 100644 integrations/opencti/scripts/get_incidents.py create mode 100644 integrations/opencti/scripts/get_indicators.py create mode 100644 integrations/opencti/scripts/get_observables.py create mode 100644 integrations/opencti/scripts/incident_create.py create mode 100644 integrations/opencti/scripts/incident_delete.py create mode 100644 integrations/opencti/scripts/incident_types_list.py create mode 100644 integrations/opencti/scripts/indicator_create.py create mode 100644 integrations/opencti/scripts/indicator_field_add.py create mode 100644 integrations/opencti/scripts/indicator_field_remove.py create mode 100644 integrations/opencti/scripts/indicator_types_list.py create mode 100644 integrations/opencti/scripts/indicator_update.py create mode 100644 integrations/opencti/scripts/label_create.py create mode 100644 integrations/opencti/scripts/label_list.py create mode 100644 integrations/opencti/scripts/marking_definition_list.py create mode 100644 integrations/opencti/scripts/observable_create.py create mode 100644 integrations/opencti/scripts/observable_delete.py create mode 100644 integrations/opencti/scripts/observable_field_add.py create mode 100644 integrations/opencti/scripts/observable_field_remove.py create mode 100644 integrations/opencti/scripts/observable_field_update.py create mode 100644 integrations/opencti/scripts/organization_create.py create mode 100644 integrations/opencti/scripts/organization_list.py create mode 100644 integrations/opencti/scripts/relationship_create.py create mode 100644 integrations/opencti/scripts/relationship_delete.py create mode 100644 integrations/opencti/scripts/relationship_list.py create mode 100644 integrations/opencti/scripts/test_connection.py diff --git a/integrations/opencti/incident-types/opencti-incident.yaml b/integrations/opencti/incident-types/opencti-incident.yaml new file mode 100644 index 0000000..785ce65 --- /dev/null +++ b/integrations/opencti/incident-types/opencti-incident.yaml @@ -0,0 +1,3 @@ +name: "OpenCTI Incident" +color: "#1f6feb" +icon: "alert" diff --git a/integrations/opencti/manifest.yaml b/integrations/opencti/manifest.yaml new file mode 100644 index 0000000..6549676 --- /dev/null +++ b/integrations/opencti/manifest.yaml @@ -0,0 +1,378 @@ +id: opencti +name: OpenCTI +version: 1.0.0 +description: "OpenCTI threat-intelligence platform (GraphQL API, compatible with OpenCTI 5.x/6.x). Query and manage observables, indicators, incidents, relationships, labels, markings, organizations and external references; ingest OpenCTI incidents with an OCSF mapper. Runs on a remote engine. Requires the Python 'pycti' library on the engine host (pip install pycti)." +changelog: "1.0.0 — Initial release: observables (list/create/delete/field update/add/remove), indicators (list/create/update/field add/remove, types), incidents (list/create/delete, types) with OCSF ingestion mapper, relationships (list/create/delete), and reference data (organizations, labels, marking definitions, external references)." +category: threat_intel + +# Per-instance configuration. The integration talks to the OpenCTI GraphQL API at +# /graphql using the user API key. Run it on a remote engine that can +# reach the OpenCTI platform. The bundled scripts require the Python 'pycti' +# library on the engine host: pip install pycti +config_schema: + properties: + base_url: + type: string + description: "OpenCTI base URL, e.g. https://opencti.company.com (scripts append /graphql)" + api_key: + type: string + description: "OpenCTI API key (user profile → API access → API KEY)" + x-soar-sensitive: true + insecure: + type: boolean + description: "Trust any TLS certificate (not secure)" + default: false + required: + - base_url + - api_key + +# Documented for reference; pycti builds the Authorization header itself from the +# api_key (Bearer token) when the scripts construct the client. +auth: + - id: apikey + type: api_key + in: header + name: Authorization + value_template: "Bearer {{secret}}" + secret_field: api_key + +commands: + # ── Observables ─────────────────────────────────────────────────────────── + - id: get_observables + name: opencti-get-observables + description: "List/search observables, optionally filtered by type, score and free text." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max observables to return (default 50, max 500)" } + score_start: { type: number, description: "Minimum score to filter by (0-100)" } + score_end: { type: number, description: "Maximum score to filter by (0-100)" } + score: { type: string, description: "A specific score (0-100) or 'Unknown'" } + observable_types: { type: string, description: "Comma-separated types: ALL, Account, Domain, Email, File, Host, IP, IPv6, Registry Key, URL (default ALL)" } + last_run_id: { type: string, description: "Pagination cursor from a previous call (OpenCTI.Observables.LastRunID)" } + search: { type: string, description: "Observable value to filter by (partial allowed)" } + all_results: { type: boolean, description: "Fetch all results, ignoring limit (default false)" } + required: [] + outputs_schema: { properties: {} } + - id: observable_create + name: opencti-observable-create + description: "Create a new observable (optionally creating a related indicator)." + risk: safe_write + inputs_schema: + properties: + type: { type: string, description: "Observable type: Account, Domain, Email, File-MD5, File-SHA1, File-SHA256, Host, IP, IPv6, Registry Key, URL" } + value: { type: string, description: "Observable value" } + created_by: { type: string, description: "Organization ID (see organization_list)" } + marking_id: { type: string, description: "Marking definition ID (see marking_definition_list)" } + label_id: { type: string, description: "Label ID (see label_list / label_create)" } + external_references_id: { type: string, description: "External reference ID (see external_reference_create)" } + description: { type: string, description: "Observable description" } + score: { type: number, description: "Observable score 0-100 (default 50)" } + create_indicator: { type: boolean, description: "Also create a related indicator (default false)" } + required: [type, value] + outputs_schema: { properties: {} } + - id: observable_delete + name: opencti-observable-delete + description: "Delete an observable by ID." + inputs_schema: + properties: + id: { type: string, description: "Observable ID" } + required: [id] + outputs_schema: { properties: {} } + - id: observable_field_update + name: opencti-observable-field-update + description: "Update an observable field (score or description)." + risk: safe_write + inputs_schema: + properties: + id: { type: string, description: "Observable ID" } + field: { type: string, description: "Field to update: score or description" } + value: { type: string, description: "New value" } + required: [id, field, value] + outputs_schema: { properties: {} } + - id: observable_field_add + name: opencti-observable-field-add + description: "Add a marking definition or label to an observable." + risk: safe_write + inputs_schema: + properties: + id: { type: string, description: "Observable ID" } + field: { type: string, description: "Field to add: marking or label" } + value: { type: string, description: "Label ID or marking definition ID to add" } + required: [id, field, value] + outputs_schema: { properties: {} } + - id: observable_field_remove + name: opencti-observable-field-remove + description: "Remove a marking definition or label from an observable." + inputs_schema: + properties: + id: { type: string, description: "Observable ID" } + field: { type: string, description: "Field to remove: marking or label" } + value: { type: string, description: "Label ID or marking definition ID to remove" } + required: [id, field, value] + outputs_schema: { properties: {} } + + # ── Indicators ──────────────────────────────────────────────────────────── + - id: get_indicators + name: opencti-get-indicators + description: "List/search indicators with optional filters and pagination." + risk: read + inputs_schema: + properties: + search: { type: string, description: "Indicator value to filter by (partial allowed)" } + created_by: { type: string, description: "Creator organization ID" } + creator: { type: string, description: "Creator user ID" } + created_after: { type: string, description: "Created-after date (YYYY-MM-DDThh:mm:ss.sssZ)" } + created_before: { type: string, description: "Created-before date (YYYY-MM-DDThh:mm:ss.sssZ)" } + valid_until_after: { type: string, description: "Valid-until-after date" } + valid_until_before: { type: string, description: "Valid-until-before date" } + valid_from_after: { type: string, description: "Valid-from-after date" } + valid_from_before: { type: string, description: "Valid-from-before date" } + indicator_types: { type: string, description: "Comma-separated indicator types (see indicator_types_list)" } + label_id: { type: string, description: "Label ID to filter by" } + limit: { type: number, description: "Max indicators to return (default 50, max 500)" } + last_run_id: { type: string, description: "Pagination cursor (OpenCTI.Indicators.LastRunID)" } + all_results: { type: boolean, description: "Fetch all results, ignoring limit (default false)" } + required: [] + outputs_schema: { properties: {} } + - id: indicator_create + name: opencti-indicator-create + description: "Create a new indicator (STIX pattern built from value + observable type)." + risk: safe_write + inputs_schema: + properties: + name: { type: string, description: "Indicator name" } + indicator: { type: string, description: "Indicator value" } + main_observable_type: { type: string, description: "Main observable type: Account, Domain, Email, File-MD5, File-SHA1, File-SHA256, IP, IPv6, Registry Key, URL" } + indicator_types: { type: string, description: "Comma-separated indicator types (see indicator_types_list)" } + description: { type: string, description: "Indicator description" } + confidence: { type: number, description: "Confidence 0-100 (default 50)" } + score: { type: number, description: "Score 0-100 (default 50)" } + valid_from: { type: string, description: "Valid-from date (YYYY-MM-DDThh:mm:ss.sssZ)" } + valid_until: { type: string, description: "Valid-until date (YYYY-MM-DDThh:mm:ss.sssZ)" } + created_by: { type: string, description: "Creator organization ID" } + label_id: { type: string, description: "Label ID" } + marking_id: { type: string, description: "Marking definition ID" } + external_references_id: { type: string, description: "External reference ID" } + create_observables: { type: boolean, description: "Also create the related observable (default false)" } + required: [name, indicator, main_observable_type] + outputs_schema: { properties: {} } + - id: indicator_update + name: opencti-indicator-update + description: "Update an existing indicator's fields." + risk: safe_write + inputs_schema: + properties: + id: { type: string, description: "Indicator ID" } + name: { type: string, description: "Indicator name" } + indicator_types: { type: string, description: "Comma-separated indicator types" } + description: { type: string, description: "Indicator description" } + confidence: { type: number, description: "Confidence 0-100 (default 50)" } + score: { type: number, description: "Score 0-100 (default 50)" } + valid_from: { type: string, description: "Valid-from date" } + valid_until: { type: string, description: "Valid-until date" } + label_id: { type: string, description: "Comma-separated label IDs" } + marking_id: { type: string, description: "Comma-separated marking IDs" } + external_references_id: { type: string, description: "Comma-separated external reference IDs" } + required: [id] + outputs_schema: { properties: {} } + - id: indicator_field_add + name: opencti-indicator-field-add + description: "Add a marking definition or label to an indicator." + risk: safe_write + inputs_schema: + properties: + id: { type: string, description: "Indicator ID" } + field: { type: string, description: "Field to add: marking or label" } + value: { type: string, description: "Label ID or marking definition ID to add" } + required: [id, field, value] + outputs_schema: { properties: {} } + - id: indicator_field_remove + name: opencti-indicator-field-remove + description: "Remove a marking definition or label from an indicator." + inputs_schema: + properties: + id: { type: string, description: "Indicator ID" } + field: { type: string, description: "Field to remove: marking or label" } + value: { type: string, description: "Label ID or marking definition ID to remove" } + required: [id, field, value] + outputs_schema: { properties: {} } + - id: indicator_types_list + name: opencti-indicator-types-list + description: "List all indicator types (vocabulary)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + + # ── Incidents (ingestion source) ────────────────────────────────────────── + - id: get_incidents + name: opencti-get-incidents + description: "Fetch OpenCTI incidents for ingestion or search. Returns the entities list; use it as the alert-rule results path." + risk: read + inputs_schema: + properties: + search: { type: string, description: "Incident value to filter by (partial allowed)" } + created_by: { type: string, description: "Creator organization ID" } + creator: { type: string, description: "Creator user ID" } + created_after: { type: string, description: "Created-after date / incremental watermark (YYYY-MM-DDThh:mm:ss.sssZ)" } + created_before: { type: string, description: "Created-before date" } + incident_types: { type: string, description: "Comma-separated incident types (see incident_types_list)" } + label_id: { type: string, description: "Label ID to filter by" } + limit: { type: number, description: "Max incidents to return (default 50, max 500)" } + last_run_id: { type: string, description: "Pagination cursor (OpenCTI.Incidents.LastRunID)" } + all_results: { type: boolean, description: "Fetch all results, ignoring limit (default false)" } + required: [] + outputs_schema: { properties: {} } + ingest: + results_path: entities + dedup_key: id + incremental_field: created_after + - id: incident_create + name: opencti-incident-create + description: "Create a new incident." + risk: safe_write + inputs_schema: + properties: + name: { type: string, description: "Incident name" } + incident_type: { type: string, description: "Incident type name (see incident_types_list)" } + confidence: { type: number, description: "Confidence 0-100 (default 50)" } + severity: { type: string, description: "Severity: low, medium, high, critical" } + description: { type: string, description: "Incident description" } + source: { type: string, description: "Incident source" } + objective: { type: string, description: "Incident objective" } + created_by: { type: string, description: "Creator organization ID" } + first_seen: { type: string, description: "First seen (YYYY-MM-DDThh:mm:ss.sssZ)" } + last_seen: { type: string, description: "Last seen (YYYY-MM-DDThh:mm:ss.sssZ)" } + label_id: { type: string, description: "Label ID" } + marking_id: { type: string, description: "Marking definition ID" } + external_references_id: { type: string, description: "External reference ID" } + required: [name] + outputs_schema: { properties: {} } + - id: incident_delete + name: opencti-incident-delete + description: "Delete an incident by ID." + inputs_schema: + properties: + id: { type: string, description: "Incident ID" } + required: [id] + outputs_schema: { properties: {} } + - id: incident_types_list + name: opencti-incident-types-list + description: "List all incident types (vocabulary)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + + # ── Relationships ───────────────────────────────────────────────────────── + - id: relationship_create + name: opencti-relationship-create + description: "Create a relationship between two entities." + risk: safe_write + inputs_schema: + properties: + from_id: { type: string, description: "Source entity ID" } + to_id: { type: string, description: "Target entity ID" } + relationship_type: { type: string, description: "Relationship type (e.g. related-to, indicates, targets, uses, mitigates; default related-to)" } + description: { type: string, description: "Relationship description" } + confidence: { type: number, description: "Confidence 0-100" } + required: [from_id, to_id] + outputs_schema: { properties: {} } + - id: relationship_delete + name: opencti-relationship-delete + description: "Delete a relationship by ID." + inputs_schema: + properties: + id: { type: string, description: "Relationship ID" } + required: [id] + outputs_schema: { properties: {} } + - id: relationship_list + name: opencti-relationship-list + description: "List relationships from a given entity." + risk: read + inputs_schema: + properties: + from_id: { type: string, description: "Source entity ID" } + limit: { type: number, description: "Max relationships to return (default 50, max 200)" } + last_run_id: { type: string, description: "Pagination cursor (OpenCTI.Relationships.relationshipsLastRun)" } + required: [from_id] + outputs_schema: { properties: {} } + + # ── Reference data ──────────────────────────────────────────────────────── + - id: organization_list + name: opencti-organization-list + description: "List all organizations." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max organizations to return (default 50, max 200)" } + last_run_id: { type: string, description: "Pagination cursor (OpenCTI.Organizations.organizationsLastRun)" } + required: [] + outputs_schema: { properties: {} } + - id: organization_create + name: opencti-organization-create + description: "Create a new organization." + risk: safe_write + inputs_schema: + properties: + name: { type: string, description: "Organization name" } + description: { type: string, description: "Organization description" } + reliability: { type: string, description: "Reliability: A, B, C, D, E, F" } + required: [name] + outputs_schema: { properties: {} } + - id: label_list + name: opencti-label-list + description: "List all labels." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max labels to return (default 50)" } + last_run_id: { type: string, description: "Pagination cursor (OpenCTI.Labels.labelsLastRun)" } + required: [] + outputs_schema: { properties: {} } + - id: label_create + name: opencti-label-create + description: "Create a new label." + risk: safe_write + inputs_schema: + properties: + name: { type: string, description: "Label name" } + required: [name] + outputs_schema: { properties: {} } + - id: marking_definition_list + name: opencti-marking-definition-list + description: "List all marking definitions." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max marking definitions to return (default 50)" } + last_run_id: { type: string, description: "Pagination cursor (OpenCTI.MarkingDefinitions.markingsLastRun)" } + required: [] + outputs_schema: { properties: {} } + - id: external_reference_create + name: opencti-external-reference-create + description: "Create an external reference." + risk: safe_write + inputs_schema: + properties: + url: { type: string, description: "External reference URL" } + source_name: { type: string, description: "External reference source name" } + required: [url, source_name] + outputs_schema: { properties: {} } + + - id: test_connection + name: opencti-test-connection + description: "Verify connectivity and credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + +ingestion: + command: get_incidents + mapper: get_incidents + default_incident_type: "OpenCTI Incident" diff --git a/integrations/opencti/mappers/get_incidents.yaml b/integrations/opencti/mappers/get_incidents.yaml new file mode 100644 index 0000000..f021379 --- /dev/null +++ b/integrations/opencti/mappers/get_incidents.yaml @@ -0,0 +1,18 @@ +name: "OpenCTI Incidents → OCSF" +description: "Maps an OpenCTI incident (get_incidents, results_path = entities) to OCSF finding fields. Each entity is a normalized incident object emitted by the get_incidents script." +field_mappings: + title: "name" + severity: "severity = 'critical' ? 5 : (severity = 'high' ? 4 : (severity = 'medium' ? 3 : 2))" + description: "description" +# results_path = entities; source_path is JSONata over ONE incident object. +# Paths absent from a given incident are skipped at ingestion, so extra entries are safe. +ocsf: + # ── Finding ─────────────────────────────────────────────────────── + - { source_path: "id", ocsf_field: "finding_info.uid" } + - { source_path: "name", ocsf_field: "finding_info.title" } + - { source_path: "description", ocsf_field: "finding_info.desc" } + - { source_path: "created", ocsf_field: "finding_info.created_time" } + - { source_path: "updatedAt", ocsf_field: "finding_info.modified_time" } + - { source_path: "incidentTypes", ocsf_field: "finding_info.types" } + - { source_path: "objective", ocsf_field: "message" } + - { source_path: "confidence", ocsf_field: "confidence_score" } diff --git a/integrations/opencti/scripts/external_reference_create.py b/integrations/opencti/scripts/external_reference_create.py new file mode 100644 index 0000000..3a0bbfb --- /dev/null +++ b/integrations/opencti/scripts/external_reference_create.py @@ -0,0 +1,61 @@ +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") + + +try: + result = client().external_reference.create( + source_name=I.get("source_name"), + url=I.get("url"), + ) +except Exception as e: + fail("Failed to create external reference.", detail=str(e)) + +if not result: + fail("Failed to create external reference.") + +out({"id": result.get("id"), "message": f"External reference created. id: {result.get('id')}"}) diff --git a/integrations/opencti/scripts/get_incidents.py b/integrations/opencti/scripts/get_incidents.py new file mode 100644 index 0000000..6564e80 --- /dev/null +++ b/integrations/opencti/scripts/get_incidents.py @@ -0,0 +1,120 @@ +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") + + +def get_incidents(client, search="", created_by=None, creator=None, created_after=None, + created_before=None, incident_types=None, label=None, limit=500, + last_run_id=None, get_all=False): + filters = {"mode": "and", "filters": [], "filterGroups": []} + if label: + filters["filters"].append({"key": "objectLabel", "values": [label], "operator": "eq", "mode": "or"}) + if created_by: + filters["filters"].append({"key": "createdBy", "values": [created_by], "operator": "eq", "mode": "or"}) + if creator: + filters["filters"].append({"key": "creator_id", "values": [creator], "operator": "eq"}) + if incident_types: + filters["filters"].append({"key": "incident_types", "values": incident_types, "operator": "eq", "mode": "or"}) + if created_after: + filters["filters"].append({"key": "created_at", "values": [created_after], "operator": "gt"}) + if created_before: + filters["filters"].append({"key": "created_at", "values": [created_before], "operator": "lt"}) + return client.incident.list( + after=last_run_id, first=limit, withPagination=True, getAll=get_all, filters=filters, search=search + ) + + +search = I.get("search", "") +created_by = I.get("created_by") +creator = I.get("creator") +created_after = I.get("created_after") +created_before = I.get("created_before") +incident_types = as_list(I.get("incident_types")) +label = I.get("label_id") +limit = as_int(I.get("limit", 500), 500) +last_run_id = I.get("last_run_id") +get_all = as_bool(I.get("all_results", "false")) + +try: + raw = get_incidents( + client(), + search=search, + created_by=created_by, + creator=creator, + created_after=created_after, + created_before=created_before, + incident_types=incident_types, + label=label, + limit=limit, + last_run_id=last_run_id, + get_all=get_all, + ) +except Exception as e: + fail("Can't list incidents from OpenCTI.", detail=str(e)) + +last_run = None if get_all else raw.get("pagination", {}).get("endCursor") +incidents_list = raw if get_all else raw.get("entities", []) + +normalized = [] +for incident in incidents_list: + normalized.append({ + "id": incident.get("id"), + "name": incident.get("name"), + "description": incident.get("description"), + "source": incident.get("source"), + "confidence": incident.get("confidence"), + "severity": incident.get("severity"), + "objective": incident.get("objective"), + "createdBy": (incident.get("createdBy") or {}).get("name") or "", + "creators": [c.get("name") for c in incident.get("creators", [])], + "labels": [l.get("value") for l in incident.get("objectLabel", [])], + "incidentTypes": incident.get("incident_types"), + "created": incident.get("created"), + "updatedAt": incident.get("updated_at"), + }) + +out({"entities": normalized, "pagination": {"endCursor": last_run}}) diff --git a/integrations/opencti/scripts/get_indicators.py b/integrations/opencti/scripts/get_indicators.py new file mode 100644 index 0000000..5ef13f3 --- /dev/null +++ b/integrations/opencti/scripts/get_indicators.py @@ -0,0 +1,125 @@ +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") + + +def get_indicators(client, search, created_by, creator, created_after, created_before, + valid_until_after, valid_until_before, valid_from_after, valid_from_before, + indicator_types, label, limit, last_run_id, get_all): + filters = {"mode": "and", "filters": [], "filterGroups": []} + if label: + filters["filters"].append({"key": "objectLabel", "values": [label], "operator": "eq", "mode": "or"}) + if created_by: + filters["filters"].append({"key": "createdBy", "values": [created_by], "operator": "eq", "mode": "or"}) + if creator: + filters["filters"].append({"key": "creator_id", "values": [creator], "operator": "eq"}) + if indicator_types: + filters["filters"].append({"key": "indicator_types", "values": indicator_types, "operator": "eq", "mode": "or"}) + if created_after: + filters["filters"].append({"key": "created_at", "values": [created_after], "operator": "gt"}) + if created_before: + filters["filters"].append({"key": "created_at", "values": [created_before], "operator": "lt"}) + if valid_from_after: + filters["filters"].append({"key": "valid_from", "values": [valid_from_after], "operator": "gt"}) + if valid_from_before: + filters["filters"].append({"key": "valid_from", "values": [valid_from_before], "operator": "lt"}) + if valid_until_after: + filters["filters"].append({"key": "valid_until", "values": [valid_until_after], "operator": "gt"}) + if valid_until_before: + filters["filters"].append({"key": "valid_until", "values": [valid_until_before], "operator": "lt"}) + return client.indicator.list( + after=last_run_id, first=limit, withPagination=True, getAll=get_all, filters=filters, search=search + ) + + +search = I.get("search") +created_by = I.get("created_by") +creator = I.get("creator") +created_after = I.get("created_after") +created_before = I.get("created_before") +valid_until_after = I.get("valid_until_after") +valid_until_before = I.get("valid_until_before") +valid_from_after = I.get("valid_from_after") +valid_from_before = I.get("valid_from_before") +indicator_types = as_list(I.get("indicator_types")) +label = I.get("label_id") +limit = as_int(I.get("limit", 50), 50) +last_run_id = I.get("last_run_id") +get_all = as_bool(I.get("all_results", "false")) + +try: + raw = get_indicators( + client(), search, created_by, creator, created_after, created_before, + valid_until_after, valid_until_before, valid_from_after, valid_from_before, + indicator_types, label, limit, last_run_id, get_all, + ) +except Exception as e: + fail("Can't list indicators from OpenCTI.", detail=str(e)) + +last_run = None if get_all else raw.get("pagination", {}).get("endCursor") +indicators_list = raw if get_all else raw.get("entities", []) + +normalized = [] +for ind in indicators_list: + normalized.append({ + "id": ind.get("id"), + "name": ind.get("name"), + "description": ind.get("description"), + "pattern": ind.get("pattern"), + "validFrom": ind.get("valid_from"), + "validUntil": ind.get("valid_until"), + "score": ind.get("x_opencti_score"), + "confidence": ind.get("confidence"), + "createdBy": ind.get("createdBy").get("name") if ind.get("createdBy") else "", + "creators": [c.get("name") for c in ind.get("creators", [])], + "labels": [l.get("value") for l in ind.get("objectLabel", [])], + "indicatorTypes": ind.get("indicator_types"), + "created": ind.get("created"), + "updatedAt": ind.get("updated_at"), + }) + +out({"indicators": normalized, "lastRunID": last_run}) diff --git a/integrations/opencti/scripts/get_observables.py b/integrations/opencti/scripts/get_observables.py new file mode 100644 index 0000000..626e609 --- /dev/null +++ b/integrations/opencti/scripts/get_observables.py @@ -0,0 +1,137 @@ +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", +} +OPENCTI_TO_TYPE = { + "User-Account": "Account", "Domain-Name": "Domain", "Email-Addr": "Email", "StixFile": "File", + "X-OpenCTI-Hostname": "Host", "IPv4-Addr": "IP", "IPv6-Addr": "IPv6", + "Windows-Registry-Key": "Registry Key", "Url": "URL", +} + + +def build_observable_list(observable_list): + result = [] + if "ALL" in observable_list: + result = ["User-Account", "Domain-Name", "Email-Addr", "StixFile", "X-OpenCTI-Hostname", + "IPv4-Addr", "IPv6-Addr", "Windows-Registry-Key", "Url"] + else: + result = [TYPES_TO_OPENCTI.get(o.lower(), o) for o in observable_list] + return result + + +def get_observables(client, observable_types, score=None, limit=500, last_run_id=None, search="", get_all=False): + observable_type = build_observable_list(observable_types) + filters = { + "mode": "and", + "filters": [{"key": "entity_type", "values": observable_type, "operator": "eq", "mode": "or"}], + "filterGroups": [], + } + if score: + filters["filters"].append({"key": "x_opencti_score", "values": score, "operator": "eq", "mode": "or"}) + return client.stix_cyber_observable.list( + after=last_run_id, first=limit, withPagination=True, getAll=get_all, filters=filters, search=search + ) + + +observable_types = as_list(I.get("observable_types", "ALL")) or ["ALL"] +last_run_id = I.get("last_run_id") +limit = as_int(I.get("limit", 50), 50) +start = as_int(I.get("score_start", 0), 0) +end = as_int(I.get("score_end", 100), 100) +score = I.get("score") +search = I.get("search", "") +get_all = as_bool(I.get("all_results", "false")) + +scores = None +if score: + if str(score).lower() == "unknown": + scores = [None] + elif str(score).isdigit(): + scores = [score] + else: + fail("Invalid score was provided.") +elif start or end: + scores = [str(n) for n in range(start, end + 1)] + +try: + raw = get_observables( + client(), + observable_types, + score=scores, + limit=limit, + last_run_id=last_run_id, + search=search, + get_all=get_all, + ) +except Exception as e: + fail("Can't list observables from OpenCTI.", detail=str(e)) + +last_run = None if get_all else raw.get("pagination", {}).get("endCursor") +observables_list = raw if get_all else raw.get("entities", []) + +normalized = [] +for o in observables_list: + normalized.append({ + "type": OPENCTI_TO_TYPE.get(o["entity_type"], o["entity_type"]), + "value": o.get("observable_value"), + "id": o.get("id"), + "createdBy": o.get("createdBy").get("id") if o.get("createdBy") else None, + "score": o.get("x_opencti_score"), + "description": o.get("x_opencti_description"), + "labels": [l.get("value") for l in o.get("objectLabel", [])], + "marking": [m.get("definition") for m in o.get("objectMarking", [])], + "externalReferences": o.get("externalReferences"), + }) + +out({"observables": normalized, "lastRunID": last_run}) diff --git a/integrations/opencti/scripts/incident_create.py b/integrations/opencti/scripts/incident_create.py new file mode 100644 index 0000000..d07cd9e --- /dev/null +++ b/integrations/opencti/scripts/incident_create.py @@ -0,0 +1,72 @@ +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") + + +try: + result = client().incident.create( + name=I.get("name"), + incident_type=I.get("incident_type"), + confidence=as_int(I.get("confidence", 50), 50), + severity=I.get("severity"), + description=I.get("description"), + source=I.get("source"), + objective=I.get("objective"), + createdBy=I.get("created_by"), + first_seen=I.get("first_seen"), + last_seen=I.get("last_seen"), + objectLabel=I.get("label_id"), + objectMarking=I.get("marking_id"), + externalReferences=I.get("external_references_id"), + ) +except Exception as e: + fail("Can't create incident in OpenCTI.", detail=str(e)) + +if result.get("id"): + out({"id": result.get("id"), "message": f"Incident created. id: {result.get('id')}"}) +else: + fail("Incident creation failed.") diff --git a/integrations/opencti/scripts/incident_delete.py b/integrations/opencti/scripts/incident_delete.py new file mode 100644 index 0000000..a3a2c71 --- /dev/null +++ b/integrations/opencti/scripts/incident_delete.py @@ -0,0 +1,55 @@ +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") + + +try: + client().stix_domain_object.delete(id=I.get("id")) +except Exception as e: + fail("Can't delete incident in OpenCTI.", detail=str(e)) + +out({"success": True, "message": "Incident deleted."}) diff --git a/integrations/opencti/scripts/incident_types_list.py b/integrations/opencti/scripts/incident_types_list.py new file mode 100644 index 0000000..b9c1d29 --- /dev/null +++ b/integrations/opencti/scripts/incident_types_list.py @@ -0,0 +1,64 @@ +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") + + +query = """ + query OpenVocabFieldQuery($category: VocabularyCategory!, $orderBy: VocabularyOrdering, $orderMode: OrderingMode) { + vocabularies(category: $category, orderBy: $orderBy, orderMode: $orderMode) { + edges { node { id name description } } + } + } +""" + +try: + result = client().query(query, {"category": "incident_type_ov"}) + edges = result["data"]["vocabularies"]["edges"] +except Exception as e: + fail("Can't list incident types from OpenCTI.", detail=str(e)) + +out({"incidentTypes": [edge["node"] for edge in edges]}) diff --git a/integrations/opencti/scripts/indicator_create.py b/integrations/opencti/scripts/indicator_create.py new file mode 100644 index 0000000..a872ff1 --- /dev/null +++ b/integrations/opencti/scripts/indicator_create.py @@ -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")}) diff --git a/integrations/opencti/scripts/indicator_field_add.py b/integrations/opencti/scripts/indicator_field_add.py new file mode 100644 index 0000000..68e8bf7 --- /dev/null +++ b/integrations/opencti/scripts/indicator_field_add.py @@ -0,0 +1,64 @@ +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") + + +field = I.get("field") + +try: + if field == "marking": + client().stix_domain_object.add_marking_definition(id=I["id"], marking_definition_id=I["value"]) + elif field == "label": + client().stix_domain_object.add_label(id=I["id"], label_id=I["value"]) + else: + fail(f"Invalid field: {field}.") +except SystemExit: + raise +except Exception as e: + fail(f"Can't add {field} to indicator in OpenCTI.", detail=str(e)) + +out({"success": True, "message": f"Added {field}."}) diff --git a/integrations/opencti/scripts/indicator_field_remove.py b/integrations/opencti/scripts/indicator_field_remove.py new file mode 100644 index 0000000..1e64bab --- /dev/null +++ b/integrations/opencti/scripts/indicator_field_remove.py @@ -0,0 +1,64 @@ +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") + + +field = I.get("field") + +try: + if field == "marking": + client().stix_domain_object.remove_marking_definition(id=I["id"], marking_definition_id=I["value"]) + elif field == "label": + client().stix_domain_object.remove_label(id=I["id"], label_id=I["value"]) + else: + fail(f"Invalid field: {field}.") +except SystemExit: + raise +except Exception as e: + fail(f"Can't remove {field} from indicator in OpenCTI.", detail=str(e)) + +out({"success": True, "message": f"Removed {field}."}) diff --git a/integrations/opencti/scripts/indicator_types_list.py b/integrations/opencti/scripts/indicator_types_list.py new file mode 100644 index 0000000..834a8ee --- /dev/null +++ b/integrations/opencti/scripts/indicator_types_list.py @@ -0,0 +1,64 @@ +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") + + +query = """ + query OpenVocabFieldQuery($category: VocabularyCategory!, $orderBy: VocabularyOrdering, $orderMode: OrderingMode) { + vocabularies(category: $category, orderBy: $orderBy, orderMode: $orderMode) { + edges { node { id name description } } + } + } +""" + +try: + result = client().query(query, {"category": "indicator_type_ov"}) +except Exception as e: + fail("Can't list indicator types from OpenCTI.", detail=str(e)) + +edges = result["data"]["vocabularies"]["edges"] +out({"indicatorTypes": [edge["node"] for edge in edges]}) diff --git a/integrations/opencti/scripts/indicator_update.py b/integrations/opencti/scripts/indicator_update.py new file mode 100644 index 0000000..cc4e4c7 --- /dev/null +++ b/integrations/opencti/scripts/indicator_update.py @@ -0,0 +1,114 @@ +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") + + +indicator_id = I.get("id") +name = I.get("name") +description = I.get("description") +confidence = I.get("confidence") +score = I.get("score") +valid_from = I.get("valid_from") +valid_until = I.get("valid_until") +indicator_types = I.get("indicator_types") +label_id = I.get("label_id") +marking_id = I.get("marking_id") +external_references_id = I.get("external_references_id") + +update_fields = [] +if name: + update_fields.append({"key": "name", "value": name}) +if description: + update_fields.append({"key": "description", "value": description}) +if confidence: + update_fields.append({"key": "confidence", "value": as_int(confidence)}) +if score: + update_fields.append({"key": "x_opencti_score", "value": as_int(score)}) +if valid_from: + update_fields.append({"key": "valid_from", "value": valid_from}) +if valid_until: + update_fields.append({"key": "valid_until", "value": valid_until}) +if indicator_types: + update_fields.append({"key": "indicator_types", "value": indicator_types.split(",")}) +if label_id: + update_fields.append({"key": "objectLabel", "value": label_id.split(",")}) +if marking_id: + update_fields.append({"key": "objectMarking", "value": marking_id.split(",")}) +if external_references_id: + update_fields.append({"key": "externalReferences", "value": external_references_id.split(",")}) + +mutation = """ + mutation IndicatorEditionOverviewFieldPatchMutation($id: ID!, $input: [EditInput!]!, $commitMessage: String, $references: [String]) { + indicatorFieldPatch(id: $id, input: $input, commitMessage: $commitMessage, references: $references) { + id name confidence description valid_from valid_until x_opencti_score indicator_types + } + } +""" + +variables = { + "id": indicator_id, + "input": update_fields, + "commitMessage": None, + "references": None, +} + +try: + result = client().query(mutation, variables) +except Exception as e: + fail("Can't update indicator in OpenCTI.", detail=str(e)) + +patched = result.get("data", {}).get("indicatorFieldPatch") +if patched: + out({ + "id": patched.get("id"), + "name": patched.get("name"), + "validFrom": valid_from, + "validUntil": valid_until, + "message": "Indicator updated.", + }) +else: + fail("Can't update indicator in OpenCTI.") diff --git a/integrations/opencti/scripts/label_create.py b/integrations/opencti/scripts/label_create.py new file mode 100644 index 0000000..0a84f01 --- /dev/null +++ b/integrations/opencti/scripts/label_create.py @@ -0,0 +1,58 @@ +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") + + +try: + result = client().label.create(value=I.get("name")) +except Exception as e: + fail("Failed to create label.", detail=str(e)) + +if not result: + fail("Failed to create label.") + +out({"id": result.get("id"), "message": f"Label created. id: {result.get('id')}"}) diff --git a/integrations/opencti/scripts/label_list.py b/integrations/opencti/scripts/label_list.py new file mode 100644 index 0000000..bb3a724 --- /dev/null +++ b/integrations/opencti/scripts/label_list.py @@ -0,0 +1,62 @@ +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") + + +try: + labels = client().label.list( + first=as_int(I.get("limit", 50), 50), + after=I.get("last_run_id"), + withPagination=True, + ) +except Exception as e: + fail("Failed to list labels.", detail=str(e)) + +out({ + "labels": [{"value": l.get("value"), "id": l.get("id")} for l in labels.get("entities", [])], + "labelsLastRun": labels.get("pagination", {}).get("endCursor"), +}) diff --git a/integrations/opencti/scripts/marking_definition_list.py b/integrations/opencti/scripts/marking_definition_list.py new file mode 100644 index 0000000..5dbfd6a --- /dev/null +++ b/integrations/opencti/scripts/marking_definition_list.py @@ -0,0 +1,62 @@ +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") + + +try: + marks = client().marking_definition.list( + first=as_int(I.get("limit", 50), 50), + after=I.get("last_run_id"), + withPagination=True, + ) +except Exception as e: + fail("Failed to list marking definitions.", detail=str(e)) + +out({ + "markingDefinitions": [{"value": m.get("definition"), "id": m.get("id")} for m in marks.get("entities", [])], + "markingsLastRun": marks.get("pagination", {}).get("endCursor"), +}) diff --git a/integrations/opencti/scripts/observable_create.py b/integrations/opencti/scripts/observable_create.py new file mode 100644 index 0000000..d41fc97 --- /dev/null +++ b/integrations/opencti/scripts/observable_create.py @@ -0,0 +1,97 @@ +import json, os, sys +import io + +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", +} +FILE_TYPES = { + "file-md5": "file.hashes.md5", "file-sha1": "file.hashes.sha-1", "file-sha256": "file.hashes.sha-256", +} + + +observable_type = I.get("type") +data = {"type": TYPES_TO_OPENCTI.get(observable_type.lower(), observable_type), "value": I.get("value")} +if observable_type == "Registry Key": + data["key"] = I.get("value") +if observable_type == "Account": + data["account_login"] = I.get("value") + +simple_observable_key = None +simple_observable_value = None +if "file" in observable_type.lower(): + simple_observable_key = FILE_TYPES.get(observable_type.lower(), observable_type) + simple_observable_value = I.get("value") + +try: + sys.stdout = io.StringIO() + try: + result = client().stix_cyber_observable.create( + simple_observable_key=simple_observable_key, + simple_observable_value=simple_observable_value, + type=observable_type, + createdBy=I.get("created_by"), + objectMarking=I.get("marking_id"), + objectLabel=I.get("label_id"), + externalReferences=I.get("external_references_id"), + simple_observable_description=I.get("description"), + x_opencti_score=as_int(I.get("score", "50"), 50), + observableData=data, + createIndicator=as_bool(I.get("create_indicator", "false")), + ) + finally: + sys.stdout = sys.__stdout__ +except Exception as e: + sys.stdout = sys.__stdout__ + fail("Can't create observable in OpenCTI.", detail=str(e)) + +out({"id": result.get("id"), "value": I.get("value"), "type": observable_type}) diff --git a/integrations/opencti/scripts/observable_delete.py b/integrations/opencti/scripts/observable_delete.py new file mode 100644 index 0000000..d0f03d4 --- /dev/null +++ b/integrations/opencti/scripts/observable_delete.py @@ -0,0 +1,55 @@ +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") + + +try: + client().stix_cyber_observable.delete(id=I.get("id")) +except Exception as e: + fail("Can't delete observable in OpenCTI.", detail=str(e)) + +out({"success": True, "message": "Observable deleted."}) diff --git a/integrations/opencti/scripts/observable_field_add.py b/integrations/opencti/scripts/observable_field_add.py new file mode 100644 index 0000000..2e1d4f2 --- /dev/null +++ b/integrations/opencti/scripts/observable_field_add.py @@ -0,0 +1,69 @@ +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") + + +field = I.get("field") +observable_id = I.get("id") +value = I.get("value") + +try: + if field == "marking": + result = client().stix_cyber_observable.add_marking_definition( + id=observable_id, marking_definition_id=value + ) + elif field == "label": + result = client().stix_cyber_observable.add_label(id=observable_id, label_id=value) + else: + fail("Invalid field was provided.") +except Exception as e: + fail("Can't add field to observable in OpenCTI.", detail=str(e)) + +if result: + out({"success": True, "message": "Added {0}.".format(field)}) +else: + fail("Can't add {0} to observable in OpenCTI.".format(field)) diff --git a/integrations/opencti/scripts/observable_field_remove.py b/integrations/opencti/scripts/observable_field_remove.py new file mode 100644 index 0000000..fb944ef --- /dev/null +++ b/integrations/opencti/scripts/observable_field_remove.py @@ -0,0 +1,66 @@ +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") + + +field = I.get("field") +observable_id = I.get("id") +value = I.get("value") + +try: + if field == "marking": + result = client().stix_cyber_observable.remove_marking_definition( + id=observable_id, marking_definition_id=value + ) + elif field == "label": + result = client().stix_cyber_observable.remove_label(id=observable_id, label_id=value) + else: + fail("Invalid field was provided.") +except Exception as e: + fail("Can't remove field from observable in OpenCTI.", detail=str(e)) + +out({"success": True, "message": "Removed {0}.".format(field)}) diff --git a/integrations/opencti/scripts/observable_field_update.py b/integrations/opencti/scripts/observable_field_update.py new file mode 100644 index 0000000..21bbed2 --- /dev/null +++ b/integrations/opencti/scripts/observable_field_update.py @@ -0,0 +1,63 @@ +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") + + +KEY_TO_CTI_NAME = {"description": "x_opencti_description", "score": "x_opencti_score"} + + +field = I.get("field") +if field not in KEY_TO_CTI_NAME: + fail("Invalid field was provided.") +key = KEY_TO_CTI_NAME[field] + +try: + result = client().stix_cyber_observable.update_field(id=I.get("id"), key=key, value=I.get("value")) +except Exception as e: + fail("Can't update observable field in OpenCTI.", detail=str(e)) + +out({"id": result.get("id"), "message": "Observable updated."}) diff --git a/integrations/opencti/scripts/organization_create.py b/integrations/opencti/scripts/organization_create.py new file mode 100644 index 0000000..2727e01 --- /dev/null +++ b/integrations/opencti/scripts/organization_create.py @@ -0,0 +1,64 @@ +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, Identity +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") + + +try: + identity = Identity(client()) + result = identity.create( + name=I.get("name"), + type="Organization", + x_opencti_reliability=I.get("reliability"), + description=I.get("description"), + ) +except Exception as e: + fail("Failed to create organization.", detail=str(e)) + +if not result: + fail("Failed to create organization.") + +out({"id": result.get("id"), "message": f"Organization created. id: {result.get('id')}"}) diff --git a/integrations/opencti/scripts/organization_list.py b/integrations/opencti/scripts/organization_list.py new file mode 100644 index 0000000..980e383 --- /dev/null +++ b/integrations/opencti/scripts/organization_list.py @@ -0,0 +1,63 @@ +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") + + +try: + orgs = client().identity.list( + types="Organization", + first=as_int(I.get("limit", 50), 50), + after=I.get("last_run_id"), + withPagination=True, + ) +except Exception as e: + fail("Failed to list organizations.", detail=str(e)) + +out({ + "organizations": [{"name": o.get("name"), "id": o.get("id")} for o in orgs.get("entities", [])], + "organizationsLastRun": orgs.get("pagination", {}).get("endCursor"), +}) diff --git a/integrations/opencti/scripts/relationship_create.py b/integrations/opencti/scripts/relationship_create.py new file mode 100644 index 0000000..37fb662 --- /dev/null +++ b/integrations/opencti/scripts/relationship_create.py @@ -0,0 +1,64 @@ +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") + + +try: + result = client().stix_core_relationship.create( + fromId=I.get("from_id"), + toId=I.get("to_id"), + relationship_type=I.get("relationship_type") or "related-to", + description=I.get("description"), + confidence=as_int(I.get("confidence")) or None, + ) +except Exception as e: + fail("Failed to create relationship.", detail=str(e)) + +if not result: + fail("Failed to create relationship.") + +out({"id": result.get("id"), "relationshipType": result.get("relationship_type")}) diff --git a/integrations/opencti/scripts/relationship_delete.py b/integrations/opencti/scripts/relationship_delete.py new file mode 100644 index 0000000..9975d20 --- /dev/null +++ b/integrations/opencti/scripts/relationship_delete.py @@ -0,0 +1,55 @@ +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") + + +try: + client().stix_core_relationship.delete(id=I.get("id")) +except Exception as e: + fail("Failed to delete relationship.", detail=str(e)) + +out({"success": True, "message": "Relationship deleted."}) diff --git a/integrations/opencti/scripts/relationship_list.py b/integrations/opencti/scripts/relationship_list.py new file mode 100644 index 0000000..8e58a9c --- /dev/null +++ b/integrations/opencti/scripts/relationship_list.py @@ -0,0 +1,72 @@ +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") + + +try: + rels = client().stix_core_relationship.list( + fromOrToId=I.get("from_id"), + first=as_int(I.get("limit", 50), 50), + after=I.get("last_run_id"), + withPagination=True, + ) +except Exception as e: + fail("Failed to list relationships.", detail=str(e)) + +cursor = rels.get("pagination", {}).get("endCursor") + +relationships = [] +for rel in rels.get("entities", []): + relationships.append({ + "id": rel.get("id"), + "relationshipType": rel.get("relationship_type"), + "fromId": rel["from"]["id"], + "toId": rel["to"]["id"], + "toEntityType": rel["to"]["entity_type"], + }) + +out({"relationships": relationships, "relationshipsLastRun": cursor}) diff --git a/integrations/opencti/scripts/test_connection.py b/integrations/opencti/scripts/test_connection.py new file mode 100644 index 0000000..34e1bcd --- /dev/null +++ b/integrations/opencti/scripts/test_connection.py @@ -0,0 +1,55 @@ +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") + + +try: + client().label.list(first=1, withPagination=True) +except Exception as e: + fail("Connection failed.", detail=str(e)) + +out({"status": "ok"})