Supply Chain Security — The Attacks Developers Ignore
SolarWinds. Log4Shell. event-stream. Learn why supply chain attacks succeed, how to detect vulnerable dependencies in .NET and npm, generate SBOMs, and implement SLSA build provenance.
Why Supply Chain Attacks Succeed
Your code is a small fraction of what runs in production. A typical .NET application pulls in 200-500 NuGet packages. A Node.js app can have 1,000+ npm packages. You wrote none of them. You trust all of them implicitly.
Three attacks that changed the industry:
SolarWinds (2020) — Attackers compromised SolarWinds build pipeline and inserted malicious code into the Orion software update. 18,000 organisations installed the update. The malicious code was signed by SolarWinds own certificate. Traditional antivirus and firewalls were useless because the code came from a trusted vendor.
Log4Shell (2021) — A critical RCE vulnerability in Log4j 2, a Java logging library used in thousands of products. Most teams did not know they were using it because it was a transitive dependency — a dependency of a dependency of a dependency. Finding it required scanning the entire dependency tree, not just direct dependencies.
event-stream (2018) — A maintainer of a popular npm package transferred ownership to an attacker. The attacker added a malicious transitive dependency that stole cryptocurrency wallet credentials. The package had 2 million downloads per week.
The pattern in all three: the attack entered through code you trust but do not control.
The Transitive Dependency Explosion
Direct dependencies are the packages you explicitly reference. Transitive dependencies are everything those packages pull in. The ratio is typically 1:10 to 1:50.
# See your full .NET dependency tree including transitive packages
dotnet list package --include-transitive
# See only packages with known vulnerabilities
dotnet list package --vulnerable
# Include transitive vulnerabilities — the Log4Shell scenario
dotnet list package --vulnerable --include-transitive# npm equivalent
npm audit
# Detailed JSON report
npm audit --json | jq '.vulnerabilities | to_entries[] | {name: .key, severity: .value.severity}'Run these in CI on every pull request and fail the build if high or critical vulnerabilities are found.
OWASP Dependency-Check
OWASP Dependency-Check scans your project against the NVD (National Vulnerability Database) and generates an HTML or JSON report:
# Install and run on a .NET project
dotnet tool install --global dotnet-dependency-check
dependency-check --project "MyAPI" --scan ./src --format HTML --out ./reportsAdd it to GitHub Actions:
- name: Run OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: "MyAPI"
path: "."
format: "HTML"
args: "--failOnCVSS 7"GitHub Dependabot and Renovate
Dependabot (built into GitHub) automatically opens PRs when a dependency has a known vulnerability or a newer version is available:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
microsoft-packages:
patterns: ["Microsoft.*", "System.*"]
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"Renovate (open-source, more configurable) supports auto-merge for patches:
{
"extends": ["config:base"],
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"automerge": false,
"reviewers": ["security-team"]
}
]
}Pinning vs Ranges
Version ranges (^1.2.3, ~1.2.3) allow automatic updates but also allow a compromised new version to enter your build silently. Pinning (1.2.3) locks the exact version but requires explicit updates.
For production applications, pin your direct dependencies. Use lock files to pin transitive dependencies.
<!-- Enable lock files in .NET — add to your .csproj -->
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<!-- In CI, fail if lock file is out of date -->
<RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
</PropertyGroup>In CI, RestoreLockedMode=true makes the build fail if anyone added a package without committing the updated lock file. This prevents dependency confusion attacks.
SBOM with CycloneDX for .NET
An SBOM (Software Bill of Materials) is a complete inventory of every component in your software — name, version, license, vulnerabilities. It is to software what an ingredients list is to food.
# Install CycloneDX .NET tool
dotnet tool install --global CycloneDX
# Generate SBOM in CycloneDX JSON format
dotnet CycloneDX ./MyAPI.sln -o ./sbom -j
# The output sbom/bom.json lists every NuGet package with:
# - name, version, purl (package URL)
# - license
# - known vulnerabilities cross-referenced from NVDPublish your SBOM as a build artifact attached to your GitHub release. The US Executive Order on Cybersecurity (EO 14028) mandates SBOMs for software sold to federal agencies. Enterprise customers increasingly require them for procurement.
Signed Commits and Tags (GPG)
Signing your commits and release tags with GPG provides proof that a commit came from a specific person and was not tampered with after signing:
# Generate a GPG key
gpg --full-generate-key
# Configure git to use your key
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
# Sign a release tag
git tag -s v1.2.3 -m "Release 1.2.3"
git push origin v1.2.3
# Verify a tag
git tag -v v1.2.3In GitHub, verified (signed) commits show a Verified badge. Require signed commits on protected branches via branch protection rules.
SLSA Build Provenance
SLSA (Supply chain Levels for Software Artifacts) is a framework for build provenance — cryptographic proof of how a software artifact was built. Four levels:
| Level | Requirements | |-------|-------------| | SLSA 1 | Build process is documented, provenance is generated | | SLSA 2 | Signed provenance, hosted build service (GitHub Actions) | | SLSA 3 | Hardened build platform, non-falsifiable provenance | | SLSA 4 | Two-person review, hermetic builds, reproducible |
GitHub Actions provides SLSA 3 provenance with the slsa-framework/slsa-github-generator action:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- uses: actions/checkout@v4
- name: Build
run: dotnet publish -c Release -o publish
- name: Generate hash
id: hash
run: echo "hashes=$(sha256sum publish/* | base64 -w0)" >> $GITHUB_OUTPUT
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"Consumers verify the provenance with slsa-verifier before using your artifact.
When a Vulnerability Is in a Transitive Dependency You Do Not Control
This is the Log4Shell scenario. You do not depend on the vulnerable library directly — some library you use does. Your options in order of preference:
- Update the direct dependency — The library maintainer has likely released a patched version already.
- Override the transitive dependency version — In NuGet, add a direct reference at the safe version:
<!-- Override a transitive dependency version in .csproj -->
<ItemGroup>
<PackageReference Include="VulnerableTransitivePackage" Version="2.1.0" />
</ItemGroup>- Apply a virtual patch — WAF rules that block the exploit pattern buy time while you find a permanent fix.
- Fork and patch — Last resort. Fork the direct dependency, apply the patch, reference your fork. Set a reminder to remove the fork when upstream is fixed.
- Remove the feature — If the vulnerable component is only needed for a non-critical feature, disable or remove the feature.
Your Weekly Supply Chain Checklist
- Run dotnet list package --vulnerable --include-transitive in CI on every PR
- Review Dependabot and Renovate PRs within 48 hours for security updates
- Generate and publish an SBOM on every release
- Enable RestoreLockedMode in CI to enforce lock files
- Sign release tags with GPG
- Check that no package references point to private registries that could be hijacked (dependency confusion attacks target private package names that are also registered publicly)
- Review permissions granted to GitHub Actions workflows — least privilege applies to CI too
Supply chain security is not a one-time audit. It is a continuous practice integrated into your development workflow.
Enjoyed this article?
Explore the Security & Compliance learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.