You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(scan): grade end-of-life findings by how close they are
--block-eol was binary: anything already past its end-of-life date failed the
build, and anything not yet past it was invisible. A runtime that died two years
ago and one that dies next quarter are not the same problem, and a gate that
cannot tell them apart gets switched off.
Each EOL date is now mapped to a synthetic severity by its horizon (retired,
within 30 days, this quarter, next quarter) using the organisation's own mapping
from its quality-gate policy. Those four eol*Severity columns have existed, with
sensible defaults, since the quality-gate table was created, and nothing has ever
read them -- because until organisations were seeded a policy row,
cli.quality-gate-get answered {"config": null} for every org alive and the CLI
never got as far as looking. A bucket set to "skip" produces nothing at all.
Blocking behaviour is deliberately unchanged. Only findings at or above the block
floor fail the build, and the floor defaults to critical, which is the retired
bucket's default severity -- so exactly what failed a build yesterday fails it
today, and everything closer to the horizon is reported rather than enforced.
Grading arrived with a CLI release, not with any org's decision to adopt it, and
a release that turned "your runtime dies next quarter" into a red build would
break CI for every customer who has ever passed --block-eol. --block-eol-severity
opts into more.
The CLI fetches three kinds of organisation policy from vdb-api. This document covers what each fetch does with the answer, and (the part worth reading) what `{"config": null}` used to mean and no longer does.
6
+
7
+
Server side: [vdb-api/.repo/auth-and-tenancy.md](https://github.com/vulnetix/vdb-api/blob/main/.repo/auth-and-tenancy.md). Identity model: [vulnetix-authentic-aws/.repo/identity-model.md](https://github.com/vulnetix/vulnetix-authentic-aws/blob/main/.repo/identity-model.md).
|`POST /v2/cli.quality-gate-get`|`CliQualityGateGet`|`applyOrgQualityGate` at scan time (`cmd/quality_gate_enforce.go`); `config get quality-gate` (`renderQualityGateGet`) |
14
+
|`POST /v2/cli.package-firewall-get`|`CliPackageFirewallGet`|`config get package-firewall` (`renderPackageFirewallGet`) |
15
+
|`POST /v2/cli.ai-firewall-baseline`|`CliAiFirewallBaseline`|`ai-firewall baseline`; composed into `ai-firewall apply` when `spec.baseline.enabled`|
16
+
17
+
All three are read-only, all three carry an empty payload. The org is resolved from the authenticated request, and none of them creates anything server-side.
18
+
19
+
The uuid in the API key identifies a **principal**. The policy that comes back belongs to that principal's **tenant**, resolved server-side by `TenantOrgID`. A Teams member and their owner run the same `vulnetix scan` and are gated by one policy, which is the entire point of the split.
AQG-->>Scan: no-op, scan flags and builtin defaults stand
34
+
end
35
+
AQG->>API: POST /v2/cli.quality-gate-get (ApiKey = principal)
36
+
API->>API: TenantOrgID(principal) → tenant
37
+
API->>SA: SELECT ... FROM "CliQualityGateConfig" WHERE "orgUuid" = tenant
38
+
alt lookup fails
39
+
API-->>AQG: error
40
+
AQG-->>Scan: no-op, scan flags stand (verbose notes it)
41
+
end
42
+
SA-->>API: row (or none)
43
+
API-->>AQG: {"config": {...}} or {"config": null}
44
+
45
+
alt config == null
46
+
AQG-->>Scan: "no policy configured", scan flags stand
47
+
else config present
48
+
loop each of the 9 enforcement fields
49
+
AQG->>AQG: field null? → leave the caller's local alone
50
+
AQG->>AQG: field set? → OVERWRITE the local; ORG POLICY ALWAYS WINS
51
+
end
52
+
AQG-->>Scan: locals now carry the org's enforcement
53
+
end
54
+
```
55
+
56
+
Two semantics are decided and should not be relitigated without a reason.
57
+
58
+
**Org policy always wins.** A value the org has set overrides even an explicitly-passed CLI flag. `noteOverride` prints `--severity high superseded by org policy: critical` under `--verbose`. A security policy a developer can turn off with a flag is not a policy.
59
+
60
+
**NULL means "not set", not "false".**`qgConfigBool` / `qgConfigInt` / `qgConfigString` each return a second boolean that is false when the field is absent or JSON `null`, and the caller then leaves its local untouched. This is what makes the seeded defaults safe: a freshly-provisioned org has all nine enforcement columns NULL, so its CI behaves exactly as it did before it had a policy row at all.
61
+
62
+
## What `{"config": null}` used to mean
63
+
64
+
It meant **every organisation**, and it meant that for the entire life of the product.
65
+
66
+
Nothing ever seeded a `CliQualityGateConfig` row. Not `register`, not the OIDC callback, not the Stripe webhook. The three doors each did their own thing and none of them wrote a line of policy. So `POST /v2/cli.quality-gate-get` returned `{"config": null}` for every org that had ever existed, and `applyOrgQualityGate` took its "no policy configured" early return every single time. The four EOL-to-severity columns (`eolNextQuarterSeverity`, `eolThisQuarterSeverity`, `eolWithin30DaysSeverity`, `eolRetiredSeverity`), all with perfectly good defaults, were unreachable dead schema. The feature was wired end to end and could not fire.
67
+
68
+
Since `ProvisionOrg` ([vdb-site/.repo/org-provisioning.md](https://github.com/vulnetix/vdb-site/blob/main/.repo/org-provisioning.md)), a new org is seeded at creation with the EOL buckets populated and the nine enforcement columns left NULL. So today:
69
+
70
+
-**`{"config": null}`** means an org that predates the seeder and has not been backfilled. It is now the exception, not the rule.
71
+
-**`{"config": {...}}` with every enforcement field null** means a seeded org that has not opted into enforcement. This is the normal state of a new org, and it is why an upgrade does not suddenly start failing builds that used to pass. Enforcement is a decision a human makes.
72
+
-**`{"config": {...}}` with fields set** means an owner configured it, and it wins.
73
+
74
+
The CLI needs no change for any of this: `renderQualityGateGet` already prints a "no policy configured" line for a nil config and "not set" for each null field, and `applyOrgQualityGate` already treats the two cases differently. What changed is which branch actually gets taken.
75
+
76
+
## Package firewall
77
+
78
+
`CliPackageFirewallGet` returns `{"config": {...}|null, "mirrors": [...]}`. For a nil config, `renderPackageFirewallGet` reports that no policy is configured and that the proxy defaults apply, which is now literally true: the proxy's in-memory defaults are the same open values the columns declare, and package-firewall no longer writes a row on a read ([package-firewall/.repo/org-policy-resolution.md](https://github.com/vulnetix/package-firewall/blob/main/.repo/org-policy-resolution.md)).
79
+
80
+
A seeded org has a permissive config row (every threshold `0`, every block `false`) plus the mirrors package-firewall backfills for each ecosystem. The `mirrors` array is populated from the tenant's config row, so every member of an org sees the same mirror set.
81
+
82
+
## AI firewall baseline
83
+
84
+
`CliAiFirewallBaseline` fetches the server's recommended guardrail set. It is composed into `ai-firewall apply` when `spec.baseline.enabled` is true, and it **soft-fails on 404** unless `--baseline-required`. An older server simply may not have the endpoint.
85
+
86
+
The same baseline is what vdb-site seeds onto a new org, and this is deliberate: vdb-site embeds the output of vdb-api's `cmd/baseline-export` rather than keeping a hand-copied second list, so "baseline 2026.07.1" cannot be one set of rules over the wire and a different set in the database. 23 guardrails, 21 enabled; `injection-delimiter-spoof` and `pii-phone` ship disabled because a guardrail that cries wolf teaches an organisation to switch the firewall off.
87
+
88
+
The practical consequence for `apply`: on a freshly provisioned org, the guardrails the server reports as the baseline are already present as rows, so a first `apply` with the baseline composed in is close to a no-op rather than a bulk create. Guardrails reconcile by **name**, which is what makes that idempotent.
89
+
90
+
## Related documents
91
+
92
+
-[vdb-api/.repo/auth-and-tenancy.md](https://github.com/vulnetix/vdb-api/blob/main/.repo/auth-and-tenancy.md). How the API key becomes a tenant, and the 401/403/409/503 rules the CLI's retry logic sees.
93
+
-[vdb-site/.repo/org-provisioning.md](https://github.com/vulnetix/vdb-site/blob/main/.repo/org-provisioning.md). What gets seeded and why the defaults are asymmetric.
94
+
-[vulnetix-authentic-aws/.repo/identity-model.md](https://github.com/vulnetix/vulnetix-authentic-aws/blob/main/.repo/identity-model.md), the hub.
cmd.Flags().Bool("block-malware", false, "Exit with code 1 when any dependency is a known malicious package.")
4176
4268
cmd.Flags().Bool("no-malscan", false, "Skip the in-process malscan-engine pass over local dependency install dirs.")
4177
4269
cmd.Flags().Bool("block-eol", false, "Exit with code 1 when a runtime or package dependency is end-of-life. Runtimes: Go, Node.js, Python, Ruby. Package-level checks activate when VDB has EOL data (404s are silently skipped).")
4270
+
cmd.Flags().String("block-eol-severity", "critical", "With --block-eol, the graded severity at which an end-of-life component fails the build (critical, high, medium, low). Components graded below it are reported, not blocked. The default blocks only what is already past its end-of-life date.")
4178
4271
cmd.Flags().Bool("block-unpinned", false, "Exit with code 1 when any direct dependency uses a version range (^, ~, >=) instead of an exact pin.")
4179
4272
cmd.Flags().String("exploits", "", "Exit with code 1 when exploit maturity reaches the threshold: poc (any public exploit), active (CISA/EU KEV / actively exploited), weaponized (in-the-wild only).")
4180
4273
cmd.Flags().Bool("results-only", false, "Only output when findings exist; completely silent when the scan is clean.")
0 commit comments