Runbook: False Positive Triage¶
TL;DR
- Trigger: the scanner reports a finding, but investigation shows the package is not actually compromised.
- Common causes: version range overlap, stale rule, package name collision across registries.
- Fix by verifying against the original advisory, adjusting
affected_versionsin the rule, and reporting upstream if the advisory is wrong.
Who is this for?
Audience: Engineers who received a scanner finding and need to determine if it is a true or false positive, and security teams maintaining rules. Reading time: ~7 minutes.
What Happened¶
The gouvernante scanner reported one or more findings for a package in your lockfile, but you believe the package is not actually compromised. The scanner flagged a package+version combination that matches a rule, but the match may be incorrect.
Root Cause¶
False positives in gouvernante arise from one of these causes:
Version Range Overlap¶
The rule's affected_versions field is too broad. For example, a rule specifying >=1.7.0 when only 1.7.8 and 1.7.9 were actually compromised will flag 1.7.0 through 1.7.7 incorrectly.
Stale Rule¶
The advisory was updated after the rule was written. The npm maintainer may have yanked the malicious versions and re-published clean ones under the same version numbers (rare but possible), or the initial advisory was corrected to narrow the affected version range.
Package Name Collision¶
A rule targets a package name that exists in multiple registries or scopes. For example, a rule for crypto-utils (a malicious public package) may flag @your-org/crypto-utils (your private package) if the lockfile resolves both to the same unscoped name.
Lockfile Artifact¶
The lockfile references a version that was never actually installed. This can happen with partially committed lockfiles, merge conflicts, or lockfiles generated by a different package manager version.
How to Fix¶
Step 1: Confirm the Finding Details¶
Review the scanner output to identify the exact package, version, and rule:
gouvernante -rules /path/to/rules -lockfile ./package-lock.json -json | jq '.findings[]'
Note the rule_id, package, version, and lockfile for each finding.
Step 2: Verify Against the Original Advisory¶
Look up the rule's source advisory:
- Open the rule file (e.g.,
rules/SSC-2025-001.json). - Check the
referencesarray for advisory URLs. - Visit the advisory and confirm:
- Is the package name an exact match (including scope)?
- Is the flagged version listed as affected in the advisory?
- Has the advisory been updated or withdrawn since the rule was written?
Step 3: Check the Lockfile¶
Verify that the flagged version is actually resolved in your lockfile:
# For package-lock.json
jq '.packages | to_entries[] | select(.key | contains("example-utils"))' package-lock.json
# For pnpm-lock.yaml
grep -A 3 "example-utils" pnpm-lock.yaml
# For yarn.lock
grep -A 3 "example-utils" yarn.lock
Confirm the resolved version matches what the scanner reported. If the lockfile shows a different version than what was flagged, the lockfile may have been updated but the scanner ran against a stale copy.
Step 4: Determine the Cause¶
Based on your investigation:
| Finding | Likely Cause | Action |
|---|---|---|
| Version is in the advisory's affected range | True positive | Remediate. This is not a false positive. |
| Version is NOT in the advisory's affected range | Version overlap in rule | Fix the rule (Step 5). |
| Advisory has been withdrawn or corrected | Stale rule | Update or remove the rule (Step 5). |
| Package name is scoped differently | Name collision | Fix the rule to include scope (Step 5). |
| Version is not actually in the lockfile | Lockfile artifact | Regenerate the lockfile. |
Step 5: Fix the Rule¶
Who: Security team (or submit a PR to the rules repository).
Version overlap: Narrow the affected_versions to list only the confirmed compromised versions:
{
"package_name": "example-utils",
"affected_versions": ["=2.3.1", "=2.3.2"],
"notes": "Only 2.3.1 and 2.3.2 confirmed compromised. Earlier versions are clean."
}
Prefer exact version matches (=X.Y.Z) over ranges unless the advisory explicitly confirms a range.
Stale rule: If the advisory was withdrawn, remove the rule or add a "withdrawn": true field and document why.
Name collision: Add scope to the package name or add exclusion notes:
{
"package_name": "crypto-utils",
"affected_versions": ["*"],
"notes": "Unscoped package only. @your-org/crypto-utils is not affected."
}
Note
The scanner currently matches on unscoped package names. If your organization uses private scoped packages that collide with public malicious package names, document the collision in the rule's notes field until scoped matching is implemented.
Step 6: Report Upstream¶
If the original advisory contains incorrect information:
- GitHub Advisory: Open a discussion or suggest an improvement on the GHSA page.
- npm: Report via the npm security contact.
- Rule author: File an issue or PR on the gouvernante-rules repository with your findings.
Include:
- The rule ID and advisory URL.
- The specific version(s) incorrectly flagged.
- Evidence that the version is clean (e.g., npm provenance data, source code diff).
Step 7: Verify the Fix¶
After updating the rule, re-run the scanner:
gouvernante -rules /path/to/rules -lockfile ./package-lock.json
echo "Exit code: $?"
# Expected: 0 (if the only findings were false positives)
Confirm the false positive no longer appears and that legitimate findings (if any) are still detected.
Self-Assessment¶
- Can you list three causes of false positives in gouvernante?
- Do you know how to verify a finding against the original advisory?
- Can you explain why exact version matches are preferred over ranges in rules?
Next Steps¶
- Write better rules --> Writing Rules
- Understand version expressions --> Rule Format
- Return to runbook index --> Runbooks