Architecture¶
flowchart TB
subgraph Inputs
MAN["K8s manifests / helm template / kubectl get -o yaml"]
REG["OCI registries: digest, labels, cosign artefacts"]
GH["GitHub API: commit, author, PR"]
TRV["Trivy JSON + CISA KEV"]
TET["Tetragon JSON events"]
SIG["cosign verify JSON (Fulcio certificate)"]
end
SIG --> PROV
MAN --> K8S["k8s.py collector"]
REG --> PROV["provenance.py, sigstore.py"]
GH --> PROV
TRV --> ING["ingest.py"]
TET --> ING
K8S --> DS[("Dataset JSON")]
PROV --> DS
ING --> DS
DS --> G["graph.py lifecycle graph"]
G --> POL["policy.py 13 ZT controls"]
POL -. "diffed in CI" .-> REGO["stratum/policies/stratum.rego via OPA"]
G --> DET["detect.py 9 rules + novelty"]
DET --> INC["incident.py"]
POL --> INC
INC --> API["FastAPI + console"]
REGO --> GK["Gatekeeper ConstraintTemplate (PSS subset)"]
G --> NEO["neo4j.py Cypher export"]
INC --> CLI["CLI / JSON"]
Data model¶
Every collector emits the same Dataset (stratum/dataset.py), a JSON-serialisable bundle of dataclasses:
| Entity | Real source | Graph node id |
|---|---|---|
Commit |
GitHub commits API (verified) | commit:<sha> |
Build |
OCI revision label -> commit (oci:<digest12>) |
build:<id> |
Image |
manifest image ref, resolved digest, base OS from Trivy | image:<ref> |
| base image | ImageReport.os (e.g. debian 12.13) |
base:<os> |
Workload |
Deployment / StatefulSet / DaemonSet / Job / CronJob / Pod | workload:<ns>/<name> |
| pod | workload replica, or a runtime-only pod seen by Tetragon | pod:<ns>/<pod> |
ServiceAccount |
SA plus RBAC risk derived from (Cluster)Role(Binding)s | sa:<ns>/<name> |
NetworkPolicy |
NetworkPolicy objects | netpol:<ns>/<name> |
RuntimeEvent |
Tetragon process_exec / process_connect / process_kprobe |
(events, not nodes) |
ImageReport |
Trivy JSON: vulns by severity, KEV hits, shells, OCI labels | attributes on image: |
The lifecycle spine is commit -BUILDS-> build -PRODUCES-> image -DEPLOYS-> workload -RUNS-> pod, with side edges base -BASE_OF-> image, ns -CONTAINS-> workload, sa -IDENTITY_OF-> workload, netpol -GUARDS-> ns, and (on export) subject -VIOLATES-> control.
Zero-Trust controls¶
| id | control | evidence used |
|---|---|---|
| ZT-NET-01 | default-deny egress per namespace | NetworkPolicy with Egress in the namespace |
| ZT-WL-01 | no privileged / root containers (PSS baseline) | PSS baseline checks, privileged, runAsUser 0, host namespaces |
| ZT-WL-02 | Pod Security Standard restricted | remaining PSS restricted checks |
| ZT-ID-01 | dedicated workload identity | serviceAccountName unset or default |
| ZT-ID-02 | no token automount for privileged identities | effective automountServiceAccountToken and SA RBAC risk |
| ZT-ID-03 | no cluster-admin workloads | ClusterRoleBinding to cluster-admin |
| ZT-ID-04 | no cluster-wide secret read or RBAC escalation | ClusterRole rules (secrets get/list/watch, */*, bind/escalate) |
| ZT-PROV-01 | only trusted-pipeline images | image has a verified build -> commit edge |
| ZT-PROV-02 | signed builds | cosign signature artefact present |
| ZT-PROV-03 | digest-pinned images | @sha256: in the image reference |
| ZT-IMG-01 | no deny-listed / critical-CVE images | Trivy CRITICAL findings (or synthetic deny-list) |
| ZT-IMG-02 | minimal images without a shell | shell packages in the SBOM (bash, busybox, dash, ...) |
| ZT-IMG-03 | no KEV-listed vulnerabilities | Trivy CVEs ∩ CISA KEV |
Runtime rules¶
| rule | trigger | control named |
|---|---|---|
| R-ESCAPE | nsenter/unshare/chroot into PID 1 namespaces |
ZT-WL-01 |
| R-SA-TOKEN | open of .../serviceaccount/token or the projected .../serviceaccount/..<timestamp>/token path |
ZT-ID-02 |
| R-CRED-READ | ssh keys, keystores, cloud/kube credentials | ZT-ID-04 |
| R-SYS-WRITE | writes to /etc/passwd, /etc/shadow, sudoers, ... |
ZT-WL-02 |
| R-UNMANAGED | process in a container with no pod (not deployed via K8s) | ZT-PROV-01 |
| R-TMP-EXEC | exec from /tmp, /dev/shm, /var/tmp |
ZT-WL-02 |
| R-NETTOOL | nc, socat, nmap, ssh, scp, ... | ZT-IMG-02 |
| R-SHELL | interactive shell binary | ZT-IMG-02 |
| R-EGRESS | connect to a non-RFC1918/CGNAT/ULA address | ZT-NET-01 |
| A-NOVEL | per-workload novelty over (event, process, port/path) | ZT-IMG-02 |
Neo4j¶
docker compose up -d neo4j
python -m stratum export --data real --format cypher --out graph.cypher
docker compose exec -T neo4j cypher-shell -u neo4j -p stratum-lab < graph.cypher
Example queries (also in stratum.neo4j.QUERIES):
// every workload built on a given base OS release
MATCH (b:BaseImage {id: "base:debian 12.13"})-[:BASE_OF]->(:Image)-[:DEPLOYS]->(w:Workload) RETURN w.id;
// full provenance path for a pod
MATCH p=(c:Commit)-[:BUILDS]->(:Build)-[:PRODUCES]->(:Image)-[:DEPLOYS]->(:Workload)-[:RUNS]->(:Pod {id: $pod}) RETURN p;
// namespaces with no NetworkPolicy
MATCH (n:Namespace) WHERE NOT (:NetworkPolicy)-[:GUARDS]->(n) RETURN n.id;