Run it and map how it could be attacked
Somewhere in this repo is an IDOR, a query you can inject, a secret sitting in git history, and a container running wider open than it should.
What you'll build
You end this rung with Ledgerline running locally under docker compose, two test accounts you control, a committed security/attack-surface.md route inventory, and a security/threat-model.md that names assets, trust boundaries, a STRIDE pass, an OWASP Top 10 2021 mapping, and a ranked top-risks list, including one IDOR you personally reproduced with curl against your own instance.
See how we teach, before you sign up
You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:
version: "3.9"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ledgerline
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ledgerline
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://ledgerline:${DB_PASSWORD}@postgres:5432/ledgerline
JWT_SECRET: ${JWT_SECRET}
depends_on:
- postgres
volumes:
pgdata:Reading this file
image: postgres:16-alpinePinned tag, not latest, so your local rebuilds stay reproducible.POSTGRES_PASSWORD: ${DB_PASSWORD}Comes from your local .env, never commit a real value here.JWT_SECRET: ${JWT_SECRET}The signing secret for auth tokens, treat it as sensitive from day one, even in dev.depends_on:The app waits for the postgres container to start, but not for it to be ready to accept queries, that gap is a common first-boot ECONNREFUSED.
Boots the two-container local stack you use for every milestone in this rung.
That's 1 of 9 explained code blocks in this single project.
The build, milestone by milestone
- 1
Boot it and become two test users
5 guided stepsYou cannot threat model an app you have only read. Using it once as a real customer, with two separate identities, is what lets you later recognize a boundary crossing when you cause one on purpose.
- 2
Map the attack surface before touching a scanner
6 guided stepsA scanner finds instances of known bug classes. Before you run one, you need your own map of where those classes could live, in your own words, tied to real routes, so a scan result later means something to you instead of being a number to trust blindly.
- 3
STRIDE the real routes, not the app in the abstract
5 guided stepsSTRIDE gives you a checklist so you do not just rate the app on vibes. Applied per route it turns a vague worry into a specific, arguable claim you can defend or later disprove with tooling.
- 4
Map each finding to its real OWASP Top 10 2021 category
5 guided stepsSTRIDE tells you what kind of threat it is. The OWASP name ties it to an industry-standard vocabulary that a reviewer, an auditor, or the tooling you add in later rungs (Semgrep, ZAP) will also speak.
- 5
Write threat-model.md as a living document
6 guided stepsA threat model that lives in your head, or gets written once and never touched again, is worthless by rung 4. This document is the one artifact that survives across all 12 rungs, every later rung edits it instead of starting fresh.
What's inside when you start
You'll walk away with
This is portfolio-grade. Build it free.
Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.
Start building