Why Static Sites Have Supply Chains
A static site generator produces HTML, CSS, and JavaScript from source templates and content. The output is simple. The build process is not.
A typical Eleventy or Hugo project depends on:
- The generator itself — an npm package with its own dependency tree
- Plugins and transforms — Markdown processors, image optimisers, syntax highlighters, each with their own dependencies
- Build tools — bundlers, minifiers, PostCSS, Sass compilers
- CI/CD runners — GitHub Actions, GitLab CI, Cloudflare Pages build environment
- Container images — if builds run in Docker, the base image and every layer it pulls
- Node.js runtime — the specific version used to execute the build
A fresh Eleventy project with a handful of plugins easily pulls in 200–400 npm packages. Each package is a potential point of compromise. The event-stream incident (2018), ua-parser-js (2021), colors and faker (2022), and xz-utils (2024) demonstrate that supply-chain attacks against build dependencies are not hypothetical — they are routine.
The fact that your output is static HTML does not protect you if your build process is compromised. A malicious dependency can inject scripts into your output, exfiltrate environment variables (including deployment credentials), or modify content in ways that are difficult to detect.
SBOM Minimum Elements: What CISA and NTIA Require
A Software Bill of Materials (SBOM) is an inventory of every component in your software. The CISA/NTIA "Minimum Elements for a Software Bill of Materials" (published 2021, updated guidance through 2025) defines the baseline fields that an SBOM must include:
Required Data Fields
For each component in the SBOM:
| Field | Description | Example |
|---|---|---|
| Supplier Name | Entity that created the component | @11ty |
| Component Name | Name of the component | @11ty/eleventy |
| Version | Version identifier | 3.0.0 |
| Unique Identifier | PURL, CPE, or SWID | pkg:npm/@11ty/[email protected] |
| Dependency Relationship | How this component relates to others | eleventy DEPENDS_ON markdown-it |
| Author of SBOM Data | Who generated the SBOM | wplus-ci-pipeline |
| Timestamp | When the SBOM was generated | 2026-04-19T04:49:00Z |
SBOM Formats
Two standard formats dominate:
- SPDX (ISO/IEC 5962:2021) — maintained by the Linux Foundation. Widely supported, JSON and tag-value formats.
- CycloneDX — maintained by OWASP. Designed specifically for security use cases. JSON and XML formats.
Both are acceptable. For npm-based projects, CycloneDX has slightly better tooling integration.
Generating SBOMs for npm Projects
# Using CycloneDX CLI tool
npm install -g @cyclonedx/cyclonedx-npm
cyclonedx-npm --output-file sbom.json --output-format json
# Using SPDX SBOM generator
npm install -g @spdx/sbom-generator
spdx-sbom-generator -o sbom.spdx.json
In CI (GitHub Actions example):
- name: Generate SBOM
run: |
npx @cyclonedx/cyclonedx-npm --output-file sbom.json --output-format json
- name: Upload SBOM as artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
- [ ] Generate an SBOM on every production build
- [ ] Store SBOMs as build artefacts alongside deployed output
- [ ] Include the SBOM in your deployment record (even if not publicly published)
SSDF Practices Adapted for Static Sites
The NIST Secure Software Development Framework (SSDF, SP 800-218) defines practices for secure development. Not all practices apply directly to static site projects, but the core principles do. Here is how to adapt the relevant practices:
PO.1: Define Security Requirements
For a static site, security requirements include:
- [ ] No inline JavaScript in output (enforceable via CSP)
- [ ] All third-party resources loaded with Subresource Integrity (SRI) hashes
- [ ] No secrets in the repository or build output
- [ ] Dependency versions pinned in lockfiles
- [ ] Build output is reproducible from the same inputs
Document these in a SECURITY.md or equivalent file in your repository.
PS.1: Protect All Forms of Code
- [ ] Require signed commits or enforce branch protection rules
- [ ] Protect the main branch — require pull-request reviews before merging
- [ ] Use repository secrets for deployment credentials — never commit them
- [ ] Enable secret scanning (GitHub, GitLab) to catch accidental exposure
- [ ] Restrict who can modify CI/CD configuration —
.github/workflows/files should require review
PW.4: Review and/or Analyse Code
For static sites, the most impactful code review focuses on:
- Changes to
package.jsonorpackage-lock.json— any new dependency or version change - Changes to build scripts and CI configuration
- Changes to templates that include external resources
- Changes to redirect rules and header configurations
Automate what you can. Use Dependabot or Renovate to flag dependency updates, but require human review before merging.
PW.6: Verify Third-Party Components
This is where supply-chain security becomes concrete:
# Run npm audit on every build
npm audit --audit-level=high
# If using a lockfile (and you should be), verify its integrity
npm ci # This verifies the lockfile matches package.json and checks integrity hashes
Integrate into CI:
- name: Install dependencies with integrity verification
run: npm ci
- name: Audit dependencies
run: npm audit --audit-level=high
continue-on-error: false # Fail the build on high-severity vulnerabilities
PW.7: Build Securely
- [ ] Use
npm ci, notnpm installin CI —npm ciinstalls from the lockfile exactly, verifying integrity hashes.npm installmay modify the lockfile. - [ ] Pin the Node.js version — specify in
.nvmrc,.node-version, orenginesinpackage.json. Use the same version in development and CI. - [ ] Pin CI runner versions — use specific action versions (
actions/[email protected], notactions/checkout@v4) to prevent supply-chain attacks through action updates. - [ ] Minimise CI permissions — use
permissions: read-allat the workflow level and grant write access only where needed. - [ ] Isolate the build environment — CI runners should not have access to production infrastructure beyond the deployment target.
Lockfile Integrity: The Foundation
The package-lock.json (npm) or yarn.lock (Yarn) file is the most critical supply-chain artefact in your project. It contains:
- Exact versions of every dependency (direct and transitive)
- Integrity hashes (SHA-512) for every package tarball
- Resolved URLs for every package
When npm ci runs, it verifies that the downloaded package tarballs match the integrity hashes in the lockfile. If a package has been tampered with on the registry, the hash will not match, and the install will fail.
Lockfile Hygiene
- [ ] Commit the lockfile — it must be in version control. Never
.gitignoreit. - [ ] Review lockfile changes in PRs — use
npm diffor GitHub's lockfile diff to understand what changed. - [ ] Regenerate periodically — delete
node_modulesandpackage-lock.json, runnpm install, verify the result, and commit. This catches cases where the lockfile has drifted frompackage.json. - [ ] Use
npm ciin CI, always — nevernpm install.
Detecting Lockfile Manipulation
The lockfile-lint package can enforce policies on your lockfile:
npx lockfile-lint --path package-lock.json --type npm \
--allowed-hosts npm \
--allowed-schemes "https:" \
--empty-hostname false
This verifies that:
- All packages resolve to the official npm registry
- All URLs use HTTPS
- No packages resolve to unexpected hosts
Add to CI:
- name: Lint lockfile
run: npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --allowed-schemes "https:" --empty-hostname false
CI Hardening
The CI environment is a high-value target. It has access to your source code, your deployment credentials, and the ability to publish content to your production site.
GitHub Actions Hardening Checklist
- [ ] Set default permissions to read-only:
permissions: read-all - [ ] Pin action versions to full SHA, not tags:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - [ ] Enable Dependabot for GitHub Actions to receive updates when pinned actions have security fixes.
- [ ] Use environment protection rules for production deployments — require approval, restrict to specific branches.
- [ ] Limit secret exposure — use
environmentsecrets scoped to specific deployment environments rather than repository-wide secrets. - [ ] Do not allow pull requests from forks to access secrets — this is the default, but verify it.
- [ ] Enable audit logging and review periodically.
Reproducible Builds
A reproducible build produces identical output from identical inputs, regardless of when or where it runs. This is the gold standard for supply-chain integrity — if you can reproduce the build independently and get the same output, you can be confident the build was not tampered with.
For static sites, reproducibility challenges include:
- Timestamps in output — many generators embed build timestamps. Configure them to use a fixed value (e.g., the Git commit timestamp).
- Non-deterministic ordering — if file processing order varies between runs, output may differ. Pin sort orders in your configuration.
- External data fetched at build time — API calls, remote data sources. Pin or cache these inputs.
- Floating dependency versions — solved by lockfile discipline (see above).
Verify reproducibility:
# Build once
npm run build
cp -r _site/ _site_first/
# Clean and build again
rm -rf _site/
npm run build
# Compare
diff -r _site_first/ _site/
If the output differs, investigate and fix the source of non-determinism.
Practical CI Pipeline Example
Here is a complete GitHub Actions workflow incorporating SSDF-aligned practices:
name: Build and Deploy
on:
push:
branches: [main]
permissions: read-all
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version-file: '.nvmrc'
- name: Install dependencies (integrity-verified)
run: npm ci
- name: Audit dependencies
run: npm audit --audit-level=high
- name: Lint lockfile
run: npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --allowed-schemes "https:" --empty-hostname false
- name: Build site
run: npm run build
- name: Generate SBOM
run: npx @cyclonedx/cyclonedx-npm --output-file _site/sbom.json --output-format json
- name: Upload build artefacts
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: site-output
path: |
_site/
_site/sbom.json
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
steps:
- name: Download build artefacts
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: site-output
path: _site/
- name: Deploy
run: |
# Your deployment command here
echo "Deploying to production"
Gotchas
npm audit has false positives. Not every reported vulnerability is exploitable in your context. A vulnerability in a development-only dependency's test suite does not affect your production output. Use npm audit --omit=dev to focus on production dependencies, but still review dev dependency issues periodically.
Pinning action SHAs is tedious but necessary. The tj-actions/changed-files incident (2025) demonstrated that a compromised GitHub Action can exfiltrate secrets from every repository using it. SHA pinning prevents the attacker from modifying the action after you reference it.
SBOM generation captures a point-in-time snapshot. If you deploy without generating a fresh SBOM, the artefact may not reflect the actual deployed dependencies. Generate the SBOM as part of every build, not as a separate periodic task.
Dev dependencies matter for build integrity. A compromised dev dependency (build tool, linter, test framework) can modify your build output even though it does not ship to production as a runtime dependency. Audit the full dependency tree, not just dependencies.
Container base images are dependencies too. If your CI uses a custom Docker image, the packages in that image are part of your supply chain. Pin image digests, not tags, and rebuild periodically to incorporate security updates.
Verification Checklist
After implementing these practices, verify:
- [ ]
npm ciis used in CI (notnpm install) - [ ]
npm auditruns on every build and fails on high-severity issues - [ ] Lockfile is committed and reviewed in pull requests
- [ ] CI action versions are pinned to full SHAs
- [ ] Workflow permissions default to read-only
- [ ] SBOM is generated and stored with every production build
- [ ] Deployment secrets are scoped to production environments
- [ ] Secret scanning is enabled on the repository
- [ ] Branch protection rules require PR review for main branch
- [ ] Build output is reproducible (verified by building twice and comparing)
- [ ]
SECURITY.mddocuments the project's security requirements and practices
Further Reading
- NTIA Minimum Elements for a Software Bill of Materials
- NIST SP 800-218: Secure Software Development Framework
- CycloneDX SBOM Standard
- SPDX Specification
- Tamper-Evident Downloads — signing and verifying distributed artefacts
- Security hub — all security articles
- Infrastructure hub — hosting and infrastructure resources