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.processwired to theFusionEngine
over cross-service HTTP (PR #198);
/investigateswapped to theRouterOrchestratorbehind the
ROUTER_INVESTIGATEflag (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
/metricsdata 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/configno longer exportsUserConfig.apps/web/vitest.config.ts
usedimport('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.globalis no longer in the default DOM lib in@vitest/runner's typing.
packages/sdk-ts/src/client.test.tsreferenced the Node global namespace via
(global.fetch as ...); it now usesglobalThis.fetch, which is the
cross-runtime idiom and was already what every other test in the SDK suite
used. No runtime behaviour change —global === globalThisin 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 /processon the fusion service
(services/fusion/app/api/router.py). Accepts aRawAlert, returns a
FusedAlert, and is wired to the already-runningFusionWorker's engine
instance via the module-level_worker_refthe worker registers on startup.
Returns503when 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/processinside the docker-compose network), forwards
an optional bearer token, and raises on any non-2xx or transport error.
This is a deliberate contrast withapp.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 — keepsDetectAgentimport-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 bytenant_id,
cross-tenant lookups resolve to 404, and writes attach
current_user.tenant_ideven when the payload smuggles a different
one.services/api/tests/test_alerts_tenant_isolation.py— every
read/write/queue/claim path on/alertsbindstenant_idinto 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 bytenant_id, new rows bind
the caller's tenant, andemit_auditis invoked with the caller's
tenant + actor (CredentialVaultis 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
ChainLinkwith the alert title, severity chip (driven by
the canonical 5-tier ladderinfo | low | medium | high | critical),
confidence percent, MITRE technique IDs, and the deterministic narrative
reason emitted byservices/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.getAttackChainmethod +AttackChainTimeline,
AttackChainWindow,AttackChainLink,AttackChainEntityNode,
AttackChainEntityEdge,BackendAttackChainResponsetypes in
apps/web/src/lib/api.ts. The wire format matches the backendto_dict
shape exactly (nodekindrather thantype; optionalseverityand
event_timefrom_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
useSearchParamsis stateful so window-selection deep-links round-trip
cleanly under test. TheWindowSelectoris a labelled
role="group"of buttons witharia-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_astreamvalidates messages exactly once and
refuses to yield any chunk on violation;make_safe_chat_modelproxies
non-LLM attributes through but routesainvoke/astreamthrough
validation;classify_messagerejectsapi_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 toTrue.test_llm_contract_no_bypass.py(3 cases) — AST-based static gate
that walks every*.pyfile underservices/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 throughsafe_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.py—WazuhConnector
subclassesBaseConnector, pollswazuh-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@timestampseen 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 categorysiem, 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.py—aisoc plugin new <NAME> --type <kind>loads the template tree from
src/aisoc_cli/templates/<kind>/, runsstring.Templatesubstitution for
${slug},${name},${author}, and writes a project that already
validates against the manifest schema.aisoc plugin scaffoldis preserved
as an alias for backwards compatibility.pyproject.toml—force-includeships 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.mdis 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 forapi/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-generatedpostgres_password,secret_key,credential_key,
redis_auth, optionalopenai_api_key), and Artifact Registry for images.
One service account per Cloud Run service with least-privilege
secretAccessorbindings. The skeleton points at the public GHCR demo
images so a freshapplyworks zero-config; operators override via
api_image/web_image/ingest_image.apps/docs/docs/deployment/gcp.md+ sidebar entry (betweenkubernetes
andenv-vars) — quickstart, state-backend guidance, Cloud SQL Auth Proxy
notes, cost envelope, and the long-running-services follow-up plan (GKE
Autopilot foragents,realtime,connectors,alert-fusion,
threatintel,fusion).infra/terraform/gcp/README.mdmirrors 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/DescriptorPydantic models (UTC-aware).services/actions/app/live_actions/registry.py—LiveActionExecutor
ABC + module-levelLiveActionRegistry.services/actions/app/live_actions/dispatcher.py— structured logging,
error translation, dry-run + missing-credential semantics
(SIMULATED, neverPARTIAL).- Adapters wrap every existing in-tree executor (CrowdStrike, Okta, AWS SG,
Splunk) so they now show up asbuiltindescriptors. services/api/app/api/v1/endpoints/live_actions.py—discover,
dispatch,dry-runREST 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-gapped→env-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: translatecomments removed fromnl_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.py—AuditdConnectortails
/var/log/audit/audit.log, reassembles multi-record events by msg id,
decodes hexproctitle/argvblobs, and normalizes via
_severity_from_eventusingaisoc_*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 offauditd_keyfor
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
apschedulerdev-deptest_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-shotnotify_slackfrom 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, plussignature_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). ReusesSummaryCaseRow/
SummaryCommentRow/SummaryTaskRowfetchers fromcase_summaryso the
post-mortem and the live summary draw from the same source of truth.
Output is a PydanticCasePostmortemcovering 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}/postmortemwith?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/summaryand/postmortemwith 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) andfile:name. Untranslatable patterns
are counted inskipped_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.
- Pure mappers:
services/api/app/api/v1/endpoints/stix_taxii.pyPOST /stix/indicators?push_to_misp=true— response now includes a
mispblock (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 anairgap_blockedflag 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. ExistingMISP_URL/MISP_API_KEYare
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_KEYenvs 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 legacyUser-typed dependency was replaced with
the platform-standardAuthUserso JWT and API-key callers are gated by
the same code path.services/api/app/core/security.py—ROLE_PERMISSIONSnow grants
threat_intel:writetotenant_adminandsoc_leadin addition to the
existingadmin/platform_admin/threat_hunterset. 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 thatCurrentUser.require_permissionraises 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 usesrequire_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'smatch_whenagainst every
other rule's positive fixture and grades the per-rule cross-fire
FPR. Fails CI if any rule exceedsMAX_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 viaEXPECTED_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 assuites.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 throughinstall.sh/install.ps1end-to-end —
supported package managers, what gets installed, idempotency, the
uninstall.sh/uninstall.ps1graduated cleanup flags, and the
security model. apps/docs/docs/quickstart.mdadds it as Path 0 ("zero-prerequisite
bootstrap") and renumbers the demo / dev paths.apps/docs/docs/deployment/docker.mdopens 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.mdadds the installer to Get started and
corrects the connector-count copy.- Root
README.mdalready had Path 0 — verified and synced with the
architecture refresh below.
- New Docusaurus page
- v2.2 architecture surfaces are now reflected everywhere.
apps/docs/docs/architecture.mddata-flow diagram, monorepo layout,
and Service Responsibilities table now includeservices/osquery-tls,
services/osquery-extensions, andservices/slack-bot. Connector
count corrected to 50 (was 26 / 42 in stale paragraphs).docs/architecture/SYSTEM_DESIGN.mdconnector 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.mdmermaid diagram + service-map table extended with
osquery-tls,slack-bot,mcpand the corrected
Realtime/Web Consoledescriptions.
- 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.