---
name: devupdate-security-triage
description: Resolve dependency security issues that have NO clean upgrade path — advisories with no released fix, fixes blocked by parent constraints, or malicious/withdrawn packages — using DevUpdate.io's verified advisory data. Produces a mitigation (workaround, override, boundary hardening, vendor patch, or replacement) or a documented accepted risk. Use when a security issue can't be fixed by simply upgrading, when get_security_issues reports fix_unreleased, or when the user asks how to mitigate a CVE without an available fix. Requires the DevUpdate.io MCP server (https://api.devupdate.io/mcp).
---

# Security triage when there's no clean fix

Most security findings end with "upgrade to X". This skill is for the rest:
the advisory whose fix isn't released yet, the fix a parent constraint
won't let you reach, the package that turned out to be malicious. The goal
is always a documented exit, and the mitigation ladder in §3 runs in order
of preference: **advisory workaround → override taken → upstream watch +
boundary hardening → vendor patch → package replaced.** When none of those
land, the only remaining exit is **risk explicitly accepted** — owner,
reason, expiry, recorded (§4). Silent deferral is not an exit.

DevUpdate.io supplies the verified inputs: advisory ids (OSV/GHSA) checked
against your exact pin, severity, the advisory's own workaround text when
it has one (verbatim, never paraphrased), fixed-version status, and — for
blocked fixes — the parent constraint in the way. What it can't know is
whether this codebase actually exercises the vulnerable path; that
assessment is yours, made with the working tree in front of you.

## Workflow

> **If `get_security_issues` isn't working, diagnose accurately first.**
> Load it by name (clients expose MCP tools lazily, so an unloaded tool
> looks missing but isn't). A call that *errors* ("scope does not allow…",
> expired token, plan/tier gate, transport/5xx) is the connection or plan —
> name it, point the user at the connector's auth or devupdate.io/pricing. A
> tool that's simply *absent* is almost never a server fault (DevUpdate.io
> provides all its tools); it's most likely the client rationing tools when
> many connectors are active — say so and suggest a leaner session. Either
> way stop and report the real reason; don't fall back to a hand-rolled
> audit that hides the product returned nothing.

### 1. Read the advisory itself

Each issue from `get_security_issues` carries its ids and references. Open
the OSV/GHSA entry (linked) and extract: the vulnerable component (function,
module, parser, endpoint), the conditions (a config flag? a specific input
path? a transitive call?), the affected range, and any vendor guidance.
Don't triage from the one-line summary — the conditions paragraph is where
mitigations live.

### 2. Establish exposure — locally

Search the codebase for the vulnerable surface: the imported symbol, the
API call, the configuration that enables the feature. Three outcomes:

- **Exposed** — the vulnerable path is reachable from your code or your
  inputs. Proceed with full urgency for the severity.
- **Present but unverified** — the package is installed and the path is
  plausible (e.g., a transitive dep deep in a framework). Treat as exposed
  unless you can show otherwise; transitive ≠ safe.
- **Demonstrably unreachable** — e.g., the vulnerable submodule is never
  imported, the feature flag is off everywhere. Document the evidence
  (file:line of the check you did). This downgrades urgency, not the
  finding — it still goes in the report, because a future change can make
  it reachable.

### 3. Pick the mitigation — in this order

1. **The advisory's documented workaround.** When `get_security_issues`
   includes workaround text, it is quoted verbatim from the advisory —
   apply it as written (config change, disabling a feature, input
   validation at the boundary) and link the advisory id in the commit
   message. This is the highest-trust mitigation: it's the maintainer's
   own.
2. **Override / resolution** (for `fix_blocked`): force the fixed version
   past the parent constraint — npm `overrides`, yarn `resolutions`, pnpm
   `pnpm.overrides`, uv `override-dependencies`, pip constraints files.
   Then verify the *parent* still works against the overridden version:
   run its integration points, not just the suite. An override is a loan
   against the resolver — note it in the report and remove it when the
   parent catches up.
3. **Upstream watch + boundary hardening** (for `fix_unreleased` where a
   fix is in progress): apply what partial mitigation you can at your
   boundary (validate/escape the relevant input, restrict the feature),
   and record the re-check trigger. DevUpdate.io refreshes advisories
   daily — the day a fix ships, the issue flips to `fix_available` and the
   normal upgrade path takes over.
4. **Vendor patch**: `patch-package` (npm), a forked pin, or a local patch
   applied in CI — when the fix exists as an upstream commit but no
   release carries it. Pin the patch to the advisory id and treat the next
   release as the removal trigger.
5. **Replacement**: when the package is unmaintained and the advisory
   sits unfixed, evaluate alternatives — prefer candidates whose advisory
   history you can check the same way (osv.dev per package). A replacement
   is a migration, not a hotfix: scope it honestly and don't bundle it
   with other waves.

### Malicious packages are an incident, not a triage

`MAL-*` advisory ids (and typosquats/compromised releases) skip the ladder:

- Remove or pin back to a known-good version **immediately**.
- Assume install-time code ran: check postinstall scripts, CI logs, and
  what credentials the install environment could see; rotate anything
  plausibly exposed.
- Check the lockfile's history for when the bad version arrived and what
  else changed alongside it.
- Only after containment, decide the long-term replacement.

### 4. Close the loop

- **Record the outcome** where the next session will find it: the audit
  report and the repo's risk register (`SECURITY-NOTES.md` or equivalent)
  for anything not fully fixed. Include: advisory id, exposure assessment
  (with evidence), mitigation applied, residual risk, re-check trigger.
- **Expect re-listing.** `get_security_issues` keeps showing unresolved
  advisories on every audit by design — the register is what
  distinguishes "known, accepted on date X with reason Y" from "new". If
  an entry's reason no longer holds (new advisory details, new exposure),
  re-triage rather than re-accepting by habit.
- **Don't fake resolution.** No dismissing upgrades or deleting lockfile
  entries to silence a finding. If the team decides to accept a risk, the
  acceptance — owner, reason, expiry — *is* the resolution artifact.

## Pitfalls

- A workaround from a blog post is not a workaround from the advisory —
  prefer the advisory's own text; anything else, verify against the
  vulnerable-code description before trusting it.
- Overrides can silently break the parent at runtime while tests pass
  (especially when the parent is mocked). Exercise the real integration.
- "We don't use that function" claimed from memory is not an exposure
  assessment. Grep it, note the file:line, then say it.
- Severity is the advisory's CVSS, not your exposure. Report both — "CVSS
  9.8, unreachable in our usage (evidence)" — and let the reader see the
  gap rather than collapsing it.
