| name | prisma-taurus |
| description | Use when the user wants to check, fix, or verify Prisma Cloud vulnerability scan results for the Taurus Docker image (blazemeter/taurus:unstable). Run this skill on Opus 4.8 (1M context) — choosing and applying the correct CVE fix requires strong reasoning. |
Model requirement — run on Opus 4.8 (1M context)
Before doing anything else, confirm the active model is Opus 4.8 (1M context) (claude-opus-4-8[1m]). If it is not, STOP and tell the user to switch with /model (choose Opus 4.8) and re-invoke /prisma-taurus — do not proceed on a smaller model.
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, and finally posts a "ready for review" summary to Slack (best-effort).
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 a new scan when a valid one already exists (one already ran today (UTC) and after the last
unstable push) — always check first
- Fixing JMeter/Gatling-bundled CVEs — any finding whose
Path is under /root/.bzt/jmeter-taurus/ or /root/.bzt/gatling-taurus/ is out of scope. Do not repin jars and do not bump JMeter.VERSION / Gatling.VERSION (a version bump is a JMeter/Gatling fix). List them only.
- Auto-fixing scanner appeasement CVEs (
.deps.json, /var/lib/dpkg/ paths) — these must always be flagged as manual
- 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 update on 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
JENKINS_TOKEN
GITHUB_TOKEN
If any of the three above is missing, stop and tell the user which one is absent.
Optional — Slack notification (step 17 only):
SLACK_BOT_TOKEN
SLACK_SPARTA_CHANNEL_ID
These two are optional and best-effort: if either is unset, the skill skips the Slack post (step 17) and continues — it never blocks the run and never prompts for them. (The bot must be a member of the channel, or the post returns not_in_channel.)
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 by its Path (the path is the definitive identifier of what the vulnerable component belongs to). Apply the categories in this order — the first match wins:
1. JMeter / Gatling bundled → DO NOT FIX, monitor only:
Path contains /root/.bzt/jmeter-taurus/ (e.g. /root/.bzt/jmeter-taurus/5.5/lib/tika-core-1.28.3.jar)
Path contains /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:
Path contains /usr/share/dotnet/sdk/ (Roslyn / .deps.json metadata patches)
Path contains /var/lib/dpkg/ (OS package version string manipulation)
3. Auto-fixable → apply automatically (remaining CVEs where Fix Status starts with fixed in):
| 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 | bump global npm (npm i -g); if the latest npm still bundles the vulnerable version → monitor. 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) |
empty path or OS path (/usr/lib/, /lib/) — excluding /usr/lib/node_modules/npm/node_modules/ (npm-internal, see row above) | OS package | Dockerfile apt-get install --only-upgrade |
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.
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): Jenkins tree= queries contain [ and ], which curl treats as a glob range — an un-escaped one fails with bad 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:
result is SUCCESS
- The
IMAGE_URL parameter value is blazemeter/taurus:unstable
The scan is valid only if BOTH are true:
- Scan timestamp is after the
unstable image 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 / 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, Jira ticket, and PR must lead with what taurus could actually act on — not the raw total.
Headline (use everywhere):
Fixable in taurus, this run: <X> detected → <Y> fixed.
Counting basis — X and Y are distinct CVEs:
- X = distinct in-scope fixable-in-taurus CVEs at baseline:
Fix Status starts with fixed in, AND NOT JMeter/Gatling, AND NOT Ubuntu Pro ESM (+esmN/~esm1), AND NOT a binary-internal lib (k6 Go libs under /usr/bin/k6), AND NOT distro pip apt can't reach, AND NOT scanner-appeasement (.deps.json/Roslyn, /var/lib/dpkg — those are manual, classification category 2, and belong in Z). (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 whose old version is confirmed gone in the branch re-scan through this repo's own
Dockerfile / requirements.txt (a change made this run, or an existing unpinned line that picked up the fix). If Y < 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.
- 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, no-released-patch (open/needed/deferred), Ubuntu ESM, scanner-appeasement/manual (
.deps.json/Roslyn, /var/lib/dpkg).
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:
Path is NOT under /root/.bzt/jmeter-taurus/ or /root/.bzt/gatling-taurus/ (those are out of scope — category 1)
Fix Status starts with fixed in
- Path does NOT match scanner appeasement patterns
If there are no auto-fixable CVEs → report that and list any manual items, then stop.
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.
- 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
RUN steps (gem installs, apt upgrades, sed patches, extra pip install) → put the # remove when … line directly above the RUN.
- JSON-based fixes (npm
overrides seeded via a printf'd package.json) → JSON has no comment syntax, so put the marker on the surrounding Dockerfile RUN/echo line that writes the file, and record the per-package removal conditions in vulnerability_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 — Path under /usr/local/lib/python3.x/... and the package name is in requirements.txt → bump the existing pin. Extract the fixed version from Fix Status (fixed in 2.7.0 → 2.7.0):
urllib3==2.6.3 → urllib3==2.7.0
- Transitive dep not in
requirements.txt — Path under /usr/local/lib/python3.x/... but the package name is absent from requirements.txt → add an explicit pin to requirements.txt. It feeds the wheel's install_requires (see setup.py), so the Dockerfile's pip install /tmp/bzt*.whl step resolves the fixed version into the image. If the requirements resolver can't accommodate the pin, use a dedicated Dockerfile pip install step instead (case 3). Do not rely on PipInstaller.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.txt fails → add a dedicated pip 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 —
Path contains /setuptools/_vendor/... → cannot be pinned directly; only bumping the pinned setuptools== in requirements.txt clears it. Bump setuptools if a patched, compatible version exists; otherwise skip a low-value vendored finding and list it.
- System / distro Python —
Path under /usr/lib/python3/dist-packages/... → not a requirements.txt fix; the package came from apt, not pip. Upgrade or remove it in the Dockerfile (apt-get install --only-upgrade <pkg> or apt-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.VERSION in bzt/modules/jmeter.py / bzt/modules/gatling.py to 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"
class Newman(NPMPackage):
PACKAGE_NAME = "newman"
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):
curl -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'))"
If the latest npm bundles the fixed version → the existing npm i -g npm@11 clears 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 own package.json, which references unpublished workspace deps (@npmcli/docs), and the build fails with a registry 404. Verified failing in taurus build #571 and taurus-cloud MOB-51270 build #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@11 picks 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_URL and DOTNET_SHA512 values in the Dockerfile FROM system-deps AS runtimes stage.
-
Update ALL hardcoded SDK version strings in the sed commands below (the .deps.json metadata patch lines) to match the new SDK version number.
OS packages (Dockerfile)
When a CVE is in a system package (path is empty or in /usr/, /lib/, /bin/):
- Look up the fixed package version on NVD
- Add a targeted upgrade in the Dockerfile at the
FROM system-deps stage, after the main apt-get install block:
# Fix <CVE-ID>: upgrade <package> to <fixed-version>
RUN apt-get update && apt-get install -y --no-install-recommends --only-upgrade <package>=<fixed-version>
Use --no-install-recommends (consistent with the rest of the Dockerfile — keeps upgrades from pulling extra recommended packages).
Ruby gems (Dockerfile)
When a CVE is in a Ruby gem (path contains /usr/local/rbenv/):
Install the explicit fixed version in the Dockerfile after the rbenv setup block — pin the version with gem install <gem> -v <fixed-version>, never gem update (which jumps to latest and is not reproducible/auditable). Add --no-document to skip rdoc/ri generation (smaller image, faster build):
# Fix <CVE-ID>: upgrade <gem> to <fixed-version>
RUN eval "$(${RBENV_ROOT}/bin/rbenv init -)" && gem install <gem> -v <fixed-version> --no-document && rbenv rehash
⚠️ Default-gem / cached-archive caveat (verified the hard way — see vulnerability_history.md): gems that ship with Ruby (net-imap, erb, json, psych, uri, etc.) are default/bundled gems, and Prisma reads the OLD version from three places that gem install/gem update leave behind — even though gem list shows only the patched version:
- gemspec —
…/specifications/<gem>-<old>.gemspec or …/specifications/default/<gem>-<old>.gemspec
- lib dir —
…/gems/<gem>-<old>
- cached archive —
…/cache/<gem>-<old>.gem ← easily missed; this is what kept net-imap 0.5.8 flagged even after the gemspec + lib dir were removed (build #567). gem uninstall refuses to remove default gems.
Remove all three for the OLD version only, purge the gem cache, then assert the old version is gone so the build fails loudly instead of silently shipping it. Use exact old-version strings — never broad globs like net-imap-0.* or erb-4.*, which also match the patched version (e.g. 0.5.15) and would delete the fix:
RUN eval "$(${RBENV_ROOT}/bin/rbenv init -)" && \
gem install net-imap -v 0.5.15 --no-document && gem install erb -v 4.0.4.1 --no-document && \
GEMS_DIR="$(ruby -e 'print Gem.dir')" && \
for OLD in net-imap-0.5.8 erb-4.0.4; do \
rm -rf "$GEMS_DIR/gems/$OLD" "$GEMS_DIR/specifications/$OLD.gemspec" \
"$GEMS_DIR/specifications/default/$OLD.gemspec"; done && \
rm -f "$GEMS_DIR"/cache/*.gem && rbenv rehash && \
if find "${RBENV_ROOT}" -name 'net-imap-0.5.8.gem*' -o -name 'erb-4.0.4.gem' -o -name 'erb-4.0.4.gemspec' | grep -q .; then \
echo 'ERROR: vulnerable Ruby gem still on disk' >&2; exit 1; fi
(substitute the actual gem/version pairs). Purging cache/*.gem wholesale is safe — cache archives aren't needed at runtime. Always confirm in the branch scan (step 14) that the old version is no longer flagged; Successfully installed in the build log is NOT proof.
8. Branch and worktree setup
Print a status line before each step.
[1/6] Determine branch name
Check if the branch already exists:
git branch --list "CVE-fixes_$(date +%Y-%m-%d)*"
- If not → use
CVE-fixes_YYYY-MM-DD
- If exists → append index:
CVE-fixes_YYYY-MM-DD-2, -3, etc.
[2/6] Verify .worktrees/ is in .gitignore
grep -q "^\.worktrees" .gitignore || echo ".worktrees" >> .gitignore
[3/6] Create git worktree
git fetch origin master && \
git worktree add .worktrees/<branch-name> -b <branch-name> origin/master
All subsequent steps (fixes, tests, commit, push) run from inside .worktrees/<branch-name>/.
9. Apply all auto-fixes inside the worktree
Repeat all the fix steps from step 7 — but now operating on files inside .worktrees/<branch-name>/ instead of the main working directory.
10. Run unit tests
After applying all fixes inside the worktree, run the full unit test suite. This takes 10–20 minutes — tell the user and that this is expected.
cd .worktrees/<branch-name> && python -m nose2 -s tests/unit -v
If tests fail → stop. Do NOT remove the worktree. Do NOT commit.
Report:
- Which tests failed and the error output
- The worktree path:
.worktrees/<branch-name>
- The branch name so the user can investigate
Tell the user:
"Tests failed. The worktree has been left at .worktrees/<branch-name> on branch <branch-name> for you to investigate. Once you've resolved the failures, you can commit and push manually from that directory."
If tests pass → continue to the coverage gate below.
Coverage gate (predict the codecov/project check before pushing)
The PR's codecov/project check uses codecov defaults (target: auto, threshold: 0%) — any net coverage decrease over bzt/ (excluding bzt/resources, per .codecov.yml) fails it and blocks merge. Detect this locally before committing/pushing.
If branch coverage < base coverage → the PR will be blocked. Take action before pushing:
- Prefer adding/extending a unit test that covers the lines/branches the change left uncovered.
- If the drop comes from a change that collapses a previously-tested branch (e.g. making two constants equal), reconsider whether that change is even in scope — JMeter/Gatling version bumps are not (category 1), and that was the original cause.
- Only push once branch coverage ≥ base coverage.
11. Commit the fixes (do NOT push yet)
Commit so the local review (step 12) has a concrete diff, but do not push — the review may amend this commit, and the branch should be reviewed before the expensive build + re-scan (step 14) runs on it.
Stage and commit (from inside the worktree). Stage only files that actually changed — the in-scope fix targets are requirements.txt, the Dockerfile, and bzt/modules/javascript.py (npm direct PACKAGE_NAME):
cd .worktrees/<branch-name> && \
git add requirements.txt Dockerfile bzt/modules/javascript.py
(Do not stage bzt/modules/jmeter.py or bzt/modules/gatling.py — bumping JMeter.VERSION/Gatling.VERSION is category 1, out of scope. bzt/modules/java/tools.py is a permitted-but-discouraged target: stage it only if you deliberately bumped a JarTool version constant (TestNG, JUnit, org.json, etc.) for a specific reason — recipe in vulnerability_history.md ("When a Java jar has a CVE"). bzt/modules/services.py only holds the runtime PipInstaller.pip_constraints, which doesn't affect the scanned image, so it's normally not part of a CVE fix.)
Commit message:
Fix CVE vulnerabilities - <comma-separated list of packages bumped>
Auto-fixed by prisma-taurus skill.
CVEs fixed: <comma-separated CVE IDs>
Do not push yet — the push is step 13, after the local review.
12. Local code review — before the push (amend into the commit)
Review the committed-but-unpushed diff (Dockerfile, requirements.txt, and any bzt/**/*.py) with an independent reviewer before pushing, so any accepted fix is amended into the commit and the branch is pushed once, clean — and the build + re-scan (step 14) then validates the already-reviewed image (reviewing after the ~30-min scan would invalidate it). It catches judgment mistakes a self-check misses (a broad gem-purge glob that also deletes the patched version, an apt pin so exact it breaks on the next security-pocket bump, a fix that won't actually land, a || true that masks a failure).
Pick the reviewer by what's available — this step must never block or fail the run because a review tool is missing (a "not available"/"unknown skill" error is not a failure; fall through to the next option):
- If the
superpowers:requesting-code-review skill is available, use it (preferred — an independent agent).
- Else, if the
/code-review skill is available, use it (diff-scoped review).
- Else (neither installed), skip the review and go straight to the push (step 13). Note in the PR body that no automated review ran. Do not error out.
Then, whichever ran (if any):
- Invoke the chosen review skill on the committed fix diff.
- Triage every comment yourself — do not auto-apply. Decide change vs keep as-is with technical rigor (as in
superpowers:receiving-code-review), and justify each call:
- Accept when it improves correctness, robustness, or matches an in-repo convention (e.g. use
<gem>-<old>.gem* like the net-imap line instead of an exact .gem in the default-gem purge assertion; keep an apt --only-upgrade unpinned so a security-pocket bump doesn't later break the build).
- Reject/keep when the current change is already correct, or the comment conflicts with the skill's classification rules (e.g. a suggestion to fix a JMeter/Gatling-bundled, ESM, or scanner-appeasement finding — out of scope here; keep it as-is).
- If you accept changes, apply them,
git add the changed files, then git commit --amend --no-edit into the fix commit — safe, because nothing is pushed yet (never force-push). (Without the git add, --amend re-uses the previous tree and your accepted edit is silently dropped — never pushed, never in the PR.)
- Record the outcome for the PR body: which comments were applied, which were consciously kept and why.
13. Push the branch (NO PR yet)
Only after the review-and-amend pass. The branch must exist on origin so taurus-branch-builder can build it. Do not create a PR here — the PR is created in step 15, only after the branch scan confirms a reduction.
cd .worktrees/<branch-name> && git push -u origin <branch-name>
The worktree can be removed now (the branch is on origin). Run this from the repo root:
git worktree remove .worktrees/<branch-name>
14. Build the branch into an image and re-scan it
Trigger taurus-branch-builder to build the pushed branch into a Docker image, run integration tests, and run a Prisma scan on the resulting image. This is the verification gate before any PR.
curl -s -o /dev/null -D - -X POST -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/taurus-branch-builder/buildWithParameters?branch_name=<branch-name>&run_integration=true&push_docker=true&public_docker=false&publish_internal_python=false&PERFORM_PRISMA_SCAN=true"
Parameter rationale:
run_integration=true — catches breakage unit tests can't (the Dockerfile gem/npm/apt steps and the JMeter/Gatling version bumps only run for real here).
push_docker=true — pushes to the internal GCR registry (us.gcr.io/verdant-bulwark-278/taurus:<branch>-<build>); twistcli scans that image. Required for the scan.
public_docker=false — never publish a branch image to the public registry.
PERFORM_PRISMA_SCAN=true — runs twistcli images scan --details and prints the full vulnerability table inline in the build console.
Capture the Location: header (queue item URL), poll the queue item for executable.number to get the build number, then poll the build until building=false (typically ~30–40 min with integration; ~14 min without). Tell the user the build number and that you are waiting.
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/taurus-branch-builder/<BUILD>/api/json?tree=building,result"
If result is FAILURE → integration or the build broke. Stop, do NOT create a PR, and give the console URL:
https://blazect-jenkins.blazemeter.com/job/taurus-branch-builder/<BUILD>/console
15. Compare scan results and decide whether to PR
Fetch the build console and parse the twistcli table (it starts at the line Scan results for: image us.gcr.io/...:<branch>-<build>):
curl -sL -u "$JENKINS_USERNAME:$JENKINS_TOKEN" \
"https://blazect-jenkins.blazemeter.com/job/taurus-branch-builder/<BUILD>/console"
Strip ANSI codes and count rows by severity (critical/high/medium/low). Parse the whole table — it is large; do not truncate the byte range.
Then do two checks:
- Per-fix verification — for every package you fixed, confirm its vulnerable version is no longer flagged in the branch scan. If a package is still flagged at the old version (e.g.
net-imap 0.5.8), that fix did NOT land — diagnose it (see the default-gem caveat under "Ruby gems") before proceeding.
- Aggregate comparison — compare the branch counts (total + per severity) against the baseline scan from steps 4–5.
Decision gate (keyed on the all-severity branch-scan total, not on the X→Y reporting framing from step 5):
- Vulnerabilities went down AND no fixed package is still flagged at its old version → proceed to create the PR (below).
- Vulnerabilities did not improve, OR a fix silently failed to land → do NOT create a PR. Report the comparison, the failed fixes and why, and stop. Tell the user the branch is pushed and the build number so they can decide.
Before creating the PR — create a Jira Story for the fixes and reference its key in the PR.
Do this only after the decision gate passes (never for a run that didn't reduce vulnerabilities — otherwise you leave an orphan ticket). Load the deferred Atlassian (Jira) MCP tools first, e.g. ToolSearch with select:mcp__claude_ai_Atlassian_Rovo__atlassianUserInfo,mcp__claude_ai_Atlassian_Rovo__searchJiraIssuesUsingJql,mcp__claude_ai_Atlassian_Rovo__createJiraIssue,mcp__claude_ai_Atlassian_Rovo__editJiraIssue,mcp__claude_ai_Atlassian_Rovo__getTransitionsForJiraIssue,mcp__claude_ai_Atlassian_Rovo__transitionJiraIssue (add getAccessibleAtlassianResources if you need the cloudId).
Site / cloudId: perforce.atlassian.net = 2accdbdb-9d65-4c22-b174-5d4a9d437c59. Project MOB (name "R&D"), issue type Story.
| Field | Value |
|---|
| Project | MOB |
| Issue type | Story |
| Summary | generic, e.g. Fix CVE vulnerabilities in blazemeter/taurus Docker image (YYYY-MM-DD) |
| Description | Lead with the fixable-detected/fixed framing (see step 5's reporting rule): Fixable in taurus, this run: <X> detected → <Y> fixed. Then the table of CVEs fixed and confirmed in the branch image (CVE ID, package, old → new, severity). Collapse everything not-actionable-here into one categorized count (Z) — never a per-CVE scoreboard. The raw baseline→branch total may appear only as a parenthetical (with at least crit/high) — never as the opening number, and never asserted to sum with X/Y. |
| Assignee | the developer running the skill — atlassianUserInfo accountId; never hardcode a person |
| Labels | ["ai_assisted"] |
Scrum Team (customfield_10067) | required on create — hardcoded to Sparta, option id 21405 |
Product (customfield_10350) | required on create — hardcoded to Blazemeter, option id 21409 |
Sprint (customfield_10020) | the team's active sprint — resolve dynamically (step 2) and set it (step 4); it is not auto-assigned |
| Status | transition to In Progress after creation |
⚠️ The MOB Story create screen rejects a create that omits customfield_10067 (Scrum Team) or customfield_10350 (Product) — both are mandatory and have no default. For now they are hardcoded to this team's values (Scrum Team = Sparta 21405, Product = Blazemeter 21409). If another team starts running this skill, switch these to per-run values derived from the runner's own recent MOB issue instead.
atlassianUserInfo → the runner's accountId (assignee).
- Resolve the active sprint — read it off the runner's most recent issue currently in an open sprint (the Atlassian MCP has no board/sprint tool and only
read/write:jira-work scope, so the Agile API for "active sprint" is unavailable — reading an existing issue's Sprint field is the scope-free path):
searchJiraIssuesUsingJql
jql: project = MOB AND assignee = currentUser() AND sprint IN openSprints() ORDER BY updated DESC
fields: ["customfield_10020"]
The active sprint = the entry in the top result's customfield_10020 array whose state == "active" → its numeric id (e.g. 27845 = 26-Q2-S6). (The "resolve dynamically / set it" pointers above are to sub-steps 2 and 4 of this numbered list, not to top-level skill steps 2/4.)
- Fallback: if the runner has no open-sprint issue, resolve via
project = MOB AND "Scrum Team" = "Sparta" AND sprint IN openSprints() ORDER BY updated DESC. If still none, create without a sprint and flag it for manual assignment.
createJiraIssue — projectKey=MOB, issueTypeName=Story, summary, description, assignee_account_id=<accountId>, additional_fields: {"labels":["ai_assisted"], "customfield_10067":{"id":"21405"}, "customfield_10350":{"id":"21409"}}. Capture the returned key, e.g. MOB-XXXXX. (The Sprint field is usually not on the create screen — set it in the next step.)
- Set the sprint:
editJiraIssue on the new key with fields: {"customfield_10020": <activeSprintId>} (numeric id). If the write is rejected (some boards restrict sprint assignment via API), flag the sprint for manual assignment — don't block the PR.
getTransitionsForJiraIssue → find the In Progress transition id → transitionJiraIssue.
- Read the ticket back (
customfield_10020, status) to confirm the sprint landed and status is In Progress before moving on.
Reference the key in the PR, not the commit. The fix commit was already made and pushed at step 13 (before this ticket exists), so the key can't be in it — and that's fine. Put the key in the PR title and body only (next step). Do not amend or force-push the fix commit to backfill the key: leaving the verified commit untouched keeps it matching exactly the image already built and scanned by taurus-branch-builder.
Before creating the PR — reconcile vulnerability_history.md on the SAME fix branch (so it ships in this PR, no separate PR):
vulnerability_history.md is a problem-solving reference, not a log of what happened. Its job is to help a future run (a) fix similar vulnerabilities faster and (b) know when a temporary fix can be removed because a newer upstream version resolved the original CVE. Update it in place — improve the matching recipe or pattern; do not append a blow-by-blow "what happened today" entry.
At the end of every run, reconcile it for two things:
- A durable lesson — a fix that silently didn't land and its root cause, a new not-buildable category, a corrected recipe, or a non-obvious decision worth preserving. The most valuable of these are only known after the branch scan in this step. Fold it into the matching "Patterns" recipe (or add a new recipe), not into a dated timeline.
- A removal condition — if this run pruned a now-unneeded fix (step 7) or added a new temporary fix, record under that recipe what the fix is for and the upstream condition that makes it removable, so a future run can prune it confidently.
If the run applied known recipes cleanly and there is no new lesson and nothing to prune, change nothing and go straight to the PR — don't pad the file. When there is something to record, commit it on the fix branch alongside the code fixes and push, so the doc update is part of this same PR rather than a separate one:
cd .worktrees/<branch-name>
git add .claude/skills/prisma-taurus/vulnerability_history.md
git commit -m "Document <lesson> in prisma-taurus history"
git push origin <branch-name>
Caveat — the history file must exist on the base branch (master) for this to work. The fix branch is cut from origin/master; vulnerability_history.md is only present there once the prisma-taurus skill itself has been merged to master. If the skill is not yet on master (e.g. still on a feature branch), the file won't be in the fix branch's worktree — in that case commit the history update on whatever branch the skill lives on instead, and note in the PR that the history doc lives elsewhere. Once the skill is merged, this caveat no longer applies and the history update rides along in the same PR every run.
Create the PR (only when the gate passes). Do this in two independent steps so creating the PR never depends on Copilot being available.
15a — create the PR (must succeed on its own; do NOT put --reviewer here): include the Jira key in the title so Jira ↔ GitHub link automatically.
gh pr create --repo Blazemeter/taurus --base master \
--title "<MOB-XXXXX>: CVE fixes - $(date +%Y-%m-%d)" --body-file <body.md>
If gh is not on PATH, create via the GitHub API using $GITHUB_TOKEN (POST /repos/Blazemeter/taurus/pulls).
15b — request GitHub Copilot's review (best-effort; must NEVER fail the run):
if ! command -v gh >/dev/null 2>&1; then
echo "gh not installed — skipping Copilot reviewer request (PR already created; request it manually if wanted)"
else
gh pr edit <pr-number-or-url> --repo Blazemeter/taurus --add-reviewer @copilot 2>/dev/null \
|| gh api repos/Blazemeter/taurus/pulls/<number>/requested_reviewers \
-f 'reviewers[]=copilot-pull-request-reviewer[bot]' 2>/dev/null \
|| echo "Copilot reviewer not added (needs gh >= 2.88.0 and Copilot code review enabled) — PR created regardless; request it manually if wanted"
fi
Keeping 15b separate is deliberate: a bad --reviewer on gh pr create can fail the whole create call and leave no PR. By creating first and requesting Copilot after, the PR is guaranteed and the Copilot request is a harmless add-on. (This is GitHub Copilot's own PR review — separate from and in addition to the step-12 local pre-push review.) Never block or error the run because Copilot couldn't be added.
PR body should include (apply the step-5 reporting rule — lead with fixable-detected/fixed, not the raw total):
- Jira:
<MOB-XXXXX> (the ticket created above).
- Headline first — Fixable in taurus, this run:
<X> detected → <Y> fixed. Distinct CVEs; count only this repo's own Dockerfile / requirements.txt fixes confirmed gone in the branch scan. This is the opening line — do not open with the raw total or any JMeter/Gatling count. If Y < X, list which didn't land and why.
- Table of the fixed CVEs: CVE ID, severity, package, old version → new version.
- Note: "Verified against the
taurus-branch-builder image scan (build #) before opening — integration passed."
- Local code-review outcome (from step 12): which review comments were applied vs kept, and why.
- Not actionable in this repo — one categorized COUNT only (never per-CVE):
<Z> = monitor-only JMeter/Gatling bundled jars (incl. criticals), k6 binary internals, no-released-patch (open/needed/deferred), Ubuntu ESM, scanner-appeasement/manual (.deps.json/Roslyn, /var/lib/dpkg). State this run's criticals explicitly and computed (e.g. "all N criticals are monitor-only JMeter/Gatling" — verify each run, don't assume).
- If you show the raw baseline→branch totals at all, put them in a single parenthetical with at least crit/high — never as the headline. (X/Y are distinct CVEs; the raw total counts per-occurrence, so it won't sum arithmetically with X — context, not a checksum.)
- Note: "The following CVEs require manual intervention (see below)."
- Decision gate is unchanged by this framing — it keys on the all-severity branch-scan total going down (see the gate above), not on X/Y.
16. Wait for and triage Copilot's review (one round only)
After 15b, wait for Copilot to finish reviewing, then triage its comments — but do this exactly once. This is best-effort: if Copilot never runs, don't block.
[16a] Detect completion (bounded poll). If gh is not on PATH, skip step 16 entirely (note it in the summary) — don't burn the poll. Otherwise poll until Copilot posts its review — a review by copilot-pull-request-reviewer[bot] with a non-null submitted_at is the signal (Copilot also drops out of requested_reviewers when done, but the submitted review is sufficient):
PR=<number>
for i in $(seq 1 20); do
finished=$(gh api repos/Blazemeter/taurus/pulls/$PR/reviews \
--jq '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]" and .submitted_at!=null)] | length' 2>/dev/null)
[ "$finished" -ge 1 ] 2>/dev/null && break
sleep 15
done
If the cap elapses with no Copilot review, note "Copilot review did not complete in time" and go to step 17 — do not block.
[16b] Triage the comments. Read Copilot's inline comments (gh api repos/Blazemeter/taurus/pulls/$PR/comments) and the review body. Triage each with the same judgment as step 12 (accept if it improves correctness/robustness/convention; keep-as-is if it conflicts with the classification rules — e.g. never patch a JMeter/Gatling-bundled, ESM, or scanner-appeasement finding). Justify each call.
[16c] Apply accepted changes — then STOP (do not re-request Copilot). If you accept any comments, first re-add the worktree on the fix branch if it was removed at step 13 (git worktree add .worktrees/<branch-name> <branch-name>), apply them to the Dockerfile / requirements.txt / bzt/**/*.py, git add the changed files, and commit + push one follow-up commit on the same branch. This is a single round: after this push, do NOT request Copilot again and do NOT wait for another Copilot review — even though the push may auto-trigger one. Leave any resulting new Copilot review for the human reviewer; record in the summary which comments were applied and which were kept.
⚠️ Prisma-specific: re-verify a material Dockerfile change. Copilot's comments are usually robustness nits (e.g. a .gem* glob vs an exact .gem, an exact-apt-pin brittleness, a || true scope) that do not change which CVEs the image clears — a single push without re-scan is fine. But if an accepted Copilot fix materially changes the Dockerfile (a different package/version, a removed layer), the pushed image no longer matches the one verified at steps 14–15. In that case, re-trigger the branch build + re-scan (step 14) and re-confirm the decision gate (step 15) before considering the PR verified — the "verified before merge" guarantee is the whole point of this skill. If that re-confirmed gate does NOT pass, do not leave the PR looking verified: convert it to a draft (gh pr ready <pr> --undo) or post a blocking comment, stop, and report — restore it only once a passing re-scan exists, and update the PR body's build-number note to that new build.
17. Post the "ready for review" notification to Slack (best-effort; must NEVER fail the run)
After step 16 — the PR exists, Copilot's single triage round is done (or timed out), and any accepted follow-up is pushed — post one summary to the team Slack channel so a human knows the PR is basically ready to approve. Best-effort, exactly like the Copilot request (15b): it must never fail, block, or error the run.
- Only when there is a PR. If the decision gate (step 15) failed and no PR was created, skip this step entirely.
- Skip cleanly if unconfigured. If
SLACK_BOT_TOKEN or SLACK_SPARTA_CHANNEL_ID is unset, skip the post, note "Slack notification skipped (SLACK_* not set)" in the summary, and continue. Never prompt.
- Content = only the CVEs that landed (the ✅ "Fixed" set,
Y). No pendings, no not-landed, no out-of-scope, no notes — just the fixed list.
Message format (locked). Header line + fixed CVEs grouped by package, each package's CVEs listed vertically. unfurl_links:false. The count is computed from the fixed list (= Y), never hand-typed. Build/send in Python (avoids shell-quoting pain), reading both values from the environment:
import json, os, subprocess
token = os.environ.get("SLACK_BOT_TOKEN")
channel = os.environ.get("SLACK_SPARTA_CHANNEL_ID")
if not (token and channel):
print("Slack notification skipped (SLACK_BOT_TOKEN / SLACK_SPARTA_CHANNEL_ID not set)")
else:
def esc(s):
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
pr_num = <PR number>
pr_url = f"https://github.com/Blazemeter/taurus/pull/{pr_num}"
jira = "<MOB-XXXXX>"
groups = [
]
total = sum(len(c) for _, _, c in groups)
lines = []
for pkg, bump, cves in groups:
lines.append(f"• *{esc(pkg)}* {esc(bump)}")
for c in cves:
lines.append(f" ◦ {esc(c)}")
jira_seg = f" · <https://perforce.atlassian.net/browse/{jira}|{jira}>" if jira and jira != "<MOB-XXXXX>" else ""
header = (f":hammer_and_wrench: *Prisma CVE fixes ready for review* — "
f"<{pr_url}|taurus #{pr_num}> · {total} CVEs fixed{jira_seg}")
payload = {
"channel": channel, "unfurl_links": False,
"text": f"Prisma CVE fixes ready for review — taurus #{pr_num}",
"blocks": [{"type": "section",
"text": {"type": "mrkdwn", "text": header + "\n" + "\n".join(lines)}}],
}
try:
r = subprocess.run(
["curl","-s","-X","POST","https://slack.com/api/chat.postMessage",
"-H", f"Authorization: Bearer {token}",
"-H", "Content-type: application/json; charset=utf-8",
"--data", json.dumps(payload)],
capture_output=True, text=True, timeout=30)
resp = json.loads(r.stdout or "{}")
print("Slack notification posted" if resp.get("ok")
else f"Slack notification failed: {resp.get('error')} (continuing)")
except Exception as e:
print(f"Slack notification errored: {e} (continuing)")
Non-fatal errors to note but never block on: not_in_channel (invite the bot to the channel), channel_not_found (wrong id), invalid_auth (bad/rotated token). Record the outcome (posted / skipped / failed) in the final summary; the PR stands regardless.
Format is intentionally minimal and shared. The same bot ("Sparta Scan") and the same format are reused by the taurus-cloud skills (prisma-taurus-cloud and mend-taurus-cloud, with a Mend tag instead of Prisma) and across repos (the link text is <repo> #<PR>). Keep the header/emoji/grouping identical so the channel reads consistently — only the scan-type tag and repo name differ.
18. Report manual intervention and out-of-scope items
First, list the JMeter/Gatling findings that are intentionally not fixed:
═══════════════════════════════════════════════
ℹ JMETER / GATLING — NOT FIXED (out of scope)
═══════════════════════════════════════════════
These live in the bundled JMeter/Gatling distributions and are not remediated by this skill
(no jar repinning, no JMeter/Gatling version bump). Listed for awareness only.
<count> findings — <N critical, M high, ...>
<one line per CVE: CVE-ID | severity | package version | path>
Then the manual section:
═══════════════════════════════════════════════
⚠ MANUAL INTERVENTION REQUIRED
═══════════════════════════════════════════════
The following CVEs cannot be auto-fixed and require manual changes to the Dockerfile.
They have NOT been included in the PR above.
<for each manual CVE>
CVE: <CVE-ID> | Severity: <severity> | CVSS: <score>
Package: <package> <version>
Path: <path>
Why manual: <reason — e.g. "Roslyn version string in .deps.json metadata file">
What to do: <specific instruction — see below>
For .deps.json / Roslyn findings:
The scanner flags old Roslyn version strings in .deps.json metadata files inside the .NET SDK. These are not real vulnerable binaries — the actual Roslyn binaries are up to date. Fix options:
- Bump the .NET SDK version (already handled if a .NET SDK CVE was auto-fixed above)
- Add a
sed command in the Dockerfile to patch the version string in the specific file — see the existing sed pattern in the Dockerfile as a reference
- Add a Prisma Cloud suppression for this CVE ID — preferred if the binary is not actually vulnerable
For /var/lib/dpkg/ findings:
The scanner flags a package version string in dpkg's status database. Fix options:
- Update the actual package to the flagged version if it's available in the Ubuntu repositories
- Add a
sed command in the Dockerfile to patch /var/lib/dpkg/status — see the existing Firefox version patch in the Dockerfile as a reference
- Add a Prisma Cloud suppression — preferred if the actual installed version is not vulnerable
19. Final summary
End with a complete status summary:
═══════════════════════════════════════════════
SUMMARY
═══════════════════════════════════════════════
Fixable in taurus (this run): <X> detected → <Y> fixed
[distinct CVEs; this repo's own Dockerfile/requirements fixes, verified gone in branch scan #<BUILD>]
Branch image scan (taurus-branch-builder #<BUILD>): integration <pass/fail>
Jira: <MOB-XXXXX> (Story, ai_assisted, In Progress, assigned to <runner>) <sprint set | sprint NEEDS manual assignment> · PR #<number>
✅ Fixed (in PR, old version gone in branch scan):
- <CVE-ID>: <package> <old> → <new>
- ...
(of which self-resolved via an existing unpinned line / npm bump, no new change this run:
<CVE-ID>: <package> — <why>)
✗ Fixable but did NOT land (excluded from PR):
- <CVE-ID>: <package> — <reason: deferred for cause (breaking major jump / setuptools-vendored /
low value), OR attempted-but-failed (e.g. default-gem version still on disk)>
- ...
⚠ Manual intervention required (NOT in PR):
- <CVE-ID>: <package> — <one-line reason>
- ...
ℹ Not actionable in this repo — not auto-fixed this run (count only): <Z> — monitor-only
JMeter/Gatling bundled jars (incl. criticals), k6 binary internals, no-released-patch
(open/needed/deferred), Ubuntu ESM, scanner-appeasement/manual (.deps.json/Roslyn, /var/lib/dpkg)
(the ⚠ manual-intervention items above are the detail behind Z's scanner-appeasement/manual
portion — a subset of Z, NOT an additional count)
criticals this run: <state it — e.g. "all N monitor-only JMeter/Gatling (Tika)"; do NOT assume>
(raw scan, reconciliation only: baseline <a> → branch <b> [crit A→A', high B→B'])
Copilot review: <triaged N comments — M applied, K kept | did not complete in time | skipped (no gh)>
Slack: <posted to #channel / skipped (SLACK_* not set) / failed: <error>> (step 17; never blocks)
Next steps:
1. Review and merge PR #<number> (verified to reduce vulnerabilities in the image)
2. After merge, Jenkins builds a new blazemeter/taurus:unstable from master
3. Re-run /prisma-taurus to confirm the fixes are reflected in the unstable scan
4. Address manual-intervention items and any fixable ones that didn't land
If the decision gate in step 15 did NOT pass, end instead with: the comparison, which fixes failed to land and why, the pushed branch name and build number, and that no PR was created — hand it to the user to decide.
Researching a CVE
For each vulnerability, look up sources in this order:
- NVD —
https://nvd.nist.gov/vuln/detail/<CVE-ID>
- GitHub Advisory Database —
https://github.com/advisories?query=<CVE-ID>
- OSV —
https://osv.dev/vulnerability/<CVE-ID>
- Snyk —
https://security.snyk.io/vuln/<CVE-ID>
- Vendor advisory — follow the reference link from NVD
Reference
| What | URL |
|---|
On-demand scan job (baseline scan of unstable) | https://blazect-jenkins.blazemeter.com/job/prisma-cloud-ondemand-scan/ |
| All builds + params | .../api/json?tree=builds[number,timestamp,result,building,actions[parameters[name,value]]] |
| Taurus artifact (CSV) | .../prisma-cloud-ondemand-scan/<BUILD>/artifact/taurus.csv |
| Build console | .../prisma-cloud-ondemand-scan/<BUILD>/console |
| Branch builder (build+integration+scan a branch) | https://blazect-jenkins.blazemeter.com/job/taurus-branch-builder/ |
| Branch builder trigger | .../taurus-branch-builder/buildWithParameters?branch_name=<b>&run_integration=true&push_docker=true&public_docker=false&PERFORM_PRISMA_SCAN=true |
| Branch builder console (twistcli scan table) | .../taurus-branch-builder/<BUILD>/console |
| Branch image (internal GCR) | us.gcr.io/verdant-bulwark-278/taurus:<branch>-<build> |
| Queue item | https://blazect-jenkins.blazemeter.com/queue/item/<ID>/api/json |
| Docker Hub unstable tag | https://hub.docker.com/v2/repositories/blazemeter/taurus/tags/unstable |
| .NET 8.0 release metadata | https://dotnetcli.azureedge.net/dotnet/release-metadata/8.0/releases.json |
| Vulnerability fix history | vulnerability_history.md (in this skill's folder) — consult this before fixing any CVE |
taurus-branch-builder parameters
| Param | Type | Use for CVE verification |
|---|
branch_name | string | the pushed fix branch |
run_integration | bool | true — verify the fixes don't break runtime |
push_docker | bool | true — internal GCR; required so twistcli can scan the image |
public_docker | bool | false — never publish a branch image publicly |
publish_internal_python | bool | false |
PERFORM_PRISMA_SCAN | bool | true — scan the built image, results printed in the console |