Repository navigation
feat(ci): record time-limited security finding dispositions for the release gate #3671
Description
Sub-issue of #3559. This is a deliberately narrow first version of Section 4 (Security finding dispositions) of the design in #3559 (comment).
User Story
As a maintainer running the release train, I want to record a reviewed, time-limited decision about each High/Critical security finding, so that a known backlog doesn't block releases and every deferred finding still comes back for action on a schedule.
Problem Statement
The Security Scan gate in release-tag.yml has two states. It either fails on every HIGH/CRITICAL finding from CodeQL, Trivy, and Zizmor (fail-on-static-findings: true), or it ignores all of them (false). There's no way to say "this finding has been reviewed, and here's the decision."
For example, v0.1.0-pre.11 (run 35997548356) produced 37 findings: Zizmor 27, CodeQL Rust 8, Trivy 2. Those collapse to 10 distinct issues. After review:
| ID | Issue | Findings | Decision |
|---|---|---|---|
| V1 | OIDC client skips IdP TLS verification whenever --gateway-insecure is set |
CodeQL rust/disabled-certificate-check x2 |
Defer to 0.1.1, top priority |
| V5 | CI container image pinned to floating ci:latest |
Zizmor unpinned-images x17 |
Defer to 0.1.1, top priority |
| V2 | certgen --dry-run prints private keys |
CodeQL rust/cleartext-logging x3 |
Defer; fix in a 0.1.x release before 0.2.0 |
| V6 | ${{ }} expressions expanded inside run: |
Zizmor template-injection x5 |
Defer; fix before 0.2.0 |
| V7 | Composite actions write to GITHUB_ENV |
Zizmor github-env x3 |
Defer; fix before 0.2.0 |
| V8 | Write permissions granted at workflow level | Zizmor excessive-permissions x2 |
Defer; fix before 0.2.0 |
| V9 | Supervisor image has no USER |
Trivy DS-0002 | Defer; fix before 0.2.0 |
| V10 | Gateway container lacks readOnlyRootFilesystem |
Trivy KSV-0014 | Defer; fix before 0.2.0 |
| V3 | Provider profile export "logs" the TLS key path | CodeQL rust/cleartext-logging x2 |
Reject: false positive, not exploitable |
| V4 | Login prints the user's own username | CodeQL rust/cleartext-logging x1 |
Reject: not exploitable |
The current mechanisms can't record any of these decisions:
.trivyignore.yamlaccepts false positives only, by its own rule.- Zizmor inline ignores have no expiry and no approver.
- GitHub code-scanning dismissals were rejected in the feat: gate stable promotion on security qualification instead of pre-release publication #3559 design because they're SARIF-only and not code-reviewed. The gate also counts findings from local SARIF files, so dismissals don't affect it.
Impact / Why This Matters
With strict enforcement on, one unrelated backlog finding holds up stable promotion. The only escape is turning fail-on-static-findings back off, which is the #3552 override that #3559 argues against. That removes the signal for every finding, including new ones, and records nothing about what was reviewed or when it must be fixed.
Proposed Design
Disposition record
A repository file, security-dispositions.yaml, is the single record of security finding decisions. It changes only through PRs, reviewed under CODEOWNERS. Each entry covers one issue and has:
- Findings matched: one or more scanner findings. One entry must be able to cover a group (V5 covers 17 Zizmor findings) without also matching unrelated new findings. The matching key (rule + path, rule + path + line, or scanner fingerprint) is a design decision for this issue. It must survive routine line drift.
- Disposition:
deferredorrejected. The full feat: gate stable promotion on security qualification instead of pre-release publication #3559 design also hasnot-affected, which is out of scope unless the design needs it. - Severity: the scanner's rating and the assessed rating. The SLA uses the assessed rating.
- Reason: a short justification. For rejections, why the finding isn't exploitable or relevant.
- Approver: the maintainer who made the decision.
- First seen: the date the finding was first reported.
- Expires (deferrals only): a date. The default is first-seen plus the SLA for the assessed severity (see below). An earlier date may be set.
Gate behavior
- A HIGH/CRITICAL finding with a matching, current entry doesn't fail the gate.
- A finding with no entry, or whose deferral has expired, fails the gate as it does today.
rejectedentries don't expire.- The gate output and the step summary list every finding covered by an entry: disposition, approver, and expiry. Deferred findings stay visible; they just stop blocking.
- Entries that no longer match any finding are reported as stale so they can be removed.
- Scanner execution errors still fail, unchanged.
Remediation SLAs
Deferral expiry defaults to these fix windows, keyed on assessed severity:
| Severity | Default SLA |
|---|---|
| Critical | 7 days |
| High | 14 days |
| Medium | 30 days |
| Low | 90 days |
The SLAs are published in SECURITY.md or CI.md.
Initial entries
This issue lands with entries for V1–V10, using the decisions in the table above. The expiry dates come from the release milestones, which stand in for the intended release targets:
- V1, V5 (assessed Medium and High): expire 2026-10-06, the due date of the OpenShell 0.1.1 milestone. That is earlier than the SLA dates: 2026-10-08 for V5 (High, 14 days) and 2026-10-24 for V1 (Medium, 30 days).
- V2, V6–V10 (assessed Low): expire 2026-11-17, the due date of the OpenShell 0.2.0 milestone. That is earlier than the 90-day Low SLA (2026-12-23).
- V3, V4: rejected, no expiry.
Two things follow from this. When a release target comes before the SLA date, the target wins. And if a milestone date moves, its deferral dates go stale. Both favor the version-based deferral below.
To explore: version-based deferral
Date expiry only approximates the intent behind "fix in 0.1.1" and "fix before 0.2.0." A version-based deferral would name a release instead: the deferral blocks that release until the finding is addressed. Under that model:
- V1 and V5 would block v0.1.1 from releasing.
- V2 and V6–V10, deferred to
0.1.x, would block no 0.1.x release, but would block v0.2.0. That's how "addressed in any v0.1.x release" would behave.
The first version implements date-based expiry only. The design should note where version-based deferral would plug in: which version the gate compares against (candidate pre-release base or stable target), and how it interacts with RFC 0014 promotion. It should also say whether version and date deferral could coexist on one entry.
Research
- Severity mapping: decide how scanner ratings map onto the four SLA tiers. That covers CodeQL security-severity scores and Trivy and Zizmor HIGH/CRITICAL levels.
- Finding identity: check how stable each scanner's finding identifiers are across runs and code changes. That covers CodeQL
partialFingerprints, Zizmor's SARIF output, and Trivy IDs plus target. Use what you find to pick the matching key.
Acceptance Criteria
-
security-dispositions.yamlexists, with a documented schema covering the fields above. - The release Security Scan gate honors current entries: covered findings don't fail it; uncovered or expired findings do.
- Gate output lists every covered finding with its disposition, approver, and expiry, and flags stale entries.
- The file contains entries for V1–V10 as described above. A re-run of Release Tag at a commit carrying the same findings passes the static-findings gate.
- An expired deferral is shown to fail the gate (unit test or fixture).
- The SLAs and the scanner-to-tier severity mapping are documented in
SECURITY.mdorCI.md. - Version-based deferral is written up as a follow-up: either in this issue's design notes or as a linked issue.
-
CI.mdandarchitecture/build.mddescribe the disposition mechanism.
Alternatives Considered
- Turn
fail-on-static-findingsback tofalseuntil 0.1.1. Rejected in feat: gate stable promotion on security qualification instead of pre-release publication #3559: it removes the signal for new findings as well as known ones, and records no decisions. - Tool-native suppressions only (
.trivyignore.yaml, Zizmor inline ignores, CodeQL query filters). They have no approver, no expiry, and no single place to review..trivyignore.yamlalso forbids accepted risks by its own rule. - GitHub code-scanning dismissals. Not code-reviewed, and the gate reads local SARIF files, so dismissals don't affect it.
- The full feat: gate stable promotion on security qualification instead of pre-release publication #3559 Section 4 design. That version also has
not-affected, requires every native suppression (such as thedeny.tomlignores) to have a matching entry, and plugs into release qualification. Deferred to keep this unblocking change small. This file should be the one the full design extends.
Agent Investigation
- Findings, per-issue severity, impact, and reachability analysis come from Release Tag run 35997548356 at
a8f98ec09. - A separate blocker: in that run,
Release Qualificationsucceeded butReleaseand the other publish jobs were still skipped. GitHub skips a job when any ancestor job failed, and the suite jobs are ancestors throughqualification-result. This is the increment 0 follow-up in the feat: gate stable promotion on security qualification instead of pre-release publication #3559 design, and it needs its own fix. Dispositions alone won't prevent the skip when a non-security suite fails.
Activity
purp commentedon Sep 24, 2026
Security finding dispositions for the release gate (#3671)
Design spec. Issue: #3671, sub-issue of #3559.
Goal
Let maintainers record a reviewed, time-limited decision about a HIGH/CRITICAL static finding (CodeQL, Trivy, Zizmor), so a known backlog stops blocking releases while every deferral still expires and comes back. Anything not covered by a current decision fails the gate exactly as today.
Out of scope: Codex Security findings, cargo-deny advisories, the full #3559 Section 4 design (
not-affected, native-suppression reconciliation, qualification integration), and version-based deferral (written up as a follow-up).Decisions
security-dispositions.tomlat the repo root. TOML because Python's stdlib parses it (tomllib), and the CodeQL runner has no guaranteed YAML parser.deny.tomlis precedent.codex-security.ymlpattern). Release Auto-Tag dispatches with--ref main, so pre-releases readmain's file. A pushed stable tag reads its own. This lets a re-run of an existing tag pick up newly merged dispositions and lets the draft branch be tested by dispatch.sla_exceptionreason.The count cap is accepted for now and flagged for reconsideration in the draft PR. Per-path caps derived from real alert numbers replace the hand-set
maxfrom the earlier draft.File schema
id<rule>/<lowest alert number>, prefixed withcodeql/ortrivy/for those scanners (Zizmor rule IDs already carryzizmor/). Must equal the value derived fromruleandalerts, so it's unique by construction.scannercodeql,trivy, orzizmor.rulerust/cleartext-logging, TrivyKSV-0014or a CVE ID, Zizmorzizmor/template-injection.dispositiondeferredorrejected.scanner_severitycritical,high.assessed_severitycritical,high,medium,low,none.noneonly withrejected.reasonapproverfirst_seencreated_at, UTC date).first_seen_atexpiresfirst_seen+ SLA(assessed_severity), unlesssla_exceptionis set. Forbidden onrejected.sla_exceptionalertsValidation errors (exit 2): TOML syntax errors, unknown or missing fields, wrong types,
expiresrules above, anidthat doesn't match its derived value, an alert number that appears twice across entries, or two entries covering the same scanner + rule + path.Evaluator
tasks/scripts/security_dispositions.py, stdlib only, withtasks/scripts/security_dispositions_test.pyalongside it. It follows thecodex_security_range.pypattern.security-severityof at least 7.0, not suppressed as accepted, rule looked up intool.driverandtool.extensions.determinations.severity == "High"and notignored. The rule iszizmor/<ident>and the path is the location'sverbatim_pathwithout a leading./.trivy-scan.sh gateuses): misconfigurations and vulnerabilities at HIGH/CRITICAL. The rule is the check or CVE ID. The path isTargetfor misconfigurations andPkgNamefor vulnerabilities.--language, only CodeQL rules prefixed<language>/are considered.expireson or after today), and the path's count is within its alert count.expires.::error/::warningannotations.Workflow wiring
In
codeql.yml,workflow-security.yml, andtrivy-scan.yml:security_dispositions.pyandsecurity-dispositions.tomlfrom the workflow revision into$RUNNER_TEMP, with a sparse checkout, before the scanned revision is checked out or used.fail-on-findingsis true, or to a warning when it's false. Exit 2 always fails.--language ${{ matrix.language }}. Keep the existing summary's total counts.trivy-scan.sh gatekeeps producing its table for the summary, but pass/fail comes from the evaluator.trivy-scan-test.shis updated to match.reports/zizmor-high.json..github/CODEOWNERSgetssecurity-dispositions.toml @NVIDIA/openshell-codeowners. Whether branch protection enforces code-owner review is not verified here.Initial entries
From Release Tag run 35997548356 (
v0.1.0-pre.11). Every deferral carries thesla_exceptionabove. The approver ispurpunless changed in review.rust/disabled-certificate-checkzizmor/unpinned-imagesrust/cleartext-loggingzizmor/template-injectionzizmor/github-envzizmor/excessive-permissionsDS-0002KSV-0014rust/cleartext-loggingrust/cleartext-loggingThe "V" labels are this doc's shorthand only. The file uses the derived ids.
first_seenandfirst_seen_atcome from each entry's earliest alert, resolved during implementation.Notes:
mainhas a newzizmor/template-injectionfinding: alert fix(ci): remove legacy wheel publishing machinery #331 atrelease-canary.yml:405, created 2026-09-24. It isn't covered by any entry here, so the next pre-release will fail on it unless it's fixed or a disposition is added. That decision belongs to maintainers.Testing
--languagefiltering;expirespast the SLA with and withoutsla_exception.security-scan.ymlfrom the draft branch withcandidate_ref=v0.1.0-pre.11and confirm the CodeQL, Zizmor, and Trivy static gates pass with the entries in place.mise run pre-commit.Documentation
SECURITY.md: remediation SLAs, and the rule that the clock starts at first detection.CI.md: how to add, extend, or remove a disposition, and how to find alert numbers.architecture/build.md: a short description of the disposition gate.Follow-ups (not in this change)
[disposition.alerts]blocks from the code-scanning API.purp commentedon Sep 24, 2026
I'm going to put up a draft PR as a way to look at the approach. Not excited about this big of a late-breaking change, and probably won't push to land it before 0.1.0 (or maybe at all).
purp commentedon Sep 24, 2026
Had a skim through it and not comfortable trying to land this before 0.1.0, so we'll manage things manually for the 0.1.0 release then revisit this.
github-actions commentedon Oct 12, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.