Hands-On Labs · The Capstone · Start here

Secure a pipeline, stage by stage

Every earlier page in this course explained a control. This is where you install one. You'll take over Vulnerly — one small, deliberately vulnerable sample app and pipeline, seeded with the exact failure modes this course keeps coming back to — and lock it down across seven connected parts: threat model it, gate it with SAST and secrets scanning, add an SCA gate, harden and sign the container, scan its infrastructure-as-code and enforce policy, run DAST against a staging deploy, then ship compliance evidence and wire up monitoring. Each part hands the next one a pipeline that's a little more locked down than the one before, and nothing here is graded by a person — every part has an objective, checkable "done when."

☺ Explain it like I'm 10

Reading about a lock doesn't teach you to pick one, and reading about a good lock doesn't teach you to install one either. This is the part where you get the screwdriver out. You'll take a small app that was built with every door left unlocked on purpose, and go around the house yourself, room by room, until nothing is left open — and you'll know it worked because you'll have tried the door afterward, not because someone told you it was fine.

🦉Your host for this track: Professor Owl — he laid out the whole secure SDLC back in Foundations, and now hands each stage to the specialist who actually owns it: Rocky, Timmy, Ellie, Benny, Pip, Recon, Sol, Nutty, and Foxy. Nobody signs off a stage until its "done when" is actually true.
⚠ Before you start

Everything below is local and throwaway — no production system, no real customers, no cloud bill you didn't choose to run up. You'll want Docker (or Podman), Git, Node.js (LTS) with npm, a local Kubernetes cluster via kind (minikube works too — translate the commands yourself), kubectl, and a GitHub account for a real Actions pipeline (GitLab CI works the same way; adjust the YAML). Each part introduces its own scanner or CLI as you reach it — Part 2 doesn't need cosign yet, so don't install everything up front. If you provision the Part 5 infrastructure against a real cloud account rather than a sandbox like LocalStack, keep it to the smallest possible footprint and tear it down (terraform destroy) the moment you're done. Tear the cluster down too when you're finished: kind delete cluster --name vulnerly-dev.

The app you're locking down

☺ Like you're 10: One small app, built badly on purpose, so you have real locks to install instead of imaginary ones.

Vulnerly is a small Node.js/Express service backed by Postgres, shaped like the payments-reconciliation API from this course's own Ledgerly case study — a fintech-style service a handful of clients hit to check transaction status. It is not the real Ledgerly; that story already ended, six weeks and four new controls later. Vulnerly is a fresh, throwaway sandbox built specifically for this capstone, seeded with the same shapes of mistake Ledgerly's postmortem named, plus a few more, so you can find and fix them yourself instead of reading about someone else doing it. Every seeded weakness maps to exactly one capstone part:

vulnerly/                          # github.com/<you>/vulnerly
├── app/
│   ├── src/                       # Express routes + the Postgres client
│   ├── package.json               # a stale lodash/jsonwebtoken-class dependency, pinned on purpose
│   └── .env                       # a real-looking payments-processor key, committed on purpose
├── Dockerfile                     # FROM node:18, runs as root, nothing pinned — Part 4 rewrites this
├── infra/
│   └── main.tf                    # public-read S3 bucket + a 0.0.0.0/0 security group — Part 5
├── k8s/
│   └── deploy.yaml                # the Vulnerly Deployment + Service Parts 4–6 apply
└── .github/workflows/ci.yml       # a pipeline with zero required checks — Parts 2–6 each add one

The cluster is created once with kind create cluster --name vulnerly-dev and stays up for the whole capstone. It holds two namespaces: vulnerly for the app itself, and platform for the add-ons you'll install along the way — Kyverno in Part 4, DefectDojo in Part 7. Keep that split consistent and you'll never be confused about where something lives.

One app, seven stages — each closes exactly one seeded weakness 1 · Threat model closes: no trust boundary 2 · SAST + secrets closes: leaked key, SQLi 3 · SCA gate closes: vulnerable dependency 4 · Sign image closes: root, unsigned image 5 · IaC policy closes: public bucket + open SG 6 · DAST run closes: XSS, IDOR 7 · Compli- ance closes: no evidence, no watch Same app throughout — Part 3's fix has to survive Part 6's scan, and Part 7 proves all six earlier fixes are still true.

The seven stages

☺ Like you're 10: Seven checkboxes, each one a real lock you actually installed — not a description of one someone else installed.

Work them in order — Part 3's SCA gate assumes Part 2's scanning gates already exist in the pipeline, and Part 7's compliance evidence has nothing to collect if Parts 2 through 6 haven't produced any findings yet. Each row below is its own deep, standalone page with runnable manifests and config, not just prose — this hub just tracks whether you've cleared each one. Checking a box here only tracks your own progress; the actual proof lives in your terminal, your CI run, and your cluster.

0 / 7 capstone parts complete
1Threat model the app — 🦝 Rocky & 🦉 Professor Owl
Draw Vulnerly as a data-flow diagram — browser, the API, Postgres, and the S3 export job — and mark the trust boundary where the public internet meets the API. Walk each element through STRIDE and rank your top five threats. Use OWASP Threat Dragon to produce a real, saved model rather than a mental list.
Done when: a saved Threat Dragon model (or equivalent DFD) exists with at least five ranked threats, and the SQL-injection and IDOR paths you'll fix in Parts 2 and 6 both already appear in it — found by the model, not stumbled into later.
2Wire in SAST & secrets scanning — 🐢 Timmy & 🐘 Ellie
Add gitleaks (or TruffleHog) as a required pre-merge check so the committed .env gets caught. Add Semgrep (or CodeQL/SonarQube) as a required check flagging the string-concatenated SQL query. Fix both findings for real — move the payments key into HashiCorp Vault instead of just deleting the file, and rewrite the query to use parameters.
Done when: a fresh branch that reintroduces either the leaked key or the raw SQL string-concat is automatically rejected by CI — not flagged, rejected — and the fix, not a rotation, is what's in git log.
3Add an SCA gate — 🐢 Timmy
Run Trivy (or Snyk / OWASP Dependency-Check) against Vulnerly's dependency manifest — report-only first, exactly like Ledgerly staged its own SCA rollout, so you can triage the backlog before anything blocks a merge. Then flip the check to build-blocking on critical and high findings, and bump the seeded vulnerable dependency. Generate a CycloneDX SBOM with Syft on every build.
Done when: the pipeline fails on the seeded critical CVE, passes clean once the dependency is bumped, and an SBOM artifact is attached to every build — not just the ones that failed.
4Harden & sign the container — 🦫 Benny & 🐦 Pip
Rewrite the Dockerfile as a pinned, non-root, multi-stage build on a slim or distroless base. Scan the rebuilt image with Trivy or Grype until it's clean of criticals. Sign it keylessly with cosign/Sigstore, then add a Kyverno policy in the vulnerly namespace that rejects any image at admission that isn't signed.
Done when: kubectl apply of an unsigned test image is rejected by the policy, and the signed image deploys and runs cleanly as a non-root user.
5Scan IaC & enforce policy — 🤖 Recon & 🦥 Sol
Run Checkov (or tfsec) against infra/main.tf as a required check. Fix the public-read S3 ACL and the security group open to 0.0.0.0/0. Write an OPA/Conftest policy that blocks both patterns before terraform apply ever runs. If you provisioned against a real account or a LocalStack sandbox, run Prowler or ScoutSuite once to see the same gap Sol would find reviewing it by hand.
Done when: terraform plan for the original permissive config is blocked by the policy check, and the fixed plan applies with no open findings.
6Run DAST against staging — 🐢 Timmy & 🦝 Rocky
Deploy Vulnerly to a staging namespace on your kind cluster. Run an OWASP ZAP baseline scan, then a full active scan, against it. Confirm ZAP independently rediscovers the reflected XSS and the IDOR — the same two threats your Part 1 model already flagged. Fix both, then re-scan until the report comes back clean, or with only explicitly accepted findings, documented as such.
Done when: a ZAP report from before the fix shows the XSS and IDOR findings, and a re-scan after the fix shows neither.
7Ship compliance evidence & monitor — 🐿️ Nutty & 🦊 Foxy
Import the findings from Parts 2–6 into DefectDojo as one product, one engagement per stage. Write one InSpec (or OpenSCAP) profile checking two or three of the controls you just built — the container runs non-root, the bucket isn't public — against the real environment. Wire a Wazuh rule (or equivalent) that fires if a credential-shaped string ever shows up in a commit again, so Part 2's fix can't silently rot.
Done when: DefectDojo shows one product with findings rolled up from every earlier stage, your InSpec profile passes against the hardened environment and fails against a deliberately reverted one, and the monitoring rule fires on a test event you trigger yourself.
◆ Key idea

Part 1 comes before any tool runs, on purpose. A scanner can only find the shape of bug it already knows to look for; a threat model is what tells you the SQL-injection path and the IDOR are worth looking for in the first place, before Part 2's gate and Part 6's scan go find them for real. Skip the threat model and you're still going to find both — you're just going to find them the way Ledgerly found its leaked key: by accident, after the fact, from whoever got there first.

Why this order, and what "done" actually means

☺ Like you're 10: The order isn't arbitrary — each part needs the one before it to already be true, the same way you can't sign a container image before you've decided it's clean.

The seven parts trace this course's own maturity ladder, roughly the shape the DSOMM/SAMM/BSIMM page names explicitly: design-time analysis (Part 1) before build-time gates (Parts 2–3), build-time gates before deployment hardening (Parts 4–5), deployment hardening before runtime verification (Part 6), and none of it counts as a program rather than a one-time fix until it's evidenced and monitored (Part 7). Finish all seven and you'll have hands-on reps in every category the CDP exam tests — it's a hands-on, task-based exam itself, five live challenges against a running environment, no multiple choice — so this capstone is closer to exam rehearsal than a side exercise. Pair it with the CDP study plan for where it fits in a full prep schedule.

Once all seven are checked, run one more pass end to end: push a branch that reintroduces the leaked key, the raw SQL, the vulnerable dependency, an unsigned image, the public bucket, and the XSS — all at once, deliberately — and confirm the pipeline stops every single one before anything reaches staging. That single run is the real done-when for the whole capstone, not the seven checkboxes above; the checkboxes just track your own progress toward being ready to run it.

🎬 At the Shift-Left Squad
🦉

Professor Owl: Seven parts, one app, in order. Rocky opens with the threat model — everyone else is just closing what he finds.

🦝

Rocky: Found four things worth ranking in under an hour. The SQL string-concat and the IDOR are both in there — remember those, you'll need them again in Parts 2 and 6.

🐢

Timmy: And they won't get past me twice. SAST, secrets, SCA, then DAST at the end to check the whole thing from outside — I gate four of the seven parts myself.

🦫

Benny: I'll rebuild the image clean — non-root, pinned, distroless. Pip, you're not letting it run unsigned though, are you?

🐦

Pip: Not a chance. No signature, no admission. That's the whole point of Part 4.

🤖

Recon: The bucket and the security group are mine and Sol's. A policy that blocks them before apply beats a human noticing them after.

🐿️

Nutty: And when all six of you are done, I file the evidence — every finding, every fix, mapped to a control. A fix nobody can prove happened didn't happen, as far as an auditor's concerned.

✓ Checkpoint

1. What is Vulnerly, and why does this capstone use a fresh sandbox instead of just re-running the real Ledgerly incident? 2. Name the seven parts in order, and for each one, the single class of vulnerability it closes. 3. Why does Part 1 (threat modeling) have to come before Part 2's scanners run, rather than starting with tooling directly? 4. What two things both have to be true for Part 4 to count as "done"?

Check your answers
  1. Vulnerly is a small, deliberately vulnerable Node.js/Postgres sample app built specifically for this capstone, shaped like Ledgerly's own payments-reconciliation service but not the same system — Ledgerly's story already resolved, six weeks after its incident, with four new controls in place. Vulnerly exists so you can find and fix the same shapes of mistake yourself, hands-on, without touching (or contradicting) that already-finished story.
  2. 1) Threat model — closes an undocumented trust boundary. 2) SAST & secrets — closes a leaked key and a SQL injection. 3) SCA gate — closes a known-vulnerable dependency. 4) Harden & sign the container — closes a root, unsigned image. 5) IaC & policy — closes a public bucket and an open security group. 6) DAST — closes a reflected XSS and an IDOR. 7) Compliance & monitoring — closes the absence of evidence and of any alert that would catch a regression.
  3. A scanner only finds the shape of bug it's already configured to look for. The threat model is what identifies the SQL-injection and IDOR paths as worth looking for in the first place — without it, Parts 2 and 6 would still eventually surface both issues, but only by accident, the same way Ledgerly found its own leaked key: after the fact, from whoever got there first.
  4. Both must hold at once: an unsigned test image has to be actively rejected by the Kyverno admission policy, and the properly signed, hardened image has to deploy and run cleanly as a non-root user. Either one alone — a policy that blocks everything, or a clean image with no enforcement behind it — doesn't count.

Ready to start? Open Part 1 — Threat Model the App. Want the shorter, one-page version of every concept these seven parts put into practice first? Read the Ledgerly case study or the security checklist before you begin.