02 — Platform Architecture¶
| Status | IMPLEMENTED (C1–C13) |
| Related ADRs | 001, 004–008, 014, 018–020, 022–025, 027 |
1. Architectural stance¶
The platform is an assurance layer. It does not replace the security tools the organisation already owns or has scaffolded. It adds what none of them has: application context, a control model, an evidence ledger, a risk engine and maturity derivation.
| Concern | System of record | Why |
|---|---|---|
| Source, workflow runs, GHAS alerts (CodeQL, secret scanning, Dependabot), repo settings | GitHub | Authoritative and already licensed. We read; we do not copy the logic. |
| SBOM component inventory and continuous CVE matching | Dependency-Track | Re-scores existing SBOMs as new CVEs land, without re-running CI. Infra ADR-0001. |
| Scanner output normalisation and cross-tool dedup (200+ parsers) | DefectDojo | Writing parsers is undifferentiated work. |
| Cloud posture findings | Defender for Cloud / AWS Security Hub | Authoritative per cloud. |
| Security events and incidents | Your SIEM: Sentinel, or any SIEM over HTTPS (Splunk HEC, Elastic, …) or syslog | SOC tooling. |
| Work tracking | Jira | Where engineers work. |
| Applications, ownership, classification | Platform | Nobody else has it. |
| Controls, framework mappings, control state | Platform | |
| Evidence ledger (immutable) | Platform | |
| Contextual risk priority, SLA, exceptions | Platform | Tool severities lack application context. |
| SAMM / ASVS maturity | Platform |
Finding lifecycle rule: DefectDojo normalises and dedups. The platform holds a projection of each finding, enriched with application context and risk priority, and owns the Jira ticket and SLA clock. A finding is closed only by evidence: a later scan of the same scope that no longer reports it. Closing a Jira ticket is not enough (ADR-018).
2. Context diagram¶
flowchart LR
subgraph Eng[Engineering]
Dev[Developers / Champions]
Repo[(GitHub repos<br/>.security/ contract)]
WF[Reusable security workflows<br/>pinned by SHA]
end
subgraph GH[GitHub Enterprise Cloud]
GHAPI[REST/GraphQL API]
GHAS[GHAS: CodeQL,<br/>secret scanning, Dependabot]
ATT[Artifact attestations<br/>Sigstore]
end
subgraph Plat[FortProof]
HOOK[Webhook receiver<br/>internet-facing, write-only]
API[API + BFF]
UI[Web UI]
WRK[Workers]
DB[(PostgreSQL)]
BLOB[(Evidence store<br/>immutable blob)]
end
subgraph Tools[Existing security tooling]
DT[Dependency-Track]
DD[DefectDojo]
end
subgraph Ext[Enterprise services]
ENTRA[Identity provider - OIDC]
JIRA[Jira]
SENT[SIEM: Sentinel / HTTPS / syslog]
DFC[Defender for Cloud]
SH[AWS Security Hub]
FEEDS[EPSS · CISA KEV · OSV]
end
Dev --> Repo --> WF
WF -- SARIF --> GHAS
WF -- evidence bundle + attestation --> ATT
GH -- webhooks --> HOOK
WRK -- GitHub App (read) --> GHAPI
WRK -- forward SBOM --> DT
WRK -- forward scan results --> DD
WRK -- pull findings --> DD
WRK -- pull component / vuln data --> DT
WRK --> JIRA
WRK -- curated events --> SENT
WRK -- read --> DFC
WRK -- read --> SH
WRK -- read --> FEEDS
UI --> API
API --> DB
WRK --> DB
WRK --> BLOB
API -- OIDC --> ENTRA
3. Logical components¶
| # | Component | Responsibility | Status |
|---|---|---|---|
| C1 | Web UI | Overview, applications, evidence explorer with the verification chain, findings, supply chain, control catalogue, maturity, leadership report, exceptions, assessments, integrations and administration; each role sees only its pages. No business logic. | IMPLEMENTED |
| C2 | API service + BFF | REST /api/v1, OpenAPI 3.1. OIDC session for humans (BFF), OIDC client credentials for service clients. AuthZ enforcement point. |
IMPLEMENTED |
| C3 | Webhook receiver | Only internet-facing component. Verifies the GitHub HMAC or GitLab secret token, dedups by delivery ID, enqueues, returns 202. No read path to data. | IMPLEMENTED |
| C4 | Ingestion workers | Fetch workflow-run and job artifacts from the source control provider, verify identity and attestation, hash, store to WORM, write evidence records, optionally forward to Dependency-Track and DefectDojo. | IMPLEMENTED |
| C5 | Connector workers | Scheduled and event pulls: GitHub and GitLab settings, OSV, CISA KEV, EPSS, endoflife.date, and optionally DefectDojo, Dependency-Track, Defender for Cloud and Security Hub. Each connector is a plugin behind one interface. | IMPLEMENTED |
| C6 | Control evaluator | Applies versioned control rules to evidence. Produces control-state records with an explanation. | IMPLEMENTED (48 evaluators) |
| C7 | Risk engine | Computes contextual priority and SLA per finding from a versioned, configurable model. | IMPLEMENTED |
| C8 | Maturity engine | Derives SAMM activity and practice scores from control states plus manual assessments. ASVS coverage. | IMPLEMENTED (ASVS status per requirement: planned) |
| C9 | Workflow engine | Exceptions, manual assessments, onboarding, approvals. State machines with audit. | IMPLEMENTED |
| C10 | Ticketing integration | Jira, GitHub Issues or GitLab Issues: create, update and transition with deterministic dedup keys. | IMPLEMENTED |
| C11 | Event export | Curated security events to one SIEM (Sentinel, HTTPS/Splunk HEC or syslog); notifications through chat webhooks (Slack, Teams). | IMPLEMENTED |
| C12 | Scheduler | Freshness expiry, exception expiry, SLA breach detection, feed refresh. | IMPLEMENTED |
| C13 | Reporting / snapshots | Monthly snapshots for trend reporting: SAMM maturity and posture metrics per application and org unit. The current month is provisional (refreshed by the scheduler); finished months are published and immutable. aap snapshots backfill computes earlier months as of each month end from the evidence on record. |
IMPLEMENTED |
The components are logical. Physically, they ship as three deployables to keep operational load proportionate (ADR-025):
| Deployable | Contains | Scaling |
|---|---|---|
aap-web |
C1 static assets + C2 API/BFF | HTTP, 2–6 replicas |
aap-hook |
C3 | HTTP, 2–4 replicas, separate ingress and identity |
aap-worker |
C4–C13, selected by queue name | 1–N replicas (scale on queue depth, for example with KEDA) |
4. Technology choices (summary; detail and alternatives in ADRs)¶
| Area | Choice | Main alternative considered | ADR |
|---|---|---|---|
| Language (API + workers) | Python 3.12, FastAPI, SQLAlchemy 2, Alembic, Pydantic v2 | Go; TypeScript/NestJS | ADR-023 |
| Frontend | React + TypeScript (Vite), OpenAPI-generated client | Next.js SSR; server-rendered HTMX | ADR-023 |
| Database | PostgreSQL 16, managed or operated (high availability recommended) | Cosmos DB; graph DB for mappings | ADR-008 |
| Queue | PostgreSQL-backed job queue + transactional outbox (SELECT … FOR UPDATE SKIP LOCKED), behind an interface |
Azure Service Bus; Redis/Celery | ADR-024 |
| Evidence store | Write-once object storage: S3 with Object Lock, or Azure Blob with version-level immutability | DB bytea |
ADR-007, 016 |
| AuthN | OpenID Connect (any provider), BFF pattern (httpOnly session cookie); cloud access by federated identity | SPA with tokens in browser | ADR-014 |
| Source integration | GitHub App (read-only scopes) + webhooks; pull-based evidence collection | Push from CI with API keys | ADR-005 |
| Pipeline | Central reusable workflows, released by tag, consumed by SHA | Per-repo copies; external CI | ADR-006 |
| SBOM | CycloneDX; accepting SPDX is planned | SPDX primary | ADR-009, 010 |
| Hosting | Any Kubernetes (Helm chart); Docker Compose for evaluation | Azure Container Apps (the original design) | ADR-025 |
| IaC | Terraform (azurerm 4.x in the Azure example; AWS example included) | Bicep | ADR-025 |
| Observability | Structured logs and health endpoints; audit ledger in PostgreSQL, curated events to your SIEM; OpenTelemetry planned | Vendor-specific APM | ADR-027 |
| Search | PostgreSQL full-text + trigram; no dedicated search engine at this scale | OpenSearch | ADR-008 |
Why Python: security engineers who will maintain and extend it mostly write Python, and the connector and parser ecosystem is strongest in Python. The workload is I/O-bound integration, where Go's performance advantage does not matter. Rationale and the counter-argument (type safety) are in ADR-023.
5. Key flows¶
5.1 Pipeline evidence (primary path, pull-based)¶
sequenceDiagram
autonumber
participant R as App repo (default branch / PR)
participant W as Reusable workflow @sha
participant GH as GitHub
participant H as aap-hook
participant Q as Queue (PG)
participant I as Ingestion worker
participant S as Evidence store (WORM)
participant DB as PostgreSQL
participant DD as DefectDojo / DT
R->>W: workflow_call (sast/sca/secrets/iac/container/sbom)
W->>W: run tool, produce raw output + evidence-manifest.json
W->>GH: actions/attest (in-toto statement over manifest digest)
W->>GH: upload artifact "aap-evidence-<tool>"
GH-->>H: webhook workflow_run.completed (HMAC signed)
H->>Q: enqueue {delivery_id, repo_id, run_id}
I->>Q: claim job (SKIP LOCKED)
I->>GH: GET run (referenced_workflows, head_sha, event, conclusion)
I->>I: verify caller used an APPROVED workflow path@sha
I->>GH: download artifact, verify attestation (signer = approved workflow)
I->>I: verify manifest digests match raw files
I->>S: write raw + manifest (immutable, sha256-addressed)
I->>DB: INSERT evidence (append-only), outbox events
I->>DD: forward raw results (platform credential, not repo secret)
Note over DB: control evaluator re-evaluates affected controls
This removes every platform credential from consumer repositories (see the problem definition). It also means the platform verifies evidence independently instead of trusting what a pipeline tells it.
5.2 Configuration evidence (continuous)¶
The GitHub connector polls repo, ruleset and security settings (and reacts to repository, branch_protection_rule, repository_ruleset and security_and_analysis webhooks). It records C1 configured evidence, for example "default branch ruleset requires 1 reviewer and code-owner review; secret scanning push protection enabled". For controls that are configuration (branch protection), configuration is the operating state. For controls that are activities (SAST), configuration never satisfies them.
5.3 Finding lifecycle¶
stateDiagram-v2
[*] --> New: detected (DD/GHAS/DT/cloud)
New --> Triaged: auto-priority + owner mapped
Triaged --> Ticketed: P1–P3 → ticket (dedup key)
Triaged --> FalsePositive: champion + 2nd reviewer for P1/P2
Ticketed --> FixPending: ticket done / PR merged
FixPending --> Verified: re-scan on same scope no longer reports it
FixPending --> Ticketed: re-scan still reports it (ticket re-opened)
Ticketed --> Excepted: approved exception
Excepted --> Ticketed: exception expired → new SLA clock + escalation
Verified --> Closed
FalsePositive --> [*]
Closed --> Reopened: regression detected (same fingerprint)
Reopened --> Ticketed
5.4 "Which applications are affected by CVE-X?"¶
- Look up the vulnerability by CVE or GHSA ID in the
vulnerabilitiestable (synced from DT/OSV). - Join
component_vulnerabilities→components(purl + version range) →sbom_components→sboms(only the current SBOM per repo/artifact) →repositories→applications. - Return three sets: affected (current SBOM contains a vulnerable version), possibly affected (the repo has no SBOM, or the SBOM is stale per freshness policy), and not affected.
- Add deployment context: environments where the artifact digest is deployed (when deployment evidence exists), internet exposure, tier.
A stale SBOM produces unknown, never not affected. This is the most important honesty property of the CVE query.
5.5 Control evaluation¶
evidence inserted ─▶ outbox event ─▶ evaluator loads controls whose rule
subscribes to that evidence type ─▶ for each (app, control, scope):
gather candidate evidence within freshness window
apply rule (versioned) ─▶ state + explanation + evidence_ids
if state changed: INSERT control_evaluation (append-only),
update control_state (current projection),
outbox: control.state_changed
The scheduler sweep (every 5 minutes by default, worker.scheduler_interval) re-evaluates time-based freshness so that controls fall to STALE without waiting for an event.
6. Multi-tenancy¶
One organisation, many units (levels configured in config/organization.yaml). This is not SaaS multi-tenancy. The model is a single deployment with row-level scoping by organisational unit, enforced in the API and backed by PostgreSQL row-level security as defence in depth (ADR-022). Hard isolation per business unit (separate deployments) is rejected: it would break the organisation-wide views that are the point of the platform.
7. Scale envelope (design target)¶
| Dimension | Design target | Notes |
|---|---|---|
| Applications | 2,000 | |
| Repositories | 10,000 | |
| Evidence records | ~50k/day, 20M/year | Partitioned by month |
| Findings (open) | 500k | |
| SBOM components (distinct purl@version) | 2M | |
| Concurrent UI users | 200 | |
| Webhook burst | 50/s | Receiver only enqueues |
PostgreSQL handles this comfortably with partitioning and correct indexes. The queue abstraction is the escape hatch if job volume ever outgrows it (ADR-024).
8. Integration status labels¶
Every system below is created and owned by this project (ADR-031); the one-time human steps are in the integration bootstrap runbook. "Tested against" says how the connector was verified.
| Integration | Status |
|---|---|
| GitHub App + webhooks | IMPLEMENTED (github_app.py, github_settings.py, receiver). App created by aap bootstrap github-app (one org-owner click). Tested against recorded API shapes. |
| Artifact attestation verification | IMPLEMENTED (attestation.py), tested offline against a real GitHub attestation. Private repos need GitHub Enterprise Cloud (decision D-018); otherwise C2. |
| Dependency-Track 4.14.5 / DefectDojo 3.4.0 | IMPLEMENTED connectors + bootstrap, tested against the real services. Deployed by modules/scanners. |
| Jira | IMPLEMENTED (jira.py, ticketing.py), tested against a contract fake; opt-in live test. Site/project/token: aap bootstrap jira. |
| Defender for Cloud (Azure Resource Graph) | IMPLEMENTED, tested against recorded responses. Reader + Security Reader: modules/azure-reader. |
| AWS Security Hub | IMPLEMENTED, tested with moto. Role trusting the worker identity (no keys): envs/aws. |
| Sentinel | IMPLEMENTED (curated events, dedup, daily checkpoint, dispatch), tested against recorded responses. Table/DCR/rules: modules/sentinel. |
| GitLab | IMPLEMENTED (settings, CI/CD components, webhooks, GitLab Issues; evidence capped at C2, ADR-033), tested end to end against GitLab CE nightly. Token: aap bootstrap gitlab. |
| OIDC sign-in (Entra ID, Okta, Keycloak, any OpenID Connect provider) | IMPLEMENTED (OIDC BFF, sessions, CSRF, bearer for services), tested against a real OIDC server and Keycloak. Entra example: modules/entra. |
| Evidence WORM store | IMPLEMENTED: S3 with Object Lock (S3BlobStore, tested against RustFS in the Compose stack), Azure Blob with an immutability policy (AzureBlobStore, tested against Azurite; policy by modules/platform), or local disk for development. |
| Other SIEMs (Splunk, Elastic, Exabeam, …) | IMPLEMENTED: HTTPS sink (JSON or Splunk HEC) and syslog sink (RFC 5424 over TLS) |
| OSV, CISA KEV, FIRST EPSS, endoflife.date | IMPLEMENTED: built-in feeds, public, no account needed. |