Here’s a fact that reframes how you think about security: the code you wrote is a small fraction of the code running in your application. A typical modern service is maybe ten percent your logic and ninety percent dependencies — frameworks, libraries, utilities — each of which pulls in its own dependencies, often hundreds deep. Every one of those packages runs with your application’s privileges. Which means you inherit the security of everything you import, from code written by people you’ve never met. This is supply-chain security, and it has moved from a niche concern to one of the most actively exploited attack surfaces there is.
You Inherit the Security of Everything You Import
Let’s make the scale vivid. You npm install or pip install a handful of direct dependencies. Each of those has dependencies, which have dependencies — the transitive tree. It’s routine for a modest project to end up with hundreds or thousands of packages, the vast majority of which you’ve never heard of and will never read.
Attackers know this is a soft spot, and they get in through a few well-worn routes:
- Typosquatting — publishing a malicious package with a name one typo away from a popular one (
reqeustsvsrequests,python-sqlitevs the real thing). A single fat-fingered install pulls in malware. Package registries are full of these. - Compromised maintainer — a legitimate, widely-used package gets a malicious update, because the maintainer’s account was phished, or they handed the project to a bad actor, or a malicious “helpful” contributor got commit rights. This has happened repeatedly to very popular packages, and because you trust that package, the bad version flows straight into your build.
- Dependency confusion — if your company uses an internal package name (
acme-auth) that also exists (or an attacker creates it) on the public registry, your package manager may fetch the public one — the attacker’s — instead of your private one. This trick has breached major companies. - Build-system compromise — the attacker doesn’t touch your code or dependencies at all; they compromise the CI/CD pipeline that builds and ships you, injecting malicious code into the final artifact. The infamous large-scale supply-chain incidents often took this form, because the build system is trusted implicitly and sits upstream of everyone who deploys the result.
The common thread: trust flows downhill, and attackers inject themselves upstream of that trust. So your defense has to extend beyond your own code to everything you depend on and everything that builds you.
Hardening the Pipeline
The good news is that most supply-chain defenses are concrete, automatable steps that fit neatly into your CI/CD pipeline — the place where code becomes a running artifact. Put the checks there and every change is verified without anyone remembering to.
- Pin and lock dependencies. Use a lockfile (
package-lock.json,poetry.lock,go.sum) that records exact versions and hashes, so every build gets the identical, verified bytes — no silent upgrade can slip a malicious version in. Commit the lockfile. Avoid floating ranges (^1.2.0) for anything sensitive. Hash-pinning in particular defeats a registry serving you different content than you reviewed. - Scan dependencies for known vulnerabilities (SCA — Software Composition Analysis). Tools like Dependabot, Trivy, or Snyk check your dependency tree against vulnerability databases on every pull request and flag (or auto-PR) anything with a known CVE. This catches the “three-years-out-of-date library with a public exploit” problem, which is one of the most common real breach causes.
- Scan your own code (SAST — Static Application Security Testing). Tools like Semgrep or CodeQL read your source and flag dangerous patterns — string-built SQL (Lesson 4), hardcoded secrets (Lesson 7), unsafe deserialization — before they merge. They’re not perfect (false positives happen), but they catch a lot cheaply.
- Generate an SBOM (Software Bill of Materials). An SBOM is a complete inventory of everything in your build — every package and version. Its value becomes obvious the next time a big CVE drops (think Log4Shell): instead of a frantic manual hunt, you query your SBOMs and know in minutes exactly which services are affected. It turns “are we vulnerable?” from a day of panic into a quick lookup.
- Run CI with least privilege. Your pipeline is powerful and a juicy target (it can deploy to production), so treat it like production. Use scoped, short-lived tokens rather than broad long-lived credentials, protect your main branches (require review, no force-push), keep secrets out of build logs, and don’t let untrusted pull requests run privileged jobs with access to your secrets.
- Sign your artifacts. Cryptographically sign your build outputs (container images, binaries) — using something like Sigstore/cosign — so your deployment step can verify it’s running exactly what your pipeline produced, not something swapped in later. This closes the loop against build- and registry-tampering (and uses the signing from Lesson 6).
The Mindset: Dependencies Are a Standing Decision
Beyond the tools, a couple of habits of judgment matter:
- Evaluate before you add. Each dependency is a long-term trust relationship. Before pulling in a package, glance at whether it’s maintained, widely used, and reasonably scoped. Do you need a whole library for something you could write in ten lines? Fewer dependencies is less attack surface.
- Keep them updated — but not blindly. Out-of-date dependencies accumulate known vulnerabilities, so patch regularly (automated PRs help). At the same time, a brand-new release could be the compromised one, so pinning + review gives you a beat to notice. The balance is “stay current, but through a reviewed, locked process,” not “auto-upgrade everything instantly” and not “never touch it.”
- Treat the pipeline as production. The CI system deploys your code; a compromise of it is a compromise of everything. It deserves the same least-privilege, auditing, and care as your most sensitive service.
Supply-chain security can feel daunting because the surface is so large, but the practical version is encouraging: turn on dependency scanning, commit your lockfiles, generate SBOMs, lock down your pipeline’s permissions, and sign your builds. Each is a one-time setup that then runs automatically on every change — exactly the “shift left” idea we’ll formalise in Lesson 12.
Pitfalls to Avoid
- Ignoring transitive dependencies. Most of your code is indirect dependencies. Scan the whole tree, not just your direct imports.
- Floating versions, no lockfile. Invites a silent malicious upgrade. Pin exact versions with hashes and commit the lockfile.
- No dependency scanning. Out-of-date libraries with public exploits are a top breach cause. Run SCA (Dependabot/Trivy/Snyk) on every PR.
- No SBOM. When the next big CVE lands you’ll be hunting manually. Generate SBOMs so you can answer “are we affected?” in minutes.
- An over-privileged pipeline. CI can deploy to prod; a broad, long-lived token there is catastrophic if leaked. Use scoped, short-lived credentials and protected branches.
- Unsigned artifacts. Without signing, you can’t prove you’re deploying what you built. Sign builds and verify at deploy.
- Adding dependencies carelessly. Each one is standing attack surface and a trust relationship. Evaluate necessity and maintenance before adding.
Quick Recap
- Your app is mostly other people’s code; you inherit the security of every dependency, hundreds deep.
- Attacks enter via typosquatting, compromised maintainers, dependency confusion, and build-system compromise — upstream of your trust.
- Harden the CI/CD pipeline: pin + lock with hashes, scan dependencies (SCA) and code (SAST) on every PR, generate SBOMs, run CI with least privilege, and sign artifacts.
- Treat dependencies as a standing decision (evaluate before adding, update through a reviewed locked process) and the pipeline as production.
Next lesson: zooming out to the infrastructure your code runs on — cloud IAM, the blast-radius decision behind the most common cloud mistake, and defense in depth.