DevSecOps

How to Add Reachability Analysis to PR Checks

Run reachability analysis on every pull request to slash vulnerability false positives by 70%+, gate merges on exploitable findings, and keep devs focused.

Shadab Khan
Security Engineer
5 min read

Most CVEs reported against your dependencies are in code paths your application never calls. Reachability analysis — static program analysis that walks from your entry points through the dependency graph to see which vulnerable functions are actually invoked — routinely filters 70 to 90 percent of raw findings. This tutorial shows you how to run reachability analysis on every PR using Safeguard's scan_repository command, annotate the PR with only exploitable findings, gate the merge on critical reachable vulnerabilities, and keep the scan under 3 minutes so it does not slow down development. Prerequisites: a GitHub repo connected to Safeguard, gh CLI 2.40+, and 30 minutes.

What is reachability analysis?

Reachability analysis constructs a call graph from your application's entry points (main, HTTP handlers, CLI commands) and determines whether a vulnerable function in a dependency is actually invoked along any path. Three outcomes: reachable, unreachable, or unknown (dynamic dispatch the analyzer cannot resolve).

A CVE-rated-critical that lives in a code path gated by a feature flag you never enable is effectively not a risk. A CVE-rated-medium that sits inside your most-called HTTP handler is the one to fix first. Reachability reorders your backlog around actual risk.

How does it change PR triage?

Without reachability, a typical Node.js service PR surfaces 40 to 80 vulnerable-dependency findings per scan. With reachability, most PRs have zero reachable critical findings, and a single one demands immediate attention. That signal-to-noise inversion is the point.

Reviewers stop ignoring security annotations because every annotation now means something. Dismissal rates at one company we measured dropped from 94% to 12% after enabling reachability.

How do I run reachability in a PR job?

Call safeguard scan with --reachability=true in a GitHub Actions job triggered on pull_request. The CLI walks the dependency tree, builds the call graph, and emits SARIF output GitHub can annotate inline.

name: Reachability check
on: [pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      security-events: write
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - uses: safeguard/scan-action@v2
        with:
          api-token: ${{ secrets.SAFEGUARD_TOKEN }}
          reachability: true
          output-format: sarif
          fail-on: reachable-critical,reachable-high

The action uploads SARIF to GitHub Code Scanning, so reachable findings appear inline in the PR diff and in the Security tab. Unreachable findings are included but suppressed by default.

How do I read the scan output?

The CLI prints a compact summary plus a SARIF file. Each finding includes reachable: true|false|unknown, the call path, and remediation advice.

safeguard scan --path . --reachability=true --format=json | \
  jq '.findings[] | select(.reachable == true) |
    { cve: .cve, severity: .severity, path: .call_path[0:3] }'
# {
#   "cve": "CVE-2024-21538",
#   "severity": "high",
#   "path": ["handlers.Login", "auth.Validate", "crossspawn.parse"]
# }

The call_path shows the chain from your entry point into the vulnerable library function. Include it in the PR annotation so reviewers see exactly why the finding matters.

How do I gate merges on reachable findings?

Configure the Safeguard policy gate to fail the check run when any reachable critical or high finding is added by the PR. Baseline against main so existing debt does not block every PR.

- uses: safeguard/policy-gate-action@v2
  with:
    gate-name: pr-reachability
    baseline-ref: refs/heads/main
    fail-on: |
      - reachable: true
        severity: [critical, high]
        delta: added

The delta: added clause is critical — without it, a PR that touches any file in a service with existing debt would always fail. You want to block new risk, not every PR.

How do I tune scan performance?

Cache the previous scan's call graph and only re-analyze changed files. A typical Go or Node.js service drops from 3 minutes cold to 25 seconds warm. Use the cache-key input on the scan action.

- uses: actions/cache@v4
  with:
    path: .safeguard/cache
    key: safeguard-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: safeguard-${{ runner.os }}-
- uses: safeguard/scan-action@v2
  with:
    incremental: true
    reachability: true

Incremental mode keys off the lockfile hash and the changed file list. A PR that only touches docs finishes the scan in under 10 seconds because nothing in the call graph changed.

How do I handle reachability-unknown cases?

Treat unknown as a configurable tier — some teams choose to fail on it, others to pass with a warning. Dynamic dispatch (reflection in Java, eval in Node, interface method calls in Go) is where analyzers lose precision.

safeguard scan --path . \
  --reachability=true \
  --treat-unknown-as=warn \
  --format=junit --output=scan.xml

Start with warn for the first two weeks, then move to fail if your unknown rate is under 5%. For codebases heavy in reflection, stay at warn permanently and rely on severity filtering instead.

How Safeguard Helps

Reachability analysis is a core capability of Safeguard — not a bolt-on. Griffin AI runs incremental call-graph construction across JavaScript, TypeScript, Python, Go, and Java, annotating every CVE in your ingested SBOM with exact reachability status and the call path from your entry points. The platform's policy gates can require "zero reachable critical CVEs" as a merge condition with baseline-aware diffing, so you only block on new risk. SBOMs generated by Safeguard include the reachability metadata as CycloneDX extensions, making it portable to downstream consumers. Jira and Slack integrations route only reachable findings to developers, cutting triage time dramatically and rebuilding trust between security and engineering teams.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.