Model requirement — Opus 5 (1M context) or newer
Before doing anything else, confirm the active model is Opus 5 (1M context) (
claude-opus-5[1m]) or a later, more capable model. If it is not, STOP and tell the user to switch with/modeland re-invoke/prisma-taurus— do not proceed on a weaker model.This is a floor, not a pin. A newer or more capable Opus-class model than the one named here satisfies the requirement — say so and continue rather than stopping. Only an older or smaller model (Sonnet, Haiku, or a prior Opus generation) is grounds to stop. The 1M-context variant is part of the floor, not optional — this run reads a ~1000-line SKILL.md, a ~450-line history file and a 300+ row scan CSV, so a 200k-context Opus does not qualify.
If a newer model has made the named floor stale, say so in your first message and proceed — do not edit this file mid-run to bump the version; propose that change to the user as its own commit.
Why: this skill makes real, hard-to-verify decisions — classifying each CVE by
Path, choosing the right fix mechanism, recognizing traps like the Ruby default-gem problem or scanner appeasement, and reasoning about whether a fix will actually land in the image before opening a PR. Weaker models miss these and produce fixes that silently don't work. Skills run on the session's active model (there is no frontmatter field that forces a model), so the model must be set by the user before running.
Overview
End-to-end vulnerability management for the Taurus Docker image. Fetches the latest Prisma Cloud scan, classifies findings into auto-fixable and manual categories, applies all safe fixes, runs unit tests, gets an independent local code review (if a review skill is available) and amends accepted fixes before pushing, then builds the fixed branch into a Docker image and re-scans it to confirm the fixes actually reduce vulnerabilities in the real image. A PR is opened only after that branch scan shows fewer vulnerabilities than the baseline — never at an early stage; the PR then requests a GitHub Copilot review and triages it once.
Why verify before the PR: unit tests do not exercise the Dockerfile (gem/npm/apt changes) and a fix that "looks" applied can be a no-op against the scanner (e.g. gem update installs a patched gem but leaves the vulnerable Ruby default-gem version on disk, which Prisma still reports). The taurus-branch-builder Jenkins job can build any branch into an image and scan it with Prisma — so the fixes are proven in the image before a PR is created, not assumed.
When to Use
- User asks about Taurus CVEs, vulnerabilities, or security scan results
- User wants to fix vulnerabilities found in
blazemeter/taurus - User wants to verify that a previous fix resolved a CVE
- User asks to run or check the Prisma scan for Taurus
Common Mistakes
- Triggering
taurus-community-master(the publish job) without explicit approval — it overwrites the publicunstabletag and notifies the team channel. "I want the CVE gone" is NOT approval to publish; ask, state the side effects, and wait for a yes - Trying to publish with
taurus-branch-builder— it only pushes to internal GCR, never Docker Hub, so its image can't be consumed by taurus-cloud (FROM blazemeter/taurus). Nothing can promote a branch image; onlytaurus-community-masterpublishes - Reporting "nothing to do" when the real answer is "rebuild" — a finding already covered by an unpinned Dockerfile line, whose patch was published after the image was built, needs no code change but is not "nothing". Classify it as R (category 4) and say so
- Triggering a new scan when a valid one already exists (one already ran today (UTC) and after the last
unstablepush) — always check first - Fixing JMeter/Gatling-bundled CVEs — any finding whose
Pathis under/root/.bzt/jmeter-taurus/or/root/.bzt/gatling-taurus/is out of scope. Do not repin jars and do not bumpJMeter.VERSION/Gatling.VERSION(a version bump is a JMeter/Gatling fix). List them only. - Auto-fixing scanner appeasement CVEs (
.deps.json/ Roslyn paths) — these must always be flagged as manual. Note/var/lib/dpkg/statusis NOT appeasement — it's the normal path for every OS/apt package; see category 2 - Assuming a package is uncovered because it's absent from the
--only-upgradelist — it may be installed unpinned elsewhere in the Dockerfile, including as an automatic dependency of something else, which makes the finding R. Prove provenance from/var/log/apt/history.logbefore adding an entry (see "Prove provenance" under OS packages) - Removing the worktree after a test failure — leave it in place for the user to investigate
- Opening the PR before the branch scan confirms a reduction — push the branch, build+re-scan it via
taurus-branch-builder, compare against the baseline, and create the PR ONLY if vulnerabilities went down. The PR comes only after that verification — never early. - Trusting that a Dockerfile fix landed just because the build succeeded — always confirm in the branch scan that each fixed package's vulnerable version is actually gone.
gem updateon a Ruby default gem (e.g.net-imap,erb) installs the patched version but leaves the old default-gem version on disk, which the scanner still reports.
Image target
| Image | Registry | Tag | Artifact |
|---|---|---|---|
blazemeter/taurus |
Docker Hub (public) | unstable |
taurus.csv (CSV) |
unstable is always the latest master build — pushed by Jenkins on every merge to master. This is the image to scan before a release.
Credentials
Read from environment variables — never prompt, never hardcode:
JENKINS_USERNAME # BlazeMeter Jenkins username
JENKINS_TOKEN # Jenkins API token
GITHUB_TOKEN # GitHub personal access token
If any of the three above is missing, stop and tell the user which one is absent.
Jira (used by the ticket-creation step before the PR) is accessed through the Atlassian MCP, not an env var — it authenticates as the developer running the skill (that's how the ticket gets assigned to them). No token to set. Note: interactively-authenticated MCP servers may be absent in headless/cron runs; if the Atlassian tools aren't available, create the PR without a Jira ticket and flag that the ticket must be created manually — don't block the PR.
Fix classification rules
Before applying any fix, classify each CVE — first through the category-0 pre-filter, which keys on Fix Status rather than Path, then by its Path (for everything else the path is the definitive identifier of what the vulnerable component belongs to). Apply the categories in this order — the first match wins, with one exception: a finding that matches category 3 (auto-fixable) must ALSO be tested against category 4 (rebuild-clearable) before any fix is applied. Category 4 is not reached by first-match-wins — it is a mandatory gate on category 3, because an R finding looks exactly like an auto-fixable one and "fixing" it produces a brittle pin for something already handled.
0. Pre-filter — not fixable in a standard build → straight to Z, before any path matching. These are recognised by Fix Status or by the kind of component rather than by directory, so a path-based category would never catch them. Screening them out first is what keeps the OS row of category 3 from being handed something apt can't install:
- Ubuntu Pro ESM — the only fixed version(s) offered end in
+esmNor~esmN(e.g.7:6.1.1-3ubuntu5+esm5). If a non-ESM fixed version is also listed, use that one and do not pre-filter. Not installable without an Ubuntu Pro subscription; plainapt-getfails and--only-upgradecannot reach it. 55 of the 165 OS findings in scan #216 were this (ffmpeg ×39, gnuplot ×7, python-pip ×3, openexr ×3, gst-plugins-bad1.0, qtbase-opensource-src, fonttools) — i.e. the majority of OS findings with afixed inversion. Do not add these to the upgrade list. - Go libraries compiled into a binary —
Pathis/usr/bin/k6(net/http,crypto/tls,encoding/xml, …). Only an upgrade of the k6 apt package can change them, never a library-level fix. - JDK CVEs whose only fix is a later Java major —
Pathunder/usr/lib/jvm/…withFix Statuslikefixed in 22.0.1, 18, 12(no 11.x fix exists). Unreachable byapt --only-upgrade; moving JMeter off OpenJDK 11 is a runtime-compatibility decision, not a CVE-run change. - The distro (apt)
pip—Path/usr/lib/python3/dist-packages/pip-*.dist-info. Noble's pip fixes are+esmNor a pip version apt doesn't carry.
Note the distinction from category 4: these have no reachable fix at all (monitor), whereas an R finding's fix exists and lands by itself on a rebuild. Both mean "no code change", for opposite reasons. Full rationale in
vulnerability_history.md→ "Findings that are NOT fixable in a standard build".
1. JMeter / Gatling bundled → DO NOT FIX, monitor only:
Pathcontains/root/.bzt/jmeter-taurus/(e.g./root/.bzt/jmeter-taurus/5.5/lib/tika-core-1.28.3.jar)Pathcontains/root/.bzt/gatling-taurus/(e.g./root/.bzt/gatling-taurus/3.9.5/lib/netty-codec-http-4.1.92.Final.jar)
These jars (netty, log4j, tika, batik, xstream, jackson, logback, pebble, dnsjava, json-smart, json-path, commons-*, etc.) ship inside the JMeter/Gatling distributions. Taurus does not pin them individually, and we do not remediate them — not by replacing jars, and not by bumping the JMeter or Gatling version (a version bump is itself a JMeter/Gatling fix, only partially clears the CVEs, and carries runtime/behavioral risk). Display them in the report under "JMeter/Gatling — not fixed (out of scope)", never patch them.
2. Scanner appeasement → flag as manual, never auto-fix:
Pathcontains/usr/share/dotnet/sdk/(Roslyn / .deps.json metadata patches)
⚠️
/var/lib/dpkg/statusis NOT in this category — it is the normal path for every OS/apt package. This scanner reports all OS findings at/var/lib/dpkg/status(verified: scans #204, #215, #216 contain zero empty-path rows and 135/153/165 dpkg-path rows respectively). Treating that path as appeasement writes off every OS finding in the scan — in #216 that would have been 165 of 316 rows, including 16 with a real, non-ESMfixed inapt version (71 before removing Ubuntu Pro ESM). Route these to the OS package row of category 3 (then the category-4 gate), exactly like the libheif worked example in category 4 — whose finding also sat at/var/lib/dpkg/status.What genuinely belongs here is the action, not the path: do not
sed-patch/var/lib/dpkg/statusto change a version string and silence the scanner (see the Firefox patch atDockerfile:118-121— the pattern not to repeat;vulnerability_history.mdcommit 5 argues it should not have been done). A Prisma Cloud suppression is the preferred route for a genuine false positive; the sed is a gated last resort, reachable only through step 17.
3. Auto-fixable → apply automatically (remaining CVEs where Fix Status starts with fixed in):
⚠️ Gate first: before applying anything from this table, run the category-4 (rebuild-clearable) check below. If the finding is R, it needs no change at all — stop here for that finding.
| Path pattern | Fix type | Where to fix |
|---|---|---|
/root/.bzt/newman/node_modules/ |
npm override | Dockerfile newman package.json printf block |
/root/.bzt/selenium-taurus/mocha/node_modules/ |
npm override | Dockerfile mocha package.json printf block |
/root/.bzt/selenium-taurus/*/node_modules/ |
npm direct package | bzt/modules/javascript.py PACKAGE_NAME constant |
/usr/lib/node_modules/npm/node_modules/ |
npm-internal bundled dep | Check the latest npm's lockfile: if it bundles the fixed version → R (the existing npm i -g npm@11 picks it up on rebuild); if it still bundles the vulnerable version → monitor (no upstream fix reachable). There is no clean in-place override (see "npm-internal bundled deps" section) |
/usr/local/rbenv/ or /usr/local/lib/ruby/ |
Ruby gem | Dockerfile gem install <gem> -v <fixed> + default-gem/cache cleanup (see Ruby gems section) |
/var/lib/dpkg/status (the usual case), OS path (/usr/lib/, /lib/), or empty path — excluding /usr/lib/node_modules/npm/node_modules/ (npm-internal, see row above) |
OS package | Apply the category-0 pre-filter, then the category-4 (R) gate — provenance before date (see step 0 of the OS packages section). Most OS findings are R. If not R → add the package to the Dockerfile's existing unpinned --only-upgrade list (see OS packages section) |
Python pkg, Path under /usr/local/lib/python3.x/... |
Python package | requirements.txt (feeds the wheel's install_requires) or a dedicated Dockerfile pip install step — classify into 5 cases, see Python section |
Python pkg, Path under /usr/lib/python3/dist-packages/... |
distro Python (apt) | Dockerfile apt-get install --only-upgrade/apt-get purge (NOT requirements.txt) |
Note: the
/root/.bzt/jmeter-taurus/*and/root/.bzt/gatling-taurus/*paths are intentionally absent from this auto-fixable table — they belong to category 1 (do not fix). Do not reintroduce JMeter/Gatling jar fixes or version bumps.
CVEs where Fix Status is not fixed in ... (e.g. needed, deferred, or empty) → display only, do not attempt to fix.
4. Rebuild-clearable (R) → NO code change; a republish of current master clears it.
A finding can be fixed in <v> and fully handled by the repo already, yet still show up — because the image is older than the patch. The Dockerfile resolves several things at build time rather than pinning them (every unpinned apt-get install — whose automatic dependencies are resolved fresh too — plus apt-get install --only-upgrade <pkgs> with no version pins, npm i -g npm@11, bzt -install-tools, FROM ubuntu:24.04), and Jenkinsfile builds with --no-cache. So identical code produces a different image at a different time. Nothing to fix — the fix is already in the Dockerfile; the published image just predates it.
Classify a finding as R when ALL of these hold:
Fix Statusstarts withfixed in, and- the package is already covered by an unpinned line — it's in the
--only-upgradelist, or is installed unpinned by any earlierapt-get installin the Dockerfile, including as an automatic dependency that is never named there (prove this per step 0(a) of the OS packages section; this is the source most often missed), or comes fromnpm i -g,bzt -install-tools, or the base image — and - a release containing the fix exists upstream now and the unpinned line would pick it up on a rebuild, and
- the fixed version was published after the scanned image's build started.
R vs monitor — the distinguishing test. Both mean "no code change", so keep them apart: the fix exists now and an existing unpinned line would install it → R (a rebuild clears it). The fix does not exist in any release that line would pick up → monitor (nothing clears it yet). Example:
undiciwas monitor while npm bundled the old version, and became R once an npm release bundled the fix. In scan #203 the npm-internal findings (ip-address, brace-expansion, undici, tar) were monitor, because npm 11.19.0 and 12.0.2 both still bundled the vulnerable versions.
Compare against the BUILD START, not the push. The publishing pipeline runs the Prisma scan and the full integration suite between docker build and docker image push — 30–60 min. A patch published 20 minutes before the push is still absent from the image, so using the push time misclassifies it as "not R" and sends you down the pin path.
# (a) build-start time of the build that produced the scanned image
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/taurus-community-master/api/json?tree=builds%5Bnumber%2Ctimestamp%2Cresult%5D" \
| python3 -c "import json,sys,datetime; d=json.load(sys.stdin); [print(b['number'], datetime.datetime.utcfromtimestamp(b['timestamp']/1000)) for b in d['builds'][:5]]"
# (b) when the fixed version hit the Ubuntu pocket — OS packages only.
# <pkg> must be the BINARY package name (Prisma reports the SOURCE name:
# source `libheif` ships binary `libheif1`; `binary_name=libheif` returns NOTHING).
curl -s "https://api.launchpad.net/1.0/ubuntu/+archive/primary?ws.op=getPublishedBinaries&binary_name=<binary-pkg>&exact_match=true&version=<fixed-version>&status=Published" \
| python3 -c "import json,sys; [print(e['pocket'], e['date_published']) for e in json.load(sys.stdin)['entries']]"
Expect several rows (one per architecture, and per Security/Updates pocket) — that's normal; read the earliest date_published. Get the binary name from the Dockerfile's --only-upgrade list (that's the name apt installs).
Evidence required per source — an R claim without evidence is not allowed:
| Source | Evidence that the fix exists and post-dates the build |
|---|---|
| OS package (apt) | Launchpad date_published (above) |
npm-internal (npm i -g npm@11) |
the npm/cli lockfile check in the "npm-internal bundled deps" section — confirm the latest npm bundles the fixed version |
bzt -install-tools npm packages |
the registry's published version + its release date |
base image — only after dpkg-query confirms the base image actually ships the package (step 0(a) of the OS packages section); otherwise use the "OS package (apt)" row |
the ubuntu:24.04 tag's own push time on Docker Hub |
Decide — and bias every ambiguity toward changing nothing:
- fix published after build start → R. Report it; do not add a version pin (brittle — the build breaks when Ubuntu supersedes that exact version and drops it from the pocket).
- fix published before build start and still flagged → NOT R. The fix isn't landing (stale layer, or the package isn't in the unpinned list). Treat it as auto-fixable and act — add the package to the existing unpinned list.
- evidence missing, empty (
entries: []), or the date falls inside the build→push window → INCONCLUSIVE. Classify as monitor, change nothing, and say it was inconclusive. Never resolve ambiguity by adding a pin.
Worked example (2026-08-20, scan #203): libheif 1.17.6-1ubuntu4.6 flagged, fixed in 1ubuntu4.7. libheif1 was already in the unpinned upgrade list, and all three timestamps line up in this order on 2026-08-19: the image's build started 12:06:22Z (taurus-community-master #14894), it was pushed 12:30:07Z, and only then did noble-security publish the fix at 12:36:56Z — i.e. the patch did not yet exist while the image was being built, so no version of that image could contain it. Correct action: zero code changes, republish. Confirmed by scan #204 on the republished image: 295 → 294, libheif gone, nothing new.
Steps
1. Verify credentials
: "${JENKINS_USERNAME:?JENKINS_USERNAME is not set}" && \
: "${JENKINS_TOKEN:?JENKINS_TOKEN is not set}" && \
: "${GITHUB_TOKEN:?GITHUB_TOKEN is not set}" && \
echo "credentials OK"
2. Check if last scan is still valid
curl gotcha (
-g): Jenkinstree=queries contain[and], which curl treats as a glob range — an un-escaped one fails withbad range in URL. The commands below URL-encode the brackets (%5B/%5D) and so are safe as written. If you ever write the brackets literally (as the reference table does for readability), add-g(globoff):curl -sS -g -u "$JENKINS_USERNAME:$JENKINS_TOKEN" ".../api/json?tree=foo[bar]".
Get the timestamp when unstable was last pushed to Docker Hub:
curl -s "https://hub.docker.com/v2/repositories/blazemeter/taurus/tags/unstable" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d['last_updated'])"
Find the most recent successful Jenkins scan for taurus:unstable:
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/prisma-cloud-ondemand-scan/api/json?tree=builds%5Bnumber%2Ctimestamp%2Cresult%2Cactions%5Bparameters%5Bname%2Cvalue%5D%5D%5D"
Look for the most recent build where:
resultisSUCCESS- The
IMAGE_URLparameter value isblazemeter/taurus:unstable
The scan is valid only if BOTH are true:
- Scan timestamp is after the
unstableimage push timestamp (correctness — the scan must reflect the current master image) - Scan ran today (same UTC calendar day as now) — freshness, since CVE feeds update daily and a re-scan of an unchanged image surfaces newly-disclosed CVEs
If valid → skip to step 4 using that build number.
Otherwise (no scan today, even if the image is unchanged) → continue to step 3 and trigger a fresh scan.
First check if a scan is already running:
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/prisma-cloud-ondemand-scan/api/json?tree=builds%5Bnumber%2Ctimestamp%2Cresult%2Cbuilding%2Cactions%5Bparameters%5Bname%2Cvalue%5D%5D%5D"
If the most recent build has building: true and IMAGE_URL is blazemeter/taurus:unstable → tell the user a scan is already running, wait for it (poll every 20 seconds), then use that build's result. If it finishes with FAILURE → stop and give the user the console URL.
3. Trigger a new scan (one time only — no retries)
curl -s -o /dev/null -D - -X POST \
-u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/prisma-cloud-ondemand-scan/buildWithParameters?IMAGE_URL=blazemeter/taurus:unstable"
Capture the Location: header — it contains the queue item URL (e.g. .../queue/item/XXXXX/).
Wait for build number by polling the queue item every 10 seconds until executable.number appears:
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/queue/item/<ID>/api/json"
Wait for the build to finish by polling every 20 seconds. Tell the user the build number and that you are waiting (typically 10–20 minutes):
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/prisma-cloud-ondemand-scan/<BUILD>/api/json?tree=building,result"
If result is FAILURE → stop and give the console URL:
https://blazect-jenkins.blazemeter.com/job/prisma-cloud-ondemand-scan/<BUILD>/console
4. Fetch the scan artifact
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/prisma-cloud-ondemand-scan/<BUILD>/artifact/taurus.csv"
5. Parse, classify, and display
Order by severity: critical → high → medium → low/unassigned, then by CVSS descending within each severity.
Display a table with columns:
- CVE ID
- Severity
- CVSS
- Package + Version
- Fix Status
- Fix Type (
auto-fix/rebuild-clearable/jmeter-gatling (out of scope)/manual/no fix available)
Reporting rule — lead with "fixable detected → fixed", never the raw total
The raw scan total (e.g. 290) is misleading as a headline: the large majority of findings are things this repo can never fix (JMeter/Gatling bundled jars incl. every critical, k6 binary internals, Ubuntu ESM, no-released-patch), so "fixed 20 of 290" makes a complete run look like it did almost nothing. Every report and PR must lead with what taurus could actually act on — not the raw total. (The Jira ticket is the exception: it is a task, not a report — see step 15's field table. It states the problem and what to do, never a <X> detected → <Y> fixed summary.)
Headline (use in the run report and the PR body — not the Jira ticket):
Fixable in taurus, this run:
<X>detected →<Y>fixed. Rebuild-clearable:<R>— no code change needed; a republish of current master clears these (omit the line whenR = 0).
Counting basis — X and Y are distinct CVEs:
- X = distinct in-scope fixable-in-taurus CVEs at baseline:
Fix Statusstarts withfixed in, AND NOT JMeter/Gatling, AND NOT Ubuntu Pro ESM (only fixed version(s) ending+esmN/~esmN), AND NOT a binary-internal lib (k6 Go libs under/usr/bin/k6), AND NOT distro pip apt can't reach, AND NOT a JDK finding whose only fix is a later Java major (/usr/lib/jvm/…,fixed in 22.0.1, 18, 12— no 11.x fix exists), AND NOT scanner-appeasement (.deps.json/Roslyn — those are manual, classification category 2, and belong in Z). A/var/lib/dpkg/statuspath does NOT exclude a finding from X — that is the normal OS/apt path, so such findings are in scope and usually resolve as R (see category 2). (Includes the items we then defer/skip for cause — setuptools-vendored, breaking npm major jumps, low-value — so the denominator is honest about what was considered.) - Y = the subset fixed by a change made this run to this repo's own
Dockerfile/requirements.txt, whose old version is confirmed gone in the branch re-scan. A finding that cleared without any change this run (an existing unpinned line or annpm@11bump picking the fix up) is R, not Y — even though the branch scan shows it gone, which it will either way. IfY < X, list which didn't land and why.
Every finding lands in exactly one bucket so nothing is silently omitted:
- Fixed (Y) — verified gone in the branch image.
- Fixable but did NOT land (X − Y) — deferred/skipped for cause, or attempted-but-failed. R (rebuild-clearable) is a labeled subset of this bucket, not a fourth bucket — an R finding is in X (it is fixed-in and in scope) and not in Y (no code change landed it this run), but it is NOT "deferred for cause": label it "clears on rebuild, no code change". So
X,YandZstill partition every finding;Ronly tags some ofX − Y, and must never be added to X, Y or Z as an extra count. - Not actionable in this repo (Z) — collapse into ONE categorized count, never a per-CVE scoreboard: monitor-only JMeter/Gatling bundled jars (incl. criticals), k6 binary internals, JDK-later-major-only, distro pip, no-released-patch (open/needed/deferred), Ubuntu ESM, scanner-appeasement/manual (
.deps.json/Roslyn). (In-scope OS/apt findings at/var/lib/dpkg/statusbelong in X, not here.)
taurus IS the base image — there is no "base image tag bump" and no "base-owned (cleared by a base tag)" bucket; those exist only in taurus-cloud (which builds FROM blazemeter/taurus). Omit them here.
Show the raw all-severity total only as a parenthetical for reconciliation, with at least crit/high (e.g. "raw scan: baseline 290 → branch 269 [crit 4→4, high 44→44]"). X/Y are distinct CVEs; the raw total counts per-occurrence and is much larger — do NOT assert an arithmetic identity between them. Treat the raw total as context, not a checksum.
Presentation only. This framing changes reporting, not the decision gate: step 15's gate still keys on the all-severity branch-scan total going down, not on X/Y.
You may still display the full per-CVE table (ordered critical → high → medium → low, CVSS desc) for completeness, but the summary sentence above the table must use the fixable-detected/fixed framing.
6. Collect auto-fixable CVEs
Gather all CVEs where:
Pathis NOT under/root/.bzt/jmeter-taurus/or/root/.bzt/gatling-taurus/(those are out of scope — category 1)Fix Statusstarts withfixed in- Path does NOT match scanner appeasement patterns (
/usr/share/dotnet/sdk/only — not/var/lib/dpkg/status, which is the normal OS/apt path; see category 2) - The only fixed version(s) do NOT end in
+esmN/~esmN(Ubuntu Pro ESM —apt-getcannot install it), and the finding is not k6-binary-internal, JDK-later-major-only or distro pip (see category 0)
If there are no auto-fixable CVEs → report that and list any manual items. ("No auto-fixable CVEs" means after the category-4 gate has run: R findings start out looking auto-fixable and are removed from that set by the gate, so a run can legitimately reach this branch with R > 0.)
Before stopping, report any R (rebuild-clearable) findings — see classification category 4. "Nothing to fix in the repo" and "a republish removes N CVEs" are both true at the same time, and stopping without mentioning R leaves a real, zero-code win on the table (and leaves taurus-cloud consuming a stale image). If R > 0, report it and follow "Republishing to clear R findings" below instead of just stopping.
7. Apply all auto-fixes
Work in priority order: critical → high → medium → low. For each CVE apply the fix according to the classification table above.
Before adding new fixes — prune stale ones
Past runs left temporary fixes in the Dockerfile and requirements.txt: npm overrides blocks, forced gem install + default-gem purges, apt-get install --only-upgrade pins, .deps.json/dpkg sed patches, and any pip install step outside the normal requirements install. Before adding anything new, re-evaluate each one:
- Does the CVE it was added for still appear in the current baseline scan (step 5)? If the parent package or base image now ships the fixed version (the CVE is gone from the baseline), remove that line on the fix branch. The branch re-scan (step 14) is the safety net — if a removal regresses, the scan catches it before the PR.
- Is the entry inert? An
--only-upgradeentry for a package that is also installed unpinned earlier in the same stage can never fire in the--no-cachemaster build (see step 7's OS-packages step 0). Confirm with the provenance check, and note that the branch re-scan cannot validate this one — an inert entry and a working one look identical in the scan. Verify by re-reading/var/log/apt/history.logon the rebuilt image instead. - If the CVE is still present → leave the fix in place.
Check vulnerability_history.md for any recorded removal condition before deciding.
Convention — mark every temporary fix with its removal condition. Each temporary fix this skill adds must carry a # remove when <upstream condition> comment (e.g. # remove when nodejs ships npm with glob >= 11.1.0) so a future run can find and re-evaluate it. Where the comment goes depends on the fix:
- Dockerfile
RUNsteps (gem installs, apt upgrades, sed patches, extrapip install) → put the# remove when …line directly above theRUN. - JSON-based fixes (npm
overridesseeded via aprintf'dpackage.json) → JSON has no comment syntax, so put the marker on the surrounding DockerfileRUN/echoline that writes the file, and record the per-package removal conditions invulnerability_history.md.
A self-pruning runtime guard — like the Newman NEWMAN_VERSION = 6.2.2 check that skips the override once Newman moves — satisfies this too. When the condition is non-obvious, also record it under the relevant recipe in vulnerability_history.md.
Python packages — classify by Path (not every Python fix is requirements.txt)
Decide the mechanism from the finding's Path:
- Pinned in
requirements.txt—Pathunder/usr/local/lib/python3.x/...and the package name is inrequirements.txt→ bump the existing pin. Extract the fixed version fromFix Status(fixed in 2.7.0→2.7.0):urllib3==2.6.3 → urllib3==2.7.0 - Transitive dep not in
requirements.txt—Pathunder/usr/local/lib/python3.x/...but the package name is absent fromrequirements.txt→ add an explicit pin torequirements.txt. It feeds the wheel'sinstall_requires(seesetup.py), so the Dockerfile'spip install /tmp/bzt*.whlstep resolves the fixed version into the image. If the requirements resolver can't accommodate the pin, use a dedicated Dockerfilepip installstep instead (case 3). Do not rely onPipInstaller.pip_constraints(bzt/modules/services.py) — that constraints file only governs packages bzt installs at runtime (pip install -t <target>), not the image site-packages the scanner reads. - Resolver constraint blocks the pin — another dependency caps the version, so re-resolving
requirements.txtfails → add a dedicatedpip install <pkg>==<fixed>step in the Dockerfile right after the BZT-install pip step, with a# remove when <condition>marker (see the pruning convention above). - Vendored inside setuptools —
Pathcontains/setuptools/_vendor/...→ cannot be pinned directly; only bumping the pinnedsetuptools==inrequirements.txtclears it. Bump setuptools if a patched, compatible version exists; otherwise skip a low-value vendored finding and list it. - System / distro Python —
Pathunder/usr/lib/python3/dist-packages/...→ not arequirements.txtfix; the package came fromapt, not pip. Upgrade or remove it in the Dockerfile (apt-get install --only-upgrade <pkg>orapt-get purge <pkg>), exactly like an OS package.
JMeter / Gatling jars — DO NOT FIX
Any CVE whose Path is under /root/.bzt/jmeter-taurus/ or /root/.bzt/gatling-taurus/ is out of scope (classification category 1). Do not:
- replace or repin individual bundled jars, and
- bump
JMeter.VERSION/Gatling.VERSIONinbzt/modules/jmeter.py/bzt/modules/gatling.pyto refresh them.
A version bump is itself a JMeter/Gatling change: it only partially clears the jar CVEs (e.g. netty would still be below the fixed 4.1.135), carries runtime/behavioral risk that unit tests don't catch, and can trip the coverage gate. Simply list these findings in the report under "JMeter/Gatling — not fixed (out of scope)" and move on. (The JarTool constants in bzt/modules/java/tools.py — TestNG, JUnit, etc. — are also JMeter-side tooling; leave them alone too.)
npm direct packages (bzt/modules/javascript.py)
Each npm tool has a PACKAGE_NAME constant:
class Mocha(NPMPackage):
PACKAGE_NAME = "mocha@11.7.5" # versioned — bump after @
class Newman(NPMPackage):
PACKAGE_NAME = "newman" # UNVERSIONED — tracks npm latest; not bumped here
For a versioned constant (e.g. Mocha), bump the version after @ to the fixed version from Fix Status. Newman's PACKAGE_NAME has no version ("newman"), so it tracks npm's latest and can't be pinned here — its transitive-dep CVEs are handled by the Dockerfile overrides block (guarded on Newman 6.2.2), see the npm overrides section below. (Confirm the actual constant values in bzt/modules/javascript.py before editing — they change as packages are bumped.)
npm transitive dependency overrides (Dockerfile)
When a CVE is in a transitive dependency of Newman or Mocha that cannot be fixed by bumping the parent package, use npm overrides.
Newman (/root/.bzt/newman/node_modules/):
Find the existing printf block in the Dockerfile that seeds /root/.bzt/newman/package.json. It looks like:
RUN npm install newman --prefix /tmp/newman-check --silent \
&& NEWMAN_VERSION=$(node -p "require('/tmp/newman-check/node_modules/newman/package.json').version") \
&& rm -rf /tmp/newman-check \
&& if [ "$NEWMAN_VERSION" = "6.2.2" ] && [ ! -f /root/.bzt/newman/package.json ]; then \
echo "Applying Newman dependency overrides for CVE fix (...)" \
&& mkdir -p /root/.bzt/newman \
&& printf '{\n "overrides": {\n "existing-pkg": "^X.Y.Z",\n ...\n }\n}\n' > /root/.bzt/newman/package.json; \
...
fi
Add the new package to the overrides object inside the printf string. Update the echo message to mention the new package.
Mocha (/root/.bzt/selenium-taurus/mocha/node_modules/):
Find the printf block that seeds /root/.bzt/selenium-taurus/mocha/package.json and add the override there.
npm-internal bundled deps (Dockerfile) — /usr/lib/node_modules/npm/node_modules/
Deps bundled inside the global npm CLI itself (undici, glob, cross-spawn, …), installed by npm i -g npm@N. The only safe fix is to bump npm; otherwise monitor.
- Bump npm. Check what the latest npm bundles (it's pinned in npm's own lockfile):
If the latest npm bundles the fixed version → the existingcurl -s https://raw.githubusercontent.com/npm/cli/v<NPM_VERSION>/package-lock.json \ | python3 -c "import json,sys;d=json.load(sys.stdin);print(d['packages'].get('node_modules/<dep>',{}).get('version'))"npm i -g npm@11clears it on rebuild. - If the latest npm still bundles the vulnerable version → MONITOR. Do not try to patch it in place:
cd /usr/lib/node_modules/npm && npm install <dep>@<fixed>does not work — it makes npm re-resolve its ownpackage.json, which references unpublished workspace deps (@npmcli/docs), and the build fails with a registry404. Verified failing in taurus build #571 and taurus-cloudMOB-51270build #1.- The only mechanism that would replace the dep is copying a temp-installed folder over npm's bundled one — that's the brittle "manual unpack" the cross-spawn lesson (commit #4) warns against. Don't.
- So the correct action is monitor: re-check each run; the fix lands automatically once an npm release bundles the patched dep and
npm i -g npm@11picks it up.
.NET SDK bump (Dockerfile)
When a CVE is in a .NET SDK component:
Fetch the release metadata to find the latest 8.x SDK with security fixes:
curl -s "https://dotnetcli.azureedge.net/dotnet/release-metadata/8.0/releases.json" \ | python3 -c " import json,sys data = json.load(sys.stdin) latest = data['releases'][0]['sdk'] print('version:', latest['version']) print('url linux-x64:', [f['url'] for f in latest['files'] if f['rid']=='linux-x64' and f['name'].endswith('.tar.gz')][0]) print('hash:', [f['hash'] for f in latest['files'] if f['rid']=='linux-x64' and f['name'].endswith('.tar.gz')][0]) "Update the
DOTNET_URLandDOTNET_SHA512values in the DockerfileFROM system-deps AS runtimesstage.Update ALL hardcoded SDK version strings in the
sedcommands below (the.deps.jsonmetadata patch lines) to match the new SDK version number.
OS packages (Dockerfile)
When a CVE is in a system package (path is /var/lib/dpkg/status, empty, or in /usr/, /lib/, /bin/):
Run the category-4 (R) gate first — in this order, because (a) selects which evidence row (b) uses. Only continue past this step if the finding is NOT R.
(a) Prove provenance — where is this package actually installed from? Do NOT reason from the Dockerfile text alone. R requires the package to be "already covered by an unpinned line", and absence from the
--only-upgradelist does not mean absence of coverage: a package can be installed unpinned by any earlierapt-get install, including as an automatic dependency that is never named in the Dockerfile. Ask the image, not the source:# confirm you are inspecting THE SCANNED image first — compare against the CSV's `Image ID` # column; `unstable` is a mutable tag and may have been republished since the baseline scan. docker image inspect --format '{{.Id}}' blazemeter/taurus:unstable # which apt invocation installed it, at what version, and was it "automatic"? docker run --rm --entrypoint sh blazemeter/taurus:unstable -c \ "grep -B4 '<pkg>:amd64' /var/log/apt/history.log | tail -8" # and is it in the base image at all? (also a mutable tag — reason about tod
…(truncated)