Lock Files, Hash Verification, and SBOM Generation
Lock Files, Hash Verification, and SBOM Generation
The Failure
The inventory service is written in Java with Gradle. The build.gradle file specifies dependency versions, but Gradle resolves transitive dependencies dynamically. A transitive dependency, commons-text, is updated from 1.10.0 to 1.11.0 between two builds of the same commit. The new version introduces a behavior change in string interpolation that causes the inventory reservation logic to silently truncate SKU codes longer than 32 characters. The bug reaches staging. The QA team catches it after two days, but the root cause takes another day to identify because the source code did not change.
The build was not reproducible. The same commit produced different artifacts on Monday and Wednesday because a transitive dependency changed.
The Mechanism
Lock File Enforcement by Language
Each language ecosystem has a different mechanism for locking dependencies:
Node.js: package-lock.json records exact versions and integrity hashes (SHA-512). npm ci installs from the lock file without resolving newer versions.
Go: go.sum records cryptographic hashes of module contents. go mod verify checks that downloaded modules match the recorded hashes.
Python: pip install --require-hashes -r requirements.txt requires every line in requirements.txt to include a hash. If the downloaded package does not match the hash, the install fails. Use pip-compile (from pip-tools) to generate a requirements.txt with hashes from a requirements.in.
Java (Gradle): gradle/verification-metadata.xml records checksums for all dependencies. --write-verification-metadata sha256 generates the file. Gradle verifies checksums on every build.
SBOM Formats
SPDX (Software Package Data Exchange) is an ISO standard (ISO/IEC 5962:2021). Used by the Linux Foundation and required by some government procurement policies.
CycloneDX is an OWASP standard focused on security use cases. Includes vulnerability data (VEX), service dependencies, and licensing. More compact than SPDX for the same content.
The decision rule: use SPDX if compliance requirements specify it. Use CycloneDX if the primary consumer is a vulnerability scanner like Dependency-Track. Both are supported by Syft, Trivy, and Grype.
The Implementation
Gradle Dependency Verification
// build.gradle.kts
// HARDENED: Enable dependency verification
tasks.register("verifyDependencies") {
doLast {
exec {
commandLine("gradle", "--write-verification-metadata", "sha256", "dependencies")
}
}
}
<!-- gradle/verification-metadata.xml (generated, committed to repo) -->
<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata>
<components>
<component group="org.apache.commons" name="commons-text" version="1.10.0">
<artifact name="commons-text-1.10.0.jar">
<sha256 value="a1b2c3d4e5f6..." origin="Generated by Gradle"/>
</artifact>
</component>
</components>
</verification-metadata>
# HARDENED: Gradle build with dependency verification in CI
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Build with verification
run: ./gradlew build --dependency-verification strict
- name: Verify no verification changes needed
run: git diff --exit-code gradle/verification-metadata.xml
SBOM Generation with Syft
# HARDENED: SBOM generation and attachment to image
jobs:
sbom:
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Generate SBOM from image
uses: anchore/sbom-action@v0
with:
image: ${{ env.IMAGE }}@${{ needs.build.outputs.image-digest }}
format: spdx-json
output-file: checkout-service.sbom.spdx.json
- name: Upload SBOM as artifact
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: checkout-service.sbom.spdx.json
retention-days: 90
- name: Attach SBOM to image with cosign
env:
COSIGN_EXPERIMENTAL: "1"
run: |
cosign attach sbom \
--sbom checkout-service.sbom.spdx.json \
${{ env.IMAGE }}@${{ needs.build.outputs.image-digest }}
Attaching the SBOM to the image with cosign means anyone who pulls the image can also retrieve its SBOM. The SBOM travels with the artifact, not as a separate file that can get lost.
Python Requirements with Hashes
# Generate requirements.txt with hashes from requirements.in
pip-compile --generate-hashes requirements.in -o requirements.txt
# requirements.txt (generated, committed to repo)
fastapi==0.115.0 \
--hash=sha256:abc123...
uvicorn==0.30.6 \
--hash=sha256:def456...
pydantic==2.9.2 \
--hash=sha256:ghi789...
# HARDENED: Python install with hash verification
- name: Install dependencies
run: pip install --require-hashes -r requirements.txt
The Gate
The gate is the combination of lock file verification and SBOM-based scanning:
- Lock file verification catches undeclared dependency changes at build time.
- SBOM generation captures every component in the artifact.
- SBOM-based scanning catches known vulnerabilities in those components.
If any step fails, the pipeline stops. The developer sees which dependency caused the failure and whether it is a version drift issue (lock file) or a vulnerability issue (scan).
The Recovery
When a dependency hash does not match:
- Compare the expected hash (from the lock file) with the actual hash (from the registry).
- If the registry hash changed for the same version, this is a potential supply chain attack. Do not proceed. Report to the registry.
- If the lock file is outdated (a developer ran
npm installwithout committing), update the lock file and commit.
When an SBOM scan finds a vulnerability:
- Check if the vulnerable component is reachable in the application’s code path.
- If reachable, update the dependency. If no fix exists, evaluate mitigation (WAF rules, input validation).
- If not reachable, add to the ignore list with justification and review date.