Skip to content

Improvements to H1 Audit skill - #163

Draft
mandreko-bitwarden wants to merge 2 commits into
mainfrom
h1-audit-improvements
Draft

Improvements to H1 Audit skill#163
mandreko-bitwarden wants to merge 2 commits into
mainfrom
h1-audit-improvements

Conversation

@mandreko-bitwarden

@mandreko-bitwarden mandreko-bitwarden commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📔 Objective

This inverts the direction of traversal in the auditing-hackerone-vulns skill so HackerOne is the source of truth instead of Jira, and folds in a batch of correctness fixes I've been running in my working copy.

The audit used to start from a project = VULN JQL sweep, which meant any open HackerOne report that never got a VULN ticket created was invisible to it. It now enumerates open reports across both programs (bitwarden VDP and bitwarden-bbp Bug Bounty) via the HackerOne MCP and correlates outward to VULN tickets, child engineering items, fix PRs, and release state.

New action tokens

  • 🆕 Create VULN Ticket for an open report with no linked VULN
  • 🟣 Close HackerOne Report for a VULN already Closed or Rejected while the report stayed open
  • 🏁 Close Out for a Verified VULN whose fix is confirmed in production but whose ticket was never moved to Closed

A reconciliation sweep and a 🗂️ Orphaned VULNs section catch open Jira tickets whose report was already closed, which a pure H1-first traversal would otherwise miss.

Status handling

Child statuses gain a Blocked category, so On Hold work reports as stalled rather than collapsing into ⚪ Waiting, plus Ready for Dev and In QA. The query exclusion list widens to (Done, Closed, Rejected, Resolved, Canceled), because VULN-project status not in (Done, Verified) does not actually exclude Rejected or Closed.

Correctness fixes on the GitHub side

The PR search --jq filter now reads .pull_request.merged_at. The Search Issues API returns no mergedAt field, so the old filter reported every PR as unmerged. Release inclusion picks the earliest release whose publishedAt postdates the merge and confirms it by grepping the tag-to-tag commit range for the PR number, so a missing cherry-pick reports as 🔵 Monitor with an engineering flag rather than counting as shipped.

Documented API constraints

These are all things I hit in practice and kept re-learning:

  • Report IDs are base64 GIDs that need decoding
  • The Jira reference lives in get_report_activities, not get_report
  • search_issues silently drops the issuelinks field, so linkedIssues() is required
  • search_reports requires program_handles and returns HTTP 500 for the open and needs-more-info states
  • Jira's Fix Version "(Released)" annotation is untrustworthy

Also adds tool-usage guardrails against interpreter pipes, file writes, heredocs, shell orchestration, and 2>/dev/null error suppression.

Note on history

I force-pushed over the previous two commits on this branch. They had bumped the plugin to 1.3.0, which main then released independently as an unrelated ADR-alignment change, so the branch was conflicting. This version sits at 1.4.0 on top of current main and the changelog is sequential again.

@withinfocus withinfocus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still hoping to see the prompting for dates when a code change happens. Automation in Jira can be added for some of that to actually prompt someone, but if you are already querying GitHub I would think this can call out what to set a confirmation date to.

@@ -11,109 +11,153 @@ allowed-tools: mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__search
| 🔴 | **Update VULN Status** | Child item has progressed (In Progress/Review) but VULN is still at a lower status |
| 🟡 | **Mark Remediated** | Child item is Done — set Remediation Date to merged PR date and move VULN to Remediated |
| 🟢 | **Verify & Close** | Fix is in a release that has already shipped — verify in prod, add Confirmation Date, close HackerOne |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 As we discussed in chat, the confirmation date usually gets set shortly after one is remediated, so you know when the release will go out and therefore the validation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep trying to figure out a good way to calculate the confirmation date, and haven't come up with anything. We have to take the merge date of a PR, compare it to a release cut date, and then decide which release it will be in accordingly. But getting the release calendar into this skill is a bit of a mess each time I try. This is one of those things that may be easier to handle manually?

Do you have any thoughts on how to handle this better?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should just do something like prompt the user to enter the confirmation date.

Comment thread plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md Outdated
The audit started from a `project = VULN` JQL sweep, so any open HackerOne
report that never got a VULN ticket created was invisible to it. The skill
now enumerates open reports across both programs (bitwarden VDP and
bitwarden-bbp Bug Bounty) via the HackerOne MCP and correlates outward to
VULN tickets, child engineering items, fix PRs, and release state.

New action tokens cover the gaps this exposes. 🆕 Create VULN Ticket fires
on an open report with no linked VULN, 🟣 Close HackerOne Report on a VULN
that is already Closed or Rejected while the report stayed open, and 🏁
Close Out on a Verified VULN whose fix is confirmed in production but whose
ticket was never moved to Closed. A reconciliation sweep and 🗂️ Orphaned
VULNs section catch open Jira tickets whose report was already closed,
which a pure H1-first traversal would otherwise miss.

Child-status handling gains a Blocked category so On Hold work reports as
stalled instead of collapsing into ⚪ Waiting, plus Ready for Dev and In QA.
The query exclusion list widens to (Done, Closed, Rejected, Resolved,
Canceled), since VULN-project `status not in (Done, Verified)` does not
actually exclude Rejected or Closed.

Two correctness fixes on the GitHub side. The PR search `--jq` filter reads
`.pull_request.merged_at`, because the Search Issues API returns no
`mergedAt` field and the old filter reported every PR as unmerged. Release
inclusion now picks the earliest release whose `publishedAt` postdates the
merge and confirms it by grepping the tag-to-tag commit range for the PR
number, so a missing cherry-pick reports as 🔵 Monitor with an engineering
flag rather than counting as shipped.

Also documents the API constraints hit in practice (base64 GID report ids,
the Jira reference living in get_report_activities, search_issues silently
dropping issuelinks, search_reports returning HTTP 500 on the open and
needs-more-info states) and adds tool-usage guardrails against interpreter
pipes, file writes, heredocs, shell orchestration, and error suppression.
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Claude Code validation

Result: Issues found

Validated PR #163 (h1-audit-improvements) against merge base 0a5f03a. The changeset bumps bitwarden-security-engineer 1.3.0 → 1.4.0 and rewrites the auditing-hackerone-vulns skill to traverse HackerOne-first instead of Jira-first. Plugin validation, skill review, and the configuration/security scan all ran. Version consistency across the four required locations is correct, no credentials were found, and no file attempts to direct this review. The run is marked Issues found because the changeset newly grants Bash(echo *) — a tool grant wider than the skill needs, which the changelog describes as a tightening.

Critical

None.

Major

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:4 — Newly added Bash(echo *) permits arbitrary file writes, contradicting the skill's own rules. The grant is new in this PR (the 1.3.0 frontmatter had no local shell primitives beyond gh). Its only use in the body is feeding a literal newline-separated GID string into a decoder at lines 87–89. But a trailing-wildcard Bash(...) rule is a prefix match on the command string, and shell redirection is not a command separator, so echo <anything> > <any path> is auto-approved with no prompt — including ~/.claude/settings.json, ~/.bashrc, or files in the working repo. Line 21 of this same file says "Do not write files. Do not write to /tmp/, to any file path, or use heredocs", and line 28 says "Keep the pipeline to echo and base64 -d only" — but body prose is not an enforcement boundary; allowed-tools is. The grant permits exactly the class of action the skill spends two rules forbidding, and the skill ingests contributor-controlled HackerOne report titles and activity comments wholesale in Step 2. Fix: move the GID decode into a checked-in script invoked through a narrow grant, e.g. Bash(${CLAUDE_PLUGIN_ROOT}/scripts/decode-report-gids.sh *), so the only executable surface is reviewable code that ships with the plugin. If echo must stay, note in the skill that no allowed-tools pattern can constrain redirection, and justify the grant explicitly.

  • plugins/bitwarden-security-engineer/CHANGELOG.md:25 — The allowed-tools entry describes a tightening that, against the released baseline, is a widening. The entry reads "allowed-tools tightened. Bash(base64 *) narrowed to Bash(base64 -d) … Unused Bash(gh search prs *) and mcp__hackerone__get_current_user grants were dropped". Neither Bash(base64 *) nor mcp__hackerone__get_current_user exists at the merge base — both appeared and disappeared within this branch (traced across 0a5f03a0d13b9ed4aba11), so the entry documents intra-branch churn rather than the 1.3.0 → 1.4.0 delta. The actual change to the Bash surface is: added Bash(echo *) and Bash(base64 -d); removed Bash(gh release list *) and Bash(gh search prs *). Local shell execution is new capability here, and Bash(echo *) — the broadest grant in the list — is not mentioned anywhere in the entry. Fix: rewrite the bullet to state the net change against 1.3.0 and name Bash(echo *) with its justification, so an operator reading the release notes can see what they are accepting.

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:1 — This PR pushed the skill from inside the size target to 70% over it: 1,877 words / 229 lines → 5,097 words / 432 lines (+3,220 words, +172%), against a 1,000–3,000 word target. It is now by far the largest skill in the plugin; the next largest sibling is perform-security-review/SKILL.md at 1,927 words and the plugin median is ~1,010. Largest contributors: Step 5 release inclusion (lines 192–248, +545), new Step 2 (110–150, +487), the Step 8 output template (312–404, +419), Step 1 (49–106, +410), tool usage rules (18–29, +305), edge cases (420–432, +292). Fix: extract to references/ as described below; the two extractions alone should bring the core back near the ceiling.

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:213-248, :100-104 — Reference-grade detail added inline, in the one skill directory that has no references/. Three of the eight sibling skills in this plugin ship references/ (analyzing-code-security, perform-security-review, reviewing-security-architecture) and threat-modeling ships both references/ and examples/; this directory has neither, and this PR is what created enough material to need one. Lines 213–248 (~615 words) are a self-contained release-verification procedure: two release strategies, the 250-commit compare cap and its pagination protocol, a worked web-v2025.5.0...web-v2025.6.0 example, clients monorepo tag types, and the ahead/identical/diverged semantics of the compare status field. Lines 100–104 (~140 words) are a base64 encoding appendix — byte arithmetic, GNU-vs-BSD base64 divergence, and a contingency for 8-digit report IDs that do not exist yet. Fix: create references/release-verification.md and move lines 213–248, leaving the 3-line decision list at 233–238 plus a pointer; move the base64 appendix alongside it, keeping the operative instruction at lines 84–98.

Minor

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:312-404 — The output template (~859 words) is nine near-identical markdown tables differing only in column set; this PR grew it ~419 words by adding Program | Submitted | Sev | Title columns and appending the 🆕, 🟣, 🏁, and 🗂️ tables. The operative guidance is the 11 formatting notes at 406–416 and the omit-empty-sections rule at 310. Fix: move the template to examples/audit-report.md, following the threat-modeling/examples/ precedent already established in this plugin.

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:100, :102, :104, :231, :240 — Style drift from imperative instruction to justification prose in newly added material: line 100 opens "Why the batch decode is valid:", line 102 explains a GNU/BSD platform difference, line 104 hedges against a report-ID length change that has not happened, line 231 narrates an observed total_commits: 281, and line 240 is a block quote on why tag-to-tag compare is preferred. The rationale is worth keeping. Fix: move it to references/ rather than the always-resident core.

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:3 — The description does not advertise two user-visible outcomes this PR added: Step 7's reconciliation sweep producing the 🗂️ Orphaned VULNs section (lines 291–304), and the 🆕 Create VULN Ticket path (line 37). Queries a user would plausibly type — "which HackerOne reports have no Jira ticket?", "find orphaned VULN tickets" — map to no listed trigger. The description got shorter overall (590 → 571 chars), so there is room. Fix: add a clause such as "…find HackerOne reports with no VULN ticket, reconcile orphaned VULN tickets whose reports are already closed…".

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:4mcp__hackerone__get_report is newly granted but never invoked. Three of the four new mcp__hackerone__* grants are used: list_programs (line 54), search_reports (line 67), get_report_activities (line 141). get_report appears only at line 82 in passing and at line 144 to say the data is not there ("The Jira link lives in the activity timeline, not in the get_report payload"); every field the workflow records at line 105 comes from search_reports. (Reported independently by both plugin validation and skill review.) Fix: drop it from allowed-tools, or add the step that needs it.

  • plugins/bitwarden-security-engineer/README.md:29 — The new Requirements table lists get_issue_remote_links among the Jira tools "the audit correlates against", but no step in the rewritten body calls it: Step 2 correlates via description parsing and get_report_activities, Step 3 via linkedIssues(). The grant itself predates this PR and is out of scope; the newly written documentation asserting the dependency is not. Fix: drop it from the table, or name the step that uses it.

  • plugins/bitwarden-security-engineer/skills/auditing-hackerone-vulns/SKILL.md:87-89 — The three base64 GIDs in the worked example decode to HackerOne report IDs 2865048, 3572086, and 3969362, which appear to be live rather than placeholders. These are not credentials and report IDs alone disclose little, but every other example in the file uses an obviously templated form (NNNNNNN, CHILD-KEY, REPO). Fix: re-encode placeholder IDs for consistency.

What passed

  • Version consistency — 1.4.0 in .claude-plugin/marketplace.json:57, plugins/bitwarden-security-engineer/.claude-plugin/plugin.json:3, and root README.md:20. The plugin's agent file carries no version field. MINOR is the correct bump for added capability with nothing removed.
  • Manifest and structure — valid JSON, kebab-case name matching the directory, valid semver, well-formed author/homepage/repository/keywords. The skill sits at the auto-discoverable skills/<name>/SKILL.md path with name: matching its directory. No hooks/, commands/, .mcp.json, or mcpServers block in this plugin, so those checks are N/A.
  • Frontmatter — valid YAML; name and description present on the changed skill; the body stays consistent with the rewritten tool list (line 213 correctly notes gh release list is no longer granted, and the gh search prs removal is matched by the rewrite to gh api search/issues at line 201).
  • Referenced files — the one external pointer added by this PR (line 29, "Requirements section of the plugin README") resolves: plugins/bitwarden-security-engineer/README.md:22 has that section, added in the same PR. No broken paths.
  • Prompt injection (CWE-1427) — clean. No file in the changeset contains text addressed to a reviewer, validator, or CI, no hidden HTML comments, and no invisible or zero-width characters. The imperative prose at SKILL.md:18–29 is addressed to the skill's own executing agent, which is the legitimate genre for the file.
  • Credentials — no API keys, tokens, passwords, or connection strings in any changed file. Every token match in the diff is the skill's own "action token" vocabulary. The only long base64 literals are the report GIDs noted above. No settings.local.json in the changeset.

Checks run

Check Status
Plugin structure Skipped — run as a dedicated workflow step before this review; see the job log and check status
Marketplace Skipped — run as a dedicated workflow step before this review; see the job log and check status
Version bump Skipped — run as a dedicated workflow step before this review; see the job log and check status
Plugin validation (AI) Ran — 2 major, 3 minor
Skill review (AI) Ran — 2 major, 3 minor
Configuration & security Ran — secret scan clean; no CLAUDE.md, .claude/, agent, command, hook, or skill support file changed, so nothing routed

Two notes on coverage. The changeset touched no CLAUDE.md, .claude/ path, agent, command, hooks.json, or skill support file under references//examples/, so the configuration review's targeted sub-skills had nothing to route to; its always-on secret scan and injection check ran over every changed file and are reported above. The detecting-secrets enrichment from bitwarden-security-engineer was not available in this session, so the manual pattern scan was the fallback.

Narrows the tool grants, fixes four correctness bugs the validator caught,
and documents the skill's external dependencies.

The `Bash(base64 *)` grant permitted reading an arbitrary file in a skill
whose own rules forbid touching the filesystem, so it narrows to
`Bash(base64 -d)`, which only matches the stdin decode form the skill
actually uses. Unused `Bash(gh search prs *)` and
`mcp__hackerone__get_current_user` grants are dropped. Commit-message
matching moves out of a `grep` pipe and into the `gh api --jq` expression
via `test()`, so no `grep` grant is needed and the pipelines no longer use
ungranted commands while the rules claim otherwise. `Bash(echo *)` is added
for the base64 input it was always feeding.

Cherry-pick verification no longer trusts a truncated commit range. The
compare endpoint caps `.commits` at 250 and reports no error when it
truncates, which a normal clients release range exceeds
(`web-v2025.5.0...web-v2025.6.0` reports 281 and returns 250), so a fix
shipped in the tail was reported as a missed cherry-pick and sent
engineering after nothing. The check now compares `total_commits` against
the returned count, paginates the remainder, and treats an unmatched
truncated range as inconclusive.

`mergeCommit` returns to the `gh pr view` field list, and that call is now
unconditional for direct-push repos. Dropping it left no step returning a
commit SHA, so release state for `bitwarden/sm-action` and similar repos
could not be determined at all.

The Step 2 activity-thread fallback no longer dead-ends when it succeeds.
Only the negative case was handled, so a VULN whose description had been
reformatted past the tokenizer could be found and then silently dropped,
which is the exact miss the traversal order exists to prevent. The
reconciliation sweep also stops filing internally-sourced VULNs as orphans
with empty HackerOne cells, splitting on whether a report reference exists
at all.

Consistency fixes: the 🟣 branch names the same five terminal statuses the
queries exclude, Blocked is wired into the In Progress branch, the ➖ table
row matches the tree, and the Step 5 gate covers a VULN with no children.
The Blocked description now states what the skill does rather than claiming
a separate category it never routes to.

Per review feedback, the prompt-avoidance rationale is gone. Approval-prompt
behavior is a property of the run mode a user takes on, not something a
skill should encode, and framing the prompt rather than the grant as the
failure condition is what produced the ungranted-command bug. The
substantive rules stay, minus the reasoning about what does or does not
prompt.

Also documents the operator-configured HackerOne MCP server and the
bitwarden-atlassian-tools dependency in the plugin README, tells the skill
what to do when those tools are absent, corrects a platform-specific claim
about base64 whitespace handling, trims the description toward the length
guideline, and splits the changelog entry across Added/Changed/Fixed.
@mandreko-bitwarden

Copy link
Copy Markdown
Contributor Author
  • the auditing-hackerone-vulns skill is not listed in the agent's skills: block

This is definitely on purpose. This is a stand-alone skill

@bitwarden bitwarden deleted a comment from github-actions Bot Aug 26, 2026

@withinfocus withinfocus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the validator comment -- those majors should be resolved. The skill should be broken out to references and probably thinned into several skills; you have a lot of special cases through this and it can't all be in one skill file for this to operate effectively. "Auditing" these is really a large request that needs composition.

Claude loves to make giant changelogs too, so once the above is done make sure that's compressed to meet our norms.

@mandreko-bitwarden

mandreko-bitwarden commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

See the validator comment -- those majors should be resolved. The skill should be broken out to references and probably thinned into several skills; you have a lot of special cases through this and it can't all be in one skill file for this to operate effectively. "Auditing" these is really a large request that needs composition.

Claude loves to make giant changelogs too, so once the above is done make sure that's compressed to meet our norms.

Yea, I'm going to move this back to Draft for now. I was working on it last night, and seeing similar suggestions too. It still needs additional work at this point and shouldn't be poised as ready for review.

[EDIT: Doh! It is already in Draft..]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants