Engineering notes
Decisions, and why we made them.
Short, specific, written by the people who made the change. No case studies, no customer names.
May 2026
Why our own scanner is untrusted
When we built appsec-unified the obvious shortcut was to run it in-process in the worker: it is our code, it is fast, and it avoids a Kubernetes Job. We refused. A scanner parses hostile input by definition — repositories, images, lockfiles — and a parser bug in our engine is exactly as dangerous as one in anybody else's.
So the native engine runs as a Job in the same trust band as Trivy and Semgrep, receives the same split secrets and reports through the same signed callback. A CI gate bans any direct call to its execute method and any scanner module that constructs a finding row. If the engine breaks, scans fail closed. There is no privileged fallback, and that is the point.
August 2026
An inferred edge never completes a path
The first lineage prototype matched images to repositories by tag name. It looked great in demos and was wrong often enough to be dangerous: a lineage view that says a CVE is on the internet when it is not costs trust the first time an engineer checks.
We now classify every edge — observed, derived or inferred — and strict views use only the first two. Inferred edges are still shown as candidates, because they are useful hints. But the view will say source_gap before it will draw a line it cannot justify, and a missing hop is never persisted, because storing an absence as an edge would fabricate a relationship.
August 2026
Blast radius that excludes siblings
Our first blast-radius number walked the affects verb in reverse from a subject, which reached every sibling finding on the same package. Counts in the hundreds appeared on packages that ran in two workloads. Impressive, useless.
affects is now deliberately absent from the propagation map. Blast radius follows creation and deployment edges — produces, stored_in, runs_image, routes_to, exposes — bounded by hop count and entity count. The number now means what an engineer assumes it means: how many workloads, services and ingresses carry the thing.
May 2026
Disposition is a human write
Automated triage is attractive until you audit it. A model that flips a finding to false positive leaves you with a status and no accountable decision. We made disposition a single API with a mandatory reason code, blocked AI and worker writes to governance fields at the ORM layer, and then added a database trigger that refuses any disposition change without a bound human principal — because an ORM guard is only as good as the code path that goes through it.
AI triage still exists. It writes suggestions and every prompt is audited, accepted or rejected. It never writes the verdict.
May 2026
Two-stage language detection
The worker that plans a scan must not clone the repository: cloning is hostile-input handling and belongs in the scan band. But the tool plan depends on the languages present.
The compromise is two stages. The worker plans from URL heuristics without touching code, always including the native engine and the baseline tools. The scan pod reports the languages it actually found, and the plan is refined there. A previous version pre-appended the SBOM tools and then skipped auto-detection because the list was already non-empty — three tools launched instead of eight. The regression test for that bug is one of the ones we are fondest of.
August 2026
The verifier's negative case
A truth gate on this website was supposed to fail the build if a rendered page claimed a certification we do not hold. It passed for a day while such a sentence was on the contact page, because the grep flag it relied on does not exist in BusyBox. The gate could not fail.
The gate now runs its own regex against a sentinel sentence and fails if the sentinel does not match, before it looks at a single page. Every verifier we write gets the same treatment: prove it can detect the thing before you trust it to.