@groupby/ai-dev 0.5.15 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/teams/fhr-orchestration/skills/create-jira/SKILL.md +47 -0
- package/teams/fhr-orchestration/skills/create-jira/TEMPLATE.md +10 -0
- package/teams/fhr-orchestration/skills/create-plan/SKILL.md +48 -0
- package/teams/fhr-orchestration/skills/create-plan/TEMPLATE.md +13 -0
- package/teams/fhr-orchestration/skills/init-harness/SKILL.md +164 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.CLAUDE.md +88 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.architecture.md +152 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.coding-guidelines.md +320 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.glossary.md +55 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.harness-gap-log.md +59 -0
- package/teams/fhr-orchestration/skills/use-case-writer/SKILL.md +123 -0
- package/teams/fhr-orchestration/skills/use-case-writer/TEMPLATE.md +39 -0
- package/teams/fhr-orchestration/skills/write-spec/SKILL.md +56 -0
- package/teams/fhr-orchestration/skills/write-spec/TEMPLATE.md +29 -0
- package/teams/firstspirit-caas/mcp/jira-tools.py +2122 -0
- package/teams/firstspirit-caas/mcp/test_bamboo_artifacts.py +315 -0
- package/teams/firstspirit-caas/mcp/test_jira_links.py +192 -0
- package/teams/firstspirit-caas/mcp/test_update_jira_ticket.py +161 -0
- package/teams/firstspirit-caas/resources/mcp-setup.md +57 -0
- package/teams/firstspirit-caas/skills/chronicle/SKILL.md +105 -0
- package/teams/firstspirit-caas/skills/create-jira-ticket/SKILL.md +183 -0
- package/teams/firstspirit-caas/skills/get-jira-ticket/SKILL.md +125 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/SKILL.md +316 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/references/suppression.md +79 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/gen_suppression.py +116 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_json_report.py +183 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_report.py +255 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_gen_suppression.py +171 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_json_report.py +244 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_report.py +464 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.html +39 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.json +76 -0
- package/teams/firstspirit-caas/skills/handle-weekly-cve-plans/SKILL.md +238 -0
- package/teams/firstspirit-caas/skills/retrospect/SKILL.md +94 -0
- package/teams/firstspirit-caas/skills/retrospect/extract_prompts.py +392 -0
- package/teams/firstspirit-caas/skills/review/SKILL.md +131 -0
- package/teams/firstspirit-caas/skills/review-finalize/SKILL.md +103 -0
- package/teams/firstspirit-caas/skills/specify/SKILL.md +239 -0
- package/teams/firstspirit-caas/skills/summarise-for-jira-comment/SKILL.md +162 -0
- package/teams/firstspirit-caas/skills/write-releasenotes/SKILL.md +177 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handle-red-cve-plan
|
|
3
|
+
description: 'Use when the user gives a red/failing Bamboo build URL and asks to fix, remediate, or triage its dependency CVEs — phrases like "fix the CVEs in this Bamboo build", "handle-cve for <bamboo url>", "remediate the dependency-check findings from CI". Distinct from handle-cve: this skill is driven by a Bamboo build URL and parses the published dependency-check report artifact (the JSON report when present, falling back to the HTML report), not a user-supplied local file.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# handle-red-cve-plan
|
|
7
|
+
|
|
8
|
+
Input: a red Bamboo plan/build URL. Output: **normally a Bitbucket PR** that fixes or
|
|
9
|
+
suppresses every active CVSS >= 7.0 finding — **except when the remediation is suppressions
|
|
10
|
+
only**, in which case the output is a single **unpushed local commit on the default branch**
|
|
11
|
+
for the invoker to review (no branch, no PR; see step 7). Runs fully autonomously — no
|
|
12
|
+
confirmation stops. Operates on the current working directory, assumed to be the checked-out
|
|
13
|
+
target repo.
|
|
14
|
+
|
|
15
|
+
**Parameters:**
|
|
16
|
+
- `ticket` (**required**) — the JIRA key that tracks this remediation (e.g. `PX-1234`). Drives
|
|
17
|
+
the fix branch (step 4): the skill reuses an existing branch for this ticket in the repo, or
|
|
18
|
+
creates `bugfix/<ticket>-handle-weekly-cves`. **Invoked without a ticket → hard stop and ask
|
|
19
|
+
for one** (see Hard stops). The orchestrator `handle-weekly-cve-plans` always supplies it.
|
|
20
|
+
- `useSonatype` (optional flag) — when the user specifies it (e.g. "fix the CVEs …
|
|
21
|
+
useSonatype"), the verify step (step 6) runs `./gradlew dependencyCheckAggregate
|
|
22
|
+
-DuseSonatype`; when omitted, it runs `./gradlew dependencyCheckAggregate` without the
|
|
23
|
+
flag. This mirrors how the Bamboo plan itself was configured (the weekly Sonatype-OSS
|
|
24
|
+
build passes it) so local verification matches CI.
|
|
25
|
+
|
|
26
|
+
## When NOT to use
|
|
27
|
+
|
|
28
|
+
- A local JSON dependency-check report is already in hand and no PR/push is wanted — use
|
|
29
|
+
`handle-cve` (read-only, never invokes Gradle or Git).
|
|
30
|
+
- Single ad-hoc CVE with no scanner/Bamboo context.
|
|
31
|
+
|
|
32
|
+
## Constraints (non-negotiable)
|
|
33
|
+
|
|
34
|
+
- Act only on **active** findings with **CVSS >= 7.0** (exclude the report's suppressed
|
|
35
|
+
vulnerabilities — both `parse_json_report.py` and `parse_report.py` already do this: the JSON
|
|
36
|
+
parser reads only the `vulnerabilities` array and never `suppressedVulnerabilities`, the HTML
|
|
37
|
+
parser keeps only entries that still carry a suppress button).
|
|
38
|
+
- **Only update an existing dependency declaration.** Never add a new entry for a
|
|
39
|
+
transitive dependency.
|
|
40
|
+
- Suppression file is **auto-discovered, never created**. If suppression is needed and no
|
|
41
|
+
file is found → **STOP and report**.
|
|
42
|
+
- Each CVE is suppressed **at most once** across the whole file — one block per CVE, listing
|
|
43
|
+
every affected `<packageUrl>`. Never emit the same `<cve>` in two blocks.
|
|
44
|
+
- Suppression type: **when in doubt, temporary** (default 2 months). Permanent only
|
|
45
|
+
for a confident false positive across all affected artifacts; mixed fate → temporary.
|
|
46
|
+
- **Suppressions-only remediation** (the only change is the suppression file, no bumps) **on a
|
|
47
|
+
freshly-created step-4 branch** → **no branch and no PR**: a single commit directly on the
|
|
48
|
+
latest default branch, **never pushed automatically** and **with no ticket number in the
|
|
49
|
+
message**, handed back to the invoker as the skill's output (see step 7, Path A). If step 4
|
|
50
|
+
reused an existing ticket branch, suppressions-only takes Path B instead (never bypass an
|
|
51
|
+
existing branch).
|
|
52
|
+
|
|
53
|
+
## Workflow
|
|
54
|
+
|
|
55
|
+
### 1. Resolve the build
|
|
56
|
+
Parse the URL to a plan/build key. If it ends in `/latest` or is a bare plan key,
|
|
57
|
+
normalize to `{PLAN}-latest` and call `mcp__plugin_planetexpress_jira-tools__get_bamboo_build`
|
|
58
|
+
to get the concrete build number. Confirm the build is Failed/red before proceeding. Auth or
|
|
59
|
+
resolution failure → hard stop.
|
|
60
|
+
|
|
61
|
+
### 2. Fetch the report
|
|
62
|
+
The report artifact's **name and layout vary by plan** — do not hardcode either. Some plans
|
|
63
|
+
publish it as `owasp dependency check report` with the report files under `reports/`; others
|
|
64
|
+
(e.g. haupia / `SMASE-WDCS`) publish `Report DependencyCheckAggregate` with the files at the
|
|
65
|
+
artifact root. Resolve by the **file**, not the artifact name.
|
|
66
|
+
|
|
67
|
+
**Prefer the JSON report; fall back to HTML.** The JSON (`dependency-check-report.json`) is the
|
|
68
|
+
richer, structured source and is parsed without regex; the HTML is the backup for plans whose
|
|
69
|
+
CI run has not yet started publishing JSON.
|
|
70
|
+
|
|
71
|
+
- `mcp__plugin_planetexpress_jira-tools__list_bamboo_artifacts <build_key>` → across all
|
|
72
|
+
directory artifacts, look for a file whose relative name ends in
|
|
73
|
+
`dependency-check-report.json`. If present, take its `path`; that is the report to fetch and
|
|
74
|
+
the parse in step 3 is JSON. If **no** `.json` is present, fall back to the file whose name
|
|
75
|
+
ends in `dependency-check-report.html` (prefer it over the `.xml`/`.csv`/`.sarif` siblings)
|
|
76
|
+
and the step-3 parse is HTML.
|
|
77
|
+
- `mcp__plugin_planetexpress_jira-tools__get_bamboo_artifact` with the `build_key` positional
|
|
78
|
+
argument and `href=<that path>`. It returns as `localPath`. If **neither** a
|
|
79
|
+
`dependency-check-report.json` nor a `dependency-check-report.html` file is present in any
|
|
80
|
+
artifact → hard stop (report the artifacts found).
|
|
81
|
+
|
|
82
|
+
### 3. Parse
|
|
83
|
+
Pick the parser to match the file fetched in step 2:
|
|
84
|
+
- **JSON** → `python "<plugin-root>/scripts/parse_json_report.py" <localPath> --out /tmp/cve-worklist.json`
|
|
85
|
+
- **HTML** → `python "<plugin-root>/scripts/parse_report.py" <localPath> --out /tmp/cve-worklist.json`
|
|
86
|
+
|
|
87
|
+
Both emit the **same worklist schema** (`findings` + `unparsed`), so the rest of the workflow
|
|
88
|
+
is identical regardless of source. Read the worklist. `findings` empty **and** `unparsed` empty
|
|
89
|
+
→ nothing to do; report and stop. Non-empty `unparsed` → hard stop (see Hard stops below)
|
|
90
|
+
regardless of `findings`.
|
|
91
|
+
|
|
92
|
+
Each finding carries the report's own advisory data — use it, don't discard it:
|
|
93
|
+
- `description` — the advisory prose; often names the fixed release and the affected range
|
|
94
|
+
in words (e.g. "5.4.2 and earlier").
|
|
95
|
+
- `affectedVersions` — the "Vulnerable Software & Versions" entries, e.g.
|
|
96
|
+
`cpe:...:httpcomponents_core:* versions up to (including) 5.4.2` — the affected boundary.
|
|
97
|
+
- `references` — advisory URLs (NVD, vendor lists, and the Sonatype guide page, which
|
|
98
|
+
states the exact fixed version). Fetch these only if the description/affectedVersions
|
|
99
|
+
don't already answer the fixed-version question.
|
|
100
|
+
- `includedBy` — **JSON parse only** (the HTML parser cannot supply it): the package URLs
|
|
101
|
+
that pull this dependency in. For a transitive finding this is the ancestor chain step 5.2
|
|
102
|
+
needs, so when it is present the `dependencyInsight` call there becomes a confirmation of a
|
|
103
|
+
known ancestor rather than a discovery. Absent (empty) on HTML-sourced worklists — fall back
|
|
104
|
+
to `dependencyInsight` as before.
|
|
105
|
+
|
|
106
|
+
### 4. Prepare the fix branch (ticket-driven)
|
|
107
|
+
Do this **before** editing anything — every bump, suppression, and the verify happen on this
|
|
108
|
+
branch. The branch is chosen from the `ticket` parameter: reuse the ticket's existing branch if
|
|
109
|
+
one is already in the repo, otherwise create a fresh one off the **latest default branch**.
|
|
110
|
+
|
|
111
|
+
- **Determine the default branch:** do not assume `master`. Derive it from the remote:
|
|
112
|
+
`git symbolic-ref refs/remotes/origin/HEAD` (or `git remote show origin` if unset). Call it
|
|
113
|
+
`<default>`.
|
|
114
|
+
- `git fetch origin`, then look for an **existing branch for this ticket** — local or remote —
|
|
115
|
+
matching the ticket key: `git branch -a --list "*<ticket>*"` (e.g. `*PX-1234*`). Resolve by
|
|
116
|
+
how many match:
|
|
117
|
+
- **Exactly one** → **reuse it as-is.** `git switch <branch>` (tracking the remote if it is
|
|
118
|
+
remote-only). Do **not** reset it onto `<default>` — commit on top of whatever work it
|
|
119
|
+
already carries. Record that the branch was **reused** (step 7 needs this).
|
|
120
|
+
- **None** → **create it off the latest tip:**
|
|
121
|
+
`git switch -c bugfix/<ticket>-handle-weekly-cves origin/<default>`. Record that the branch
|
|
122
|
+
was **newly created**.
|
|
123
|
+
- **More than one** → **hard stop** and report the candidates; do not guess which is the
|
|
124
|
+
ticket's branch.
|
|
125
|
+
- If the working tree has unrelated uncommitted changes that would be carried across or block
|
|
126
|
+
the switch, STOP and report rather than discarding them.
|
|
127
|
+
- This branch is the working base for fixing + verifying. Suppressions-only remediation goes to
|
|
128
|
+
Path A **only when this branch was newly created** — see step 7.
|
|
129
|
+
|
|
130
|
+
### 5. Fix each finding (grouped by dependency)
|
|
131
|
+
Detect the repo's declaration mechanism: version catalog (`gradle/libs.versions.toml`),
|
|
132
|
+
direct `dependencies {}`, `constraints {}`, or `platform()`/BOM. **A bump is the preferred
|
|
133
|
+
fix — but only when the latest available version actually clears the CVE.** Get the affected
|
|
134
|
+
range / fixed version **from the report first** (the finding's `affectedVersions`,
|
|
135
|
+
`description`, and `references` from step 3) — that is the first place to look; **web-search
|
|
136
|
+
only when the report doesn't state it.** Then check the latest available version against that
|
|
137
|
+
range: if the latest is outside the affected range (i.e. a fixed release exists), bump to it;
|
|
138
|
+
if the latest version *still contains* the vulnerability, a bump is pointless — do not bump,
|
|
139
|
+
fall through to suppression. Suppression is the last resort. Try the fixes in this order and
|
|
140
|
+
stop at the first that works:
|
|
141
|
+
|
|
142
|
+
1. **Direct dependency with an existing entry** — if the vulnerable dependency (or its GAV)
|
|
143
|
+
is itself declared and the **latest available version clears the CVE**, bump that entry to
|
|
144
|
+
the latest version. (Take the latest, not merely the lowest fixed release — but only if
|
|
145
|
+
the latest is in fact a fixed, non-affected version.)
|
|
146
|
+
2. **Transitive dependency → bump the direct ancestor that pulls it in.** A vulnerable dep
|
|
147
|
+
with no direct declaration is often fixable without adding a new entry: find the
|
|
148
|
+
direct/parent dependency that brings it in and bump *that* existing declaration to its
|
|
149
|
+
latest available version — **provided that bump resolves the transitive dep to a version
|
|
150
|
+
that clears the CVE.**
|
|
151
|
+
- Identify the ancestor from the OWASP report's "included by" / `includedBy` data and
|
|
152
|
+
confirm with `./gradlew dependencyInsight --dependency <group:artifact>` (add
|
|
153
|
+
`--configuration <name>` if needed). This shows the resolution path from a declared
|
|
154
|
+
dependency down to the vulnerable one.
|
|
155
|
+
- Bump the declared ancestor, then re-run `dependencyInsight` (or rely on step 6's
|
|
156
|
+
verify) to confirm the transitive version is now a CVE-clearing one. If the ancestor's
|
|
157
|
+
latest still pulls the vulnerable version, this path does not clear it — fall through.
|
|
158
|
+
3. **Existing constraint / BOM / platform entry** — if the repo already pins the version via
|
|
159
|
+
a `constraints {}` block or a `platform()`/BOM, and a CVE-clearing version exists, update
|
|
160
|
+
that existing pin to the latest available (clearing) version. (Only update an entry that
|
|
161
|
+
already exists — see the hard rule below.)
|
|
162
|
+
4. **Suppress — only when no existing declaration can be bumped to a CVE-clearing version**
|
|
163
|
+
(nothing to update, no released version clears the CVE, or the only available fix would
|
|
164
|
+
require a new entry). A **false positive** is also fixed by a bump when the latest version
|
|
165
|
+
makes the finding go away; suppress a false positive only when no clearing bump exists.
|
|
166
|
+
- Discover the suppression file per `references/suppression.md`. Not found → STOP.
|
|
167
|
+
- **One block per CVE.** Before writing, check whether the CVE is already suppressed (in
|
|
168
|
+
the file or in a block you've added this run). If so, add the new affected `<packageUrl>`
|
|
169
|
+
to that existing block instead of creating a second one — never emit the same `<cve>`
|
|
170
|
+
twice. The same CVE across several artifacts (e.g. `httpcore` 4.x and `httpcore5`) is one
|
|
171
|
+
block with repeated `--pkgurl`.
|
|
172
|
+
- **When in doubt between permanent and temporary, choose temporary** (default, 2 months).
|
|
173
|
+
Pass `--permanent` only for a confident false positive across *all* affected artifacts;
|
|
174
|
+
if a CVE is FP for one artifact but real-yet-unfixable for another, the single block is
|
|
175
|
+
temporary.
|
|
176
|
+
- Generate the block with `python "<plugin-root>/scripts/gen_suppression.py"` (repeat
|
|
177
|
+
`--pkgurl` for each affected artifact; `--permanent` only per the rule above, else the
|
|
178
|
+
temporary default) and insert it into the file's `<suppressions>` root.
|
|
179
|
+
|
|
180
|
+
**Hard rule:** never *add* a brand-new direct declaration (or a new constraint) for a
|
|
181
|
+
transitive dependency just to pin it. Only *update entries that already exist*. If the only
|
|
182
|
+
way to pin the fixed version would be a new entry, that path is unavailable — fall through to
|
|
183
|
+
suppression (step 5.4).
|
|
184
|
+
|
|
185
|
+
After any bump, remove any now-obsolete `<suppress>` entry for a CVE no longer reported.
|
|
186
|
+
|
|
187
|
+
**Compile check after bumps.** A latest-version bump can break the build. After applying all
|
|
188
|
+
bumps (and before the step-6 verify), confirm the project still **compiles — main and test
|
|
189
|
+
sources**: `./gradlew classes testClasses` (this compiles both without running tests). Fix
|
|
190
|
+
any compilation errors the bump introduced (API changes, moved packages, renamed methods).
|
|
191
|
+
**Do not run the tests** — CI runs them later; you only guarantee it compiles. If compilation
|
|
192
|
+
cannot be made to pass, STOP and report (hard stop) rather than pushing a broken build.
|
|
193
|
+
|
|
194
|
+
Read `references/suppression.md` before writing any suppression.
|
|
195
|
+
|
|
196
|
+
### 6. Verify
|
|
197
|
+
Run `./gradlew dependencyCheckAggregate` in the repo — append `-DuseSonatype` if the
|
|
198
|
+
`useSonatype` parameter was specified (`./gradlew dependencyCheckAggregate -DuseSonatype`),
|
|
199
|
+
otherwise run it without the flag. Locate the freshly written report under `build/reports/`
|
|
200
|
+
and re-parse it with the matching parser: **prefer `dependency-check-report.json` via
|
|
201
|
+
`parse_json_report.py`**, falling back to `dependency-check-report.html` via `parse_report.py`
|
|
202
|
+
only if the project does not emit JSON locally. Confirm **0** findings
|
|
203
|
+
>= 7.0 remain **and** `unparsed` is empty. If residuals remain, loop back to step 5. Non-empty
|
|
204
|
+
`unparsed` at this step is also a hard stop — never treat it as convergence. If it cannot
|
|
205
|
+
converge or Gradle fails → STOP and report; do not push. Dependency-check's own validation of
|
|
206
|
+
the suppression file (it fails the build on malformed suppression XML) is the guardrail
|
|
207
|
+
against a corrupt suppression file reaching this step.
|
|
208
|
+
|
|
209
|
+
### 7. Commit — fork on what changed
|
|
210
|
+
|
|
211
|
+
First decide which path applies by inspecting exactly what step 5 changed:
|
|
212
|
+
|
|
213
|
+
**Path A — suppressions-only.** The *only* changed file is the suppression file (no
|
|
214
|
+
dependency-declaration edits, no code, no other files at all) **and the step-4 branch was
|
|
215
|
+
newly created** (not a reused ticket branch). Then **do not** open a branch or PR — produce a
|
|
216
|
+
single local commit on the latest default branch and hand it back:
|
|
217
|
+
|
|
218
|
+
- Move onto the latest default branch, carrying the suppression edit: `git switch <default>`,
|
|
219
|
+
then `git merge --ff-only origin/<default>` to bring it to the latest tip. If local
|
|
220
|
+
`<default>` cannot fast-forward (it has diverged), STOP and report — do not commit on a
|
|
221
|
+
stale or diverged base.
|
|
222
|
+
- The freshly-created fix branch from step 4 has no commits; delete it:
|
|
223
|
+
`git branch -D bugfix/<ticket>-handle-weekly-cves` (use the actual branch name).
|
|
224
|
+
- Stage **only** the suppression file (exact path; never `git add -A`/`-u`).
|
|
225
|
+
- Commit with a message describing the suppression(s) that **contains NO ticket number** —
|
|
226
|
+
no `PX-…`, `HP-…`, or any `[A-Z]+-\d+` token anywhere in subject or body. For example:
|
|
227
|
+
`chore(security): suppress <CVE-ids> dependency-check findings`.
|
|
228
|
+
- **Do not push.** Skip steps 8 (release notes) and 9 (PR).
|
|
229
|
+
- **Output of the skill = this commit.** Show the invoker the commit SHA, its message, and the
|
|
230
|
+
suppression diff, and stop. Pushing is the invoker's decision.
|
|
231
|
+
|
|
232
|
+
**Path B — anything else** (any dependency bump, any change beyond the suppression file, **or**
|
|
233
|
+
suppressions-only on a **reused** ticket branch — never delete or bypass an existing ticket
|
|
234
|
+
branch). The fix branch already exists from step 4 — commit onto it:
|
|
235
|
+
|
|
236
|
+
- Stage **only** the specific files edited in step 5 — the dependency-declaration file(s)
|
|
237
|
+
(e.g. `gradle/libs.versions.toml`, `build.gradle[.kts]`) and, if touched, the suppression
|
|
238
|
+
file, by exact path: `git add <dependency-file-path> <suppression-file-path>`. **Never**
|
|
239
|
+
`git add -A` or `git add -u` — this skill pushes and opens a PR autonomously, and sweeping
|
|
240
|
+
unrelated working-tree changes into that commit is not acceptable.
|
|
241
|
+
- Commit: `git commit -m "fix(<repo>): remediate CVSS>=7 dependency-check findings"`. Do
|
|
242
|
+
**not** push yet — release notes (step 8) are derived from committed changes and get
|
|
243
|
+
**amended into this same commit**, so it must not be pushed until step 8 completes.
|
|
244
|
+
|
|
245
|
+
### 8. Release notes
|
|
246
|
+
*(Path B only — skipped entirely in the suppressions-only Path A, which ends at step 7.)*
|
|
247
|
+
|
|
248
|
+
Invoke the `write-releasenotes` skill (via the Skill tool) with **pre-approval** so it writes
|
|
249
|
+
without a confirmation gate — this skill runs autonomously (e.g. "write and apply the release
|
|
250
|
+
notes for this CVE remediation, no need to confirm"). `write-releasenotes` reads the **committed**
|
|
251
|
+
changes on the branch (hence step 7 commits first), derives the ticket key/type **from the
|
|
252
|
+
branch name** (which carries `<ticket>`), scaffolds per-language `.adoc` entries via the
|
|
253
|
+
project's `create*` Gradle task when missing, and fills them in house style.
|
|
254
|
+
|
|
255
|
+
- If `write-releasenotes` reports **not applicable** (no releasenotes subproject and no `create*`
|
|
256
|
+
Gradle tasks), that is a notice, not an error — proceed to step 9 and put the CVE summary
|
|
257
|
+
in the PR body instead.
|
|
258
|
+
- If it wrote or updated notes: stage **only** the generated/updated `.adoc` files and any
|
|
259
|
+
version file the `create*` task bumped, by exact path (same never-`git add -A/-u` rule as
|
|
260
|
+
step 7), then **amend them into the step-7 commit**: `git commit --amend --no-edit`. This
|
|
261
|
+
keeps the remediation and its release notes as a single commit. (Safe because the branch
|
|
262
|
+
has not been pushed yet — no force-push needed.)
|
|
263
|
+
|
|
264
|
+
Then push the branch (single commit).
|
|
265
|
+
|
|
266
|
+
### 9. PR
|
|
267
|
+
*(Path B only — the suppressions-only Path A produces a local commit and no PR.)*
|
|
268
|
+
|
|
269
|
+
The Bitbucket project/repo slugs are derived from the repo's `origin` remote URL (not asked
|
|
270
|
+
of the user). If no open PR exists for the branch, create one with
|
|
271
|
+
`mcp__plugin_planetexpress_jira-tools__create_bitbucket_pr`, targeting the default branch
|
|
272
|
+
(`<default>` from step 4). Body: a per-CVE summary — CVE id, score, and whether it was bumped
|
|
273
|
+
(old→new version) or suppressed (temp until <date> / permanent, with reason).
|
|
274
|
+
|
|
275
|
+
## Hard stops (abort, report, never push)
|
|
276
|
+
|
|
277
|
+
| Situation | Behaviour |
|
|
278
|
+
|---|---|
|
|
279
|
+
| Invoked without a `ticket` | Report and ask for the JIRA key before doing anything |
|
|
280
|
+
| More than one existing branch matches the ticket key (step 4) | Report the candidates and stop; do not guess the branch |
|
|
281
|
+
| Bamboo auth / URL resolution failure | Report and stop before fetching anything |
|
|
282
|
+
| Neither a `dependency-check-report.json` nor `.html` file in any build artifact (step 2) | Report the artifacts found and stop |
|
|
283
|
+
| Suppression needed but no suppression file found | Report and stop; never create one |
|
|
284
|
+
| Bumps break compilation (`classes`/`testClasses`) and can't be fixed | Report and stop; do not push a build that doesn't compile |
|
|
285
|
+
| Gradle verify fails or residual >= 7.0 findings remain | Report and stop; do not push |
|
|
286
|
+
| Either parser returns any non-empty `unparsed` entries (initial parse or verify) | Report and stop; never silently proceed as if 0 findings |
|
|
287
|
+
| Suppressions-only path (7A) but local default branch can't fast-forward to `origin/<default>` | Report and stop; don't commit on a diverged/stale base |
|
|
288
|
+
|
|
289
|
+
## Known limitations
|
|
290
|
+
|
|
291
|
+
- Every active finding is classified by **severity** — a numeric CVSS score, or, for scoreless
|
|
292
|
+
advisories (common for JS/NPM), the qualitative band. Both parsers apply the identical rule
|
|
293
|
+
(`parse_json_report.py` from the typed `cvssv2/v3/v4` and `severity` fields — taking the worst
|
|
294
|
+
numeric across the CVSS versions present; `parse_report.py` by scraping the HTML). Then:
|
|
295
|
+
- **below HIGH** (score < 7.0, or a low/medium/none band) → **excluded** — out of scope, does
|
|
296
|
+
not redden the build. A medium/low unscored advisory is *not* a hard stop.
|
|
297
|
+
- **at/above HIGH & a CVE id** → a normal `findings` entry (a scoreless HIGH/CRITICAL carries
|
|
298
|
+
`score: null` with the band in `severity`).
|
|
299
|
+
- **at/above HIGH & no CVE id** (e.g. a HIGH/CRITICAL `GHSA-*`) → `unparsed`. This skill only
|
|
300
|
+
remediates CVEs, so it cannot fix these — routing them to `unparsed` (a hard stop) prevents
|
|
301
|
+
converging to "0 findings" on a build that is red because of one. Such a build stops for
|
|
302
|
+
human triage rather than being reported as fixed. Identifier (`cpe`) buttons, present on
|
|
303
|
+
every dependency, are always ignored.
|
|
304
|
+
- The `unparsed` bucket is a fail-safe signal, not noise: an at/above-HIGH non-CVE finding, or
|
|
305
|
+
an active finding whose severity could be determined **neither** numerically **nor** by band.
|
|
306
|
+
Treat any non-empty `unparsed` as a hard stop, identical in severity to the other hard stops
|
|
307
|
+
above — never converge or report success while it is non-empty. (`unparsed` is deduplicated by
|
|
308
|
+
advisory id + file, so the same finding on the same artifact never appears twice.)
|
|
309
|
+
- Malformed suppression XML is guarded at the verify step: `dependencyCheckAggregate` itself
|
|
310
|
+
validates the suppression file and fails the build if it's invalid.
|
|
311
|
+
|
|
312
|
+
## Out of scope
|
|
313
|
+
|
|
314
|
+
- Creating a suppression file that doesn't exist.
|
|
315
|
+
- Adding new declarations for transitive-only dependencies (suppress instead).
|
|
316
|
+
- Merging the PR — open it and stop; merge is the user's call.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Suppression policy & syntax (dependency-check)
|
|
2
|
+
|
|
3
|
+
## Finding the suppression file (auto-discover, never create)
|
|
4
|
+
1. Read the repo's Gradle config for the configured path:
|
|
5
|
+
`dependencyCheck { suppressionFile "..." }` or `suppressionFiles = [...]`
|
|
6
|
+
(check `build.gradle`, `build.gradle.kts`, and any `gradle/*.gradle`).
|
|
7
|
+
2. If not configured there, glob `*suppress*.xml` — repo root first, then repo-wide.
|
|
8
|
+
3. **If no file is found, STOP and report.** Do not create one. Do not suppress.
|
|
9
|
+
|
|
10
|
+
## When to suppress vs bump
|
|
11
|
+
- Prefer a version bump of an **existing** declaration. Never add a new entry for a
|
|
12
|
+
transitive dependency.
|
|
13
|
+
- Suppress only when: no existing declaration to bump, no patched version exists, or the
|
|
14
|
+
finding is a false positive.
|
|
15
|
+
|
|
16
|
+
## One suppression per CVE — never suppress the same CVE twice
|
|
17
|
+
- A given CVE id must appear in **at most one** `<suppress>` block across the entire file.
|
|
18
|
+
Before writing a suppression, scan the file (and any blocks you're adding this run) for the
|
|
19
|
+
CVE. If it's already covered, **do not add a second block** — extend the existing one.
|
|
20
|
+
- The same CVE often affects **several declared artifacts** (e.g. `httpcore` 4.x and
|
|
21
|
+
`httpcore5` 5.x). Suppress it **once**, in a single block that lists every affected
|
|
22
|
+
`<packageUrl>` (dependency-check allows unbounded `<packageUrl>` in one `<suppress>`). Do
|
|
23
|
+
**not** emit one block per artifact for the same CVE.
|
|
24
|
+
- `gen_suppression.py` takes repeated `--pkgurl` for exactly this — one block, many packages.
|
|
25
|
+
|
|
26
|
+
## Permanent vs temporary
|
|
27
|
+
- **When in doubt, choose temporary.** A temporary suppression expires and re-flags; a wrong
|
|
28
|
+
permanent one hides a real CVE forever. Only go permanent when it is *confidently* a pure
|
|
29
|
+
false positive across **all** affected artifacts.
|
|
30
|
+
- If a CVE is a false positive for one artifact but a real-yet-unfixable finding for another
|
|
31
|
+
(mixed fate) → the single block is **temporary** (the tie-break rule above).
|
|
32
|
+
- **False positive (confident, all artifacts) → permanent**: omit `until`. Notes state
|
|
33
|
+
concisely why it does not apply (e.g. CPE mismatch, unreachable code path).
|
|
34
|
+
- **Otherwise → temporary**: `until="<today + 2 months>Z"` (the default; 2 months).
|
|
35
|
+
- One `<suppress>` block may cover multiple `<cve>` entries under a single notes summary.
|
|
36
|
+
|
|
37
|
+
## Notes content — concise technical reason only
|
|
38
|
+
The `<notes>` text states **why the CVE is suppressed** and nothing else. Keep it to the
|
|
39
|
+
technical fact (CPE mismatch, unreachable code path, no patched release yet, patch not yet
|
|
40
|
+
scheduled). Then stop.
|
|
41
|
+
|
|
42
|
+
- **Never mention a JIRA ticket** — no `PX-…`, `HP-…`, or any `[A-Z]+-\d+` key. The `ticket`
|
|
43
|
+
parameter drives the fix branch; it does **not** track the suppression, so "Tracked by PX-…"
|
|
44
|
+
is simply false.
|
|
45
|
+
- **No boilerplate.** Do not append filler like "Expires; re-flags if unresolved" or "Added to
|
|
46
|
+
unblock builds" — the `until` attribute already encodes expiry; the sentence adds nothing.
|
|
47
|
+
|
|
48
|
+
## Syntax
|
|
49
|
+
```xml
|
|
50
|
+
<suppress until="2026-09-13Z">
|
|
51
|
+
<notes><![CDATA[No patched 4.x release; HTTP/2 HPACK decoder path does not exist in the 4.x line.]]></notes>
|
|
52
|
+
<packageUrl regex="true">pkg:maven/org\.apache\.httpcomponents/httpcore@.*</packageUrl>
|
|
53
|
+
<packageUrl regex="true">pkg:maven/org\.apache\.httpcomponents\.core5/httpcore5@.*</packageUrl>
|
|
54
|
+
<cve>CVE-2026-54399</cve>
|
|
55
|
+
<cve>CVE-2026-54428</cve>
|
|
56
|
+
</suppress>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Generate blocks with `scripts/gen_suppression.py` — it handles the purl→regex escaping and
|
|
60
|
+
the today+2-months default date, and takes **repeated `--pkgurl`** to list every affected
|
|
61
|
+
artifact in one block:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
python "<plugin-root>/scripts/gen_suppression.py" \
|
|
65
|
+
--pkgurl pkg:maven/org.apache.httpcomponents/httpcore@4.4.16 \
|
|
66
|
+
--pkgurl pkg:maven/org.apache.httpcomponents.core5/httpcore5@5.4.2 \
|
|
67
|
+
--cve CVE-2026-54399 --cve CVE-2026-54428 --notes "..."
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Insert new blocks inside the root `<suppressions>` element of the discovered file. The script
|
|
71
|
+
emits `<suppress>` at column 0 with 4-space child indent; when inserting, **re-indent the whole
|
|
72
|
+
block to match the surrounding blocks** (typically `<suppress>` one level in, children one level
|
|
73
|
+
deeper) so the file stays consistent — dependency-check ignores the whitespace, but a mismatched
|
|
74
|
+
block is noise in review. Omit `--until` for the temporary default (2 months); pass `--permanent`
|
|
75
|
+
only for a confident, all-artifacts false positive.
|
|
76
|
+
|
|
77
|
+
## Removing obsolete suppressions
|
|
78
|
+
After a successful bump, if an existing `<suppress>` entry now targets a CVE that no
|
|
79
|
+
longer appears in the report, remove that entry.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate an OWASP Dependency-Check <suppress> block for a finding.
|
|
3
|
+
|
|
4
|
+
Temporary suppression: pass --until YYYY-MM-DD (or let the skill compute
|
|
5
|
+
today + 2 months). Permanent (false positive): omit --until.
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import datetime
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import textwrap
|
|
12
|
+
|
|
13
|
+
NOTE_WIDTH = 100
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pkgurl_to_regex(package_url: str) -> str:
|
|
17
|
+
"""Escape a Maven purl and match any version: `...@4.4.16` -> `...@.*`.
|
|
18
|
+
|
|
19
|
+
re.escape() does not escape `/` or `:` on Python >= 3.7, so no further
|
|
20
|
+
unescaping of those characters is needed after re.escape().
|
|
21
|
+
"""
|
|
22
|
+
base = package_url.split("@", 1)[0]
|
|
23
|
+
return re.escape(base) + "@.*"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def default_until(today: datetime.date, months: int = 2) -> str:
|
|
27
|
+
month = today.month - 1 + months
|
|
28
|
+
year = today.year + month // 12
|
|
29
|
+
month = month % 12 + 1
|
|
30
|
+
day = min(today.day, 28)
|
|
31
|
+
return f"{year:04d}-{month:02d}-{day:02d}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def wrap_notes(notes: str) -> list:
|
|
35
|
+
"""Wrap notes for readability inside the CDATA. Breaks on whitespace only
|
|
36
|
+
(break_long_words / break_on_hyphens off) so CVE ids and version strings
|
|
37
|
+
like `CVE-2026-54428` or `5.5-beta1` are never split. Caller-supplied
|
|
38
|
+
newlines are honoured as paragraph boundaries. Returns physical lines."""
|
|
39
|
+
lines = []
|
|
40
|
+
for para in notes.split("\n"):
|
|
41
|
+
if not para.strip():
|
|
42
|
+
lines.append("")
|
|
43
|
+
continue
|
|
44
|
+
lines.extend(textwrap.wrap(
|
|
45
|
+
para, width=NOTE_WIDTH,
|
|
46
|
+
break_long_words=False, break_on_hyphens=False))
|
|
47
|
+
return lines
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def suppress_block(package_urls, cves: list, notes: str, until) -> str:
|
|
51
|
+
"""One <suppress> block. `package_urls` is a single purl or a list — the
|
|
52
|
+
same CVE affecting several declared artifacts is suppressed ONCE, in one
|
|
53
|
+
block listing every affected packageUrl (dependency-check allows unbounded
|
|
54
|
+
<packageUrl>). Never emit a separate block per artifact for the same CVE."""
|
|
55
|
+
if isinstance(package_urls, str):
|
|
56
|
+
package_urls = [package_urls]
|
|
57
|
+
open_tag = f'<suppress until="{until}Z">' if until else "<suppress>"
|
|
58
|
+
# One level of child indent, matching the dependency-check file's 4-space
|
|
59
|
+
# convention. The block is emitted with <suppress> at column 0; the skill
|
|
60
|
+
# re-indents the whole block by the file's base indent on insertion.
|
|
61
|
+
ind = " "
|
|
62
|
+
pkg_lines = "\n".join(
|
|
63
|
+
f'{ind}<packageUrl regex="true">{pkgurl_to_regex(p)}</packageUrl>'
|
|
64
|
+
for p in package_urls
|
|
65
|
+
)
|
|
66
|
+
cve_lines = "\n".join(f"{ind}<cve>{c}</cve>" for c in cves)
|
|
67
|
+
# A literal "]]>" inside notes would close the CDATA section early and
|
|
68
|
+
# corrupt the XML. Split it across two CDATA sections instead.
|
|
69
|
+
safe_notes = notes.replace("]]>", "]]]]><![CDATA[>")
|
|
70
|
+
note_lines = wrap_notes(safe_notes)
|
|
71
|
+
# Short notes stay on one line; anything that wraps (or carries explicit
|
|
72
|
+
# line breaks) is laid out on its own indented lines for readability.
|
|
73
|
+
if len(note_lines) <= 1:
|
|
74
|
+
inner = note_lines[0] if note_lines else ""
|
|
75
|
+
notes_el = f"{ind}<notes><![CDATA[{inner}]]></notes>"
|
|
76
|
+
else:
|
|
77
|
+
body = "\n".join(f"{ind}{ln}".rstrip() for ln in note_lines)
|
|
78
|
+
notes_el = f"{ind}<notes><![CDATA[\n{body}\n{ind}]]></notes>"
|
|
79
|
+
return (
|
|
80
|
+
f"{open_tag}\n"
|
|
81
|
+
f"{notes_el}\n"
|
|
82
|
+
f"{pkg_lines}\n"
|
|
83
|
+
f"{cve_lines}\n"
|
|
84
|
+
f"</suppress>"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv):
|
|
89
|
+
ap = argparse.ArgumentParser()
|
|
90
|
+
ap.add_argument("--pkgurl", action="append", required=True,
|
|
91
|
+
help="Affected package purl; repeat to cover the same CVE across "
|
|
92
|
+
"multiple declared artifacts in one block")
|
|
93
|
+
ap.add_argument("--cve", action="append", required=True)
|
|
94
|
+
ap.add_argument("--notes", required=True)
|
|
95
|
+
group = ap.add_mutually_exclusive_group()
|
|
96
|
+
group.add_argument("--until", help="YYYY-MM-DD; omit for the default (today + 2 months)")
|
|
97
|
+
group.add_argument("--permanent", action="store_true",
|
|
98
|
+
help="False-positive only: permanent suppression, no 'until'")
|
|
99
|
+
ns = ap.parse_args(argv)
|
|
100
|
+
|
|
101
|
+
# Policy: false positive -> permanent (no until); everything else ->
|
|
102
|
+
# temporary (today + 2 months). Omitting --until must NEVER silently
|
|
103
|
+
# produce a permanent suppression.
|
|
104
|
+
if ns.permanent:
|
|
105
|
+
until = None
|
|
106
|
+
elif ns.until:
|
|
107
|
+
until = ns.until
|
|
108
|
+
else:
|
|
109
|
+
until = default_until(datetime.date.today())
|
|
110
|
+
|
|
111
|
+
print(suppress_block(ns.pkgurl, ns.cve, ns.notes, until))
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
sys.exit(main(sys.argv[1:]))
|