Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
{
"name": "bitwarden-security-engineer",
"source": "./plugins/bitwarden-security-engineer",
"version": "1.3.0",
"version": "1.3.1",
"description": "Application security engineering assistant for vulnerability triage, threat modeling, and secure code analysis."
},
{
Expand Down
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"ECDH",
"ECIES",
"editui",
"epss",
"ESL",
"evals",
"excalidraw",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A curated collection of plugins for AI-assisted development at Bitwarden. Enable
| [bitwarden-devops-engineer](plugins/bitwarden-devops-engineer/) | 0.1.5 | DevOps engineering assistant: workflow compliance linting, action security auditing, and org-wide CI/CD remediation |
| [bitwarden-init](plugins/bitwarden-init/) | 1.2.1 | Initialize and enhance CLAUDE.md files with Bitwarden's standardized template format |
| [bitwarden-product-analyst](plugins/bitwarden-product-analyst/) | 0.1.5 | Product analyst agent for creating comprehensive Bitwarden requirements documents from multiple sources |
| [bitwarden-security-engineer](plugins/bitwarden-security-engineer/) | 1.3.0 | Application security engineering: vulnerability triage, threat modeling, and secure code analysis |
| [bitwarden-security-engineer](plugins/bitwarden-security-engineer/) | 1.3.1 | Application security engineering: vulnerability triage, threat modeling, and secure code analysis |
| [bitwarden-software-engineer](plugins/bitwarden-software-engineer/) | 1.0.0 | Software engineer agent for a Bitwarden product team. Implements stories, tasks, and bugs with code quality, performance, security, and team comms in mind. |
| [bitwarden-testing-tools](plugins/bitwarden-testing-tools/) | 1.0.0 | Testing tools for analyzing and improving test quality across Bitwarden's repositories. |
| [claude-config-validator](plugins/claude-config-validator/) | 1.1.1 | Validates Claude Code configuration files for security, structure, and quality |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bitwarden-security-engineer",
"version": "1.3.0",
"version": "1.3.1",
"description": "Application security engineering assistant for vulnerability triage, threat modeling, and secure code analysis at Bitwarden.",
"author": {
"name": "Bitwarden",
Expand Down
21 changes: 21 additions & 0 deletions plugins/bitwarden-security-engineer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ All notable changes to the `bitwarden-security-engineer` plugin will be document
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.1] - 2026-07-29

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.

♻️ DEBT: Patch bump for a release carrying an ### Added section conflicts with the repo's versioning rule and this plugin's own precedent.

Details and fix

.claude/CLAUDE.md assigns MINOR to "new features, backward-compatible additions" and PATCH to "bug fixes, documentation updates, security patches." This entry adds a new Step 2 assessment question, a new scope-resolution decision table, and a new transitive-pin decision row β€” all filed under ### Added.

The plugin's history bumps MINOR for exactly this shape of change: 1.3.0 added ADR-alignment guidance to existing skills, and 1.1.0 revised threat-modeling guidance. 1.0.1 is the PATCH precedent, and it was "Simplified ... guidance" with a ### Changed section only.

Suggest 1.4.0:

echo "y" | ./scripts/bump-plugin-version.sh bitwarden-security-engineer 1.4.0

The ### Fixed entries alone would justify a patch, but the release as a whole is additive.


### Added

- `reviewing-dependencies` skill: impact assessment now starts by asking whether the dependency ships or is build-time only, using the alert's `scope` field, and requires confirming absence from shipped artifacts before relying on it.
- `reviewing-dependencies` skill: the action decision now resolves dependency scope first β€” a `development` finding confirmed absent from every shipped artifact is declared Not Affected / Informational and tracked with the next routine parent-tooling upgrade, rather than being routed to "update immediately" by the fix-availability table.
- `reviewing-dependencies` skill: added a decision row distinguishing transitive findings whose fix is installable from those where a parent pins the vulnerable version, where overriding the pin risks breaking the parent.

### Changed

- `reviewing-dependencies` skill: the CVSS impact-assessment step now specifies CVSS v3.0 as the version used for Bitwarden's vulnerability reporting and triage, and explains how to get there from the v3.1 vectors GitHub publishes.
- `reviewing-dependencies` skill: the Dependabot alert-gathering query now emits the CVSS v3 vector needed for v3.0 scoring, plus `scope`, `manifest`, `vulnerable_range`, and `first_patched` β€” the fields the assessment steps ask about.

### Fixed

- `reviewing-dependencies` skill: the alert-gathering query returned only the first page of results, silently discarding alerts beyond 100. Page order is not severity-ranked, so a critical finding could be dropped. Now uses `--paginate --slurp` so the severity sort is global rather than per-page.
- `reviewing-dependencies` skill: the alert-gathering query claimed to sort by severity but performed no sort.
- `reviewing-dependencies` skill: guarded the CVSS v3 score read, which returns `0` rather than `null` for advisories carrying only a v4.0 vector, making high-severity alerts appear to score zero.
- `reviewing-dependencies` skill: corrected the Grype examples β€” `--only-fixed` filters by fix availability rather than severity, and `--fail-on` sets the exit code without filtering output.
- `reviewing-dependencies` skill: corrected the documented Grype table columns against grype 0.116.1 β€” the column is `FIXED IN`, the default set also includes `EPSS` and `RISK`, and `FIXED IN` is dropped entirely when no finding has a fix. Added the `fix.state` JSON field as the reliable alternative to parsing the table.

## [1.3.0] - 2026-07-21

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,37 @@ description: This skill should be used when the user asks to "review Dependabot
### Step 1: Gather Alerts

```bash
# List all open Dependabot alerts sorted by severity
gh api /repos/{owner}/{repo}/dependabot/alerts --jq '.[] | select(.state == "open") | {number, severity: .security_vulnerability.severity, package: .security_vulnerability.package.name, ecosystem: .security_vulnerability.package.ecosystem, summary: .security_advisory.summary}'
# List ALL open Dependabot alerts, sorted by severity. Notes on why this is
# shaped the way it is β€” each of these is a trap worth avoiding:
# - --paginate is required: a single page caps at 100 and silently drops the
# rest. Page order is not severity-ranked, so a critical can be on page 2.
# - --slurp merges pages into one array so the sort is global, not per-page.
# gh rejects --slurp together with --jq, so pipe to jq instead.
# - The API cannot sort by severity (sort= takes only created, updated,
# epss_percentage), so sort in jq.
# - scope tells you development (build/CI only) vs runtime (ships to users).
# This usually decides the triage outcome β€” see Step 2 and Step 3.
# - cvss_v3.score returns 0 rather than null when an advisory carries no v3
# vector, so it is guarded; unguarded it reads 0 on high-severity findings.
gh api --paginate --slurp "/repos/{owner}/{repo}/dependabot/alerts?state=open&per_page=100" \
| jq '
[ .[][]
| (.security_advisory.cvss_severities // {}) as $c
| { number,
severity: .security_vulnerability.severity,
package: .dependency.package.name,
ecosystem: .dependency.package.ecosystem,
scope: .dependency.scope,
manifest: .dependency.manifest_path,
vulnerable_range: .security_vulnerability.vulnerable_version_range,
first_patched: .security_vulnerability.first_patched_version.identifier,
cvss_v3_vector: $c.cvss_v3.vector_string,
cvss_v3_score: (if $c.cvss_v3.vector_string then $c.cvss_v3.score else null end),
cvss_v4_vector: $c.cvss_v4.vector_string,
summary: .security_advisory.summary }
]
| sort_by({ critical: 0, high: 1, medium: 2, low: 3 }[.severity])
| .[]'

# Filter by severity
gh api "/repos/{owner}/{repo}/dependabot/alerts?severity=critical&state=open"
Expand All @@ -22,20 +51,34 @@ gh api /repos/{owner}/{repo}/dependabot/alerts/{alert_number}

For each alert, determine:

1. **Is the vulnerable code path reachable?** β€” Does the application actually use the vulnerable function/feature of the dependency?
2. **Is it a direct or transitive dependency?** β€” Transitive vulnerabilities may be harder to fix but still pose real risk.
3. **What is the CVSS score and exploit availability?** β€” A high CVSS with a public exploit needs immediate action. A medium CVSS with no known exploit can be scheduled.
4. **What versions are affected and what versions fix it?** β€” Check if updating is a minor bump or a breaking change.
1. **Does the dependency ship, or is it build-time only?** β€” Start here; it usually decides the outcome. `scope` on the alert reports `development` or `runtime`. Treat `development` as a strong signal, not proof: confirm the package is absent from every shipped artifact (extension, desktop, web vault, CLI, server image) before relying on it, because some build-time packages inline code into published output. A build-time-only package has no live process in production for an attacker to reach.

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.

⚠️ IMPORTANT: dependency.scope is nullable, and neither Step 2 nor the Step 3 scope table says what to do when it is absent.

Details and fix

The Dependabot alerts API documents dependency.scope as development, runtime, or null β€” GitHub cannot always resolve execution scope from a manifest, so the field is frequently empty in practice (commonly on non-npm ecosystems). The Step 1 query now surfaces scope verbatim, so a null will reach the triager.

Step 2 item 1 asserts scope "reports development or runtime", and the Step 3 scope table has rows only for those two values. A null-scope alert therefore matches no row at the exact point where the whole triage now branches. Best case the reader falls through to the runtime table; worst case they infer "no scope reported" means "not a runtime dependency" and land on Not Affected / Informational for something that ships. Naming the safe default removes the guess.

Suggested β€” append to item 1:

scope may also be absent; GitHub cannot always resolve it. Treat a missing scope as runtime until manifest inspection proves otherwise.

and add a row to the scope table:

| `scope` absent or unresolved | Treat as `runtime` and use the table below until manifest inspection proves the dependency is build-time only |

Reference: List Dependabot alerts for a repository (dependency.scope: development, runtime, null)

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.

πŸ’­ @sognefej this looks like a legit gap. I double-checked, and the cited reference does state scope could be null. I didn't confirm if that's true in practice though. I think the suggested update is worth making, if you can confirm that's the actual behavior.

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.

Roger, I'll do some testing πŸ‘

2. **Is the vulnerable code path reachable?** β€” Does the application actually use the vulnerable function/feature of the dependency?
3. **Is it a direct or transitive dependency?** β€” Transitive vulnerabilities may be harder to fix but still pose real risk. Identify the parent that pins it (`npm ls <package>`), because when a parent pins a vulnerable version the fix may not be available to you directly.
4. **What is the CVSS score and exploit availability?** β€” Score in **CVSS v3.0**, Bitwarden's standard for reporting and triage of vulnerabilities. GitHub publishes v3.1 vectors for recent advisories (v3.0 appears only on older imported CVEs), so transcribe `cvss_severities.cvss_v3.vector_string` into a v3.0 calculator β€” v3.0 and v3.1 share the same base metrics, so the vector maps directly. Some advisories carry a v4.0 vector only, or no vector at all; v4.0 metrics do not map onto v3.0, so score those by hand from the advisory details rather than reusing the v4.0 number. A high CVSS with a public exploit needs immediate action. A medium CVSS with no known exploit can be scheduled.
5. **What versions are affected and what versions fix it?** β€” `vulnerable_range` and `first_patched` on the alert give both. Check whether reaching `first_patched` is a minor bump, a breaking change, or blocked behind a parent package that pins the vulnerable version.

### Step 3: Decide on Action

| Situation | Action |
| ----------------------------------------- | ---------------------------------------------- |
| Fix available, minor version bump | Update immediately |
| Fix available, major version bump | Evaluate breaking changes, schedule update |
| No fix available, code path reachable | Implement workaround or replace dependency |
| No fix available, code path not reachable | Document and monitor, set review date |
| Vulnerability in transitive dependency | Use overrides/resolutions to pin fixed version |
Resolve scope first. A package that never reaches a shipped artifact cannot be attacked in production, and treating it as though it can produces pointless urgency and risky churn.

| Scope | Action |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `development`, confirmed absent from every shipped artifact | Declare **Not Affected / Informational**. Record the reachability reasoning explicitly, then track the bump with the next routine upgrade of the parent tooling rather than as a hotfix |
| `development`, but the package or its output lands in a shipped artifact | Treat as `runtime` and use the table below |
| `runtime` | Use the table below |

A `development` finding still needs the declaration written down. "It's a devDependency" is not by itself an assessment β€” state which artifacts were checked and why the code path cannot be reached in production.
Comment on lines +62 to +70

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.

⚠️ IMPORTANT: Scope-first triage judges reachability only in production, so build-pipeline compromise is silently excluded.

Details and fix

Line 62 and the development row both reason exclusively about production: "a package that never reaches a shipped artifact cannot be attacked in production" and "no live process in production for an attacker to reach." That is correct as far as it goes, but a build-time dependency still executes with access to CI runners, signing material, registry tokens, and the build output itself. event-stream, ua-parser-js, and xz-utils were all reachable purely through the build/install path.

As written, an advisory for arbitrary code execution or malicious code in a build tool that ships nothing gets routed to Not Affected / Informational and deferred to "the next routine upgrade of the parent tooling." That is the wrong outcome for that class.

Suggest carving it out β€” e.g. an extra row before the development/absent row:

| `development`, but the advisory is malicious code, install-script execution, or build tampering | Treat as urgent regardless of shipping. The build pipeline is a live target: CI credentials, signing material, and the produced artifact are all in reach |

and softening line 54's closing sentence to scope the claim to production runtime rather than to attackers generally.

Reference: CWE-1357, SLSA build-integrity threats


For `runtime` findings, and for `development` findings that turned out to ship:

| Situation | Action |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fix available, minor version bump | Update immediately |
| Fix available, major version bump | Evaluate breaking changes, schedule update |
| No fix available, code path reachable | Implement workaround or replace dependency |
| No fix available, code path not reachable | Document and monitor, set review date |
| Transitive, and the fixed version is installable | Use overrides/resolutions to pin the fixed version |
| Transitive, but a parent pins the vulnerable version | Overriding the parent's pin can break it. Prefer waiting for a parent release that bumps its own pin, and track it; override only when the finding is runtime-reachable and the risk justifies it |

## Transitive Dependency Risk

Expand Down Expand Up @@ -90,15 +133,19 @@ grype dir:/path/to/project
# Output as JSON for programmatic processing
grype <image> -o json

# Filter by severity
grype <image> --only-fixed --fail-on high
# Show only vulnerabilities that have a fix available
grype <image> --only-fixed

# Exit non-zero (code 2) if anything at or above the given severity is found.
# This sets the exit code for CI gating; it does not filter the output.
grype <image> --fail-on high
```

**Interpreting Grype output:**

- Each finding includes: CVE ID, severity, package name, installed version, fixed version
- `Fixed` column indicates whether an update is available
- Use `--only-fixed` to focus on actionable items (vulnerabilities with available fixes)
- Table columns (grype 0.116.x) are `NAME`, `INSTALLED`, `FIXED IN`, `TYPE`, `VULNERABILITY`, `SEVERITY`, `EPSS`, `RISK`
- The column set is dynamic: `FIXED IN` is omitted entirely when no finding has a fix available. When present, a blank cell means that particular vulnerability has no fix
- Use `--only-fixed` to focus on actionable items (vulnerabilities with available fixes). For scripting, read `.matches[].vulnerability.fix.state` from `-o json` (`fixed`, `not-fixed`, `wont-fix`, `unknown`) rather than parsing the table

## Platform-Specific Guidance

Expand Down
Loading