Skip to content

Add a repeatable script for reproducing customer CVE scans - #2093

Draft
NickJosevski wants to merge 1 commit into
mainfrom
nj/cve-scan-script
Draft

Add a repeatable script for reproducing customer CVE scans#2093
NickJosevski wants to merge 1 commit into
mainfrom
nj/cve-scan-script

Conversation

@NickJosevski

@NickJosevski NickJosevski commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Background

Customers scan the files on their deployment targets. They do not scan this repo or the NuGet graph the give different answers.

dotnet list package --vulnerable walks the NuGet graph and reports build-time reference shims that contribute no runtime assembly. No customer scanner ever sees those shims.

Results

Adds scripts/scan-calamari-cves/scan.sh.

./scripts/scan-calamari-cves/scan.sh                # latest main CI build
./scripts/scan-calamari-cves/scan.sh 2026.3.508     # a specific version
./scripts/scan-calamari-cves/scan.sh --local        # your working tree

The script downloads the published consolidated package, extracts every flavour and RID, prints the bundled .NET runtime version, then scans with Trivy and Grype. Two databases are used because customer scanners disagree with each other.

Results separate total matches from distinct CVEs. The same finding repeats across roughly 43 deps.json files. A customer reporting 42 vulnerabilities is routinely reporting one.

Calamari Distinct CVEs Bundled .NET
2025.3.417 9 6.0.36, end of life November 2024
2026.3.508 1 8.0.29, current patch

@NickJosevski
NickJosevski force-pushed the nj/cve-scan-script branch 2 times, most recently from 8fe03da to e1440bd Compare August 6, 2026 05:40
@NickJosevski

Copy link
Copy Markdown
Contributor Author

Follow-up: how do we become aware of changes?

Having the script in the repo is the right first step, but it only helps someone who thinks to run it. Notes below on what automating it would involve, so this can be picked up later.

The "when" has two answers, not one

Two independent things change the result:

  1. The artifact changes — new build, new bundled runtime. Needs a per-build trigger.
  2. The world changes — a CVE is published against an artifact that hasn't moved. Nothing in the repo changed. Only a schedule catches this.

Driver 2 is the one that bites. The README already records CVE-2026-44788 being absent from the audit source at 10:45 and present by 15:00 on the same day. So whichever option we pick has to include a cron — per-build alone is insufficient.

There is also a third axis the script doesn't cover yet: it scans latest, but customers run old. The versions worth watching on a schedule are the tips of each supported release branch, not just main. That's what actually shows up in customer reports.

It needs state, or it's noise

Running nightly and posting the raw output to Slack means the same 1 medium CVE every morning forever, which gets muted by week two. The alert has to be a diff. Two ways to get one:

  • Commit a baseline — a baseline.json of distinct CVE IDs plus the bundled runtime version, next to the script. The job diffs against it, alerts only on new entries, and opens a PR to update it. Side benefit: git history becomes an audit trail of what we shipped and when.
  • Let GitHub own the state — upload Trivy's SARIF to code scanning. New alert appears, fixed alert auto-closes. No diffing code to write or maintain. Substantially less code than the baseline approach.

Options

Where Pros Cons
A GitHub Actions, schedule + workflow_dispatch, Slack webhook Lives with the script, PR-reviewable. Ubuntu runners have docker; Trivy/Grype DBs can be cached. workflow_dispatch gives us a "customer reported X, scan 2025.3.417" button for free We write and maintain the diff logic. Needs the feedz auth question answered first — the script has no auth header today, so we need to confirm it works anonymously from a runner
B GitHub Actions → SARIF upload to code scanning Dedup and auto-close are handled for us; the Security tab becomes the source of truth. Slack via GitHub's own integration Only Trivy emits SARIF cleanly, so Grype's second opinion needs separate handling. Findings are against a downloaded artifact, so file paths render a little oddly in the UI
C TeamCity scheduled build Feed credentials and the build already live there. Can hang directly off the main publish build for driver 1. A build failure condition gives us alerting for free Config is server-side, so it drifts from the script in this repo. Less visible to the team day to day
D Octopus runbook + scheduled trigger Dogfooding. Version as a prompted variable is a natural fit for the on-demand case. Run history, artifacts, and an existing Slack step template Worker needs docker plus a ~270 MB download per run. Most setup effort. Config lives furthest from the code

Suggested direction

B for the scheduled watch, plus A's workflow_dispatch for the on-demand case — one workflow file covers both. Code scanning removes the state-management problem entirely, which is the part most likely to rot if we hand-roll it. Slack then only fires on genuinely new alerts.

D is worth adding later for the customer-facing ergonomics. "Give me the report for version X" is a much nicer ask as a runbook with a version prompt than as a workflow dispatch, and it's a good dogfooding story.

One addition regardless of platform

Alert on the bundled runtime drifting behind the current patch, independently of whether any CVE is reported against it. It's the leading indicator, it's a one-line comparison, and per the EOL trap documented in the README, a clean scan on an old runtime usually means nobody is publishing advisories for it any more — not that it's safe.

Reproducing a customer CVE report meant re-deriving the same steps every time.

Customers scan the files on their deployment targets. They do not scan this repo or
the NuGet graph. The surfaces give different answers. dotnet list package
--vulnerable walks the NuGet graph and reports build-time reference shims that
contribute no runtime assembly. No customer scanner ever sees those shims. The
discrepancy has been a recurring source of confusion.

The script downloads the published consolidated package, extracts every flavour and
RID, prints the bundled .NET runtime version, then scans with Trivy and Grype. Two
databases are used deliberately. Customer scanners disagree with each other and one
tool is not a baseline.

Results separate total matches from distinct CVEs. The same finding repeats across
roughly 43 deps.json files. A customer reporting 42 vulnerabilities is routinely
reporting one.

Calamari version is usually the answer. Measured 2026-08-01. Version 2025.3.417 has
9 distinct CVEs and bundles .NET 6.0.36. That runtime reached end of life in
November 2024.
Version 2026.3.508 has 1 and bundles 8.0.29.

The README covers reading results. It also records two traps found while building
the script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant