Skip to content

AiSOC v21.0.0

Choose a tag to compare

@github-actions github-actions released this 01 Nov 05:05
· 106 commits to main since this release
152c6d5

BREAKING

  • Correlation grouping narrows on missing identifiers (the Changed
    entry below). On upgrade, future alerts that carry no tactic group
    per rule instead of per entity, and alerts with no identifiers at all
    stop cross-rule merging entirely. Existing incidents are untouched;
    the measured effect on a detection-shaped cohort is reduction moving
    42.6% → 24.5% because unrelated alerts stop counting as grouped.
    There is no API change in this release; the major marks this grouping
    change plus the redefinition of published evaluation semantics
    (serving-model provenance, full-cohort denominators, closure
    contamination) that older scorecards do not carry.

Added

  • Serving-model attribution, end to end. Every published verdict-eval
    row had resolved_models: [] — 72 calls placed, answered, and
    unattributable, because the only serving-identity reader was the LiteLLM
    gateway header and the measured runs talked to an OpenAI-compatible
    endpoint directly. The extractor now reads the gateway header first
    (verified) and the response body's own model second
    (self-reported), credited only with evidence it came from the body —
    langchain falls back to the requested name when the body carries none,
    and a requested alias is never copied into serving metadata. Scorecards
    carry a provenance_status (verified / self_reported /
    unattributed / serving_mismatch / no_calls); a live run whose
    serving identity mismatches the requested model is refused as that
    model's performance and preserved as a diagnostic; the benchmark table
    gained an Attribution column with the five historical rows reading
    "unverified" rather than receiving invented attribution; and a measured
    auto-closure precision can no longer be bound to a call whose serving
    model no party vouched for — a clearing measurement over an unattributed
    call now blocks instead of authorising.

  • Full-cohort and closure accounting in the replay scorer. Selective
    malicious recall silently dropped errored cases from its denominator; the
    scorer now also publishes full-cohort recall over every labelled
    malicious case with the four buckets that sum to it (caught, abstained,
    errored, answered-as-negative), the four proposed-closure cuts
    (malicious closure rate, closure contamination, benign closure coverage,
    unknown-label share — closures_executed pinned at zero because replay
    executes nothing), request reliability as five visible counts
    (requested / attempted / completed / failed / unattempted), per-severity
    segments, and per-source attribution counts.

  • The reflex path reports its stages and its decision contract.
    Auto-triage summaries, logs and replay decisions now carry named stage
    timings (queue delay, business context, context fetch, decide,
    verification, persist, escalate, approvals, writeback) plus a structured
    decision contract — verdict, operational disposition, supporting and
    contradictory decisive claims, groundedness, missing prerequisites, and
    the escalation reason — assembled from signals that already ran, with
    None for a signal that never ran rather than an empty list pretending
    the check happened.

  • Retro-hunt rehearsal mode. RETRO_HUNT_DRY_RUN runs every sweep
    exactly as live (the warehouse scan still consumes the tenant's sweep
    budget) and withholds every mutation at the write boundary inside
    sweep_tenant: matches come back as recorded proposals, no alert opens,
    no sighting row lands, and the dedup branch leaves its counters
    untouched — so a re-hunt can be rehearsed over history with no ability
    to touch production state.

  • A live-Redis proof for the correlator, and the suite that was wired to
    nothing.
    integration.yml gains a fusion-redis-live job (keyed on
    the existing spine filter) running services/fusion/tests/integration/
    against a real server — window expiry, tenant boundaries, restart
    continuity, per-rule fallback grouping and concurrent-creator
    convergence — which also gives the windowed engine's claim-protocol
    suite its first workflow. An all-skipped run is refused rather than
    green.

  • The verdict model matrix. run_model_matrix.py --task verdict
    grades the balanced corpus per model through run_verdict_eval.py
    (still a thin collator defining no scoring), records each row's
    configuration — pins, endpoint, prompt provenance, corpus digest,
    provenance status, serving models — and the runtime in
    comparable-but-unidentifying terms, and renders the constant-answer
    baselines beside the table. The committed verdict scorecard gained the
    deterministic-fallback baseline row: balanced accuracy exactly at the
    0.250 constant floor, zero of 36 malicious cases caught — the rules
    floor every measured model is read against.

  • An annotated unsupported-claim measurement. Sixteen hand-annotated
    (rationale, evidence, label) cases measure score_claim_support with an
    exactly-pinned confusion matrix (TP 7, FP 0, FN 1, TN 8), a recorded
    known miss that must stay missed until the pins move with it, and the
    annotation-independence limit stated where the number lives. The
    measurement found and drove a real detector fix: the claim pattern
    recognised "scheduled scan" while the support keys did not, so an alert
    whose evidence recorded scheduled_scan could state the claim and still
    be refused support.

  • Agentmetry ingest template: AI coding-agent activity as an input
    (GitHub Discussion #1577). Agentmetry
    records the tool calls Claude Code, Cursor, Codex and Copilot make through
    their own hooks and correlates them into ATT&CK/ATLAS-mapped detections;
    its webhook sink posts one canonical JSON event per request, routine
    activity and detections down one URL. The discussion asked whether
    generic-json "should land" them — checked against the real template with
    real captured events: they land, but severity flattens to Unknown on every
    event including critical detections (generic-json reads a top-level
    severity; theirs lives at action.outcome / detection.severity),
    every routine tool call is promoted to an alert (class 2001 is always
    promoted — one busy agent is a queue flood), and titles, technique ids and
    the event timestamp are lost. The bundled agentmetry template fixes
    exactly that: the template language gains class_field/class_map (one
    URL, per-event-kind class: tool calls stay 6003 API Activity in the lake,
    action.type: detection routes to 2001 Security Finding, always promoted)
    and technique_fields (vendor-supplied ATT&CK ids carried as supplied
    through the same mitre_attck block the connector path emits, shared via
    the newly exported EnrichTechniqueIDs/TechniqueIDsFromPaths; ATLAS
    AML.T**** ids are preserved in evidence fields and refused from
    ATT&CK-typed ones, matching Agentmetry's own warning). The five-tier
    severity ladder survives end to end — critical maps to its own tier,
    never high. Agentmetry-observed coding agents feed the AI-estate
    inventory and graph (agentmetry joins the ai-runtime/ai-finding
    source-type branch in both consumers), the template is mintable from the
    console catalog, and the docs page states plainly what was verified
    against captured events versus what remains unexercised live. Also fixes
    the ai-estate.md table row that claimed ai-finding is class 6003 —
    the template is and was 2001.

  • Asset criticality reaches fusion confidence and action approval —
    tighten-only.
    Fusion stamps what each alert landed on at fuse time
    (enrichments["asset_context"], resolved from the tenant's own assets
    inventory by name/FQDN/IP with the highest criticality winning) and the
    confidence scorer gains a bounded asset_criticality factor (weight 0.06,
    weights rebalanced to keep summing to 1.0) — an absent stamp contributes
    exactly 0.0 and the rationale row says unknown (no inventory match)
    rather than being omitted, because "we do not know what this host is" is a
    fact an analyst needs. On the response side, evaluate_contract grades a
    verb one impact rank higher when the target asset is critical/high in the
    tenant's inventory — never lower, with two deliberate non-raises:
    READ_ONLY stays READ_ONLY (a read against a domain controller is still a
    read) and SEVERE never becomes IRREVERSIBLE (context can demand a human,
    it cannot make an undoable action undoable). The positive control is the
    point: unknown criticality grades every verb identically, so a deployment
    with no CMDB loses nothing. Both approval doors thread the resolution;
    the console's vendored copy stays byte-identical.

  • Business context reaches the triage model, and impact statements must
    cite it.
    The business-context applier's result used to die in the
    worker's local variable: a tenant set_severity mutated the alert
    silently, the model explained a severity nobody chose in the evidence,
    and the persisted verdict carried no record of which rules fired. The
    triage prompt now names the rules that fired and the mutation they made;
    the verdict may carry one optional business_impact sentence whose
    citations must resolve to those rule ids (uncited or fabricated elements
    are dropped and counted, never rendered — and the instruction is injected
    only when context exists, so an uninformed model never sees the element's
    name); both travel to the ledger event and surface in the Investigation
    Rail's new Business context section, which renders nothing when no
    rule fired. Rule ids join the injection-scanned prompt channels.

  • Asset criticality orders the investigation queue. The priority scorer
    (severity as base, asset criticality / exploited-vulnerability / internet
    exposure as bounded multipliers) existed with zero callers, so two alerts
    of equal severity — one on a domain controller, one on a meeting-room
    display — arrived in the order they were raised. build_queue now
    resolves each page against the tenant's own assets inventory in two
    batched queries (hostname/FQDN/IP-overlap match; is_exploited AND NOT remediated as the KEV signal) and orders within (bucket, SLA) by the
    score. Every row carries priority_score and a factor-by-factor
    priority_basis; no inventory means severity alone, and the basis says
    so. Proven on live Postgres with a negative control that removes the
    resolver and watches the order collapse.

  • An opt-in ack-clock escalation bridge (services/escalations, compose
    profile escalations; externally contributed). The native outbound-webhook
    subsystem is complete but its enqueue_event has no production callers, so
    a CRITICAL case nobody acknowledged produced no notification at all. The
    bridge watches aisoc_cases.acknowledged_at: unacknowledged CRITICAL cases
    get a Teams card after a grace period with capped re-nags, unacknowledged
    HIGH cases arrive as one daily digest with a pipeline-health line, and
    dedup/nag state survives restarts. Deployment-wide by design (one
    operator-configured webhook; per-tenant routing stays the native
    subsystem's job, into which this retires when the emitters are wired) and
    the webhook must be https. The sweep's cross-tenant reads are recorded on
    the tenant-predicate ratchet with that reasoning, the service is
    SLO-exempt with its reason, and its status filter is pinned against the
    case table's own CHECK constraint by test.

Changed

  • A missing identifier narrows the correlation group instead of widening
    it.
    Neither detection engine has ever emitted mitre_tactics (the
    ruleset carries technique ids only), so every detection alert's
    correlation key fell back to the literal unknown tactic and all of an
    entity's detection alerts collapsed into one incident per window;
    entity-less alerts merged tenant-wide. The fallback now scopes to the
    detection rule with an entity, never groups across rules without one,
    and groups only exact fingerprint repeats with no identifiers at all.
    Measured on a fixed detection-shaped cohort: 42.6 % reduction with 36
    cross-rule incident groups before, 24.5 % with zero after — the honest
    direction, because the difference was unrelated alerts counted as
    grouped. The tactic-carrying headline cohort is unchanged at 33.3 %. The
    correlator's creation race is closed with SET NX (concurrent first
    arrivals converge on one incident instead of orphaning an alert); the
    narrower merge/merge window is documented, not closed.

  • Verdict scorecards disclose the model-knowledge limit. Evidence
    cutoffs constrain supplied context only; each scorecard now records the
    model's training cutoff when the operator states one and unknown
    otherwise, with the disclosure that pretrained weights may postdate the
    replayed events.

Fixed

  • The rail's business-context leg reads the event the writer actually
    writes.
    Its query filtered kind LIKE 'auto_triage:%' while the ledger's
    verdict INSERT hardcodes kind = 'triage_verdict' (the auto_triage:{tier}
    string lives in the agent column) — so the section could never render on
    a real deployment while every suite stayed green, because the live test had
    seeded the assumed shape rather than the writer's. Found by the end-to-end
    QA slice (console rule row -> real worker -> real ledger -> rail), fixed,
    and the literal is now pinned on both sides: the writer's recording-pool
    test asserts the executed INSERT carries 'triage_verdict' and the
    business keys land in the payload, and the reader's live test seeds that
    same literal.

  • The context graph's business dimension authenticates. The bundle's
    incident-context fetch sent X-Tenant-ID (a header the API's auth never
    reads) with an optional AISOC_AGENTS_API_KEY bearer that no compose
    file, .env.example or Helm value delivers — so the graph's
    identity/asset/cloud/business/threat dimensions answered 401 on every
    default deployment and the prompt said "unavailable" forever. It now
    presents AISOC_AGENTS_SERVICE_TOKEN (or AISOC_SERVICE_TOKEN) with
    X-AiSOC-Tenant-ID, and an unconfigured token refuses without dialing so
    the dimension reads unavailable, never empty. A vestigial copy of the
    dead credential block was removed from the investigation tool client.

  • The rule builder could only author rules that never match. Every
    field it suggested was prefixed alert. against engines that resolve
    plain dotted paths (hostname, severity, raw_event.*), its starter
    rule used an invented target/time structure, it offered a neq
    operator neither engine implements (the term is ne) — and the preview
    endpoint manufactured a nested alert.{target,time} shape so the
    broken paths previewed as working and then never fired in production.
    The suggestion list is now the fused alert's real vocabulary, pinned on
    both sides (a web test and an api test naming the same literals), and
    the preview sample is shaped exactly like the alert the worker evaluates
    (raw_event under its real key, not metadata).

Security

  • Dependencies: the Oct-31 minimatch / glob advisory wave worked
    across all three npm lockfiles (root pnpm, apps/mobile pnpm,
    services/realtime npm). Both overrides are range-scoped to >=9.0.0,
    which is the crux of the fix rather than a detail: minimatch floors at
    11.0.1 (resolves 11.0.3) for
    GHSA-g3hv-75wg-m249
    (CVE-2026-118764, medium — globparts-amplification ReDoS) and glob at
    14.0.2 (resolves 14.0.3) for
    GHSA-3g3m-2fjr-6pw4
    (medium) — but the legacy glob@7.2.3 (not vulnerable; <9) and its
    minimatch@3.1.5 are left in place, because glob@7 + minimatch@11 is
    broken (a blanket minimatch floor forced that pairing and broke
    react-native 0.81's iOS codegen with a charAt TypeError). The
    still-<11 minimatch copies that remain (3.1.5, 5.1.9) are all
    lint/build tooling — eslint 9 and its plugins, @redocly/openapi-core,
    glob@7 via react-native/expo codegen, test-exclude, serve-handler —
    none shipped in a runtime bundle, none fed attacker-controlled patterns,
    and none upgradable without breaking the toolchain that pins them;
    tracked for a tolerable-risk dismissal pending upstream support. Verified:
    all three frozen installs pass and services/realtime (the one runtime
    service touched) builds clean on minimatch@11.

  • Dependencies: the Oct-30 npm advisory wave cleared across both
    lockfiles. Root: follow-redirects floored at 1.16.2
    (GHSA-mrwp-f3q8-3g9p,
    high) and joi at 17.13.9
    (GHSA-4528-x5fx-h6hg,
    high) — both already carried overrides from earlier waves, so the existing
    floors move rather than new keys landing (a duplicate key in
    pnpm.overrides is silently discarded by last-key-wins, which is exactly
    how a first attempt at this fix produced an unchanged lockfile). Mobile:
    node-forge overridden to 1.4.1+ (resolves 1.4.2), clearing five high
    and three low advisories
    (GHSA-7wch-p9px-956x
    et al., all fixed in 1.4.1). Published after main's last audit pass, so
    they reddened the push-event Security Audit at the v20.4.0 release
    commit through no change of its own.