Components
Control plane, scan band, and the stores.
Trusted components run in the platform namespaces. Untrusted work runs as ephemeral Jobs in a scan namespace with no route back except the callback.
Control plane · trusted
API planesFastAPI · create_app(Role)
Auth serviceOIDC · SAML · RS256
Domain workersCelery · 15 deployments
Event consumersprojections · SSE
Policy evaluatorOPA subprocess
AI agentlocal model · advisory
↔
Scan band · untrusted
Namespacesecurityvault-scans
Per-scan Jobfetcher + N tool pairs
Tool containerSV_TOOL · SV_RESULTS_DIR
Adapter containerparse · sign · POST
DeadlineSV_TOOL_DEADLINE_SECONDS
Lifetime≤ 1 h · garbage-collected
↔
Stores
PostgreSQL 16pgvector · RLS
Redisbroker · cache · nonces
ClickHousesecurity lake
Object storageS3-compatible
Vaultsecrets · signing keys
RegistryHarbor · Cosign
API planes
One application factory builds every plane from a single router registry.api-read serves queries, api-write mutations, api-callback only the scanner and webhook callbacks, api-stream server-sent events, andapi-admin cross-tenant platform administration. Each scales on its own autoscaler; a flood of callbacks cannot starve the UI.
Workers
Celery workers orchestrate; they never execute scanners. Pools are split by domain — code security, cloud security, supply chain, data security, discovery, attack surface, governance evaluation, GRC operations, ingestion, triage, AI triage, lake and backfill — so a backlog in one domain stays in that domain.
Events
State changes publish versioned events. Projection workers materialise read models such as the security-entities projection that every entity-centric screen reads; the stream plane pushes changes to open browsers.
Trust boundaries
Three boundaries every scan must cross.
A compromised scanner is the assumed case, not the edge case. The boundaries are enforced by pod specification, by Kubernetes Secrets and by the API — not by policy documents.
B1Split container
The tool container holds the scanner binary and the acquisition credentials for the target. The adapter container holds the platform token. They share one results volume, read-only on the adapter side. A scanner with remote code execution reaches the repository it was already given and nothing more.
B2Split secret
Two Kubernetes Secrets per job. scan-acquire-creds (Git or registry token) mounts only into the tool container. scan-adapter-creds(SCAN_TOKEN, BODY_SIGNING_KEY) mounts only into the adapter. No container ever holds both. The adapter hard-exits if any storage credential appears in its environment.
B3Authenticated callback
Results enter the control plane only through api-callback. The scan token is opaque, single-use, database-backed and bound to the scan, the tool and the job; the body is HMAC-signed and carries a replay nonce. Both are verified before a byte is parsed.
Not in the pod
Database credentials. Redis credentials. Vault tokens. Object-storage keys. Worker service tokens. A mounted service-account token. The banned-pattern gate in CI refuses any scanner module that imports a database driver or constructs a finding.
Native engine
appsec-unified, our own engine, is held to exactly the same rules as Trivy or Semgrep. It runs as a Job, it reports through the callback, and there is no privileged in-process fallback. If it breaks, scans fail closed.
Scan lifecycle
One state machine. No ambiguous end.
Every transition goes through one module and records its evidence. Terminal states say what happened, not just that something did.
PENDING
task dispatched
QUEUED
Job started
RUNNING
callbacks
COMPLETED
PARTIAL
FAILED · FAILED_TIMEOUT · FAILED_DEPENDENCY · FAILED_CONFIG
| From | To | Trigger | Recorded |
|---|
| — | PENDING | Scan requested | Scan row, audit event |
PENDING | QUEUED | Celery task dispatched | Task id |
QUEUED | RUNNING | Kubernetes Job started | Job name, tokens minted, per-tool deadlines |
RUNNING | COMPLETED | All tools reported | Results ingested, audit event |
RUNNING | PARTIAL | Some tools failed or missed their deadline | Per-tool outcome, partial results kept |
RUNNING | FAILED_TIMEOUT | Job deadline exceeded | Reaper verdict with the tool that timed out |
RUNNING | FAILED_DEPENDENCY | Missing credential or policy | Dependency error |
RUNNING | FAILED_CONFIG | Invalid scan configuration | Configuration error with hint |
Callback contract
The only door into the database.
The adapter posts a signed result. The callback handler verifies, then resolves it into the ontology under the tenant the scan belongs to. Nothing the pod says about its tenant is trusted.
Token
Per-scan opaque single-use SCAN_TOKEN, scope-bound to scan, tool, job and the server-derived organization. Verified by database lookup; purged when the job is garbage-collected.
Signature
X-SV-Body-Signature over canonical JSON with a key derived per scan. Constant-time comparison.
Replay
X-SV-Nonce recorded in a per-scan set. In required mode the callback fails closed if the nonce store is unreachable.
Body cap
64 MiB request-size limit at the ASGI layer before any parsing.
Tenant
Write routing uses scan.organization_id. Any organization field in the payload is advisory and ignored for authority.
Spool
Accepted callbacks are durably spooled before ingestion so a worker restart cannot lose a delivered result.
Tenancy
Row-level scope, proven in CI.
Every session binds the acting organization before the first query. Row-level security policies exist for every tenant table and are asserted by the test suite on every merge.
01Bound sessions
Authenticated requests bind app.current_org_id and the acting principal through one set of database binders. Search path is always public; there are no per-tenant schemas.
02Header is the authority
The tenant comes from the verified session, never from a caller-controlled parameter. A CI gate refuses any handler that reads a bare org_id before resolving it against the session.
03Single-resource reads
Every read, update or delete of one resource filters on organization_id in the SQL — a primary-key-only match is treated as a cross-tenant defect and blocked at review.
04Platform admin is separate
Cross-tenant admin routes take a platform-scoped session that can never bind a tenant and must run under the admin database role, or answer 503.
05Human-only verdicts
A database trigger refuses any disposition change without a bound acting principal, so no worker, reaper or AI task can flip a verdict even with a valid connection.
06Schema discipline
Forward-only SQL migrations with SHA-256 verification of applied files; CI proves a populated upgrade converges with a fresh installation before a release is cut.
Ontology pipeline
From a tool result to a lineage view.
Raw results are observations. Observations become entities, edges, logical findings, cases, attack paths and risk scores through a deterministic pipeline that emits a versioned event at each step.
Observation
accept · quarantine
Resolution
aliases · precedence
Entities & edges
provenance-tagged
Logical finding
correlation rules
Case
traversal
Attack path
model v1
Risk
Producers
SCM webhooks, CI ledgers, registry watchers, cluster state and cloud collectors each produce edges with a provenance class. Repository → Commit → Build → ContainerImage → Registry → Workload is the code-origin template the lineage view walks.
Strict paths
Lineage, blast radius and exploit views use observed and derived edges only. Inferred edges are rendered as candidates and never complete a strict path.
Read-time states
missing and conflicting hops are computed when the view is built and never persisted. Storing a missing edge would fabricate a relationship.
Bounds
Blast-radius walks are capped by hop count and entity count. Sibling findings on the same subject are excluded from the count so the number measures impact, not observation volume.
Correlation
Seeded rules compile to an evaluator over entity neighbourhoods; matches project into security cases. Rules run in shadow mode before they are allowed to create cases.
Risk
A versioned model on a fixed scale with factor groups and an explanation per score. Incremental scoring re-evaluates only what an event touched; portfolio views roll up per organization.
Cryptography
Standard primitives. Documented uses.
No invented protocols. Each algorithm below has one job.
| Where | Primitive | Detail |
|---|
| User sessions | RS256 JWT | 15-minute access token with a kid header; refresh token in an httpOnly cookie. RS256 is mandatory outside development; HS256 verification paths are closed. |
| Stored credentials | Fernet (AES-128-CBC + HMAC-SHA256) | Customer Git, registry and cloud credentials encrypted at rest; decrypted only inside the worker that mints the scan Secrets. |
| Control decisions | Ed25519 | Canonical sorted-key payload: control, decision, framework control, input hash, organization, key fingerprint, schema version, timestamp. Private key held by the evaluation service; public half distributable to auditors. |
| Evidence & inputs | SHA-256 | Evidence hashing and evaluation-input hashing so a decision can be tied to exactly what it saw. |
| Audit log | HMAC chain | Append-only records, each carrying the HMAC of its predecessor. |
| Scanner callbacks | HMAC body signature | Per-scan derived key, canonical JSON, replay nonce, constant-time compare. |
| Ticketing webhooks | HMAC + nonce | Per-organization, per-provider key with the same replay guard as scanner callbacks. |
| Images | Cosign | Platform and scanner images signed; an admission webhook can refuse unsigned images in the cluster. |
| Transport | TLS 1.2+ / 1.3 | Edge TLS at the gateway; internal mTLS from the platform PKI for service-to-service calls. |
Identity & access
Your IdP. Three-layer RBAC.
01Authentication
OIDC and SAML single sign-on, SCIM 2.0 provisioning, MFA. Okta, Microsoft Entra ID, Google Workspace and CyberArk connectors ship in the platform.
02Authorization
Permission bindings are the access authority. Eight personas — developer, AppSec, cloud, SOC, GRC, executive and more — only shape the home view; they never grant access.
03Governance writes
Finding status and disposition are governance fields. An ORM-level guard blocks AI and worker writes; a database trigger backs it up. Reason codes are mandatory.
04CI principals
Pipelines authenticate with OIDC workload identity; PR comments and gate results are posted under a CI principal with per-organization rate limits.
05Dual control
Sensitive governance actions require a second approver; approval scope is checked at the action, not at login.
06Audited prompts
Every natural-language query to the AI filter is audited whether accepted or rejected. The model is never re-prompted on failure, closing the model-probing surface.
Failure domains
What breaks, how far it reaches.
| Scenario | Blast radius | Containment |
|---|
| Scanner remote code execution | One scan; the repository it was given | Network isolation, ephemeral credentials, pod termination, no platform credentials present |
| Scanner out of memory or crash | One tool in one scan | Per-tool deadline and resource limits; scan ends PARTIAL with the other results kept |
| Worker crash | Tasks queued on that worker | Automatic restart; tasks re-queued from the broker |
| Broker unavailable | New scans wait | Redis persistence, reconnect with backoff; nothing is silently dropped |
| Nonce store unavailable | Callbacks refused | Fail closed in required mode; the adapter retries, results are not lost |
| Policy evaluator error | The decision being evaluated | DENY with OPA_EVALUATION_FAILED; never an allow |
| Database primary failure | Writes until promotion | Streaming replication and standby promotion; point-in-time recovery from WAL |
| IdP unreachable | New SSO logins | Existing tokens valid until expiry; retry with backoff |
Operations
Observable by default. Gated on every merge.
01Telemetry
Structured logging, Prometheus metrics on every plane and worker, OpenTelemetry tracing through the API and Celery. Logs are shipped to a central store per environment.
02Merge gates
Dozens of architectural-invariant checks run on every merge request: banned patterns, tenant-parameter authority, structlog floor, Cilium policy shape, bundle budgets, route manifest, credential rotation versions, accessibility on redesigned routes.
03Test layers
Unit, worker, property and integration suites in Docker; Playwright end-to-end against the production build; schema proofs on a fresh and an upgraded database.
04We scan ourselves
The platform scans its own repository and images on the same pipeline it sells, and the finding burndown is tracked in the repository.
05Supply chain
Images built rootless, signed with Cosign, pulled through a private registry mirror; dependency and base-image updates go through the same gates.
06Network policy
Cilium network policies scoped by label; an empty selector is rejected by CI because it would put every pod into default-deny at once.
Technology stack
Boring where it matters.
| Layer | Technology | Purpose |
|---|
| API | FastAPI · Pydantic v2 | Async REST with strict request models (extra="forbid", length caps) |
| Data access | SQLAlchemy 2.0 | Async sessions on the request path, sync in workers |
| Database | PostgreSQL 16 · pgvector | Primary store, audit ledger, row-level tenant policies |
| Queue | Celery 5 · Redis | Orchestration, scheduling, rate limits, replay nonces |
| Lake | ClickHouse | High-volume security events and analytics |
| Policy | Open Policy Agent | Deterministic Rego evaluation, fail closed |
| Analysis | Tree-sitter | Language grammars for the native taint analysers |
| Execution | Kubernetes · Cilium | Scan Jobs, platform deployment, network policy |
| Signing | Cosign · Ed25519 · HMAC | Images, decisions, audit chain, callbacks |
| Frontend | React 19 · TypeScript · MUI · TanStack Query · Vite | Single-page application with strict CSP |
| Testing | pytest · Vitest · Playwright · axe-core | Backend, frontend, end-to-end, accessibility |
FAQ
The questions your architect will ask.
- Where does state live?
- PostgreSQL holds entities, edges, findings, decisions and the audit ledger. ClickHouse holds high-volume events. Object storage holds artifacts and evidence bundles. Everything else — API planes, workers, scan Jobs — is stateless and replaceable.
- Can a scanner write to my database?
- No. There are no database credentials or drivers in a scanner pod, and the CI banned-pattern gate refuses any scanner module that imports one. Results reach the database only through the authenticated callback.
- What happens if the policy engine is down?
- Every evaluation error is a deny with an explicit reason code. There is no path that returns an allow without a successful evaluation.
- Does the AI ever change data?
- No. AI triage, remediation suggestions and natural-language filters are advisory. Governance fields are guarded at the ORM layer and by a database trigger; the AI has no principal and fails closed.
- How do you prove a finding really came from that commit?
- Lineage edges carry a provenance class. A repository-to-image link established by a verified attestation or an OIDC subject is observed; a link by CI cross-reference on a full SHA or digest is derived; a tag or label match is inferred and never completes a strict path. The view shows which channel established each hop.
- Can the scan band run in our environment while you host the control plane?
- Yes. An execution cluster is registered per organization with its API endpoint, authentication method (kubeconfig, OIDC or token — IRSA, GKE and AKS workload identity supported), scan namespace, network zone and parallel-job cap. It becomes eligible only after an admin approves the CA fingerprint. The worker creates the scan Job in your cluster; the adapter posts results outbound to
api-callback. Deregistration is a soft delete so the audit trail survives. - Can I run it entirely inside my own environment?
- Yes. The same Helm charts and signed images run on your Kubernetes with your PostgreSQL, Redis, ClickHouse and S3-compatible storage. No telemetry leaves unless you configure it to.