Most federal software teams have a reasonable handle on what packages their applications declare. The harder problem is knowing what actually gets resolved, downloaded, and compiled when the build runs. Those two things are not always the same, and the gap between them is where build graph integrity failures live.

Dependency resolution is not a static lookup. Package managers apply constraint-solving logic, version range evaluation, and registry priority rules every time a build executes. When resolver behavior changes, when a lockfile is absent or stale, when a nested manifest introduces a transitive dependency that was never reviewed, or when a wildcard version range resolves to a newer package than it did last week, the resulting build artifact may differ from what any developer, reviewer, or assessor approved. The build succeeds. The pipeline passes. The artifact ships. And nobody flagged the change.

SBOMs are frequently discussed in terms of what components a software product contains. That framing is correct but incomplete for this problem. An SBOM generated at build time captures the resolved state of the dependency graph, not just the declared state. When used correctly, that SBOM becomes a verifiable record of what the resolver actually produced, which can be compared against an approved graph snapshot to detect drift, unauthorized additions, and unexpected version changes before the artifact reaches a staging or production environment.

This article focuses on the mechanics of governing the dependency graph as a build artifact in federal CI/CD pipelines. It covers lockfile enforcement, resolver pinning, nested manifest handling, graph-diff policy, and ecosystem-specific controls across Java, Python, Node.js, and Go. The goal is a practical implementation framework, not a general discussion of SBOM value or build-time risk categories.

Key Takeaways

  • The resolved dependency graph is a distinct build artifact that requires its own governance controls, separate from declared dependencies in source manifests.
  • Lockfile enforcement and resolver version pinning are the primary technical controls for preventing unintended graph changes between builds.
  • SBOMs generated at build time capture resolved graph state and can be compared against approved graph snapshots to detect drift before artifacts are promoted.
  • Wildcard version ranges, floating constraints, and nested transitive manifests are the most common sources of silent graph changes in federal pipelines.
  • Ecosystem-specific resolver behavior in Maven, Gradle, pip, npm, and Go modules requires different enforcement strategies; a single policy does not cover all cases.
  • Graph-diff checks integrated into CI gates can block promotion of builds whose resolved graphs deviate from the approved baseline without requiring manual review of every dependency.

Table of Contents

The Dependency Graph as a Governed Build Artifact

Federal software governance has historically focused on source code review, static analysis, and component inventory. Those controls matter, but they operate on declared inputs. The dependency graph that a package manager resolves at build time is a derived artifact, and it can diverge from declared inputs in ways that none of those controls will catch.

When a developer declares a dependency with a version range, the package manager resolves that range against available registry versions at the time of the build. If the registry has published a new patch or minor release since the last build, the resolver may select a different version than it did previously. The build still passes. The artifact is different. No alert fires. This is not a theoretical edge case. It is routine behavior in any ecosystem that permits floating version constraints, and most do by default.

Transitive dependencies compound the problem. A direct dependency may itself declare its own dependencies with ranges or floating constraints. The full resolved graph can include dozens or hundreds of packages that no developer on the project explicitly selected. When any of those transitive packages change, the build output changes. The SBOM generated from that build will reflect the change, but only if someone is comparing it against a known-good baseline.

Treating the dependency graph as a governed artifact means capturing its resolved state at each build, establishing an approved baseline, and enforcing that deviations require explicit review and approval before the artifact can be promoted. This is the same principle applied to configuration baselines in infrastructure management, applied to the software supply chain at build time. For federal teams operating under RMF controls related to software integrity and supply chain risk management, this is not optional governance overhead. It is a control gap that assessors are increasingly examining.

The Software Bill of Materials (SBOM) generated at build time is the mechanism that makes graph governance operational. It records what the resolver actually produced, including transitive packages, resolved versions, and package source metadata. That record is the basis for comparison, attestation, and policy enforcement.

Lockfile Enforcement and Resolver Pinning

Lockfiles are the primary mechanism for making dependency resolution deterministic. When a lockfile is present and the build system is configured to honor it, the resolver does not re-evaluate version ranges against the registry. It installs exactly the versions recorded in the lockfile. The graph is fixed until someone explicitly updates the lockfile, which creates a reviewable change event.

The problem in federal pipelines is that lockfile discipline is inconsistent. Developers may generate lockfiles locally but not commit them. CI pipelines may be configured to run install commands that ignore or regenerate lockfiles. Containerized build environments may pull fresh lockfile-bypassing installs on every run. In each case, the intent to use a lockfile exists, but the enforcement does not.

Resolver version pinning is a separate but related control. The resolver itself, meaning the package manager binary and its constraint-solving logic, can change behavior between versions. A pip upgrade, an npm version change, or a Gradle wrapper update can alter how version ranges are evaluated, how conflicts are resolved, and which transitive packages are selected. Pinning the resolver version in the CI environment is as important as pinning the packages it resolves.

Registry and Mirror Configuration

Federal pipelines frequently route package downloads through internal mirrors or artifact repositories rather than public registries. This is a sound security practice, but it introduces its own graph integrity risks. If the mirror is not configured to serve exactly the same package versions as the upstream registry, or if the mirror’s index is stale, the resolver may select different packages than it would against the public registry. Worse, if the mirror configuration is not locked in the build environment, a resolver may fall back to a public registry when a package is not found in the mirror, silently introducing a different resolution path.

Registry configuration should be treated as part of the build environment definition, committed to version control, and validated as part of the CI pipeline setup. Any deviation between the registry configuration used during development and the configuration used in CI is a potential source of graph divergence.

Practitioner Reality

In practice, many federal CI pipelines inherit lockfile and registry configuration from developer workstation conventions rather than from enforced pipeline policy. A developer who runs npm install locally generates a lockfile that works on their machine. The CI pipeline runs the same command but in a container image that has a different npm version, a different registry endpoint, and no enforcement that the lockfile must be honored. The build succeeds, the SBOM reflects a different resolved graph than the developer tested against, and neither the developer nor the security team has a mechanism to detect the difference until an SBOM comparison is run against the approved baseline. Most teams are not running that comparison.

Ecosystem-Specific Controls Across Java, Python, Node.js, and Go

Dependency resolution mechanics differ enough across ecosystems that a single policy statement does not translate into consistent enforcement. Each ecosystem requires specific controls applied at the right layer of the build process.

Ecosystem Lockfile Mechanism Resolver Pinning Approach Key Graph Risk Enforcement Flag
Maven No native lockfile; use dependency:resolve with pinned versions Pin Maven wrapper version in .mvn/wrapper RELEASE and LATEST qualifiers; range declarations in POMs Enforce no range declarations in POM policy
Gradle Dependency locking via dependencyLocking block Pin Gradle wrapper version in gradle-wrapper.properties Dynamic versions (+), changing modules, resolution strategies Enable write-locks flag; fail on dynamic version detection
Python (pip) requirements.txt with pinned versions; pip-compile for full graph Pin pip version in CI environment image Unpinned requirements; resolver backtracking behavior changes Use pip install –require-hashes; validate against compiled lockfile
Node.js (npm) package-lock.json committed to source control Pin npm version in CI environment image Caret and tilde ranges; lockfile regeneration on npm install Use npm ci instead of npm install in all CI pipelines
Go go.sum file committed alongside go.mod Pin Go toolchain version in go.mod toolchain directive Module graph pruning changes across Go versions; replace directives Use GONOSUMCHECK and GOFLAGS=-mod=readonly in CI

Maven deserves particular attention in federal environments because it has no native lockfile mechanism. Many Java projects in government use Maven with version ranges or RELEASE qualifiers in their POM files, which means every build can resolve to a different artifact version without any visible change to source control. The practical control is a combination of policy enforcement against range declarations in POM files and the use of a dependency management section that pins all transitive versions explicitly. Some teams use the Maven Enforcer Plugin to fail builds that contain range declarations, which is a reasonable gate to add to a CI pipeline.

Gradle’s dependency locking feature is more capable than Maven’s options but requires explicit activation. Teams that have not enabled the dependencyLocking block in their build scripts are running without graph pinning even if they believe their builds are reproducible. Dynamic version declarations using the plus notation are a common source of silent graph changes in Gradle projects and should be flagged as a policy violation in any federal pipeline.

Nested Manifests and Transitive Graph Depth

Nested manifests are a specific problem in polyglot repositories and in projects that embed sub-projects or vendor directories. A top-level package manifest may declare clean, pinned dependencies, but a nested manifest in a subdirectory may declare its own dependencies with different constraints. Depending on how the build system handles nested manifests, those nested declarations may or may not be merged into the top-level resolved graph. When they are merged, the result can include transitive packages that were never reviewed as part of the top-level dependency review process.

SBOM generation tools that operate at the build output level will capture these transitive packages regardless of which manifest introduced them. That is the value of build-time SBOM generation over manifest-scanning approaches. The SBOM reflects what was actually compiled into the artifact, not just what the top-level manifest declared.

SBOM-Based Graph Comparison Workflow

The operational value of build-time SBOMs for graph integrity governance depends on having a comparison workflow that runs as part of the CI pipeline, not as a post-deployment audit. The workflow needs to compare the resolved graph captured in the current build’s SBOM against an approved baseline graph, identify deviations, classify them by severity or policy relevance, and either block promotion or generate a finding that requires explicit approval.

The reference workflow below describes the five states that matter for graph integrity verification. Each state is a distinct point in the build process where the graph can be observed and compared.

Reference Workflow: Dependency Graph State Comparison

  1. Declared state – The packages and version constraints listed in source manifests (pom.xml, requirements.txt, package.json, go.mod). This is what developers and reviewers approved. It is the starting point, not the ground truth.
  2. Resolved state – The full dependency graph produced by the package manager’s constraint solver, including all transitive packages and their selected versions. This is captured by running the resolver in a dry-run or export mode before compilation begins.
  3. Downloaded state – The packages actually fetched from registries or mirrors during the build. Differences between the resolved state and the downloaded state can indicate registry configuration problems, mirror staleness, or package substitution.
  4. Compiled state – The packages linked or included in the final build artifact. In some ecosystems, particularly JavaScript bundlers and Go static linking, the compiled state may differ from the downloaded state due to tree-shaking, dead code elimination, or conditional compilation.
  5. SBOM-observed state – The component inventory captured by the SBOM generation tool from the build output. This is the authoritative record of what shipped. Comparing this against the approved baseline graph is the primary graph integrity check.

Policy gates should compare the SBOM-observed state against the approved baseline. Any package present in the SBOM that is not in the baseline, any version change from the baseline, and any package removed from the baseline should generate a finding. The classification of that finding as a blocker or a warning depends on the sensitivity of the system and the nature of the change.

Graph-diff tooling can automate this comparison. The output should be machine-readable so that CI pipeline gates can evaluate it programmatically. Human review should be reserved for findings that the automated gate cannot classify, such as new packages with no prior vulnerability data or version changes that cross major version boundaries.

Approved baseline graphs should be stored in version control alongside the source code they govern. When a developer intentionally updates a dependency, the lockfile update and the baseline graph update should be part of the same pull request, reviewed together. This creates a clear audit trail connecting the approved change to the resulting SBOM-observed state in the next build.

Common Mistakes

The most persistent mistake is treating manifest review as equivalent to graph review. A security review that examines declared dependencies in a package manifest has not reviewed the resolved graph. It has reviewed the inputs to the resolver. The resolver’s output can differ significantly, and that output is what ships. Teams that have completed a thorough manifest review and believe their dependency posture is governed have often left the resolved graph entirely unexamined.

Lockfile management is frequently incomplete. Teams commit lockfiles to source control but do not enforce that CI pipelines use them in frozen mode. The distinction between an install command that respects the lockfile and one that regenerates it is often a single flag or command variant. Without explicit CI pipeline policy that enforces the frozen install path, lockfiles provide a false sense of determinism.

Wildcard and floating version constraints are allowed to persist in production codebases because no automated gate checks for them. In many federal programs, the only check on version constraint syntax is a code reviewer who may not recognize the risk. Adding a policy check that fails the build when wildcard ranges, RELEASE qualifiers, or dynamic version notation are detected in manifests is a low-cost control that eliminates a significant source of graph drift.

SBOM generation is placed at the wrong point in the pipeline. SBOMs generated from source manifests before the build runs reflect the declared state, not the resolved state. SBOMs generated after the build from the compiled artifact reflect the SBOM-observed state. Both are useful, but they answer different questions. Teams that generate only pre-build SBOMs are not capturing the resolver’s output and cannot use those SBOMs for graph integrity comparison.

Resolver version changes are not treated as configuration change events. When a CI environment image is updated and the embedded package manager version changes, the resolver’s behavior may change in ways that affect graph resolution. Without a policy that flags resolver version changes as requiring graph revalidation, teams can unknowingly ship artifacts whose resolved graphs were produced by a different resolver than the one used to generate the approved baseline.

Graph comparison is deferred to post-deployment scanning rather than integrated as a CI gate. Post-deployment scanning can detect problems, but it cannot prevent a non-conforming artifact from being promoted. By the time a post-deployment scan identifies a graph deviation, the artifact may already be in a staging environment or under assessment. Integrating graph comparison as a promotion gate keeps the control at the point where it can block the artifact rather than flag it after the fact.

Start with an inventory of build ecosystems across the program. Most federal software programs include multiple languages and build systems, and each requires different controls. Trying to implement a single graph governance policy without first mapping the ecosystem landscape results in gaps where the policy does not apply and over-engineering where it does not fit.

For each ecosystem, establish the lockfile mechanism and enforce it in CI. This means configuring the CI pipeline to use the frozen install path, pinning the package manager version in the build environment image, and adding a policy check that fails the build if the lockfile is absent or if the install command would regenerate it. These are pipeline configuration changes, not architectural changes, and they can be implemented incrementally without disrupting existing build workflows.

Add a wildcard and floating version detection gate to the CI pipeline for each ecosystem. The specific syntax to detect varies by ecosystem, but the principle is the same: any version constraint that allows the resolver to select a version other than a specific pinned version is a policy violation that should fail the build. This gate should run before the resolver executes, so that non-conforming manifests are rejected before they produce a non-deterministic graph.

Establish an approved baseline graph for each application. The initial baseline can be generated from the first build that passes all lockfile and wildcard detection gates. Store the baseline in version control. Define the process for updating the baseline: it requires a pull request, a dependency review, and an explicit approval. The baseline update and the lockfile update should be committed together.

Integrate build-time SBOM generation into the CI pipeline at the compiled artifact stage. Configure the SBOM generation tool to capture the full transitive graph, including package source metadata and resolved versions. The SBOM format should be consistent across all applications to support automated comparison tooling.

Add a graph-diff comparison step that runs after SBOM generation and before artifact promotion. The comparison step evaluates the current build’s SBOM against the approved baseline graph and produces a structured finding report. Configure the CI gate to block promotion when the finding report contains additions, version changes, or removals that have not been explicitly approved. Route findings that require human review to the appropriate security or engineering team rather than silently passing or silently blocking.

Treat resolver version changes as configuration change events. When a CI environment image is updated, require a graph revalidation run that compares the resolved graph produced by the new resolver version against the approved baseline. If the resolver change produces a different graph, the difference must be reviewed and the baseline updated before the new environment image is used for production builds.

For programs operating under RMF, document the graph integrity controls as part of the system’s configuration management and supply chain risk management control implementations. The approved baseline graph, the lockfile, the CI gate configuration, and the SBOM generation and comparison workflow together constitute evidence that the program has implemented deterministic dependency resolution and is actively monitoring for graph drift. That evidence supports assessor review and ongoing authorization activities without requiring manual artifact-by-artifact inspection.

Expand the approach incrementally. Start with the highest-risk applications, meaning those with the most transitive dependencies, the most frequent build cadence, or the most sensitive mission function. Apply the full control set there first, validate that the workflow operates correctly in the CI environment, and then extend it to lower-risk applications. Trying to implement graph governance across an entire program portfolio simultaneously creates operational friction that often results in the controls being disabled or bypassed rather than sustained.

Anchor Defense

Need help evaluating solutions for your environment?

Whether you’re modernizing infrastructure, improving resilience, or evaluating technologies, Anchor Defense helps organizations align mission requirements with practical outcomes.


TALK TO AN EXPERT →