Skip to main content

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 TypePinning StrategyExample
GitHub ActionsFull 40-character commit SHAactions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
npmExact version (no ranges)"eslint": "9.18.0"
pipExact version with ==requests==2.31.0
Workflow npm commandsnpm ci enforcementnpm ci instead of npm install
Shell downloadsChecksum verificationsha256sum --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

CriterionSHA-Pinning (GitHub Actions)Exact-Version (npm)
Registry modelGit repositories with mutable tagsImmutable package tarballs
Mutability riskTags can be force-pushed to different commitsPublished versions are permanently immutable
Audit toolingnpm audit cross-references semver, not SHAsFull compatibility with npm audit
Lockfile integrationN/Apackage-lock.json records integrity hashes
Human readability40-char hex strings obscure the actual versionVersion 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:

CheckFailure Condition
Lockfile existencedevcontainer-lock.json is absent from the repository
SHA-256 integrityA feature entry is missing resolved or integrity with sha256: prefix
Feature coverageA 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:

FlaggedReason
npm installResolves ranges from package.json, ignoring lockfile
npm iAlias for npm install
npm updateUpgrades to latest versions within semver ranges
npm install-testCombines 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, removing node_modules first
  • npm run: Executes a script defined in package.json
  • npm test: Alias for npm run test
  • npm audit: Reports known vulnerabilities without installing
  • npx: 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/*.yml and .github/workflows/*.yaml
  • .github/actions/**/*.yml and .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 checksum
  • shasum: macOS/BSD checksum utility
  • Get-FileHash: PowerShell checksum cmdlet
  • openssl dgst -sha256: OpenSSL digest
  • sha256sum -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 SeveritySARIF LevelTrigger
HigherrorUnpinned or mutable dependency reference
MediumwarningStale pinned version with available update
LownoteInformational findings

Running Locally

# Full scan with SARIF output
./scripts/security/Test-DependencyPinning.ps1

# Results appear in logs/dependency-pinning-results.json

🤖 Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.