Dependency Pinning
Overview
HVE Core enforces dependency pinning to mitigate supply chain attacks. Every dependency reference in the repository must resolve to a specific, immutable version. The Test-DependencyPinning.ps1 scanner validates all dependency types during CI and produces SARIF reports for GitHub code scanning integration.
| Dependency Type | Pinning Strategy | Example |
|---|---|---|
| GitHub Actions | Full 40-character commit SHA | actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 |
| npm | Exact version (no ranges) | "eslint": "9.18.0" |
| pip | Exact version with == | requests==2.31.0 |
| Workflow npm commands | npm ci enforcement | npm ci instead of npm install |
| Shell downloads | Checksum verification | sha256sum --check after download |
npm: Exact-Version Enforcement
NOTE
npm dependencies use exact-version enforcement rather than SHA-pinning. Unlike GitHub Actions (where commit SHAs identify immutable source snapshots), npm packages are published as immutable registry artifacts. An exact version string like 9.18.0 is already a unique, deterministic reference.
What Is Validated
The scanner rejects any version string that contains range operators or wildcards:
{
"dependencies": {
"valid": "9.18.0",
"invalid-caret": "^9.18.0",
"invalid-tilde": "~9.18.0",
"invalid-wildcard": "9.*",
"invalid-range": ">=9.0.0 <10.0.0"
}
}
Validation Regex
^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$
This permits standard semver (1.2.3), pre-release tags (1.2.3-beta.1), and build metadata (1.2.3+build.42), while rejecting all range operators.
Why Not SHA-Pinning for npm
| Criterion | SHA-Pinning (GitHub Actions) | Exact-Version (npm) |
|---|---|---|
| Registry model | Git repositories with mutable tags | Immutable package tarballs |
| Mutability risk | Tags can be force-pushed to different commits | Published versions are permanently immutable |
| Audit tooling | npm audit cross-references semver, not SHAs | Full compatibility with npm audit |
| Lockfile integration | N/A | package-lock.json records integrity hashes |
| Human readability | 40-char hex strings obscure the actual version | Version is self-documenting |
GitHub Actions: SHA Pinning
GitHub Actions references must use full 40-character commit SHAs because action tags (like v4) are mutable Git references that can be retargeted to arbitrary commits.
# Rejected: mutable tag reference
- uses: actions/checkout@v4
# Accepted: immutable SHA reference
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.2.2
The scanner validates that the SHA is a real 40-character hexadecimal string and optionally checks staleness against the GitHub API. SHA-pin validation applies the same expanded scope as the npm-command scan: it covers workflow files (.github/workflows/*.yml and .github/workflows/*.yaml) and composite action definitions (.github/actions/**/*.yml and .github/actions/**/*.yaml).
DevContainer Features: Lockfile Integrity
DevContainer features declared in .devcontainer/devcontainer.json are pinned through a lockfile (devcontainer-lock.json) that records the exact version, OCI digest, and SHA-256 integrity hash for each feature. This follows the devcontainer lockfile spec, modeled after package-lock.json.
What Is Validated
The devcontainer-lockfile-check.yml workflow enforces three checks during PR validation:
| Check | Failure Condition |
|---|---|
| Lockfile existence | devcontainer-lock.json is absent from the repository |
| SHA-256 integrity | A feature entry is missing resolved or integrity with sha256: prefix |
| Feature coverage | A feature in devcontainer.json has no corresponding lockfile entry |
Lockfile Format
Each feature entry records the resolved OCI reference and integrity hash:
{
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "1.7.1",
"resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46...",
"integrity": "sha256:8c0de46..."
}
}
}
Regenerating the Lockfile
Rebuild the dev container to regenerate the lockfile. In VS Code, use the Dev Containers: Rebuild Container command. Commit the updated devcontainer-lock.json alongside any changes to devcontainer.json.
pip: Exact-Version Staleness Validation
The == exact-version convention applies to requirements*.txt and Pipfile-style dependency declarations.
The pip scanner validates packages that are already pinned with the == operator, checking each pinned version for staleness against PyPI.
It does not flag range specifiers (>=, ~=, <) as violations: its regex only matches package==version references, so range pins are simply not inspected.
The scanner excludes virtual environment directories (.venv, venv, .tox, .nox, __pypackages__) to avoid false positives from installed package metadata.
# Validated for staleness (exact pin in requirements.txt / Pipfile)
requests==2.31.0
flask==3.0.0
# Not flagged (range pins are not matched by the scanner)
requests>=2.31.0
flask~=3.0
Lockfile-Backed Pinning for uv Skills
Python skills managed with uv and pyproject.toml achieve exact pinning through uv.lock rather than == constraints in pyproject.toml.
The uv.lock file records the exact resolved version and a per-package integrity hash for every dependency, so a range pin in pyproject.toml is equivalent in supply-chain terms to an == pin in requirements.txt.
Range pins in pyproject.toml are therefore acceptable for uv-managed skills: prefer expressing compatible-version ranges in pyproject.toml and committing uv.lock to lock the resolved versions, syncing with uv sync --locked to verify against the lock.
NOTE
Test-DependencyPinning.ps1 does not flag range pins in pyproject.toml. Supply-chain security for uv-managed skills is enforced through uv.lock integrity hashes rather than == constraints, so no scanner violation is raised for ranged pyproject.toml dependencies.
Workflow npm Commands: npm ci Enforcement
CI workflow YAML files are scanned for npm commands that modify the dependency tree at install time. These commands resolve version ranges against the registry, producing non-deterministic installs that can pull in compromised packages.
Detected Commands
The scanner inspects run: blocks in workflow YAML with indentation-aware parsing and flags these commands:
| Flagged | Reason |
|---|---|
npm install | Resolves ranges from package.json, ignoring lockfile |
npm i | Alias for npm install |
npm update | Upgrades to latest versions within semver ranges |
npm install-test | Combines install and test in a non-deterministic way |
Safe Commands
These npm commands are not flagged because they do not modify the dependency tree:
npm ci: Installs exactly from the lockfile, removingnode_modulesfirstnpm run: Executes a script defined inpackage.jsonnpm test: Alias fornpm run testnpm audit: Reports known vulnerabilities without installingnpx: Runs a package binary without modifying dependencies
Remediation
Replace flagged commands with npm ci for deterministic, lockfile-based installs:
# Rejected: resolves version ranges from the registry
- run: npm install
# Accepted: installs exactly what the lockfile specifies
- run: npm ci
File Scope
The scanner processes files matching:
.github/workflows/*.ymland.github/workflows/*.yaml.github/actions/**/*.ymland.github/actions/**/*.yaml(composite actions)
Shell Downloads: Checksum Verification
Shell scripts that download files from the internet must verify checksums to prevent tampered or corrupted binaries from entering the build environment.
Detection
The scanner identifies download commands matching curl or wget with an HTTP/HTTPS URL. It then checks the next five lines for a checksum verification command. If no verification is found, the download is flagged.
Accepted Verification Commands
Any of these patterns within five lines of the download satisfies the check:
sha256sum: GNU coreutils checksumshasum: macOS/BSD checksum utilityGet-FileHash: PowerShell checksum cmdletopenssl dgst -sha256: OpenSSL digestsha256sum -c: Checksum file verification
Examples
# Accepted - checksum verified immediately after download
curl -Lo tool.tar.gz https://example.com/tool-v1.0.tar.gz
echo "abc123... tool.tar.gz" | sha256sum --check
# Rejected - no checksum verification after download
wget https://example.com/tool-v1.0.tar.gz
tar xzf tool-v1.0.tar.gz
Scanned Files
The scanner processes files matching .devcontainer/scripts/*.sh and scripts/*.sh, excluding fixture directories used in tests.
CI Integration
The dependency pinning scanner runs in CI as part of the security validation workflow. It produces SARIF 2.1.0 output that integrates with GitHub code scanning.
flowchart LR
A[CI Trigger] --> B[Test-DependencyPinning.ps1]
B --> C{Violations?}
C -->|None| D[✅ Pass]
C -->|Found| E[SARIF Report]
E --> F[GitHub Code Scanning]
Severity Mapping
| Scanner Severity | SARIF Level | Trigger |
|---|---|---|
| High | error | Unpinned or mutable dependency reference |
| Medium | warning | Stale pinned version with available update |
| Low | note | Informational findings |
Running Locally
# Full scan with SARIF output
./scripts/security/Test-DependencyPinning.ps1
# Results appear in logs/dependency-pinning-results.json
Related Resources
- Security Model: Supply chain threats S-1, S-2, SC-1, SC-4, and SC-6
- Branch Protection: Required status checks including dependency pinning
🤖 Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.