Skip to content

AiSOC v7.4.0

Latest

Choose a tag to compare

@github-actions github-actions released this 30 May 03:17
· 43 commits to main since this release
f77e9a6

Security-hardening and platform release. Folds in the May 27–29 hardening wave,
multi-agent routing, and multi-cloud infrastructure skeletons that landed on
main since v7.3.1.

Highlights

  • Security hardening. Prompt-injection sanitizer wired into the
    classification agents (PR #219);
    cross-tenant isolation enforced on the detection-loop suggestion lookups
    (PR #221) and on the compliance,
    phishing, and knowledge-base endpoints
    (PR #236); nightly cross-tenant
    RBAC regression gate (PR #197);
    cryptography CVEs cleared and unfixable advisories time-boxed
    (PR #229); CodeQL quality notes
    resolved (PR #224).
  • Multi-agent routing. DetectAgent.process wired to the FusionEngine
    over cross-service HTTP (PR #198);
    /investigate swapped to the RouterOrchestrator behind the
    ROUTER_INVESTIGATE flag (PR #196);
    Redis-backed scheduler singleton guard for in-process workers
    (PR #218).
  • Multi-cloud infrastructure. Serverless-container Terraform skeletons for
    GCP (Cloud Run + Cloud SQL + Memorystore) and Azure (Container Apps +
    PostgreSQL Flexible Server + Cache for Redis), mirroring the AWS/EKS reference
    file-for-file (PR #240).
  • Live dashboard & landing. Real /metrics data restored on
    tryaisoc.com/dashboard (PR #192);
    API/agents machines kept warm so the dashboard no longer 500s
    (PR #234); seed timestamps
    re-anchored so the live dashboard never goes empty
    (PR #235); landing CTAs pointed at
    the live dashboard (PR #233).
  • Dependency & CI maintenance. ~40 Dependabot upgrades across the Python,
    JS, and Go services plus CI stabilization (Ruff cleanup, OpenAPI export
    permissions, pnpm-lock dedupe).

Bump @vitejs/plugin-react 4.7.0 → 6.0.2 in apps/web

Dev-only dependency upgrade (PR #178).
@vitejs/plugin-react@6 is built against vite@8, while vitest@4 (landed in
PR #179) still ships its own internal vite@7. pnpm resolves both side-by-side
without conflict: vitest@4 uses vite@7 for the test runtime, and react() is
loaded from the vite@8-flavoured build of the plugin. Vitest is tolerant of
the plugin API surface across vite 5/6/7/8, so apps/web/vitest.config.ts
needed no further changes after the cast we already removed in #179.

No production code touched. Locally verified: web 349/349 tests pass, lint
remains at 0 errors / 76 warnings (unchanged baseline), tsc --noEmit clean,
production build succeeds.

Bump vitest 2.1.9 → 4.1.6 across the workspace

Dev-only dependency upgrade (PR #179)
across apps/web, packages/sdk-ts, and services/mcp. Vitest v3 and v4
introduced two breaking changes that surfaced in our suite:

  • vitest/config no longer exports UserConfig. apps/web/vitest.config.ts
    used import('vitest/config').UserConfig['plugins'] to bridge the vitest@2
    (vite@5 types) ↔ @vitejs/plugin-react@4 (vite@7 types) version mismatch. In
    vitest@4 both packages target vite@7, so the bridging cast is gone and
    react() is consumed directly.
  • global is no longer in the default DOM lib in @vitest/runner's typing.
    packages/sdk-ts/src/client.test.ts referenced the Node global namespace via
    (global.fetch as ...); it now uses globalThis.fetch, which is the
    cross-runtime idiom and was already what every other test in the SDK suite
    used. No runtime behaviour change — global === globalThis in Node.

Verified locally: SDK 9/9 tests pass, web 349/349 tests pass, web lint stays at
0 errors (warning count unchanged from PR #193's baseline). No production code
touched, no behavioural change to the published @aisoc/sdk package or to the
shipped web bundle.

Wire DetectAgent.process to FusionEngine via cross-service HTTP (Issue #190)

Closes #190.

Closes the missing edge in the four-agent façade: DetectAgent previously
self-described as the public detection surface but had no synchronous entry
point into the fusion pipeline — callers either had to enqueue onto Kafka and
wait, or reach into services/fusion internals directly. This change adds the
last mile so a raw alert from any caller (LLM tool calls, ad-hoc CLI, the API
gateway) runs through the same FusionEngine instance that backs the Kafka
consumer path — dedup, correlation, ML scoring, confidence labelling, and RBA
all apply identically regardless of how the alert arrived.

Three additive pieces, no behavioural changes to existing paths:

  • POST /process on the fusion service
    (services/fusion/app/api/router.py). Accepts a RawAlert, returns a
    FusedAlert, and is wired to the already-running FusionWorker's engine
    instance via the module-level _worker_ref the worker registers on startup.
    Returns 503 when the worker hasn't finished booting (Kafka consumer not
    yet attached) so callers fail loudly instead of getting a half-initialised
    pipeline. Lives at the root path — the router is mounted with no prefix in
    services/fusion/app/main.py.
  • services/agents/app/tools/fusion.py — thin async HTTP client used by
    the agents service. Posts to {FUSION_SERVICE_URL}/process (defaults to
    http://fusion:8003/process inside the docker-compose network), forwards
    an optional bearer token, and raises on any non-2xx or transport error.
    This is a deliberate contrast with app.tools.graph, which degrades
    gracefully for investigation queries: fusion is the primary detection
    plane, so a silent fallback here would lose alerts.
  • DetectAgent.process(raw_alert, *, api_token=None)
    (services/agents/app/agents/__init__.py). Classmethod delegate over the
    HTTP client — keeps DetectAgent import-light (no engine instantiation in
    the agents process) and preserves the existing back-compat aliases.

Tests lock the contract on both sides. services/fusion/tests/test_process_endpoint.py
exercises the endpoint against an ASGITransport + AsyncClient: novel
alerts return a NEW_INCIDENT envelope, replays return DUPLICATE, an
unwired worker yields 503, a worker without an engine yields 503,
malformed and bad-severity payloads return 422, and a regression guard
asserts the endpoint and worker share the same FusionEngine instance.
services/agents/tests/test_fusion_client.py uses respx to lock the
client wiring: it must post to /process (not /api/fusion/process — that
mismatch was caught and fixed during initial wiring), the Authorization
header is set if and only if a token is supplied, httpx.HTTPStatusError
propagates on 503/422, and httpx.HTTPError propagates on transport
failures. A final trio of tests pins DetectAgent.process as a faithful
delegate to the client (args pass through unchanged, errors propagate, no
swallowed exceptions).

No feature flag and no env gate: the wiring is purely additive — no existing
caller of the fusion service or the agents service changes shape, and the new
endpoint/method only fire when something explicitly invokes them.

Cross-tenant RBAC regression suite (F013, security)

Closes #159.

Pure-unit isolation suites that exercise the tenant boundary at the
endpoint-function level (no live DB, no FastAPI request cycle) so the
contract is testable in milliseconds and survives ORM churn:

  • services/api/tests/test_threat_intel_tenant_isolation.py — IOC,
    actor, and feed list/get/create/delete are scoped by tenant_id,
    cross-tenant lookups resolve to 404, and writes attach
    current_user.tenant_id even when the payload smuggles a different
    one.
  • services/api/tests/test_alerts_tenant_isolation.py — every
    read/write/queue/claim path on /alerts binds tenant_id into the
    compiled SQL or forwards it to the service layer
    (build_queue / claim_alert).
  • services/api/tests/test_llm_credentials_tenant_isolation.py
    BYOK credential GET/PUT/DELETE scope by tenant_id, new rows bind
    the caller's tenant, and emit_audit is invoked with the caller's
    tenant + actor (CredentialVault is stubbed so the assertions are
    on the persistence boundary, not crypto).

Assertions read the compiled SQL bind parameters rather than the
shape of any single query so they don't break on benign rewrites. All
three suites were mutation-tested by temporarily dropping the
tenant_id predicate in the corresponding endpoint — every dropped
predicate produced at least one failing test, confirming the suites
are wired to the right surface.

.github/workflows/cross-tenant-rbac.yml runs the three suites
nightly on main (06:30 UTC, ahead of compose-smoke-nightly so a
tenant boundary regression shows up as the first nightly signal) and
on-demand via workflow_dispatch. On failure it uploads a JUnit
report and opens a security-labelled tracking issue.

Attack-chain timeline UI (T3.3, v8.0)

/cases/{id} now ships an Attack Chain tab that visualises the ranked
timeline returned by /v1/cases/{id}/attack-chain (shipped earlier under
8df637b9). The new AttackChainPanel in
apps/web/src/components/cases/CaseWorkspace.tsx:

  • Window selector with the same vocabulary as the backend WindowLiteral
    (1h, 6h, 24h, 72h, 7d, 30d) — selection is deep-linkable via
    ?window=… and survives reload.
  • One card per ChainLink with the alert title, severity chip (driven by
    the canonical 5-tier ladder info | low | medium | high | critical),
    confidence percent, MITRE technique IDs, and the deterministic narrative
    reason emitted by services/api/app/services/attack_chain.py.
  • Entity-graph summary panel — node count grouped by kind (user,
    asset, process, ip, domain, alert), top edges, and a per-node
    severity chip when present in _entity_graph_payload.
  • SWR-keyed on (case_id, window) with skeleton, error, and empty states
    that match the rest of the case workspace.
  • New casesApi.getAttackChain method + AttackChainTimeline,
    AttackChainWindow, AttackChainLink, AttackChainEntityNode,
    AttackChainEntityEdge, BackendAttackChainResponse types in
    apps/web/src/lib/api.ts. The wire format matches the backend to_dict
    shape exactly (node kind rather than type; optional severity and
    event_time from _entity_graph_payload).
  • Coverage in apps/web/src/components/cases/CaseWorkspace.test.tsx:
    empty-state, error-state, and three data-rendering assertions
    (alert titles, confidence percent, MITRE techniques). The SWR mock is now
    key-aware so attack-chain and attack-path fetches stay isolated, and
    useSearchParams is stateful so window-selection deep-links round-trip
    cleanly under test. The WindowSelector is a labelled
    role="group" of buttons with aria-pressed, so deep-link assertions
    resolve the active option via the single pressed button inside the
    group rather than a non-existent <select> value.

Closes the UI side of T3.3 in AISOC_V8_PROGRESS.md. Pre-existing
non-blocking lint warnings in CaseWorkspace.tsx are unchanged by this
diff.

LLM input contract — static regression gate (T2.3, v8.0)

Closes T2.3 by adding the missing bypass-prevention layer on top of the
existing fail-closed validator (services/agents/app/llm/contract.py). Two
new test files in services/agents/tests/:

  • test_llm_contract_extra.py (10 cases) — fills the coverage gaps in the
    shipped contract: safe_astream validates messages exactly once and
    refuses to yield any chunk on violation; make_safe_chat_model proxies
    non-LLM attributes through but routes ainvoke / astream through
    validation; classify_message rejects api_key = '...' assignments and
    PEM private-key headers; set_contract_enforcement(False) lets raw OCSF
    through in soft mode and re-arms cleanly when flipped back to True.
  • test_llm_contract_no_bypass.py (3 cases) — AST-based static gate
    that walks every *.py file under services/agents/app/ and fails CI on
    any direct .ainvoke(...) / .astream(...) call whose receiver is not on
    an explicit allowlist (_graph, investigation_graph, graph — all
    LangGraph control-flow handles, not LLMs) or whose file is not the
    contract module itself. Ships with self-tests proving (a) a synthetic
    llm.ainvoke(...) bypass trips the detector and (b) allowlisted
    receivers do not. Adding a new agent that calls a chat model directly
    now fails the build until it routes through safe_ainvoke /
    safe_astream / make_safe_chat_model.

The survey behind this gate confirmed every existing direct chat-model call
under services/agents/app/ already goes through the safe wrapper — the
remaining .ainvoke / .astream call sites are LangGraph control-flow on
compiled graphs, which is why those receivers are explicitly allowlisted
rather than silently ignored.

LLM input contract — CI tests (T2.3, v8.0)

services/agents/tests/test_llm_contract.py exercises classify_message /
LLMInputContract.validate / validate_messages: raw OCSF-shaped JSON in a
user message fails closed when AISOC_AGENTS_LLM_CONTRACT_ENFORCED=1
(default), and prose plus summarize_structure_for_llm output passes. Tests
use {"role", "content"} dict messages so they run without importing
langchain_core (the contract already coerces LangChain BaseMessage and
dicts the same way).

Real-time graph-update WebSocket (T1.4, v8.0)

Closes the v8.0 loop between the ingest-side graph writer (T1.1) and the
operator console. services/realtime now exposes a graph WebSocket
channel reachable at /ws/graph (or piggy-backed on /ws/all) and runs a
dedicated aisoc-realtime-graph Kafka consumer group against the
security.graph_updates topic that the Go ingest writer publishes to
(services/ingest/internal/graph/writer.go). Each GraphUpdate envelope
(entity_id, change_type, ts, label, rel_type, from, to,
properties, schema_version) is fanned out to clients scoped by
tenant_id, with default as the single-tenant fallback so self-hosted
deploys without explicit tenant tagging still light up live. The new
consumer is wired alongside the existing fused-alerts consumer in
non-blocking mode: a missing or unreachable graph topic logs at warn and
never blocks the higher-priority alerts/cases/agents/insights fan-out. The
topic name honours both AISOC_GRAPH_UPDATES_TOPIC and
KAFKA_TOPIC_GRAPH_UPDATES envs (defaults to security.graph_updates so
it matches the Go writer's default in
services/ingest/internal/config/config.go without manual plumbing), and
setting it to the empty string disables the consumer entirely for tests
that don't spin up Kafka graph traffic. The Investigation Rail and Attack
Chain views (T3.3 UI, in flight) can subscribe today and pick up node /
edge mutations within ~1s of the upstream event reaching ingest.

Public weekly benchmark scoreboard at /docs/benchmark-scoreboard

Public, append-only weekly scoreboard now lives at
/docs/benchmark-scoreboard.
One row per published eval run — date, agent version, commit SHA, MITRE
accuracy, MTC p50/p95, total USD, total tokens — sourced from a
checked-in JSON file at apps/docs/static/data/scoreboard.json and
validated against scoreboard.schema.json on every docs build via the new
pnpm --filter @aisoc/docs scoreboard:check script. Substrate rows
(deterministic CI gate, no LLM) are visually separated from wet-eval rows
(real LangGraph agent, real LLM, real cost), so substrate numbers can
never be quoted as live agent performance. Includes an inline SSR-rendered
SVG sparkline of MITRE accuracy over time, no Recharts/client JS bundle
hit. The marketing /benchmark page now cross-links to the scoreboard for
the full weekly history. Wet-eval rows arrive automatically once the T5.5
weekly CI workflow lands.

Connectors — Wazuh Indexer ingest (Stage 2)

New first-class endpoint connector for Wazuh deployments. AiSOC now polls the
Wazuh Indexer API directly (no agent rewrite required) and normalizes alerts
into the platform's OCSF-aligned schema, collapsing Wazuh's native severity
ladder into the four-tier info | low | medium | high set used everywhere
else.

  • services/connectors/app/connectors/wazuh.pyWazuhConnector
    subclasses BaseConnector, polls wazuh-alerts-* indices over HTTPX with
    basic-auth, paginates time-windowed queries, retries on 5xx with capped
    backoff, and emits one normalized event per alert hit. Cursor is the
    highest @timestamp seen so reruns are idempotent.
  • services/connectors/app/connectors/__init__.py — registered in
    _CONNECTOR_CLASSES; the registry now declares 52 first-party connectors.
  • plugins/wazuh/plugin.yaml + pnpm marketplace:sync — connector ships
    as a marketplace entry under category siem, mirrored into
    apps/web/public/marketplace/index.json.
  • apps/docs/docs/connectors/wazuh.md + sidebar entry — operator setup
    walkthrough (API user + role, time-window semantics, severity collapse
    table, troubleshooting matrix).
  • services/connectors/tests/test_wazuh_connector.py — 24 unit tests
    cover schema, auth headers, time-window query shape, retry policy, every
    documented severity bucket, and the empty/error paths.

CLI — aisoc plugin new per-type templates

Replaces the old hard-coded plugin scaffold with a real templated generator
keyed on plugin kind (enricher | connector | responder | detection | widget).
Templates ship inside the aisoc-cli wheel via importlib.resources so the
CLI works unchanged after pip install aisoc-cli.

  • packages/aisoc-cli/src/aisoc_cli/main.pyaisoc plugin new <NAME> --type <kind> loads the template tree from
    src/aisoc_cli/templates/<kind>/, runs string.Template substitution for
    ${slug}, ${name}, ${author}, and writes a project that already
    validates against the manifest schema. aisoc plugin scaffold is preserved
    as an alias for backwards compatibility.
  • pyproject.tomlforce-include ships the templates tree in the wheel.
  • Tests parameterize across all five plugin types and assert the manifest
    validates and no ${...} placeholders leak through.
  • plugins/templates/README.md is now a pointer to the canonical templates
    inside the CLI package.
  • apps/docs/docs/plugins/cli.md — documents the new CLI surface and is
    added to the Plugin SDK sidebar.

Infrastructure — GCP Cloud Run + Cloud SQL Terraform skeleton

Adds a serverless-first BYOC equivalent of the existing AWS module so AiSOC
can be stood up on Google Cloud with one terraform apply. Stage 2 #15.

  • infra/terraform/gcp/ — Cloud Run for api/web/ingest, Cloud SQL
    Postgres 16 + Memorystore Redis 7.2 on private IPs through a dedicated VPC
    and Serverless VPC Access connector, Secret Manager for every credential
    (auto-generated postgres_password, secret_key, credential_key,
    redis_auth, optional openai_api_key), and Artifact Registry for images.
    One service account per Cloud Run service with least-privilege
    secretAccessor bindings. The skeleton points at the public GHCR demo
    images so a fresh apply works zero-config; operators override via
    api_image / web_image / ingest_image.
  • apps/docs/docs/deployment/gcp.md + sidebar entry (between kubernetes
    and env-vars) — quickstart, state-backend guidance, Cloud SQL Auth Proxy
    notes, cost envelope, and the long-running-services follow-up plan (GKE
    Autopilot for agents, realtime, connectors, alert-fusion,
    threatintel, fusion).
  • infra/terraform/gcp/README.md mirrors the deploy doc for module-local
    consumption.

Live Actions — generic vendor/capability dispatcher (Stage 2 #8)

Adds a vendor-pluggable response-action surface so plugins can register
executors against the existing capability taxonomy without forking the
in-tree executor list. The dispatcher always returns a typed
LiveActionResult; unknown (vendor_id, capability) pairs return FAILED
with error="executor_not_found" so the agent degrades gracefully instead
of seeing a 500.

  • services/actions/app/live_actions/models.py
    LiveActionRequest/Result/Descriptor Pydantic models (UTC-aware).
  • services/actions/app/live_actions/registry.pyLiveActionExecutor
    ABC + module-level LiveActionRegistry.
  • services/actions/app/live_actions/dispatcher.py — structured logging,
    error translation, dry-run + missing-credential semantics
    (SIMULATED, never PARTIAL).
  • Adapters wrap every existing in-tree executor (CrowdStrike, Okta, AWS SG,
    Splunk) so they now show up as builtin descriptors.
  • services/api/app/api/v1/endpoints/live_actions.pydiscover,
    dispatch, dry-run REST routes; built-ins are registered at app startup.
  • 45 new tests across models / registry / dispatcher / router / builtins
    (full actions suite: 99 passed).
  • apps/docs/docs/concepts/live-actions.md + sidebar slot.
  • Drive-by: fixed two pre-existing broken doc links flagged by the
    Docusaurus build (osctrl → aisoc-direct stub, air-gappedenv-vars).

Agents — deterministic NL→ES|QL translator + 50-pair eval set (Stage 2 #16)

Replaces the template fallback in
services/api/app/api/v1/endpoints/nl_query.py with a real, offline-friendly,
deterministic IR + renderer that emits ES|QL, KQL, and SPL and runs every
output through a lightweight grammar validator before returning. An optional
LLM enhancement path (gpt-4o-mini) is exposed via enhance_with_llm for
callers with credentials; failures fall back to the deterministic path so the
air-gapped story keeps working and the eval harness stays reproducible.

  • services/agents/app/nl_query/ — IR, grammar, translator, renderers.
  • All # TODO: translate comments removed from nl_query.py.
  • services/agents/tests/eval_data/nl_query_eval.json — 50-pair gold
    NL→ES|QL eval set.
  • services/agents/tests/test_nl_query_eval.py — 100% syntactic validity,
    100% semantic match (50/50 perfect) against gold intents.
  • Pre-existing services/agents tests still green (162 passed) when ignoring
    the asyncpg-dependent suites that fail on a fresh checkout.

Connectors — auditd file_tail + AiSOC audit.rules profile

Replaces the host-agent dependency for Linux endpoint visibility with a
file-tail connector that consumes audit.log directly, plus an opinionated
auditctl ruleset whose -k keys map 1:1 to detection rules.

  • services/connectors/app/connectors/auditd.pyAuditdConnector tails
    /var/log/audit/audit.log, reassembles multi-record events by msg id,
    decodes hex proctitle/argv blobs, and normalizes via
    _severity_from_event using aisoc_* keys baked into the audit rules
    profile. Cursor is (inode, byte_offset) so log rotation is handled.
  • profiles/auditd/aisoc.rules + profiles/auditd/README.md — ships an
    opinionated auditctl ruleset and documents install + reload.
  • detections/ — 4 new detection rules pivot off auditd_key for
    sudoers / SSH config tampering, kernel module load, and systemd
    persistence. No host-agent dependency.
  • plugins/auditd/plugin.yaml + pnpm marketplace:sync — registers the
    connector in the public marketplace.
  • apps/docs/docs/connectors/auditd.md + sidebar entry — setup doc.
  • services/connectors/tests/test_auditd_connector.py — covers schema,
    hex decode, argv reassembly, multi-record merge, severity heuristic, and
    file tailing (full connectors suite: 444 passed, excluding the
    apscheduler dev-dep test_scheduler.py).

Documentation — operator notifications & plugin lifecycle

Two new operator-facing docs pages, both registered in the Docusaurus sidebar:

  • apps/docs/docs/operations/notifications.md — complete inventory of
    every notification surface in AiSOC: Web Push to the responder PWA (VAPID,
    Redis, topic routing), Slack ChatOps via /aisoc, Slack/Teams ChatOps
    verification, one-shot notify_slack from playbooks, create_ticket
    simulation + recommended plugin path, honeytoken first-touch webhooks,
    connector freshness alerts, on-call gating, suppression / quiet-hours, and
    a per-mechanism testing recipe.
  • apps/docs/docs/plugins/lifecycle.md — operator's view of plugin
    states (Discovered → Loaded → Enabled/Disabled, plus signature_status),
    trust modes (strict | warn | disabled), filesystem + OCI discovery, the
    full operator REST API with required permissions, configuration reference,
    upgrade and rollback semantics, and the structlog events worth alerting on.

Both pages cross-link the existing concepts/live-actions, plugins/overview,
plugins/publishing, and plugins/cli pages so they sit in the right place
in the information architecture.

API — blameless case post-mortem endpoint

Mirrors the existing case auto-summary pipeline to produce a deterministic,
blameless retrospective for any case.

  • services/api/app/services/case_postmortem.py — pure builder + async
    DB orchestrator (build_case_postmortem). Reuses SummaryCaseRow /
    SummaryCommentRow / SummaryTaskRow fetchers from case_summary so the
    post-mortem and the live summary draw from the same source of truth.
    Output is a Pydantic CasePostmortem covering incident overview,
    contributing factors, detection timing/gaps, response phases (detect →
    contain → eradicate → recover), blast radius, what went well / what fell
    short, and concrete action items.
  • services/api/app/services/case_postmortem_html.py — pure HTML
    renderer matching the summary renderer (inline CSS, print-friendly,
    defensive escaping, no external assets).
  • services/api/app/api/v1/endpoints/cases.py
    GET /api/v1/cases/{case_id}/postmortem with ?format=json|html.
  • services/api/tests/test_case_postmortem.py — pure-builder + HTML
    tests including XSS escaping, deterministic ordering, and explicit
    blamelessness assertions (analyst handles must not surface in the
    narrative; the assignee header line is explicitly allow-listed).
  • apps/docs/docs/operations/case-reports.md + sidebar — operator page
    covering both /summary and /postmortem with audience, output,
    automation, and runbook archive guidance. Cases summary breadcrumb now
    points operators at both endpoints.

Threat Intelligence — STIX → MISP push (Stage 3 #20)

The threat-intel pipeline already pulled events from MISP (read-only). This
closes the loop with a write path: every STIX 2.1 indicator or bundle
published through /api/v1/threatintel/stix/... can be mirrored into the
configured MISP instance as a native event with one or more attributes.

  • services/api/app/services/misp_push.py
    • Pure mappers: parse_stix_pattern, stix_indicator_to_misp_attribute,
      stix_bundle_to_misp_event, confidence_to_threat_level. Covers
      ipv4/ipv6, domain-name, url, email-addr, file:hashes
      (MD5/SHA-1/SHA-256/SHA-512) and file:name. Untranslatable patterns
      are counted in skipped_attributes, never silently dropped.
    • MispPushClient — async httpx wrapper for /users/view/me (health),
      /events/add (push), /events/view/{id} (read-back). Every call runs
      through the air-gap gate (enforce_airgap_for_url) first.
  • services/api/app/api/v1/endpoints/stix_taxii.py
    • POST /stix/indicators?push_to_misp=true — response now includes a
      misp block (pushed, misp_event_id, misp_event_uuid, url,
      pushed_attributes, skipped_attributes, error).
    • POST /stix/bundles?push_to_misp=true — same, but the whole bundle
      becomes one MISP event.
    • GET /stix/misp/health — calls MISP /users/view/me, never echoes the
      API key back.
    • POST /stix/misp/dry-run — returns the exact MISP event payload AiSOC
      would send, plus an airgap_blocked flag for air-gapped audits.
    • Push failures are intentionally non-fatal: the AiSOC store is the source
      of truth, the MISP mirror is best-effort and surfaces the structured
      error on the same response.
  • services/api/app/core/config.py — new MISP push settings:
    MISP_VERIFY_SSL, MISP_PUSH_AUTO, MISP_PUSH_DEFAULT_DISTRIBUTION,
    MISP_PUSH_DEFAULT_THREAT_LEVEL, MISP_PUSH_DEFAULT_ANALYSIS,
    MISP_PUSH_TIMEOUT_SECONDS. Existing MISP_URL / MISP_API_KEY are
    reused from the read path.
  • services/api/tests/test_misp_push.py — 76 tests covering pure
    mappers, air-gap gating, MISP HTTP failures (401 / 5xx / timeout), the
    publish endpoints with and without push, the health probe, and the
    dry-run endpoint.
  • apps/docs/docs/integrations/misp-push.md + sidebar entry — operator
    doc with config, endpoints, the STIX→MISP type table, failure modes, and
    the dry-run-as-air-gap-proof workflow.
  • apps/docs/docs/operations/airgap.md — clarifies that the existing
    MISP_URL / MISP_API_KEY envs cover both pull and push, with a pointer
    to the new integration page.

Security — MSSP RBAC hardening on /threat-intel (Issue F013)

The /v1/threat-intel/* endpoints (IOCs, threat actors, intel feeds) were
previously gated only by get_current_user, meaning any authenticated
role
, including viewer and soc_analyst, could POST an IOC, DELETE
a feed, or create a new ThreatActor profile. In a managed-SOC / MSSP
deployment that is a privilege-escalation vector: a compromised analyst
seat can poison detections across the whole tenant by injecting false IOCs
or deleting the feed that hydrates them.

  • services/api/app/api/v1/endpoints/threat_intel.py — every route now
    declares the explicit permission it needs via
    Depends(require_permission("threat_intel:read" | "threat_intel:write")).
    Read routes (GET /iocs, /iocs/{id}, /actors, /feeds) require
    threat_intel:read; write routes (POST /iocs, DELETE /iocs/{id},
    POST /actors, POST /feeds, DELETE /feeds/{id}) require
    threat_intel:write. The legacy User-typed dependency was replaced with
    the platform-standard AuthUser so JWT and API-key callers are gated by
    the same code path.
  • services/api/app/core/security.pyROLE_PERMISSIONS now grants
    threat_intel:write to tenant_admin and soc_lead in addition to the
    existing admin / platform_admin / threat_hunter set. Without this
    the endpoint hardening would have locked out the two roles that legitimately
    need to manage tenant intel during an investigation.
  • services/api/tests/test_threat_intel_rbac.py — 38 new regression tests
    pin the role/permission map (write-roles must hold :write, read-only roles
    must not), assert that CurrentUser.require_permission raises HTTP 403 for
    under-privileged roles and 200 for privileged ones, cover the API-key code
    path including scope wildcards, and grep the endpoint module to ensure
    every route still uses require_permission(...) (so a refactor that
    silently downgrades a route fails CI).

Tracked as F013 in docs/community-feedback/2026-05-12/.

Detection quality — per-rule cross-fire FP eval gate (Issue F005)

scripts/validate_detections.py already replays each native rule against
its own positive + negative fixture (TP / TN gates), but that test cannot
catch the failure mode operators feel hardest in production: rule R
firing on an event that was meant for rule O. A single overly-broad
rule that matches every ConsoleLogin or every rundll32.exe execution
silently drives alert volume up and precision down across the whole pack
without tripping the per-rule TP/TN replay.

  • services/agents/tests/test_detection_fp_rate.py — new pytest
    suite that replays every native rule's match_when against every
    other rule's positive fixture and grades the per-rule cross-fire
    FPR. Fails CI if any rule exceeds MAX_PER_RULE_FPR (default 5%) or
    regresses on its own positive/negative fixture. Failure output groups
    the worst 10 offenders with their cross-fire targets so the operator
    can narrow the rule (or allowlist a deliberate broad-vs-narrow
    overlap via EXPECTED_CROSS_FIRES) without re-running a full eval
    sweep. Current corpus: 816 native rules evaluated, mean FPR 0.0,
    worst FPR 0.49% — well under the 5% ceiling.
  • scripts/run_evals.py — wires the new gate into the unified
    eval runner as suites.detection_fp_rate, reporting
    worst_per_rule_fp_rate (lower-is-better) alongside the existing
    alert-reduction / investigation-completeness / response-quality
    gates so dashboards and CI consume it through the same JSON shape.

Tracked as F005 in docs/community-feedback/2026-05-12/.

Documentation — install pipeline + v2.2 architecture refresh

Documentation-only refresh that aligns every install / architecture page
with the actual shipped state of the repo. No service code, schema, or
API surface changed.

  • One-click install pipeline is now a first-class doc surface.
    • New Docusaurus page apps/docs/docs/installation.md (sidebar
      position 2) walks through install.sh / install.ps1 end-to-end —
      supported package managers, what gets installed, idempotency, the
      uninstall.sh / uninstall.ps1 graduated cleanup flags, and the
      security model.
    • apps/docs/docs/quickstart.md adds it as Path 0 ("zero-prerequisite
      bootstrap") and renumbers the demo / dev paths.
    • apps/docs/docs/deployment/docker.md opens with a callout to the
      installer, refreshes every host/container port mapping against
      docker-compose.yml, splits profile-gated services
      (connectors, osquery-tls, slack-bot) out of the default stack,
      and updates the GHCR image list to the full 16-image set.
    • apps/docs/docs/intro.md adds the installer to Get started and
      corrects the connector-count copy.
    • Root README.md already had Path 0 — verified and synced with the
      architecture refresh below.
  • v2.2 architecture surfaces are now reflected everywhere.
    • apps/docs/docs/architecture.md data-flow diagram, monorepo layout,
      and Service Responsibilities table now include services/osquery-tls,
      services/osquery-extensions, and services/slack-bot. Connector
      count corrected to 50 (was 26 / 42 in stale paragraphs).
    • docs/architecture/SYSTEM_DESIGN.md connector count corrected to 50,
      Service Responsibilities table extended with the v2.2 services, and a
      new §13 — v2.2 Additions appended that documents endpoint
      telemetry (osquery TLS server + extensions), ChatOps (slack-bot),
      Responder PWA, MCP server, Investigation Ledger / Ambient Copilot,
      and the one-click install pipeline. v2 / v2.1 narrative preserved.
    • Root README.md mermaid diagram + service-map table extended with
      osquery-tls, slack-bot, mcp and the corrected
      Realtime / Web Console descriptions.
  • Connector count corrected to 50 across the repo.
    • apps/docs/docs/connectors/index.md: catalog count updated and the
      23 missing connectors added across the existing categories
      (cloud / CNAPP / vuln-mgmt, SIEM, EDR/XDR, SaaS, ITSM, network,
      endpoint fleet, container orchestration).
    • apps/docs/docs/connectors/api-coverage.md: coverage-table heading
      updated.
    • apps/web/src/components/onboarding/StartHero.tsx: in-product copy
      on the onboarding tile updated.
    • apps/docs/docs/intro.md: two stale paragraphs updated.
    • Source of truth: services/connectors/app/connectors/__init__.py
      (_CONNECTOR_CLASSES).

Old historical entries in AI_STACK_PLAN_PROGRESS.md reference 42
connectors and are intentionally left as a snapshot of the v2.1 increment
they describe.