Safeguard
Product•

Deep Transitive, Unused and Confused Dependencies in Safeguard

Safeguard SCA (GA) traces deep transitive paths and flags unused dependency candidates. Dependency confusion is stopped by how you set up package sources.

Safeguard Security Team
38 min read

Safeguard's software composition analysis (SCA, generally available) resolves your whole dependency graph, direct and transitive, and records the full path from your project to every vulnerable package, so a CVE five levels down shows exactly which of your direct dependencies brought it in. Deep dependency scanning (GA) also reports unused dependency candidates: packages in the tree that your code never imports. Dependency confusion is a different kind of risk, and the controls that stop it live mainly in how your builds are told where to fetch packages from.

This post takes the three problems in turn. For each one it covers what goes wrong, what the standards ask for, what Safeguard's docs say the product shows and how to use it step by step, and what stays with your team.

Key takeaways

  • SCA is GA. Safeguard's docs list npm, pnpm and Yarn, PyPI, Maven and Gradle, Go modules, Cargo, NuGet, Composer and Ruby Bundler, plus components found inside container image layers. Manifests and lockfiles are both read.
  • Deep dependency scanning is GA. Each finding carries the full transitive path from your project root to the vulnerable package, and the UI explains whether that chain is reachable, whether another path exists and what the safe version would change.
  • Unused dependency detection is part of deep dependency scanning (GA). It produces candidates for removal: packages present in the tree but never imported. The docs say there is no CLI command for it yet.
  • Reachability analysis is GA across the languages the docs list, including Ruby, PHP and Swift and Objective-C.
  • Policies and gates are GA. The deep scanning docs show dependency depth as a policy signal.
  • Autonomous remediation is GA and ships switched off, with every category set to Manual.
  • Dependency confusion comes down to how package managers choose between registries. The controls that prevent it (scoped registries, a single curated feed, lockfiles with hashes) belong in your package source configuration, and this post explains them.
  • The Transitive Depth framework in Safeguard's compliance suite (GA) checks dependency relationship completeness, transitive counts, circular dependencies and purl coverage on every scan.

The problem: the code you did not choose

Most of what a modern service ships is code nobody on the team picked by name. You add thirty direct dependencies. Each of those declares its own, and each of those declares more. The result is a graph where the package that matters during an incident is often several hops away from anything in your manifest.

Three distinct problems hide in that graph.

Deep transitive dependencies are the inventory problem. If your scanner, your SBOM or your people only look at the top of the tree, a vulnerable library at the fourth or fifth level does not exist as far as your process is concerned. It still runs in production.

Unused dependencies are the attack surface problem. A package declared in a manifest but never imported still gets installed, still runs install scripts in ecosystems that allow them, still shows up in your SBOM and still produces vulnerability findings your team has to triage. It adds risk and noise and gives nothing back.

Dependency confusion is the trust problem. When a build can fetch packages from more than one registry, an attacker who publishes a package with the same name as one of your internal packages on a public registry may get their code installed instead of yours. Nothing about the package is wrong from the package manager's point of view. It followed its rules.

What standards and guidance expect

None of this is new to standards bodies. The expectations are specific.

NIST SP 800-218 (SSDF version 1.1). Practice PW.4 covers reusing existing, well secured software. Task PW.4.1 asks organisations to acquire and maintain well secured third party components, and its Example 3 names obtaining provenance information such as an SBOM or source composition analysis for each component and analysing it to assess risk. Task PW.4.4 asks you to verify that acquired components comply with your requirements throughout their life cycles; its Example 2 is building automatic detection of known vulnerabilities into the toolchain, and Example 4 is checking that each component is still maintained. Task PS.3.2 asks you to collect, safeguard, maintain and share provenance data for all components of each release, for example in an SBOM, and its Example 4 says to update that data every time a component changes. "All components" does not stop at the first level.

OWASP Top 10 2025, A03 Software Supply Chain Failures. The OWASP page says you are likely vulnerable if you do not track the versions of all components, and it spells out that this includes nested, transitive dependencies. Its prevention list asks teams to track direct dependencies and their transitive dependencies, and to "reduce attack surface by removing unused dependencies" along with unnecessary features, components and files. The same page maps the category to CWE entries including CWE-1104 (use of unmaintained third party components) and CWE-1395 (dependency on a vulnerable third party component).

CWE-1357, reliance on insufficiently trustworthy component. MITRE describes a product built from separate components where one of them is not trusted enough to meet expectations for security, reliability, updateability and maintainability. A package pulled from the wrong registry is a direct example.

NTIA minimum elements for an SBOM (2021). The NTIA report frames an SBOM as a record of the components used in building software and the relationships between them. Without relationships you have a parts list, not a graph, and a parts list cannot tell you why a package is present.

OpenSSF Secure Supply Chain Consumption Framework (S2C2F). S2C2F lists dependency confusion and package substitution as a named threat and maps it to two requirements: ENF-1, securely configure your package source files (the framework names nuget.config, .npmrc, pip.conf and pom.xml), and ENF-2, enforce use of a curated open source feed. Both map back to SSDF task PO.5.2 in the framework's own table.

So the standards ask for three things that map onto the three problems: a complete, relationship aware inventory; removal of what you do not use; and control over where packages come from.

Problem one: deep transitive dependencies

Why depth defeats simple inventories

Every package manager resolves the graph differently, and that changes what "the version you ship" means.

Maven's documentation describes dependency mediation as "nearest definition": when two versions of the same artifact appear, Maven uses the one closest to your project in the tree, and if two are at the same depth, the first declaration wins. Its own example has project A depending on D 2.0 through B and C, and on D 1.0 through E. Maven picks D 1.0 because it is nearer. A scanner that reads only declared versions, or that resolves the tree differently from Maven, can report the wrong version of D.

Go takes a different approach. The Go modules reference describes minimal version selection, and from go 1.17 onwards go.mod lists an indirect requirement for every module that provides any package imported, even indirectly, by the main module. Go also gives you go mod why to explain why a module is needed.

npm, for its part, now shows only top level dependencies in npm ls unless you pass --all. The npm docs say this changed because dependency graphs have grown so large that a full dump was too much for a terminal. That is a reasonable default for a human. It is a poor default for an inventory.

The common thread: the tree is deep, the resolution rules are ecosystem specific, and the right answer comes from the resolved graph (lockfile or build tool output), not from the manifest alone.

What Safeguard's docs say deep dependency scanning does

Safeguard's deep dependency scanning docs describe walking every edge in the resolved dependency graph. At each node, the scan:

  1. Resolves the package URL (purl) and version against Safeguard's vulnerability knowledge graph.
  2. Computes license and provenance.
  3. Feeds the node into reachability analysis.
  4. Captures the full path from root to leaf, so you can trace why a dependency is present.

Shared subgraphs are deduplicated, so a library used by a hundred of your dependencies is resolved once. The docs also describe feed driven rescoring, so a deep scan is not repeated from scratch every time a new advisory lands.

The ecosystems listed for deep scanning are:

EcosystemFiles the docs say are resolved
npm, pnpm, YarnWorkspaces, peer dependencies and optional dependencies
PyPIrequirements.txt, pyproject.toml, Pipfile, uv.lock, poetry.lock
Maven, Gradlepom.xml, build.gradle, lockfiles
Go modulesgo.mod, go.sum
CargoCargo.toml, Cargo.lock
NuGetpackages.lock.json, .csproj
Composercomposer.lock
Ruby BundlerGemfile.lock
Container layersBinaries and package metadata inside image layers

If a file is not in that list, do not assume it is read. Commit lockfiles wherever your ecosystem supports them, because the lockfile is what pins the resolved graph to what actually installs.

The transitive path, field by field

According to the docs, each SCA finding carries the vulnerable component and version, the CVE or GHSA identifiers, severity with EPSS and CISA KEV context, the full transitive path from your project root to the vulnerable package, the reachability verdict where available, and the safe upgrade target.

In the Enterprise Software Supply Chain Manager (ESSCM) UI, clicking a finding shows the path. The docs' own example looks like this:

your-app
└─ @company/tools @2.1.0
   └─ lodash @4.17.21   ← vulnerable here (CVE-2021-23337)

Alongside the path, the docs say the UI explains four things:

  • The chain of imports that pulls in the vulnerable package.
  • Whether the chain is reachable, from reachability analysis.
  • Whether any alternative path exists.
  • What the safe version would be, and what other transitive changes that version would introduce.

That last point is the one teams underrate. Upgrading a transitive package rarely means editing the transitive package. It means moving the direct dependency that drags it in, or adding an override, and either of those can move other packages too. Seeing the knock on changes before you open a pull request saves a round of broken builds.

Reachability on deep findings

A deep tree produces many findings, and most of them sit in code your application never calls. Reachability analysis (GA) is how Safeguard separates the two. The reachability docs describe call graph analysis combined with EPSS, KEV, runtime context and business context into one priority score.

Support varies by language, and the docs say so plainly:

Language or ecosystemStatic reachabilityDynamic reachability
Java, Kotlin, Scala (JVM)GAGA
PythonGAGA
JavaScript, TypeScriptGAGA
GoGAGA
RustGAGA
C, C++GAGA
C#, .NETGAGA
RubyGAGA
PHPGAGA
Swift, Objective-CGAGA

For deep findings, reachability matters more than usual. A vulnerable function five levels down is reachable only if every hop above it actually calls into the next. The path in the finding tells you which packages to look at; the reachability verdict tells you whether to look today. The reachability, EPSS and KEV post covers the scoring in detail.

Depth as a policy signal

The deep scanning docs show depth used in policy rules. Two examples appear there, written like this:

rules:
  - id: no-dep-below-level-50
    condition: any(components, depth > 50 AND vulnerabilities.critical > 0)
    effect: BLOCK
  - id: warn-on-deepening
    condition: delta.max_depth > prev.max_depth + 10
    effect: WARN

The first blocks when a component deep in the tree carries a critical vulnerability. The second warns when a change makes the tree noticeably deeper than the previous version. Policies and gates are GA, and the Policies and Gates docs give each policy an action: Block fails the gate, Warn passes it with a warning, Notify passes it and sends a notification. Treat these as examples of the shape of a rule from the docs, and test any depth rule as a warning in your own tenant before you let it block a release.

The second rule is the more interesting one for most teams. A pull request that adds ten levels of depth usually adds hundreds of packages, and that is worth a human look whether or not any of them has a CVE today.

How to see deep transitive paths in Safeguard, step by step

There are three routes in, depending on where you want the work to happen. The repo connection guide compares them in depth; the steps below focus on what matters for transitive analysis.

Route A: scan on your own machine with the CLI

The Safeguard CLI is GA. It scans code, container images and SBOMs on your machine and, when you are signed in through the interactive terminal, sends only the results to the platform.

  1. Install the CLI. The docs give this command for macOS and Linux:

    curl -fsSL https://cli.safeguard.sh/install | bash
    

    On Windows, the docs use a PowerShell one liner from the same host.

  2. Run safeguard, type /login to authenticate, then /scan . to scan the folder you are in. Scanning from the interactive terminal while signed in uploads the SBOM and findings to the platform, never your source.

  3. For a scripted local scan, use safeguard scan with exactly one target:

    safeguard scan --dir .
    

    Results land under --output (default ./output), in a folder per scan, and include vuln.json, license.json and collection.json among other files.

  4. If you want the scan to resolve transitive dependencies that your lockfiles do not already pin, the usage docs list --resolve-transitive, which resolves them through deps.dev, a public open source dependency service. The deep dependency scanning docs show it combined with a depth setting:

    safeguard scan --dir . --resolve-transitive --transitive-depth 100
    

    The usage page's flag table does not list --transitive-depth, so check safeguard scan --help on your installed version for the flag, its default and the values it accepts before you put it in a script. The docs say --help is the reference when the two differ.

  5. Tell the scan which namespaces are yours. The usage docs list --org-namespace for "namespaces that identify your own private packages", with @mycompany as the example:

    safeguard scan --dir . --resolve-transitive --org-namespace @mycompany
    

    The docs describe what the flag identifies and nothing more, so read safeguard scan --help for its exact behaviour in your version. If your package names are sensitive, also agree with your security team whether a lookup against a public service is acceptable before you add --resolve-transitive at all.

  6. Read the exit code. safeguard scan exits 0 when the scan ran, whatever it found; 1 when the command itself failed; and 2 when results were written but the vulnerability scan did not run. There is no severity threshold flag on safeguard scan. Gating happens through policy gates in the platform. Treat exit code 2 as a failure in any script, or a broken scan will look like a clean one.

Route B: connect the repository in the web app

  1. Open Integrations in Safeguard and connect your Git host from the Add Project/Product tab.
  2. Pick the repositories and branches, and confirm the project name and version.
  3. Set Include Dev Dependencies deliberately. Transitive chains under build tooling are often the deepest in the graph, and whether they belong in your production inventory is a policy decision, not a scanner default.
  4. Connect, then open the project's Vulnerabilities view.

Safeguard's vulnerabilities docs say every project is rescanned daily for newly disclosed vulnerabilities, so a transitive package that becomes vulnerable next month will show up without anyone pushing a commit.

Route C: work a finding from the transitive path

This is the routine once findings exist, whichever route produced them.

  1. Filter. The Vulnerabilities view filters by severity, CVSS range, EPSS range, exploitable status, component name, fix availability, reachability status and date range. Start with exploitable, reachable findings.
  2. Open the finding and read the transitive path. Note the direct dependency at the top of the chain. That is usually what you change.
  3. Check the reachability verdict and whether an alternative path exists. If a second path pulls in the same vulnerable version, fixing only one chain will not clear the finding.
  4. Click View Fix to see the upgrade options and breaking change warnings.
  5. Choose the fix. Either move the direct dependency to a version that pulls in a safe transitive version, or use your ecosystem's override mechanism to force the transitive version. In Maven, the docs' nearest definition rule means declaring the version explicitly in your own POM wins.
  6. Open a pull request, by hand or with AI Remediate. The AI Remediate docs say Griffin upgrades vulnerable packages to fixed versions, handles transitive dependency conflicts, updates lock files and checks for breaking changes. Autonomous remediation is GA and ships switched off, with every category set to Manual, so nothing is applied until an administrator enables it. The autonomous remediation post covers the categories and approval settings.
  7. Rescan the branch and confirm the finding is gone and no new path has appeared.
  8. If the finding does not apply, use Mark False Positive with a justification, or Accept Risk with an expiry date. Both can require approval by policy.

Checking the path with your own tools

The transitive path in Safeguard should agree with your build tool. When it does not, trust neither until you know why. These native commands are the ones each project's docs describe for this job:

# npm: show the full tree, not just the top level
npm ls --all

# npm: show every path to one package
npm ls lodash

# Maven: print the resolved tree
mvn dependency:tree

# Go: explain why a module is needed
go mod why -m example.com/some/module

A mismatch usually means a stale lockfile, a monorepo workspace that was scanned from the wrong directory, or a build that installs differently in CI than on a laptop.

Problem two: unused dependencies

Why unused packages are not harmless

An unused dependency costs you in four ways.

  1. Attack surface. It is installed, and in ecosystems with install scripts it may run code at install time even if your application never imports it.
  2. Noise. Every CVE in it, and in everything beneath it, lands in your queue. Reachability will usually mark those findings unreachable, but someone still has to see them, and a policy that blocks on criticals regardless of reachability will block on them too.
  3. SBOM accuracy. Your SBOM lists it as a shipped component. Customers and auditors reading the SBOM will ask about its vulnerabilities.
  4. Maintenance drag. Upgrades of the packages you do use may be held back by version constraints from a package you do not.

OWASP's A03 prevention list says it directly: reduce attack surface by removing unused dependencies.

What Safeguard's docs say about unused dependency detection

The deep dependency scanning docs describe a reverse signal from the same graph walk: packages present in your dependency tree but never imported. The docs call these candidates for removal, and point out that every removed package reduces attack surface without code changes. The offering files list unused dependency detection as part of deep dependency scanning (GA).

Two details matter for how you use it:

  • They are candidates. "Never imported" is a statement about static imports in your source. It is a strong signal, not a verdict.
  • There is no CLI command for it yet. The docs say so. Work the candidates in the platform, not from a local scan's output files.

How to work unused dependency candidates, step by step

  1. Scan with lockfiles committed, through any of the routes above, so the graph matches what installs.
  2. List the candidates for the project in Safeguard.
  3. Sort them by what they drag in. A direct dependency that is never imported but sits on top of a deep subtree is the best removal target, because removing it removes everything beneath it that nothing else needs.
  4. Check each candidate against the ways code is loaded without an import. Before removing anything, look for:
    • build plugins, linters, test runners and code generators that are run by a tool, not imported by your code;
    • command line tools invoked from package scripts, Makefiles or CI steps;
    • packages loaded by name at runtime, through configuration files, plugin systems or reflection;
    • type definition packages and other compile time only dependencies;
    • peer dependencies that another package expects you to provide.
  5. Move, do not delete, when the package is a tool. A test runner that is a production dependency is a classification error. Moving it to development dependencies keeps it out of your production SBOM and production installs.
  6. Remove the rest in a single pull request per repository, run the full test suite and the production build, and rescan. Go teams can lean on go mod tidy, which the Go modules reference describes as removing requirements that are no longer needed.
  7. Watch the depth signal afterwards. The tree should be shallower and smaller. If your policy uses the warn on deepening rule from the docs, the next pull request that adds a large subtree will stand out against the new baseline.

What good looks like after a cleanup

After one round, most teams find a handful of genuinely dead direct dependencies, a larger number of tools filed as production dependencies, and a few false candidates that turn out to be loaded by name. The second group is often the biggest win for SBOM accuracy. Re run the cleanup every quarter, or whenever a large refactor removes a feature.

Problem three: dependency confusion

How it works

Dependency confusion abuses one fact: many package managers can be told to look in more than one place, and when they do, they need a rule for choosing between two packages with the same name.

The pip documentation describes --extra-index-url as extra URLs of package indexes to use in addition to --index-url. Once a build consults both your private index and a public one for the same name, the outcome depends on which version pip prefers, not on which index you trust more.

The PyTorch project's advisory about the December 2022 nightly incident is a clear real world account. The project says that PyTorch nightly packages installed through pip between 25 and 30 December 2022 pulled in a dependency, torchtriton, that had been compromised on PyPI, the Python Package Index. It explains that a malicious package was uploaded to PyPI under the same name as the one shipped on the PyTorch nightly index, and that because the PyPI index takes precedence, the malicious package was installed instead. Stable releases were not affected, according to the project.

Note what did not happen there. Nobody typed a wrong name and no lockfile was tampered with. The package manager followed its precedence rules, and an attacker owned the public name.

Why depth makes confusion worse

Dependency confusion is often discussed as a problem with your own direct internal packages. The graph makes it broader. If one of your internal libraries depends on another internal library by an unscoped name, every service that uses the first library inherits the exposure, possibly several levels down, where no one looks at the name. That is why the full transitive inventory and the confusion problem belong in the same conversation: you cannot protect names you do not know you depend on.

The controls that prevent it

The prevention controls are well documented and mostly live in configuration files your team owns.

  1. Scope internal names and pin each scope to one registry. The npm docs show how to associate a scope with a registry with npm config set @myco:registry=..., after which any install of a package in that scope requests it from that registry, and any publish in that scope goes there too. A scoped name tied to one registry cannot be resolved from the public registry by accident.
  2. Use one curated feed. S2C2F's ENF-2 asks you to enforce consumption from a curated feed. If every build fetches from one internal proxy that holds internal packages and mirrors approved public ones, the build never has to choose between two registries.
  3. Lock the source configuration. S2C2F's ENF-1 asks you to securely configure package source files such as nuget.config, .npmrc, pip.conf and pom.xml. Review changes to these files like code, because they are code.
  4. Prefer a single index over extra indexes in Python. Replacing --extra-index-url with one --index-url that points at a proxy removes the precedence question entirely.
  5. Use lockfiles with hashes. pip's secure installs guide describes hash checking mode, where every requirement must carry a hash and pip refuses packages that do not match. --require-hashes forces it. A lockfile with integrity hashes means a package substituted after the lock was written fails the install.
  6. Register or reserve your internal names publicly where the registry allows it, so the public name cannot be taken by someone else.
  7. Keep an inventory of internal names, including those only used transitively. This is where the next section comes in.

Where Safeguard's inventory fits

Safeguard's SCA gives you the inventory those controls depend on. Each component in a scan carries its purl, its version and the full path from your project root, so you can see every internal name your services depend on at any depth, and which public or internal package chain brings it in.

A practical routine:

  1. Scan each service with lockfiles committed, through any route above.
  2. Filter the inventory by component name for your internal prefixes and namespaces.
  3. For every internal name you find, confirm it is scoped (or otherwise namespaced) and that the scope or group is mapped to your internal registry in the build's source configuration.
  4. List any internal names that are unscoped, and plan to move them into a scope or reserve them publicly.
  5. Pass your namespaces to local scans with --org-namespace where you use --resolve-transitive, and check safeguard scan --help for what it changes in your version.

This post deliberately keeps to inventory and configuration. If you are assessing tooling that claims to detect a confusion attempt as it happens, test it against your own registry layout, because the answer depends on how your feeds are set up.

The dependency confusion explainer and the post on dependency confusion and private registries go further into attack history and registry design.

The Transitive Depth framework in Safeguard's compliance suite

Safeguard's compliance and GRC suite (GA) includes internal frameworks that turn scan results into control coverage. One of them, Transitive Depth, is aimed squarely at this topic. Its docs page says it flags components with transitive dependencies exceeding eight levels as expanded attack surface, and that it is on in your workspace unless you switch it off in Compliance › Manage.

Each scan of a product checks four requirements, all decided by the scan:

RequirementWhat the docs say Safeguard checks
TRANS-1 Dependency Relationship CompletenessAll components should have dependency relationships
TRANS-2 Transitive Dependency CountHigh transitive counts increase attack surface
TRANS-3 Circular Dependency DetectionCircular dependencies indicate architectural issues
TRANS-4 PURL Coverage for ResolutionComponents need a purl for transitive dependency resolution

The framework's Controls tab adds controls from Safeguard's control catalog that you evidence yourself, with automated tests, uploaded evidence, policies or an attestation. They include a maximum transitive depth threshold, surface area metric tracking, flattening strategy documentation, a dependency graph health score, critical path transitive vulnerability risk, ghost dependency detection and lock file consistency. Note the wording: these are controls you evidence, not scans Safeguard runs for you. The docs say no automated tests map to the framework directly, and you can link your own.

Policy templates come with it, including a transitive dependency risk management policy, a critical path reachability and ghost dependency policy, and a dependency graph health reporting policy. The continuous compliance post explains how framework readiness is scored.

Two of those scan requirements deserve a closer look.

TRANS-1 and TRANS-4 are SBOM quality checks. A component with no relationships or no purl is a component whose position in the graph cannot be resolved. If these fail, your transitive paths are incomplete, and every downstream view (reachability, depth policies, unused candidates) is working from a partial graph. Fix these first. Missing purls usually trace back to vendored code, components copied in by hand or SBOMs uploaded from a tool that does not emit them.

TRANS-3 is an architecture signal. Circular dependencies make upgrades harder, because no package in the cycle can move alone. They are worth fixing for maintainability even when no CVE is involved.

Decision table: what to use for which problem

SituationWhat Safeguard shows or doesWhat you still own
A CVE sits several levels downSCA (GA) finding with the full transitive path, reachability verdict (GA), alternative paths and safe versionChoosing between bumping the direct dependency and an override; testing
Many deep findings, little timeReachability, EPSS and KEV in one priority score; filters for reachable and exploitableDeciding what "today" means for your services
A pull request makes the tree much deeperDepth as a policy signal (policies GA), as the docs show itWriting and testing the rule as a warning first
You suspect dead packagesUnused dependency candidates from deep dependency scanning (GA), in the platformChecking each candidate for name based loading; removing it
Your SBOM is missing relationships or purlsTransitive Depth framework scan requirements TRANS-1 and TRANS-4Fixing the inputs: lockfiles, vendored code, upstream SBOM quality
Internal names could be claimed on a public registryInventory of every component with purl and path, at any depthScopes, registry mapping, a curated feed, hashed lockfiles
A safe version exists and the fix is mechanicalAI Remediate pull requests (autonomous remediation GA, ships switched off)Enabling it, approval policy, review
You want the inventory from a laptop or CI boxCLI (GA) safeguard scan, with transitive resolution flags as documentedAgreeing whether public lookups are acceptable for your package names

What Safeguard does not do

Honest limits, so you can plan around them.

  1. It does not fix your registry configuration. Scopes, registry mappings, a curated feed and pip.conf or .npmrc hygiene are files your team owns and reviews. Safeguard's inventory tells you which names matter; it does not rewrite how your builds fetch packages.
  2. Unused dependency detection has no CLI command yet. The docs say so. You work candidates in the platform, and you confirm each one by hand, because packages loaded by name, build tools and peer dependencies can look unused to a static import check.
  3. The CLI's documented flag table is not the whole story. The deep scanning docs show --transitive-depth, but the usage docs' flag table does not list it. Read safeguard scan --help for your installed version before you script around a flag.
  4. safeguard scan does not fail a build on findings. There is no severity threshold flag. Gates run in the platform. If you script a local gate, handle exit code 2 as a failure.
  5. Treat unknown reachability verdicts as potentially reachable. Where the analysis cannot decide, keep the finding in the queue.
  6. The REST API is not the route for this today. Several docs pages, including reachability and continuous scanning, carry a banner that the /v1 endpoints are not yet available. Use the CLI, CI and the web app.
  7. Transitive Depth controls are yours to evidence. The framework's scan requirements are decided by scans; its catalog controls, including ghost dependency detection and lock file consistency, need your own tests, evidence or attestation.
  8. Safeguard does not choose between a direct bump and an override for you without review. AI Remediate proposes changes and flags breaking changes, but autonomous remediation ships switched off, and every proposed change still needs a human decision until you set it otherwise.

Common mistakes

  1. Scanning manifests without lockfiles. Without a lockfile, the graph is a guess about what will install. Commit lockfiles and scan the same directory your build uses.
  2. Fixing the transitive package instead of the chain. You usually cannot edit a package five levels down. Change the direct dependency at the top of the path, or use an override, and check for a second path before calling it done.
  3. Turning on a depth rule as a blocker on day one. Start with the warn on deepening shape from the docs, collect a few weeks of results, then decide what should block.
  4. Deleting every unused candidate in one go. Plugins, CLI tools and packages loaded by name will break builds. Check each one and move tools to development dependencies instead of deleting them.
  5. Adding --resolve-transitive to a private codebase without asking. It resolves through a public service. Agree that with your security team first.
  6. Using extra indexes for convenience. --extra-index-url invites precedence surprises. Use one index that proxies the rest.
  7. Ignoring TRANS-1 and TRANS-4 failures. Missing relationships and purls quietly degrade every other view of the graph.
  8. Treating exit code 0 as clean. For safeguard scan, 0 means the scan ran. It says nothing about what it found. Gate in the platform.
  9. Forgetting internal libraries that are only used transitively. They need the same scope and registry protection as direct ones.

FAQ

How deep does Safeguard scan transitive dependencies?

Safeguard's SCA resolves the full dependency graph from your manifests and lockfiles, including deeply nested transitive dependencies, and records the full path for each finding. For local scans, the deep dependency scanning docs show safeguard scan --dir . --resolve-transitive --transitive-depth 100. Check safeguard scan --help on your installed version for the depth flag and its accepted values.

How do I find out why a transitive dependency is in my project?

Open the finding in the Vulnerabilities view. The docs say each finding shows the full transitive path from your project root to the vulnerable package, whether that chain is reachable, and whether an alternative path exists. Cross check with npm ls <package>, mvn dependency:tree or go mod why.

Can Safeguard find unused dependencies?

Yes. Deep dependency scanning (GA) reports packages that are present in your dependency tree but never imported, as candidates for removal. The docs say there is no CLI command for this yet, so you review candidates in the platform and confirm each one before removing it.

Why would an unused dependency still be a risk?

It is still installed, it may run install scripts, it appears in your SBOM, and its vulnerabilities still reach your triage queue. OWASP's Top 10 2025 lists removing unused dependencies as a prevention step for software supply chain failures.

What is dependency confusion in simple terms?

It is when a build that can fetch from more than one registry installs a public package that shares a name with one of your internal packages. The package manager follows its precedence rules, and an attacker who owns the public name gets their code into your build.

How do I prevent dependency confusion in npm and pip?

In npm, put internal packages in a scope and associate that scope with your internal registry with npm config set. In pip, use one --index-url that points at a proxy instead of adding --extra-index-url, and use hash checking mode with --require-hashes. In both, review changes to .npmrc and pip.conf like code.

Does Safeguard support reachability for transitive dependencies?

Yes. Reachability analysis (GA) feeds every node in the graph, not just direct dependencies. Static reachability is GA for JVM languages, Python, JavaScript and TypeScript, Go, Rust, C and C++, and C#, as well as Ruby, PHP, and Swift and Objective-C, according to the docs.

Can Safeguard block a release if the dependency tree gets too deep?

The deep scanning docs show depth used in policy rules, including one that blocks when a component deeper than a set level has a critical vulnerability and one that warns when a change deepens the tree. Policies and gates are GA. Test any depth rule as a warning first.

Will Safeguard open pull requests for transitive upgrades?

AI Remediate can, as part of autonomous remediation (GA). It ships switched off with every category set to Manual. When enabled, it proposes upgrades, flags breaking changes and opens pull requests for your review.

Does my source code leave my machine when I scan with the CLI?

When you scan from the interactive terminal while signed in, the docs say the runner uploads the SBOM and findings, never your source. With --resolve-transitive, package names and versions are looked up on a public dependency service, so decide whether that is acceptable for private names.

Finding the vulnerable package five levels down is half the job; the other half is getting a safe version into your build when the direct dependency above it has not moved yet. The options below apply to transitive findings as much as to direct ones.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

The first demo is a real CLI session. It installs the CLI, opens the interactive terminal with safeguard, and asks it to fix a lodash CVE in a repository. The assistant reads the finding and the advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing. It ends with /permissions, /model and /scan .. The same flow applies to a transitive finding: the fix lands on the direct dependency or override at the top of the path.

Click through it at your own pace, or open it full screen.

The second demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. This is the route to use when you want the full transitive inventory of a private repository in the platform without your source leaving the machine.

Click through it at your own pace, or open it full screen.

The third demo connects Claude to Safeguard: you add Safeguard from Claude's connector directory, where it has a community listing, sign in with OAuth, and choose which tools Claude can use. Once connected, you can ask Claude which reachable findings sit in transitive dependencies, within the tools your administrator has enabled.

Click through it at your own pace, or open it full screen.

To try it on your own code, download Safeguard from safeguard.sh/download and follow the docs.

Quick check

A CVE sits in a package four levels below your manifest. What do you usually change to fix it?

Usually the direct dependency at the top of the transitive path, moved to a version that pulls in a safe transitive version, or an override in your build tool that forces the safe version. Check Safeguard's finding for an alternative path first, because a second chain can keep the vulnerable version in the build.

Safeguard lists a package as an unused dependency candidate. Should you delete it straight away?

No. The docs call these candidates for removal. Check whether the package is a build tool, a command line tool run from scripts, a peer dependency or something loaded by name at runtime. Move tools to development dependencies, then remove what is truly unused and rerun tests and the scan.

Sources

Last reviewed: 25 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.