Commit Graph

20 Commits

Author SHA1 Message Date
f3nris e973ccb673 feat(cortex-xdr): endpoint_count counts agents below a target build (1.10.0)
agent_version_lt, compared number by number in the script, as the
endpoints API does not filter on the agent version either. Folded into
the unpushed 1.10.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaiNEwG2W5Y9fMo4qXcesr
2026-10-13 08:49:29 +00:00
f3nris 62157e5dee feat(cortex-xdr): endpoint_count counts by operational status (1.10.0)
The endpoints API has no filter on operational_status, so the endpoints
the other filters match are read 100 at a time and counted by the script.
Lets an agent reporting playbook count the agents in error, as
SentinelOne's operationalStatesNin does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaiNEwG2W5Y9fMo4qXcesr
2026-10-13 08:43:18 +00:00
f3nris 9605e1f0dc feat(agents): operator sheets for 33 integrations (lot 4)
EDR: Wraith (SentinelOne), Talon (CrowdStrike), Nyx (HarfangLab), Bastion
(Defender), Synapse (Cortex XDR), Kestrel (Sophos). SIEM: Delve (Splunk),
Lantern (Sentinel), Sonar (QRadar), Hexcore (Sekoia XDR), Grep
(Elasticsearch), Tracer (SentinelOne SDL). Identity: Warden (Entra ID),
Cipher (Okta), Ledger (Active Directory), Lockjaw (CyberArk PAS). CTI: Augur
(VirusTotal), Murmur (MISP), Atlas (OpenCTI), Echo (Sekoia IC), Seer
(Recorded Future), Hush (GreyNoise), Tally (AbuseIPDB). Sandbox: Scalpel (Joe),
Gauntlet (ANY.RUN), Prism (VMRay). Email: Hook (Proofpoint TAP), Drift
(Mimecast), Riptide (Vade). Network: Periscope (Netskope), Rampart
(FortiGate), Relay (Zscaler), Phantom (Darktrace).

Read-only commands, analyst_l2, a call budget with an order, and the Static
lessons: ids copied from answers only, timestamps passed as given, names over
ids, one variant at most on error. Each sheet passes Riposte's validator;
none is field-tested yet. Each integration gets a patch bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-11 23:00:15 +00:00
f3nris 91f6b8158b fix(cortex-xdr): give a full incident its own title (1.9.0)
Title and description both read the incident's description, so every
ticket showed the same sentence twice. The title is now the rule that
fired, with "(+N other alerts)" when several are folded in, or the
incident's console name; the description adds what each distinct rule
means.

network_artifacts[0] no longer overwrites the alert's own flow: the
artifacts are the fallback, and the destination country comes from the
artifact matching the alert's remote IP. Also maps the rule description
and id, and the XDR assignee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:25:32 +00:00
f3nris 41aa6723c5 feat(cortex-xdr): list the incident statuses and severities as enums (1.8.0)
cortex-xdr-update-incident declares the API's nine status values and
the four severities, so the playbook editor offers them in a drop-down
and the command bar refuses a misspelt one. resolved_other joins the
list; resolved_auto leaves it, Cortex XDR setting it itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhDQSYZd1mFtRsXK4ixtyB
2026-10-02 07:50:12 +00:00
f3nris beefe047a7 feat(cortex-xdr): count endpoints by filter without listing them (1.7.0)
The endpoints grid already answers with reply.total_count — the number
of endpoints matching the filters, independent of the page asked for —
so endpoint_count asks for a one-record page and reads it, where
get-endpoints has to walk the estate 100 records at a time to arrive at
the same figure, and only filters on four fields.

Filters cover the twelve list fields the API accepts (hostname, alias,
endpoint id, internal and public IP, group, platform, distribution,
status, isolation, scan status, username), add first_seen/last_seen
bounds taking ISO8601 or epoch ms, and keep a raw JSON `filters` escape
hatch for anything else. Filters are ANDed; the API has no OR.

The estate `total` comes back next to `count` so the figure reads as a
proportion, and a filter too broad to mean anything shows as matching
everything rather than passing for an answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 12:37:34 +00:00
f3nris 45e9cbd269 feat: declare a risk tier on every command in the catalog
467 of the 1592 commands carried no `risk:`. An unset risk is read as
destructive by the agent approval gate, so half the catalog's write side sat
behind the same prompt — and, in HarfangLab's case, 39 pure lookups did too.
A gate that stops everything stops being read as a signal, which is the
argument already made for the toolkit and for sentinelone (df27876).

The catalog is now 1027 read, 261 safe_write, 304 destructive, with nothing
defaulted. The line follows the one sentinelone drew: impact on the estate,
not reversibility — isolate-agent is reversible and is the canonical
destructive example.

  read        only queries; changes nothing outside Riposte
  safe_write  additive or reversible inside the tool's own case/enrichment
              plane: comments, tickets, tags, alert statuses, threat-intel
              records, sandbox submissions, collection jobs, outbound messages
  destructive changes the estate or weakens a control: containment, blocklists
              and allowlists, exclusions and detection rules, every delete,
              identity and credential operations, code on a host, generic
              API escape hatches

Three calls that were not obvious, and are now applied throughout:

- Undoing a containment is tiered like the containment it lifts. Un-isolating
  a host or un-banning an IP mid-incident is its own decision, not a return to
  a neutral state. The catalog contradicted itself here — crowdstrike's
  lift_containment was destructive while harfanglab's deisolate_from_threat
  was safe_write — so deisolate_from_threat and unquarantine_file are
  re-tiered to destructive, the only two existing tiers this pass changes.
- Retrieving a live secret is destructive: cyberark get_password, delinea
  get_secret, vault read_secret, beyondtrust get_credential. It is a GET, but
  the tier describes the stake, not the HTTP verb.
- Sending outward to humans stays safe_write — slack, teams, twilio, telegram,
  mail. Irreversible, but it is the collaboration plane, not the estate.

An object created but not yet referenced by a rule is safe_write; editing one
a rule already reads is destructive. That is why fortigate create_address and
panos create_custom_url_category are safe_write while update_address_group and
edit_custom_url_category are not.

The template and README never documented the field, which is how 467 commands
came to omit it. Both now carry the tier table and these rules, so the next
integration declares a tier instead of inheriting one.

111 packages get a MINOR bump and a changelog entry: an instance compares
versions to offer an update, so an unbumped manifest would leave the tiers
sitting in the catalog and never reaching the estates that need them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 12:55:20 +02:00
f3nris 857d2b8d57 feat(cortex-xdr): map the blocked process and its chain (v1.5.0)
action_process_image_* — the process an alert is about, the child a
prevention module blocked — was mapped nowhere in get_incidents_full, so
its command line existed only inside raw. It fills process.*, the group
the SentinelOne and Carbon Black mappers reserve for the process acted
upon. Its ancestry lands in process.lineage as OCSF Process objects,
root-first; a generation XDR leaves null drops out instead of showing as
an empty level.

Every alert's chain also travels under a process_tree context key, not an
OCSF field: process.lineage is the ancestry of ONE process, and an XDR
incident groups N alerts each with its own chain. Nodes carry their role
because the chain is positional — action_process_instance_id is null and
the *_causality_id fields name the causality group, not the node.

Also mapped: actor pid and MD5, the causality actor's command line,
endpoint FQDN/domain/MAC/OS build, protection module, agent version,
alert id, the incident id as the shared correlation key, activity kind,
and the registry/DNS/user-agent fields other alert kinds carry.
action_country is dropped when it is the literal UNKNOWN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 00:13:50 +02:00
f3nris e8b2166d30 feat(cortex-xdr): query the data lake in XQL (v1.4.0)
Four commands reach the XQL engine: cortex-xdr-xql-query runs a query and
waits for its rows, cortex-xdr-xql-start-query and
cortex-xdr-xql-get-query-results split that same work in two when the query
outlives a command, and cortex-xdr-xql-get-quota says what the day's hunting
has already cost — an XQL query is billed by the data it scans.

Three things the API makes easy to get wrong are handled here.

Past 1000 rows it stops answering with results and hands over a stream id
instead: gzipped JSON lines, unpacked into rows rather than dropped on the
playbook as an archive it cannot read.

A query carrying no limit stage is served the tenant's own default, so one is
appended (100 rows, configurable) unless the query already has its own. The
reference client does the same on a looser substring test, which also fires on
a field whose name merely contains "limit".

Waiting is a budget, not a promise: an integration command is killed at 60
seconds, so a query still running when the budget expires comes back PENDING
with its execution_id instead of failing, and the results command collects the
rows afterwards. Re-running the query would be charged against the quota a
second time — which is why the pending answer names the command to call rather
than inviting a retry.

The relative window is parsed here (24 hours, 7 days, 3 months) since the
scripts have no dateparser; an unreadable one is refused before the query is
started, rather than silently widening to the tenant default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:15:44 +02:00
f3nris 9f9ee5344d fix(cortex-xdr): put the actor where the other EDR mappers put it
HarfangLab and SentinelOne both send the process that acted to actor.* —
process.username and sourceProcessInfo.user to actor.user.name, the process
itself to actor.process.* — and keep user.name for the person the event
happened to: HarfangLab's top_impacted_users, SentinelOne's login account.

Cortex XDR filled neither slot correctly. XDR labels the acting process
actor_process_* and its account actor_effective_username; the mappers sent
the process to process.* and the account to user.name, which the OCSF
catalogue calls Target User. A rule written on actor.user.name or
actor.process.name matched HarfangLab and SentinelOne and skipped Cortex,
and a rule reading user.name as the victim read the attacker.

process.* is not free-for-all either: in the SentinelOne (targetProcessInfo)
and Carbon Black (childproc_*) mappers it holds the process acted upon, so
Cortex's actor was sitting in the target's seat.

get_incidents_full had the collision in the open: the incident's impacted
users[] and the first alert's user_name both wrote user.name, and last
non-empty wins, so the actor overwrote the impacted user on every incident
carrying both.

Tenants using the shipped templates pick this up on the next sync; a forked
mapper, or a column or rule pointed at the old Cortex paths, needs repointing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 17:03:06 +02:00
f3nris bf273b959a feat(cortex-xdr): watermark incidents on modification_time, not creation
An XDR incident is not finished when it is created. Alerts keep joining it, an
analyst changes its status, its severity is raised. A creation_time watermark
fetches it once, the watermark moves past it, and nothing that happens
afterwards ever reaches Riposte — which is precisely the content the full fetch
exists to bring in.

modified_after filters and sorts on modification_time instead, so an incident
comes back on every change and dedup on incident_id turns the second visit into
an enrichment of the incident already there. It is now what the ingest hint
prefills; created_after stays for a one-shot backfill.

Worth knowing about that enrichment: it merges context and can fill a detection
anchor that was missing, but it does not restate the incident's severity or
status. An incident XDR later raises to critical stays at the severity it was
ingested with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:50:24 +02:00
f3nris ba68d19e51 refactor(cortex-xdr): name the full fetch get-incidents-full (v1.3.1)
"fetch" said nothing next to a list of commands that all start with "get". The
command sits beside cortex-xdr-get-incidents in the picker, and the only thing
an operator needs to read there is which of the two carries everything — so the
name says it: get-incidents-full.

The id moves with it (fetch_incidents -> get_incidents_full), since the script
and the bundled mapper are bound to a command by filename. Anyone who created a
rule against the old id in the few minutes 1.3.0 was up has to point it at the
new command; the changelog says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:47:17 +02:00
f3nris a6e1245c79 feat(cortex-xdr): fetch incidents with their alerts, not a 21-field summary
incidents/get_incidents/ answers with a summary and nothing else: 21 fields,
no hosts, no users, no MITRE, no tags, and not one of the alerts the incident
aggregates. Ingesting through it leaves an incident whose raw payload says
almost nothing about what happened — and the shipped mapper had been written
for a richer shape than the endpoint ever returns, mapping hosts[0], users[0]
and mitre_* that simply are not in that response.

incidents/get_multiple_incidents_extra_data/ returns the same incidents with
39 fields, every alert in full — 156 fields each — and the file and network
artifacts. It is what the reference client fetches through (demisto/content,
CortexXDRIR.get_multiple_incidents_extra_data), and full_alert_fields must be
set or the nested alerts come back trimmed to a handful of fields.

Records arrive as {incident, alerts, network_artifacts, file_artifacts} with
each nested block wrapped as {total_count, data}. The script flattens them, so
every expression written against get_incidents keeps working — the summary's
21 fields are a subset of these 39 — while the alerts and artifacts land beside
them as plain lists, and their total_count says when a list is a sample rather
than the whole set. incident_sources is lifted into a scalar for the same
reason severity was on the alerts side: the incident-field mapper reads dotted
paths and cannot index a list.

get_incidents stays, for cheap polling, and now says in its description what it
does and does not carry.

The mapper maps the aggregate first and the first alert last, so the alert
fills in whatever the aggregate leaves silent — including the detection anchor,
since an XDR incident's detection_time is usually null while its alerts carry
theirs. Verified against the vendor's recorded response
(test_data/get_multiple_incidents_extra_data.json): 33 of 52 entries resolve,
severity critical lands on 5, source reads "XDR Agent", and the anchor falls
through to the alert's detection timestamp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:41:27 +02:00
f3nris d06f8ea413 fix(cortex-xdr): the alerts endpoint never answered — wrong request dialect
get_alerts posted the incidents body, {filters, search_from, search_to, sort},
to alerts/get_alerts_by_filter_data/. That endpoint serves the alerts GRID and
speaks another dialect entirely, so every call — before this branch as much as
after it — came back a bare HTTP 500 with no hint as to why.

Shape taken from the reference client (demisto/content, Packs/ApiModules/
Scripts/CoreIRApiModule, get_alerts_by_filter_command):

  request_data.filter_data = {
    sort:   [{FIELD, ORDER}],          # a list, uppercase keys
    paging: {from, to},                # not search_from/search_to
    filter: {AND: [{SEARCH_FIELD, SEARCH_TYPE, SEARCH_VALUE}]},
  }

Severity is an enum there (SEV_040_HIGH), and several severities are OR'd, not
passed as a list. The watermark is a RANGE, since the grid has no gte operator;
its upper bound carries five minutes of slack, because our clock and the
tenant's are not the same clock. A filterless query is bounded to the last
thirty days rather than sent empty — the reference client refuses one outright,
and the grid is not meant to be asked for a whole retention.

The response needed as much work as the request. Rows arrive wrapped as
{alert_fields, incident_fields}, and mapping through that wrapper would put an
alert_fields. prefix on every expression an operator writes, so each row is
unwrapped. Two of its fields cannot be mapped as they stand: severity is the
enum code, and status.progress carries a dot INSIDE the key, which no mapping
path can express. Both are derived into severity_name and status_progress.

The mapper follows the grid's own vocabulary — internal_id, alert_name,
agent_hostname, agent_ip_addresses — and dedup moves to internal_id, since
alert_id belongs to the other API. case_id is kept as the correlation UID: it
is the join back to the incident feed.

Verified end to end against the vendor's own recorded response
(test_data/get_alerts_by_filter_results.json): 33 of 54 OCSF entries resolve on
it, severity lands on 3, the detection anchor is set, and the paging walks
0-100, 100-200, 200-250 with the truncation flag raised only when the ceiling,
not the window, ended the fetch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:34:26 +02:00
f3nris 17318d4225 feat(cortex-xdr): ingest alerts, not only incidents (v1.3.0)
An XDR incident is an aggregate; the SOC works the detections under it. The
alerts endpoint was already exposed as a read command, but nothing could feed
an alert rule with it — no results path, no dedup key, no watermark, no mapper,
no incident type. All five are here now, so an alert rule can be pointed at
reply.alerts the same way it is pointed at reply.incidents.

get_alerts pages past the API's 100-results-per-call ceiling: an alert feed
carries far more than a hundred detections between two polls, and whatever a
single page leaves behind is never fetched again, because the next run's
watermark has already moved past it. On an incremental fetch it also sorts
oldest first, so a window larger than the limit drops its most recent alerts —
the only ones the next poll can still see — and says so via `truncated`.

Two fixes to the incident side while in the same files:

- The severity expression compared strings, which the mapping engine cannot do
  (it reads numeric comparisons only). Every test read as false, so every
  ingested incident silently took the alert rule's default severity. The bare
  field works: Riposte maps critical/high/medium/low onto 1-5 itself.
- The incident mapper carried no `time`, so the detection anchor was missing
  and MTTD stayed empty for the whole feed. creation_time fills it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:26:31 +02:00
f3nris ba6bcd19f4 fix(cortex-xdr): say what went wrong when the API answers something other than JSON (v1.2.1)
A tenant URL that is not the API host answers 200 with the console's HTML
shell, and the client turned that into "Expecting value: line 1 column 1
(char 0)" — a JSON parse error that names neither the URL nor the response.

The shared client now reports the status, content type and first bytes of any
non-JSON reply, and points at the API host the operator was meant to copy.
Around it, the URL is reduced to scheme + host so a pasted /public_api/v1 or
console path cannot double the API root, a missing key or key ID is refused
before the request, and 401/403 recalls the three usual causes (auth mode,
revoked key, clock skew).

Nonce and timestamp now ride along in standard mode too, matching the
reference client, and test_connection probes get_incidents — the read that
ingestion actually depends on — instead of the distribution versions.
2026-08-13 14:35:54 +02:00
Guillaume BOURGEOIS 0263619e30 feat(cortex-xdr): incident write-back and external alert push
Add three commands to the Cortex XDR integration (v1.2.0):
- update_incident: change status, severity, assignment and resolve
  comment on an incident (closes the SOAR ingest→action loop).
- insert_parsed_alerts: push external parsed alerts into Cortex XDR.
- insert_cef_alerts: push external CEF-format alerts.

Scripts follow the existing self-contained standard/advanced auth pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:32:47 +02:00
Guillaume BOURGEOIS 6bccbb5a50 feat(integrations): declare default ingestion (command, mapper, default type)
cortex-xdr, crowdstrike, harfanglab and splunk now ship an `ingestion` block
(fetch command + bundled mapper + default incident type) and a bundled default
incident type under incident-types/, so the instance Collect section is pre-filled
on first configuration and the default type is created on install.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 10:38:09 +02:00
Guillaume BOURGEOIS 76812ef128 feat(cortex-xdr): full command coverage (50 commands) — endpoints, scripts, exclusions, tags, audits, RBAC, distributions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:41:59 +02:00
Guillaume BOURGEOIS c068d9fa9a feat(cortex-xdr): new Cortex XDR integration — incident ingestion + OCSF mapper, IR actions, standard/advanced auth
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:28:43 +02:00