microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
docs/security/dependency-pinning.md
208lines · modecode
| 1 | --- |
| 2 | title: Dependency Pinning |
| 3 | description: How HVE Core enforces dependency pinning across GitHub Actions, npm, pip, and shell downloads with automated CI validation |
| 4 | sidebar_position: 3 |
| 5 | author: Microsoft |
| 6 | ms.date: 2026-03-02 |
| 7 | ms.topic: concept |
| 8 | keywords: |
| 9 | - dependency pinning |
| 10 | - supply chain security |
| 11 | - npm |
| 12 | - pip |
| 13 | - github actions |
| 14 | estimated_reading_time: 8 |
| 15 | --- |
| 16 | |
| 17 | ## Overview |
| 18 | |
| 19 | 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. |
| 20 | |
| 21 | | Dependency Type | Pinning Strategy | Example | |
| 22 | |-----------------------|------------------------------|-------------------------------------------------------------| |
| 23 | | GitHub Actions | Full 40-character commit SHA | `actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29` | |
| 24 | | npm | Exact version (no ranges) | `"eslint": "9.18.0"` | |
| 25 | | pip | Exact version with `==` | `requests==2.31.0` | |
| 26 | | Workflow npm commands | `npm ci` enforcement | `npm ci` instead of `npm install` | |
| 27 | | Shell downloads | Checksum verification | `sha256sum --check` after download | |
| 28 | |
| 29 | ## npm: Exact-Version Enforcement |
| 30 | |
| 31 | > [!NOTE] |
| 32 | > 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. |
| 33 | |
| 34 | ### What Is Validated |
| 35 | |
| 36 | The scanner rejects any version string that contains range operators or wildcards: |
| 37 | |
| 38 | ```json |
| 39 | { |
| 40 | "dependencies": { |
| 41 | "valid": "9.18.0", |
| 42 | "invalid-caret": "^9.18.0", |
| 43 | "invalid-tilde": "~9.18.0", |
| 44 | "invalid-wildcard": "9.*", |
| 45 | "invalid-range": ">=9.0.0 <10.0.0" |
| 46 | } |
| 47 | } |
| 48 | ``` |
| 49 | |
| 50 | ### Validation Regex |
| 51 | |
| 52 | ```text |
| 53 | ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$ |
| 54 | ``` |
| 55 | |
| 56 | 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. |
| 57 | |
| 58 | ### Why Not SHA-Pinning for npm |
| 59 | |
| 60 | | Criterion | SHA-Pinning (GitHub Actions) | Exact-Version (npm) | |
| 61 | |----------------------|------------------------------------------------|----------------------------------------------| |
| 62 | | Registry model | Git repositories with mutable tags | Immutable package tarballs | |
| 63 | | Mutability risk | Tags can be force-pushed to different commits | Published versions are permanently immutable | |
| 64 | | Audit tooling | `npm audit` cross-references semver, not SHAs | Full compatibility with `npm audit` | |
| 65 | | Lockfile integration | N/A | `package-lock.json` records integrity hashes | |
| 66 | | Human readability | 40-char hex strings obscure the actual version | Version is self-documenting | |
| 67 | |
| 68 | ## GitHub Actions: SHA Pinning |
| 69 | |
| 70 | 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. |
| 71 | |
| 72 | ```yaml |
| 73 | # Rejected: mutable tag reference |
| 74 | - uses: actions/checkout@v4 |
| 75 | |
| 76 | # Accepted: immutable SHA reference |
| 77 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.2.2 |
| 78 | ``` |
| 79 | |
| 80 | The scanner validates that the SHA is a real 40-character hexadecimal string and optionally checks staleness against the GitHub API. |
| 81 | |
| 82 | ## pip: Exact-Version Pinning |
| 83 | |
| 84 | 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. |
| 85 | |
| 86 | ```text |
| 87 | # Accepted |
| 88 | requests==2.31.0 |
| 89 | flask==3.0.0 |
| 90 | |
| 91 | # Rejected |
| 92 | requests>=2.31.0 |
| 93 | flask~=3.0 |
| 94 | ``` |
| 95 | |
| 96 | ## Workflow npm Commands: npm ci Enforcement |
| 97 | |
| 98 | 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. |
| 99 | |
| 100 | ### Detected Commands |
| 101 | |
| 102 | The scanner inspects `run:` blocks in workflow YAML with indentation-aware parsing and flags these commands: |
| 103 | |
| 104 | | Flagged | Reason | |
| 105 | |--------------------|--------------------------------------------------------| |
| 106 | | `npm install` | Resolves ranges from `package.json`, ignoring lockfile | |
| 107 | | `npm i` | Alias for `npm install` | |
| 108 | | `npm update` | Upgrades to latest versions within semver ranges | |
| 109 | | `npm install-test` | Combines install and test in a non-deterministic way | |
| 110 | |
| 111 | ### Safe Commands |
| 112 | |
| 113 | These npm commands are not flagged because they do not modify the dependency tree: |
| 114 | |
| 115 | * `npm ci`: Installs exactly from the lockfile, removing `node_modules` first |
| 116 | * `npm run`: Executes a script defined in `package.json` |
| 117 | * `npm test`: Alias for `npm run test` |
| 118 | * `npm audit`: Reports known vulnerabilities without installing |
| 119 | * `npx`: Runs a package binary without modifying dependencies |
| 120 | |
| 121 | ### Remediation |
| 122 | |
| 123 | Replace flagged commands with `npm ci` for deterministic, lockfile-based installs: |
| 124 | |
| 125 | ```yaml |
| 126 | # Rejected: resolves version ranges from the registry |
| 127 | - run: npm install |
| 128 | |
| 129 | # Accepted: installs exactly what the lockfile specifies |
| 130 | - run: npm ci |
| 131 | ``` |
| 132 | |
| 133 | ### File Scope |
| 134 | |
| 135 | The scanner processes files matching `.github/workflows/*.yml` and `.github/workflows/*.yaml`. |
| 136 | |
| 137 | ## Shell Downloads: Checksum Verification |
| 138 | |
| 139 | Shell scripts that download files from the internet must verify checksums to prevent tampered or corrupted binaries from entering the build environment. |
| 140 | |
| 141 | ### Detection |
| 142 | |
| 143 | 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. |
| 144 | |
| 145 | ### Accepted Verification Commands |
| 146 | |
| 147 | Any of these patterns within five lines of the download satisfies the check: |
| 148 | |
| 149 | * `sha256sum`: GNU coreutils checksum |
| 150 | * `shasum`: macOS/BSD checksum utility |
| 151 | * `Get-FileHash`: PowerShell checksum cmdlet |
| 152 | * `openssl dgst -sha256`: OpenSSL digest |
| 153 | * `sha256sum -c`: Checksum file verification |
| 154 | |
| 155 | ### Examples |
| 156 | |
| 157 | ```bash |
| 158 | # Accepted — checksum verified immediately after download |
| 159 | curl -Lo tool.tar.gz https://example.com/tool-v1.0.tar.gz |
| 160 | echo "abc123... tool.tar.gz" | sha256sum --check |
| 161 | |
| 162 | # Rejected — no checksum verification after download |
| 163 | wget https://example.com/tool-v1.0.tar.gz |
| 164 | tar xzf tool-v1.0.tar.gz |
| 165 | ``` |
| 166 | |
| 167 | ### Scanned Files |
| 168 | |
| 169 | The scanner processes files matching `.devcontainer/scripts/*.sh` and `scripts/*.sh`, excluding fixture directories used in tests. |
| 170 | |
| 171 | ## CI Integration |
| 172 | |
| 173 | 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. |
| 174 | |
| 175 | ```mermaid |
| 176 | flowchart LR |
| 177 | A[CI Trigger] --> B[Test-DependencyPinning.ps1] |
| 178 | B --> C{Violations?} |
| 179 | C -->|None| D[✅ Pass] |
| 180 | C -->|Found| E[SARIF Report] |
| 181 | E --> F[GitHub Code Scanning] |
| 182 | ``` |
| 183 | |
| 184 | ### Severity Mapping |
| 185 | |
| 186 | | Scanner Severity | SARIF Level | Trigger | |
| 187 | |------------------|-------------|--------------------------------------------| |
| 188 | | High | `error` | Unpinned or mutable dependency reference | |
| 189 | | Medium | `warning` | Stale pinned version with available update | |
| 190 | | Low | `note` | Informational findings | |
| 191 | |
| 192 | ### Running Locally |
| 193 | |
| 194 | ```powershell |
| 195 | # Full scan with SARIF output |
| 196 | ./scripts/security/Test-DependencyPinning.ps1 |
| 197 | |
| 198 | # Results appear in logs/dependency-pinning-results.json |
| 199 | ``` |
| 200 | |
| 201 | ## Related Resources |
| 202 | |
| 203 | * [Threat Model](threat-model.md): Supply chain threats S-1, S-2, SC-1, SC-4, and SC-6 |
| 204 | * [Branch Protection](../contributing/branch-protection.md): Required status checks including dependency pinning |
| 205 | |
| 206 | --- |
| 207 | |
| 208 | 🤖 *Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.* |
| 209 | |