Skip to content

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_versions in 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:

  1. Open the rule file (e.g., rules/SSC-2025-001.json).
  2. Check the references array for advisory URLs.
  3. Visit the advisory and confirm:
  4. Is the package name an exact match (including scope)?
  5. Is the flagged version listed as affected in the advisory?
  6. 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