Triage a security finding¶
./workflow.cmd secure runs pip-audit against the lockfile. When it reports a vulnerability, you have three responses: upgrade, override with an expiry, or accept and document.
Option 1. Upgrade if possible¶
The vast majority of findings have a fixed version available. Check the report:
./workflow.cmd secure.audit
If a fix exists upstream:
uv lock --upgrade-package <vulnerable-package>
./workflow.cmd secure.audit # confirm clean
git commit -am "fix: bump <package> for <CVE-id>"
Option 2. Override with an expiry (recommended for "can't upgrade yet")¶
The .security-overrides file at the project root lists allowed-for-now vulnerabilities with expiry dates. Each entry is a single token — the id and the optional expiry joined by ::, with no justification field:
<VULN_ID>[::YYYY-MM-DD]
The justification goes in a # comment, since anything after the id on the same line is not part of the entry:
# upstream fix pending in v2.3 — see https://github.com/pkg/issue/42
PYSEC-2024-1234::2026-06-01
Malformed entries abort the task with a file:line: <reason> message rather than being silently ignored, so a typo fails loudly instead of quietly un-suppressing (or over-suppressing) a finding. Note the failure direction that matters: a mistyped expiry like ::2020-1-1 is rejected outright rather than being read as a permanent suppression.
After the expiry date, secure.audit fails again, forcing a re-triage. Don't extend overrides without revisiting the underlying issue. Omitting the expiry suppresses the finding permanently — prefer a date, because a permanent entry is never resurfaced.
Option 3. Accept and document (rare)¶
For findings that don't apply to your usage — e.g. the vulnerable code path is gated on a feature you don't use — write an override with a thorough # justification and a far-future expiry. Keep this rare; the next maintainer needs to trust the override list.
Where suppressions can come from¶
Three sources are merged, in this precedence order, and deduplicated by vulnerability id:
| Source | Permanent entries | Leaves a record in the repo |
|---|---|---|
--ignore on the command line |
rejected | no |
AFAS_MCP_SERVER_SECURITY_OVERRIDE env var |
rejected | no |
.security-overrides |
allowed | yes |
The first two must carry an ::YYYY-MM-DD expiry. They are convenient for unblocking a single run, but nothing in the repository records that they were used — so an expiry-less entry there could mute a finding forever with no code change to review. A permanent suppression has to go in .security-overrides, where it shows up in a diff and gets read by whoever touches it next.
Whichever source they come from, the suppressions actually applied are printed with their origin before the audit runs:
Applying 1 suppression(s):
PYSEC-2024-1234 (expires 2026-06-01) from .security-overrides:12
Expired entries are reported too, naming the source, and then ignored — so the finding comes back rather than lingering behind a stale override.
Where the audit runs¶
secure.audit is not only a local command — it gates automatically, which is what makes the expiry dates above meaningful:
- Every CI run — the
securejob in.github/workflows/continuous-integration.yamlruns it alongside lint, test and build. - Daily on a schedule —
.github/workflows/security-audit.yaml. A dependency can turn out to be vulnerable, and an override can expire, with nobody pushing a commit; without the scheduled run a quiet repository would stop being checked and expiry dates would never come due. Trigger it by hand from the Actions tab (workflow_dispatch) to re-check on demand. - Pre-commit — only
secure.validate-overrides, and only when.security-overridesitself changes. That checks the file's syntax, not your dependencies.
Re-running after a triage¶
./workflow.cmd secure
This runs the full security pass: audit, SBOM generation. Audit failures abort the run before SBOM upload.
See also¶
- SBOM and security model — why pip-audit and the SBOM layer together.