This repository contains a Flatcar-specific advisory assistant for two workflows:
- new vulnerability discovery from upstream security sources
- cleanup recommendations for open Flatcar advisory issues using the current Stable production SBOM
It is intentionally conservative. The default mode is read-only, produces machine-readable JSON/YAML, and sends uncertain cases to needs_manual_review.
- Python 3.12, 3.13, or 3.14
- uv for dependency and environment management
- GNU Make (optional, but the
Makefileis the recommended entry point)
Install uv once, for example:
curl -LsSf https://astral.sh/uv/install.sh | shSet up a development environment (creates .venv/, installs the package plus the dev dependency group with ruff, mypy, pytest, pytest-cov, and pytest-mock):
make dev-installOr, using uv directly:
uv sync --group devProduction-mode install without development tooling:
make install
# or: uv syncYAML input/output is exposed as the yaml optional dependency:
uv sync --group dev --extra yamlRun make help to see the full list of targets (test, lint, format, type-check, check, fix, build, clean, ci, all).
Live GitHub issue reads can use anonymous GitHub API access, but authenticated runs are more reliable:
export GITHUB_TOKEN=...Local Foundry runs use Azure OpenAI chat completions with environment-provided credentials. The CLI does not shell out to Azure CLI at runtime; it reads .env or already-exported variables.
GITHUB_TOKEN=ghp_replace_with_your_token
FOUNDRY_BEARER_TOKEN=ey_replace_with_cognitive_services_access_token
FOUNDRY_ENDPOINT=https://<your-resource>.cognitiveservices.azure.com
FOUNDRY_DEPLOYMENT=gpt-5.4
FOUNDRY_EXTRACTION_DEPLOYMENT=gpt-5.4-mini
FOUNDRY_API_VERSION=2024-06-01FOUNDRY_DEPLOYMENT is used for relevance decisions, issue normalization, and cleanup review. FOUNDRY_EXTRACTION_DEPLOYMENT is used for first-pass advisory extraction.
Two Foundry endpoint families are supported:
- Legacy resource endpoints such as
https://<resource>.cognitiveservices.azure.comorhttps://<resource>.services.ai.azure.comcall/openai/deployments/<deployment>/chat/completions?api-version=<version>. Use a dated Azure OpenAI API version supported by that resource. - Foundry project or OpenAI v1 endpoints such as
https://<resource>.services.ai.azure.com/api/projects/<project>or a base URL ending in/openai/v1call/openai/v1/chat/completions, send the deployment name as the requestmodel, and do not send a datedapi-version. SetFOUNDRY_API_VERSION=v1for this mode, or leave the project endpoint to auto-select v1. Unsupported dated values such as2026-03-17are normalized tov1for project/v1 endpoints.
If you are already logged in locally with Azure CLI and GitHub CLI, refresh .env without printing tokens:
uv run scripts/local/populate_foundry_env.pyThe Azure access token is short-lived. Re-run the helper when FOUNDRY_BEARER_TOKEN expires, or replace it manually in .env.
GitHub Models runs require GITHUB_TOKEN. Defaults:
GITHUB_MODELS_MODEL=openai/gpt-5
GITHUB_MODELS_ENDPOINT=https://models.github.ai/inference/chat/completions
GITHUB_MODELS_API_VERSION=2022-11-28GITHUB_MODELS_REASONING_EFFORT is optional and is only sent when set, for models that support that parameter.
If GitHub Models returns 404, check that .env or your shell is not still setting an old model value such as GITHUB_MODELS_MODEL=openai/gpt-5.5. Use openai/gpt-5 unless you have verified another model ID in GitHub Models.
For GitHub Actions using GitHub Models, grant at least:
permissions:
contents: read
issues: read
models: readDiscovery dry run:
uv run scripts/local/run_discovery_dry_run.py \
--source-fixture tests/fixtures/discovery_entries.json \
--issues-fixture tests/fixtures/github_issues.json \
--debug-log reports/discovery-debug.jsonl \
--output reports/discovery-dry-run.mdCleanup dry run:
uv run scripts/local/run_cleanup_dry_run.py \
--issues-fixture tests/fixtures/github_issues.json \
--debug-log reports/cleanup-debug.jsonl \
--output reports/cleanup-dry-run.mdThe wrappers write Markdown to --output and JSON beside it. Use the CLI directly when you want to choose the machine-readable output path.
The security-triage entry point is installed by uv sync. Invoke it via uv run security-triage ... or activate the environment first (source .venv/bin/activate).
Discovery with fixtures:
uv run security-triage discovery \
--source-fixture tests/fixtures/discovery_entries.json \
--issues-fixture tests/fixtures/github_issues.json \
--sbom-fixture tests/fixtures/sbom.json \
--output reports/discovery.json \
--markdown-output reports/discovery.md \
--debug-log reports/discovery-debug.jsonlCleanup with fixtures:
uv run security-triage cleanup \
--issues-fixture tests/fixtures/github_issues.json \
--sbom-fixture tests/fixtures/sbom.json \
--output reports/cleanup.json \
--markdown-output reports/cleanup.md \
--debug-log reports/cleanup-debug.jsonlLive read-only discovery with Microsoft Foundry:
uv run security-triage discovery \
--model foundry \
--window-days 1 \
--oss-security-cache-dir reports/.cache/source-downloads \
--go-vulndb-cache-dir reports/.cache/source-downloads \
--rustsec-cache-dir reports/.cache/source-downloads \
--prompt-cache-dir reports/.cache/prompt-cache \
--output reports/discovery.json \
--markdown-output reports/discovery.md \
--debug-log reports/discovery-debug.jsonlLive read-only cleanup with Microsoft Foundry:
uv run security-triage cleanup \
--model foundry \
--prompt-cache-dir reports/.cache/prompt-cache \
--output reports/cleanup.json \
--markdown-output reports/cleanup.md \
--debug-log reports/cleanup-debug.jsonlLive read-only discovery with GitHub Models fallback:
uv run security-triage discovery \
--model github \
--oss-security-cache-dir reports/.cache/source-downloads \
--go-vulndb-cache-dir reports/.cache/source-downloads \
--rustsec-cache-dir reports/.cache/source-downloads \
--output reports/discovery.json \
--markdown-output reports/discovery.md \
--debug-log reports/discovery-debug.jsonlLive read-only cleanup with GitHub Models fallback:
uv run security-triage cleanup \
--model github \
--output reports/cleanup.json \
--markdown-output reports/cleanup.md \
--debug-log reports/cleanup-debug.jsonlDiscovery defaults to a seven-day processing window. Live sources are source-specific where structure is available:
- Gentoo uses Bugzilla REST bugs plus comments.
- oss-security uses Openwall archive day pages and thread navigation.
- Go uses the canonical Go vulnerability database at
https://vuln.go.dev/index/vulns.jsonplus per-ID OSV JSON. - Rust uses the RustSec advisory database via GitHub REST commit/file metadata and raw advisory Markdown.
For source freshness, discovery prefers the entry's published or creation time over later metadata updates for generic sources, while thread/advisory sources use their updated or modified time so new comments and modified advisories are not missed. Live source entries without a timestamp are skipped by default; include them only for intentional backfills.
discovery and cleanup accept --advisory-repo (env SECURITY_TRIAGE_ADVISORY_REPO) to choose which repository's advisory issues are read. Both default to flatcar/Flatcar when unset, preserving existing standalone behavior. The review commands below require explicit --advisory-repo/--review-repo (or their environment variables) with no implicit default, so a review batch always states exactly which repositories it describes.
security-triage discovery --advisory-repo flatcar/security-triage ...
security-triage cleanup --advisory-repo flatcar/security-triage ...discovery and cleanup are read-only: they produce machine-readable JSON/YAML reports and never write advisory issues. The only supported path for GitHub advisory mutations is the human-gated review workflow:
- Discovery/cleanup produce reports.
review createbuilds a review issue with evidence and task-list checkboxes.- A maintainer reviews the issue and, when satisfied, closes it as Completed.
review applyapplies only the checked, conflict-free actions.
security-triage review render builds the exact review issue title/body(ies) from discovery/cleanup JSON documents and writes them to local Markdown files. It never constructs a GitHub client, never reads GITHUB_TOKEN, and never makes a network call.
security-triage discovery --source-fixture tests/fixtures/discovery_entries.json \
--issues-fixture tests/fixtures/github_issues.json --sbom-fixture tests/fixtures/sbom.json \
--advisory-repo flatcar/security-triage --output reports/discovery.json
security-triage cleanup --issues-fixture tests/fixtures/github_issues.json \
--sbom-fixture tests/fixtures/sbom.json --advisory-repo flatcar/security-triage \
--output reports/cleanup.json
security-triage review render \
--discovery-json reports/discovery.json \
--cleanup-json reports/cleanup.json \
--advisory-repo flatcar/security-triage \
--review-repo flatcar/security-triage \
--run-id local-preview-1 \
--output-dir reports/review-dry-runThis writes one Markdown file per review part (for example
reports/review-dry-run/local-preview-1-part-1.md) plus a
dry-run-summary.md index. Each part file's content after the
<!-- security-triage:dry-run-body-start --> marker is byte-for-byte the
same body review create would submit to GitHub for that part, so this is a
safe way to inspect exactly what a scheduled run would produce before ever
creating anything.
security-triage review create builds the same batch and idempotently creates (or reuses, for a rerun of the same --run-id) one review issue per part, labeled security-triage/review:
security-triage review create \
--discovery-json reports/discovery.json \
--cleanup-json reports/cleanup.json \
--advisory-repo flatcar/security-triage \
--review-repo flatcar/security-triage \
--run-id "$GITHUB_RUN_ID" \
--output reports/review-create.jsonEach review issue contains, per decision group: package/component, CVEs, CVSS, SBOM/existing-issue evidence, the recommendation and rationale, the exact proposed issue title/body or comment, and one or more checkboxes carrying a hidden machine-readable action ID. A single unchecked box means no action. A decision group with more than one checked box fails closed (conflict, skipped, reported). The full manifest needed to apply the batch is embedded, base64-encoded, in a hidden HTML comment; a SHA-256 digest inside it detects accidental corruption.
security-triage review apply re-fetches the review issue fresh (never trusts a webhook payload) and applies only checked, conflict-free, schema-valid actions when the issue's close reason is exactly completed:
security-triage review apply \
--issue-number 123 \
--advisory-repo flatcar/security-triage \
--review-repo flatcar/security-triage \
--enable-all-review-actions \
--output reports/review-apply.jsonClosing as Not planned (or any reason other than completed) makes zero GitHub writes. Apply performs only the fresh read needed to verify the close reason, then exits. Before every mutation, apply re-fetches the target issue, re-checks for a duplicate (for creates) or for open state and matching package/CVE identity (for updates/cleanup), and rebases additive body changes onto the current body using the same guarded field helpers, so existing content is never removed. Comments carry a hidden action-ID marker so reruns never repost. Once every selected group reaches a terminal result, the issue is labeled security-triage/review-applied; reopening and closing it again (or rerunning apply) is then a no-op.
--enable-all-review-actions is shorthand for --enable-create-issues --enable-update-issues --enable-post-cleanup-comments --enable-close-issues. These flags gate mutations through the review-apply path only.
.github/workflows/security-triage.ymlruns discovery/cleanup daily (06:00 UTC) with Microsoft Foundry via GitHub OIDC, then callsreview create. Seedocs/github-actions-foundry-oidc.mdfor the one-time Azure setup (Portal and CLI paths, troubleshooting, and why no client secret is needed)..github/workflows/security-triage-apply.ymltriggers only onissues: closed(plus a manualworkflow_dispatchresume input) and callsreview apply. It never calls Foundry, Azure, or reruns analysis.- Both workflows pin
SECURITY_TRIAGE_ADVISORY_REPO/SECURITY_TRIAGE_REVIEW_REPO(or the equivalent--advisory-repo/--review-repoarguments) to${{ github.repository }}for battle testing in this repository.
Discovery JSON root fields include:
workflow: new_vulnerability_discoveryprocessing_windowsourcesmodelrecordserrors
Cleanup JSON root fields include:
workflow: advisory_cleanup_recommendationsbom_urlissue_queryrecordserrors
Every workflow validates its output contract before writing the document.
Review batches are not a stored document format in the same sense: review render/review create write per-part Markdown (the issue title/body) and a small JSON summary of created/existing issue URLs (--output, default reports/review-create.json); the authoritative, replayable state is the manifest embedded in each review issue itself, not a local file.
Common tasks run through the Makefile:
make test # run pytest with coverage (term, HTML, XML, branch)
make test-quick # run pytest without coverage
make lint # ruff check src/ tests/
make format # ruff format src/ tests/
make format-check # ruff format --check src/ tests/
make type-check # mypy src/ tests/
make fix # ruff format + ruff check --fix
make check # format-check + lint + type-check + test
make ci # alias for check, matches the CI pipeline
make build # uv build (wheel + sdist into dist/)
make clean # remove build/test artifactsEquivalent uv invocations if you prefer to skip Make:
uv run pytest
uv run ruff check src/ tests/
uv run ruff format src/ tests/
uv run mypy src/ tests/
uv buildCoverage output lands in htmlcov/ (HTML), coverage.xml, and the terminal report. Coverage is configured in pyproject.toml; the CLI-oriented pytest.ini mirrors the same options for direct pytest invocations.
The tests cover issue parsing, SBOM parsing, package matching, version comparison, severity/scope labels, discovery guardrails, cleanup guardrails, CLI output, GitHub Models and Foundry request shapes, source fetchers, fixture dry-run behavior, the review manifest/rendering/splitting/checkbox logic, apply-on-close gating and idempotency (using a stateful fake GitHub client), and GitHub Actions workflow policy (triggers, permissions, concurrency, and repository parameterization).
GitHub Actions workflows live in .github/workflows/:
lint.yml: ruff check, ruff format check, and mypy on push and pull request.tests.yml: pytest across Python 3.12, 3.13, and 3.14 on Ubuntu, macOS, and Windows.install.yml: builds the wheel and installs it into an isolateduv venvon all three OSes and Python versions, then verifies thesecurity-triageentry point.security-triage.yml: the guarded discovery and cleanup pipeline itself.release.yml: semantic-release driven versioning, changelog, and GitHub Releases frommain. Configuration lives inrelease.config.js.publish.yml: PyPI publishing template (disabled by default; uncomment when ready to publish).