---
title: Dependency Pinning
description: How HVE Core enforces dependency pinning across GitHub Actions, npm, pip, and shell downloads with automated CI validation
sidebar_position: 3
author: Microsoft
ms.date: 2026-06-08
ms.topic: concept
keywords:
  - dependency pinning
  - supply chain security
  - npm
  - pip
  - github actions
estimated_reading_time: 8
---

## 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:

```json
{
  "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

```text
^[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.

```yaml
# 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.

## 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](https://github.com/devcontainers/spec/blob/main/docs/specs/devcontainer-lockfile.md), 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:

```json
{
  "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 Pinning

Python dependencies must use the `==` operator for exact version pinning. The scanner excludes virtual environment directories (`.venv`, `venv`, `.tox`, `.nox`, `__pypackages__`) to avoid false positives from installed package metadata.

```text
# Accepted
requests==2.31.0
flask==3.0.0

# Rejected
requests>=2.31.0
flask~=3.0
```

## 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, 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:

```yaml
# 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`.

## 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

```bash
# 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.

```mermaid
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

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

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

## Related Resources

* [Security Model](security-model.md): Supply chain threats S-1, S-2, SC-1, SC-4, and SC-6
* [Branch Protection](../contributing/branch-protection.md): 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.*
