Threat Model a Microservices App
Build a STRIDE threat model and automated DevSecOps security pipeline.
Introduction
30 Second Summary
Every app you use handles sensitive data. Your bank, your shopping cart, your health records. Companies desperately need people who can find weaknesses in these systems before attackers do.
In this project, you will build a professional threat model for a microservices e-commerce app, then create an automated security pipeline that catches vulnerabilities on every code push.
What You'll Build
A complete STRIDE threat model and automated DevSecOps pipeline for a fictional e-commerce microservices application called ShopSecure.
By the end of this project, you'll have:
- A Data Flow Diagram with trust boundaries and a STRIDE-per-element threat enumeration document that maps attacker risks to each component of the system.
- A working GitHub Actions security pipeline running Semgrep (SAST) and Gitleaks (secret detection) that blocks vulnerable code from merging.
- A vulnerability triage report with SLA-based remediation plans and a supply chain risk analysis covering real-world attack patterns.
- Secret Mission: Write a custom Semgrep rule that detects a vulnerability pattern specific to your threat model findings.
Are there any prerequisites?
You need basic familiarity with GitHub and VS Code. No security experience is required.
Before We Start
Before you can threat model a system or build a security pipeline, you need the right tools on your machine.
In this step, you will verify that Git is working, install Node.js for the vulnerable sample app, and create a GitHub repository that will hold your threat model documentation and security pipeline.
In this step, get ready to:
- Verify Git is installed on your machine.
- Install Node.js for the vulnerable sample app.
- Create and clone your GitHub repository.
Verify Git is installed
Git tracks every change you make to your threat model and pipeline code. It also powers GitHub Actions, which will run your security scans automatically.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check if Git is installed by running this command:
git --version
You should see a version number printed, such as git version 2.54.0.
✔️ I see a version number
Git is ready to go. Move on to the next substep.
ⓧ Command not found
Git is not installed yet. Follow the instructions below for your operating system.
macOS
- Install Git by running this command:
xcode-select --install
A dialog will appear asking you to install the Xcode Command Line Tools. Click Install and wait for it to finish.
- After installation completes, verify Git by running:
git --version
Windows
- Download the Git for Windows installer from git-scm.com/download/win.
- Run the installer and follow the Git Setup wizard. Accept the default options at each screen.
- After installation, close and reopen Command Prompt.
- Verify Git by running:
git --version
Install and verify Node.js
Node.js is the JavaScript runtime you will use to run the intentionally vulnerable sample app later in this project.
- Check if Node.js is already installed by running this command:
node --version
You should see a version number printed, such as v24.16.0.
✔️ I see a version number
Node.js is ready. Move on to the next substep.
ⓧ Command not found
Node.js is not installed yet. Follow the steps below.
- Go to the official Node.js download page at nodejs.org/en/download.
- Download the LTS version for your operating system.
- Run the installer and accept the default options.
- After installation, close and reopen your terminal.
- Verify Node.js by running:
node --version
You should now see a version number printed.
Still not seeing a version number?
Make sure you closed and reopened your terminal after installation. New installs only appear on the system PATH after restarting the terminal session.
help me troubleshoot my Node.js installation
Create your GitHub repository
Your GitHub repository will store your threat model documents, vulnerable sample app, and the GitHub Actions security pipeline.
- Go to github.com and sign in to your account.
- Click the + icon in the top-right corner of the page.
- Select New repository.
- Enter appsec-threat-model-lab as the Repository name.
- Leave the repository set to Public.
- Check the box for Add a README file.
- Click Create repository.
Why public?
Public repositories on GitHub get unlimited free GitHub Actions minutes. Since you will run a security pipeline in this project, keeping it public avoids any minute caps.
Now clone the repository to your local machine so you can work on it in VS Code.
- On your new repository page, click the green Code button.
- Make sure HTTPS is selected, then click the copy icon to copy the URL.
- Switch back to your terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone the repository by running this command:
git clone [[REPO_URL="https://github.com/YOUR_USERNAME/appsec-threat-model-lab.git"]]
- Move into the cloned folder by running:
cd appsec-threat-model-lab
- Confirm the files exist by running:
ls
You should see README.md in the output.
Seeing a permission or authentication error?
If Git asks for a username and password, you may need to authenticate with a personal access token instead of a password. GitHub no longer accepts passwords for HTTPS Git operations.
help me authenticate Git with GitHub
Finally, open the project in VS Code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the appsec-threat-model-lab folder, then click Open.
You should see the appsec-threat-model-lab folder name in the VS Code Explorer sidebar, with README.md listed inside it.
What is VS Code?
VS Code is a code editor where you will write your threat model documents, vulnerable sample app, and pipeline configuration files. The Explorer sidebar on the left shows your project's file structure.
Your workspace is ready. Next up, you will map the ShopSecure e-commerce architecture using a Data Flow Diagram.
Map the System Architecture
Before you can identify where attackers might strike, you need a clear picture of how data moves through the system. Security threats live at the boundaries between components, and a diagram makes those boundaries visible.
In this step, you'll create a Data Flow Diagram (DFD) of the ShopSecure e-commerce application. A DFD maps every process, data store, and external entity in the system, along with the data flowing between them.
In this step, get ready to:
- Create a Level 1 Data Flow Diagram of ShopSecure in draw.io.
- Identify and mark trust boundaries where security controls must exist.
- Export and commit the DFD to your repository.
Create a Level 1 Data Flow Diagram in draw.io
A Level 1 DFD shows the major processes in your system, how they exchange data, and which components are external. This becomes the foundation for your STRIDE threat analysis in the next step.
- Open draw.io in your browser by navigating to:
- https://app.diagrams.net
- When the storage dialog appears, click Device to save files locally.
- Click Create New Diagram.
- Select Blank Diagram and name it shopsecure-dfd.
- Click Create.
What is a Data Flow Diagram?
A DFD uses four types of elements to represent a system. Processes (rounded rectangles or circles) transform data. Data stores (parallel lines or open-ended rectangles) hold data at rest. External entities (rectangles) are actors outside your system boundary. Data flows (arrows) show how information moves between elements.
Now you'll add each element of the ShopSecure architecture to the canvas. The system has three processes, one data store, and two external entities.
- Add the external entities first. Drag a Rectangle shape from the left panel onto the canvas.
- Double-click the rectangle and type React Frontend.
- Add a second rectangle and label it Stripe (Payment Processor).
- Place the React Frontend on the left side of the canvas and Stripe on the right side.
- Add the processes next. Drag a Rounded Rectangle (or ellipse) from the shape panel.
- Label it API Gateway (Node.js) and place it in the center of the canvas.
- Add a second rounded rectangle labeled Payments Service to the right of the API Gateway.
- Add a third rounded rectangle labeled Product Catalog Service below the API Gateway.
- Add the data store. Use a shape with two parallel horizontal lines (or an open-ended rectangle) to represent a database.
- Label it PostgreSQL Database and place it at the bottom of the canvas.
- Connect the elements with arrows to show data flows. Click an element's edge, drag to the target element, and release.
- Draw an arrow from React Frontend to API Gateway and label it HTTPS requests.
- Draw an arrow from API Gateway to Payments Service and label it Payment requests.
- Draw an arrow from API Gateway to Product Catalog Service and label it Product queries.
- Draw an arrow from Payments Service to Stripe and label it Card charge API calls.
- Draw an arrow from Product Catalog Service to PostgreSQL Database and label it SQL queries.
- Draw an arrow from API Gateway to PostgreSQL Database and label it User auth queries.
Label your arrows
Double-click any arrow to add a label. Labeling data flows is essential because threats often target the data in transit, not just the endpoints.
Identify and mark trust boundaries
A trust boundary is a line on the diagram where the level of trust changes. Data crossing a trust boundary needs security controls like authentication, encryption, or input validation.
- In draw.io, select the Rectangle shape tool from the left panel.
- Draw a large dashed rectangle around the API Gateway, Payments Service, Product Catalog Service, and PostgreSQL Database.
- To make it dashed, right-click the rectangle, select Edit Style, and add dashed=1 to the style string.
- Label this boundary Internal Network (Trust Zone).
- Make sure the React Frontend and Stripe remain outside this boundary.
- Draw a second smaller dashed rectangle around just the PostgreSQL Database.
- Label it Data Layer (Restricted).
Why do trust boundaries matter?
Every arrow that crosses a trust boundary is a potential attack surface. When you apply STRIDE in the next step, you'll focus threat analysis on these crossing points because that's where attackers can intercept, modify, or spoof data.
The ShopSecure DFD has two boundary levels. The outer boundary separates external users and services from internal infrastructure. The inner boundary isolates the database, requiring additional authorization even from internal services.
Your diagram should now show six elements connected by labeled arrows, with two dashed trust boundaries separating the zones.
Export and commit the DFD to your repository
Your DFD is the first artifact in your threat model. Committing it to Git makes it versionable and shareable with your team.
- In draw.io, click File in the top menu bar.
- Click Export as, then select PNG.
- Click Export in the dialog, then click Download.
- Save the file as shopsecure-dfd.png.
- Switch back to VS Code with your appsec-threat-model-lab repository open.
- Create the architecture directory and move the exported PNG into it by running these commands:
mkdir architecture
- Move the shopsecure-dfd.png file from your Downloads folder into the architecture folder inside appsec-threat-model-lab.
- On macOS, run:
mv ~/Downloads/shopsecure-dfd.png architecture/
- On Windows, run:
Move-Item ~\Downloads\shopsecure-dfd.png architecture\
- Confirm the file is in place by running:
ls architecture/
You should see shopsecure-dfd.png listed in the output.
- Stage, commit, and push the DFD to GitHub by running these commands:
git add architecture/
git commit -m "Add ShopSecure Level 1 Data Flow Diagram"
git push origin main
What does this commit include?
You're committing the exported DFD image to an architecture directory. This is a common pattern in security documentation. Keeping threat model artifacts in the same repo as the code makes them easy to reference during reviews and audits.
Push rejected or authentication error?
If git push fails with an authentication error, make sure you're using a personal access token or SSH key for GitHub authentication. If it says the remote contains work you don't have locally, run git pull --rebase origin main first, then push again.
Help me fix my git push authentication error
Check your GitHub repository page in your browser. You should see the architecture folder with shopsecure-dfd.png inside it.
Your system architecture is now documented and version-controlled. Next up, you'll use this DFD to systematically identify threats using the STRIDE framework.
Enumerate Threats with STRIDE
Your DFD gives you a clear picture of how data moves through ShopSecure. Now it's time to ask the critical question: what can go wrong at each point?
STRIDE is a structured framework from Microsoft that gives you six categories of threats to consider for every element in your diagram. Instead of guessing at risks, you systematically walk through each element and ask whether it could be spoofed, tampered with, repudiated, and so on.
In this step, get ready to:
- Apply STRIDE categories to each DFD element based on its type.
- Document each threat with a description and potential impact.
- Map each finding to the relevant OWASP Top 10 API Security Risk.
Apply STRIDE categories to each DFD element
Not every threat category applies to every element type. STRIDE per Element tells you exactly which categories to evaluate based on what the element is.
What is STRIDE per Element?
STRIDE stands for Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, and Elevation of Privilege.
STRIDE per Element maps specific categories to element types: processes face all six (S/T/R/I/D/E), data flows face T/I/D, and data stores face T/R/I/D. This prevents you from wasting time evaluating threats that don't apply to a given element type.
- Create the threat-model directory in your appsec-threat-model-lab project by running this command:
mkdir threat-model
- Create the stride-analysis.md file inside threat-model/ by running:
touch threat-model/stride-analysis.md
- Confirm the file exists by running:
ls threat-model/
You should see stride-analysis.md in the output.
- Open threat-model/stride-analysis.md in VS Code from the Explorer sidebar.
- Add the document header and element applicability table, then write the threat analysis for all three processes by pasting this content:
# STRIDE Threat Analysis - ShopSecure
## Element Applicability
| Element Type | Applicable STRIDE Categories |
|---|---|
| Process | S, T, R, I, D, E (all six) |
| Data Flow | T, I, D |
| Data Store | T, R, I, D |
## Process: API Gateway
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Spoofing | Attacker forges OAuth2 tokens to impersonate legitimate users | Unauthorized access to all downstream services | API2: Broken Authentication |
| Tampering | Attacker modifies request payloads between frontend and gateway | Corrupted data propagates to backend services | API1: Broken Object Level Authorization |
| Repudiation | API requests lack sufficient logging to trace malicious actions | Unable to identify attacker or scope of breach during incident response | API9: Improper Inventory Management |
| Information Disclosure | Verbose error messages expose internal architecture details | Attacker maps internal services, database hosts, and technology stack | API3: Broken Object Property Level Authorization |
| Denial of Service | Attacker floods gateway with requests without rate limiting | All downstream services become unavailable to legitimate users | API4: Unrestricted Resource Consumption |
| Elevation of Privilege | Attacker exploits missing authorization checks to access admin endpoints | Full control over platform configuration and user data | API5: Broken Function Level Authorization |
## Process: Payments Service
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Spoofing | Attacker impersonates the API gateway to send fraudulent payment requests | Unauthorized financial transactions processed | API2: Broken Authentication |
| Tampering | Attacker modifies payment amounts or recipient details in transit | Financial loss, incorrect charges to customers | API1: Broken Object Level Authorization |
| Repudiation | Completed transactions lack audit trail linking action to identity | Disputes cannot be resolved, regulatory compliance failure | API9: Improper Inventory Management |
| Information Disclosure | Payment card data exposed through insufficient encryption | PCI DSS violation, customer financial data breach | API3: Broken Object Property Level Authorization |
| Denial of Service | Attacker overwhelms payment processing with invalid requests | Legitimate customers cannot complete purchases | API4: Unrestricted Resource Consumption |
| Elevation of Privilege | Attacker escalates from customer role to process refunds | Direct financial loss and pricing manipulation | API5: Broken Function Level Authorization |
## Process: Product Catalog Service
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Spoofing | Attacker impersonates admin to modify product listings | Fraudulent products listed, pricing manipulation | API2: Broken Authentication |
| Tampering | SQL injection modifies product data or extracts database contents | Data corruption, unauthorized data exfiltration | API8: Security Misconfiguration |
| Repudiation | Product changes lack attribution to specific admin users | Cannot determine who made unauthorized catalog changes | API9: Improper Inventory Management |
| Information Disclosure | Category filter injection leaks data from other tables | Exposure of customer PII or payment records | API3: Broken Object Property Level Authorization |
| Denial of Service | Wildcard searches exhaust database connections | Catalog becomes unavailable, blocking all shopping activity | API4: Unrestricted Resource Consumption |
| Elevation of Privilege | Attacker exploits IDOR to access other users' saved carts | Privacy violation, exposure of purchasing intent | API1: Broken Object Level Authorization |
What does this document do?
The applicability table at the top is your reference for which STRIDE categories to evaluate per element type. This comes from Microsoft's STRIDE per Element methodology.
Each process gets all six categories because processes accept input, produce output, and make decisions. Notice how each threat maps to a specific OWASP API Security Risk. This connection shows interviewers you understand how theoretical threats map to industry-standard vulnerability classifications.
- Save stride-analysis.md.
You won't see a visual change yet. The next chunk adds data flow and data store threats to complete the analysis.
Document data flow and data store threats
Data flows and data stores have fewer applicable STRIDE categories than processes, but the threats they face are often the most exploitable. A tampered data flow or an exposed data store is usually the actual attack vector.
- In stride-analysis.md, add the data flow threats below your Product Catalog Service section by pasting this content:
## Data Flows
| Data Flow | STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|---|
| Frontend to API Gateway | Tampering | Man-in-the-middle modifies requests if TLS is misconfigured | Injected malicious payloads reach backend services | API8: Security Misconfiguration |
| Frontend to API Gateway | Information Disclosure | Authentication tokens intercepted on unencrypted channel | Session hijacking, full account takeover | API2: Broken Authentication |
| Frontend to API Gateway | Denial of Service | Attacker replays captured requests at high volume | Service degradation for all users | API4: Unrestricted Resource Consumption |
| API Gateway to Payments | Tampering | Internal service communication modified by compromised container | Fraudulent transactions processed with altered amounts | API8: Security Misconfiguration |
| API Gateway to Payments | Information Disclosure | Card data logged in plaintext during inter-service calls | PCI violation, data breach | API3: Broken Object Property Level Authorization |
| API Gateway to Payments | Denial of Service | Message queue flooded with invalid payment requests | Payment processing backlog, revenue loss | API4: Unrestricted Resource Consumption |
| Payments to Stripe | Tampering | Attacker modifies outbound payment requests to Stripe | Payments redirected or amounts changed | API8: Security Misconfiguration |
| Payments to Stripe | Information Disclosure | Stripe API keys exposed in logs or error messages | Attacker can process charges or issue refunds | API2: Broken Authentication |
| Payments to Stripe | Denial of Service | Excessive API calls trigger Stripe rate limiting | Legitimate payments cannot be processed | API4: Unrestricted Resource Consumption |
| API Gateway to Product Catalog | Tampering | Injected SQL passed through gateway to catalog service | Database compromise via second-order injection | API10: Unsafe Consumption of APIs |
| API Gateway to Product Catalog | Information Disclosure | Query parameters expose internal filtering logic | Attacker crafts targeted injection payloads | API3: Broken Object Property Level Authorization |
| API Gateway to Product Catalog | Denial of Service | Expensive queries forwarded without validation | Database resource exhaustion | API4: Unrestricted Resource Consumption |
Why only T, I, and D for data flows?
Data flows are passive channels. They don't make decisions (so Elevation of Privilege doesn't apply), they don't have identities (so Spoofing doesn't apply), and they don't record actions (so Repudiation doesn't apply).
What they CAN be is intercepted (Information Disclosure), modified in transit (Tampering), or overwhelmed (Denial of Service).
- Now add the data store threats below the Data Flows section:
## Data Store: PostgreSQL Database
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Tampering | SQL injection modifies or deletes records directly | Data integrity loss, corrupted orders and pricing | API8: Security Misconfiguration |
| Repudiation | Database changes lack audit logging at the row level | Cannot determine which service or user modified data | API9: Improper Inventory Management |
| Information Disclosure | Database credentials hardcoded in application config | Full database access if source code is leaked | API2: Broken Authentication |
| Denial of Service | Table locks from long-running queries block all operations | Complete application outage | API4: Unrestricted Resource Consumption |
Why does the data store include Repudiation?
Data stores record state. If changes happen without audit trails, nobody can prove who modified what. This is why data stores get R (Repudiation) while data flows do not.
- Save stride-analysis.md.
- Scroll through the full file in VS Code. You should see five major sections: Element Applicability, API Gateway, Payments Service, Product Catalog Service, Data Flows, and Data Store.
Tables look broken in the editor?
Markdown tables need exact pipe alignment to render correctly. Make sure every row has the same number of pipe characters. If a cell is empty, leave the space between pipes blank.
help me fix my Markdown table formatting in stride-analysis.md
Connect findings to OWASP Top 10 API Security Risks
Every threat in your document already maps to an OWASP Top 10 API Security Risk in the last column. This mapping shows which industry-recognized vulnerability categories your system is most exposed to.
What is the OWASP Top 10 for API Security?
OWASP maintains a ranked list of the most critical API security risks based on real-world breach data. The 2023 edition includes risks like Broken Authentication (API2), Broken Object Level Authorization (API1), and Unrestricted Resource Consumption (API4).
By mapping your STRIDE findings to these categories, you're connecting your theoretical threat model to the vocabulary that security teams and hiring managers use every day.
Look at which OWASP risks appear most frequently across your analysis. You should notice that API2 (Broken Authentication) and API4 (Unrestricted Resource Consumption) appear across nearly every element. This tells you where to focus your mitigations first.
- Commit your threat analysis to the repository by running these commands:
git add threat-model/stride-analysis.md
git commit -m "Add STRIDE per element threat analysis"
git push
What does this commit do?
This pushes your completed threat enumeration document to GitHub. The document is now a versioned artifact that demonstrates systematic threat analysis to anyone who reviews your repository.
- Check your GitHub repository in your browser. You should see the threat-model folder with stride-analysis.md inside it.
Push failed or permission denied?
Make sure you're authenticated with GitHub. If you used HTTPS to clone, you may need a personal access token. If you used SSH, ensure your key is added to your GitHub account.
help me fix my git push authentication issue
✔️ Awesome, I've got everything!
Great. Double check you've saved stride-analysis.md and that your push succeeded.
ⓧ I'd like to double check the full code
Here is the complete threat-model/stride-analysis.md file:
# STRIDE Threat Analysis - ShopSecure
## Element Applicability
| Element Type | Applicable STRIDE Categories |
|---|---|
| Process | S, T, R, I, D, E (all six) |
| Data Flow | T, I, D |
| Data Store | T, R, I, D |
## Process: API Gateway
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Spoofing | Attacker forges OAuth2 tokens to impersonate legitimate users | Unauthorized access to all downstream services | API2: Broken Authentication |
| Tampering | Attacker modifies request payloads between frontend and gateway | Corrupted data propagates to backend services | API1: Broken Object Level Authorization |
| Repudiation | API requests lack sufficient logging to trace malicious actions | Unable to identify attacker or scope of breach during incident response | API9: Improper Inventory Management |
| Information Disclosure | Verbose error messages expose internal architecture details | Attacker maps internal services, database hosts, and technology stack | API3: Broken Object Property Level Authorization |
| Denial of Service | Attacker floods gateway with requests without rate limiting | All downstream services become unavailable to legitimate users | API4: Unrestricted Resource Consumption |
| Elevation of Privilege | Attacker exploits missing authorization checks to access admin endpoints | Full control over platform configuration and user data | API5: Broken Function Level Authorization |
## Process: Payments Service
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Spoofing | Attacker impersonates the API gateway to send fraudulent payment requests | Unauthorized financial transactions processed | API2: Broken Authentication |
| Tampering | Attacker modifies payment amounts or recipient details in transit | Financial loss, incorrect charges to customers | API1: Broken Object Level Authorization |
| Repudiation | Completed transactions lack audit trail linking action to identity | Disputes cannot be resolved, regulatory compliance failure | API9: Improper Inventory Management |
| Information Disclosure | Payment card data exposed through insufficient encryption | PCI DSS violation, customer financial data breach | API3: Broken Object Property Level Authorization |
| Denial of Service | Attacker overwhelms payment processing with invalid requests | Legitimate customers cannot complete purchases | API4: Unrestricted Resource Consumption |
| Elevation of Privilege | Attacker escalates from customer role to process refunds | Direct financial loss and pricing manipulation | API5: Broken Function Level Authorization |
## Process: Product Catalog Service
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Spoofing | Attacker impersonates admin to modify product listings | Fraudulent products listed, pricing manipulation | API2: Broken Authentication |
| Tampering | SQL injection modifies product data or extracts database contents | Data corruption, unauthorized data exfiltration | API8: Security Misconfiguration |
| Repudiation | Product changes lack attribution to specific admin users | Cannot determine who made unauthorized catalog changes | API9: Improper Inventory Management |
| Information Disclosure | Category filter injection leaks data from other tables | Exposure of customer PII or payment records | API3: Broken Object Property Level Authorization |
| Denial of Service | Wildcard searches exhaust database connections | Catalog becomes unavailable, blocking all shopping activity | API4: Unrestricted Resource Consumption |
| Elevation of Privilege | Attacker exploits IDOR to access other users' saved carts | Privacy violation, exposure of purchasing intent | API1: Broken Object Level Authorization |
## Data Flows
| Data Flow | STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|---|
| Frontend to API Gateway | Tampering | Man-in-the-middle modifies requests if TLS is misconfigured | Injected malicious payloads reach backend services | API8: Security Misconfiguration |
| Frontend to API Gateway | Information Disclosure | Authentication tokens intercepted on unencrypted channel | Session hijacking, full account takeover | API2: Broken Authentication |
| Frontend to API Gateway | Denial of Service | Attacker replays captured requests at high volume | Service degradation for all users | API4: Unrestricted Resource Consumption |
| API Gateway to Payments | Tampering | Internal service communication modified by compromised container | Fraudulent transactions processed with altered amounts | API8: Security Misconfiguration |
| API Gateway to Payments | Information Disclosure | Card data logged in plaintext during inter-service calls | PCI violation, data breach | API3: Broken Object Property Level Authorization |
| API Gateway to Payments | Denial of Service | Message queue flooded with invalid payment requests | Payment processing backlog, revenue loss | API4: Unrestricted Resource Consumption |
| Payments to Stripe | Tampering | Attacker modifies outbound payment requests to Stripe | Payments redirected or amounts changed | API8: Security Misconfiguration |
| Payments to Stripe | Information Disclosure | Stripe API keys exposed in logs or error messages | Attacker can process charges or issue refunds | API2: Broken Authentication |
| Payments to Stripe | Denial of Service | Excessive API calls trigger Stripe rate limiting | Legitimate payments cannot be processed | API4: Unrestricted Resource Consumption |
| API Gateway to Product Catalog | Tampering | Injected SQL passed through gateway to catalog service | Database compromise via second-order injection | API10: Unsafe Consumption of APIs |
| API Gateway to Product Catalog | Information Disclosure | Query parameters expose internal filtering logic | Attacker crafts targeted injection payloads | API3: Broken Object Property Level Authorization |
| API Gateway to Product Catalog | Denial of Service | Expensive queries forwarded without validation | Database resource exhaustion | API4: Unrestricted Resource Consumption |
## Data Store: PostgreSQL Database
| STRIDE Category | Threat Description | Potential Impact | OWASP API Risk |
|---|---|---|---|
| Tampering | SQL injection modifies or deletes records directly | Data integrity loss, corrupted orders and pricing | API8: Security Misconfiguration |
| Repudiation | Database changes lack audit logging at the row level | Cannot determine which service or user modified data | API9: Improper Inventory Management |
| Information Disclosure | Database credentials hardcoded in application config | Full database access if source code is leaked | API2: Broken Authentication |
| Denial of Service | Table locks from long-running queries block all operations | Complete application outage | API4: Unrestricted Resource Consumption |
Your threat enumeration is complete and committed. Next up, you'll take the highest-risk threats and map them into attack trees that show exactly how an attacker would chain exploits together.
Build Attack Trees and Score Risks
Your STRIDE analysis identified dozens of threats across the ShopSecure architecture. Now you need to answer a critical question: which threats matter most?
Attack trees help you think like an attacker by mapping how individual vulnerabilities chain together into real exploits. Combined with risk scoring, they turn a flat list of threats into a prioritized action plan that tells your team exactly where to invest limited security resources.
In this step, get ready to:
- Create an attack tree mapping exploit chains for stealing payment card data.
- Score all threats using a Likelihood x Impact matrix.
- Document concrete mitigations for the top 5 risks.
Create the attack tree document
An attack tree decomposes a high-value attacker goal into the steps required to achieve it. The root node is the attacker's ultimate objective. Intermediate nodes are sub-goals. Leaf nodes are the specific exploits the attacker executes.
You will build an attack tree for the goal "Steal payment card data" since that represents the highest-impact outcome for the ShopSecure e-commerce system.
- Create a new file called attack-tree.md inside your threat-model/ folder by running this command from your repository root:
touch threat-model/attack-tree.md
- Open threat-model/attack-tree.md in VS Code and paste the following attack tree structure:
# Attack Tree: Steal Payment Card Data
## Root Goal
**Attacker Objective:** Exfiltrate customer payment card numbers from the ShopSecure platform.
## Attack Tree Structure
```
Steal Payment Card Data [ROOT]
|
+-- 1. Compromise API Gateway
| |
| +-- 1.1 Exploit SQL Injection in /api/products
| | (Inject malicious query via 'category' parameter)
| |
| +-- 1.2 Exploit Missing Authentication on /api/payments
| | (Send forged payment requests without credentials)
| |
| +-- 1.3 Abuse Open Redirect for Credential Phishing
| (Redirect admin users to attacker-controlled login page)
|
+-- 2. Intercept Data in Transit
| |
| +-- 2.1 Man-in-the-Middle Between API Gateway and Payment Service
| | (Exploit missing mTLS between internal services)
| |
| +-- 2.2 Capture Card Data from Verbose Error Messages
| (Trigger errors that leak card details in stack traces)
|
+-- 3. Compromise Data at Rest
| |
| +-- 3.1 Access Database with Hardcoded Credentials
| | (Use DB_PASSWORD found in source code)
| |
| +-- 3.2 Exploit Broken Object-Level Authorization
| (Access other users' payment records via /api/users/:id)
|
+-- 4. Compromise Third-Party Integration
|
+-- 4.1 Steal Stripe API Key from Source Code
| (Use hardcoded sk_live_ key to access payment data)
|
+-- 4.2 Intercept Stripe Webhook Callbacks
(Forge webhook events to manipulate payment state)
```
## Leaf Node Details
| Leaf Node | Exploit Technique | STRIDE Category | Prerequisite |
|-----------|------------------|-----------------|---------------|
| 1.1 | String concatenation in SQL query | Tampering | Access to /api/products endpoint |
| 1.2 | No auth middleware on route | Elevation of Privilege | Network access to API |
| 1.3 | Unvalidated redirect URL | Spoofing | Knowledge of admin email |
| 2.1 | Network sniffing on internal traffic | Information Disclosure | Access to internal network |
| 2.2 | Trigger error via /api/users/error | Information Disclosure | Access to /api/users endpoint |
| 3.1 | Read hardcoded DB_PASSWORD in source | Information Disclosure | Access to source code or repo |
| 3.2 | Enumerate user IDs sequentially | Elevation of Privilege | Authenticated session |
| 4.1 | Read sk_live_ key in server.js | Information Disclosure | Access to source code or repo |
| 4.2 | Send forged POST to webhook endpoint | Spoofing | Knowledge of webhook URL |
## Chain Analysis
**Shortest path to goal:** Node 3.1 (hardcoded credentials give direct database access with one exploit step).
**Most likely path:** Node 1.1 → database access (SQL injection is remotely exploitable and requires no authentication).
**Highest impact path:** Node 4.1 (Stripe API key gives access to ALL customer payment data across the platform).
What does this attack tree show?
The tree breaks down one attacker goal into four intermediate strategies (compromise the gateway, intercept data in transit, compromise data at rest, compromise integrations). Each strategy branches into specific leaf-node exploits that map directly back to your STRIDE findings.
The Chain Analysis at the bottom is what separates a professional threat model from an academic exercise. It tells your team which paths are shortest, most likely, and highest impact so they know where to focus defenses first.
- Save threat-model/attack-tree.md.
- Review the tree structure in VS Code's Markdown preview by pressing Cmd+Shift+V (macOS) or Ctrl+Shift+V (Windows). You should see the tree rendered as a code block with clear branching paths.
Tree looks broken in preview?
Make sure the tree diagram is inside a fenced code block (triple backticks). Without the code fence, Markdown will collapse the whitespace and lose the indentation.
If the table renders incorrectly, check that every row has the same number of pipe characters as the header row.
Help me fix my attack tree Markdown formatting.
Score risks with a Likelihood x Impact matrix
A risk register transforms your STRIDE threats from a flat list into a prioritized queue. Each threat gets a score based on how likely an attacker is to exploit it and how severe the damage would be.
- Create threat-model/risk-register.md by running:
touch threat-model/risk-register.md
- Open threat-model/risk-register.md in VS Code and paste the following risk scoring framework:
# Risk Register: ShopSecure Threat Model
## Scoring Methodology
**Likelihood** (1-5): How probable is exploitation?
- 5 = Trivial (automated tools, no auth required)
- 4 = Easy (publicly known technique, low skill)
- 3 = Moderate (requires some access or knowledge)
- 2 = Difficult (requires privileged access or chaining)
- 1 = Unlikely (requires physical access or insider)
**Impact** (1-5): How severe is the damage?
- 5 = Critical (full data breach, regulatory fines, business closure risk)
- 4 = High (significant data loss, customer trust damaged)
- 3 = Moderate (limited data exposure, service degradation)
- 2 = Low (minimal data exposed, quick recovery)
- 1 = Negligible (no data loss, cosmetic issue)
**Risk Score** = Likelihood x Impact (max 25)
## Risk Matrix
| Score Range | Priority | Response |
|-------------|----------|----------|
| 20-25 | Critical | Immediate fix required |
| 15-19 | High | Fix within current sprint |
| 10-14 | Medium | Schedule for next sprint |
| 5-9 | Low | Add to backlog |
| 1-4 | Informational | Document and monitor |
## Threat Register
| # | Threat | Element | STRIDE | Likelihood | Impact | Score | Priority |
|---|--------|---------|--------|-----------|--------|-------|----------|
| T1 | SQL injection via category parameter | API Gateway /api/products | Tampering | 5 | 5 | 25 | Critical |
| T2 | Hardcoded Stripe API key in source | server.js | Info Disclosure | 5 | 5 | 25 | Critical |
| T3 | Hardcoded DB password in source | server.js | Info Disclosure | 5 | 4 | 20 | Critical |
| T4 | Missing authentication on payments endpoint | API Gateway /api/payments | Elevation of Privilege | 5 | 4 | 20 | Critical |
| T5 | Verbose error messages leak internals | API Gateway /api/users | Info Disclosure | 4 | 4 | 16 | High |
| T6 | Open redirect enables phishing | API Gateway /redirect | Spoofing | 4 | 3 | 12 | Medium |
| T7 | No input validation on payment data | API Gateway /api/payments | Tampering | 4 | 3 | 12 | Medium |
| T8 | Missing mTLS between internal services | Data flow: API to Payment | Info Disclosure | 3 | 4 | 12 | Medium |
| T9 | No rate limiting on endpoints | All processes | Denial of Service | 4 | 2 | 8 | Low |
| T10 | Missing audit logging | All processes | Repudiation | 3 | 2 | 6 | Low |
Why Likelihood x Impact?
This simple multiplication gives you a single number to sort by. T1 and T2 both score 25 because SQL injection and leaked API keys are both trivially exploitable (automated scanners find them) and both lead to full data breaches.
In interviews, you will be asked to justify your scores. The key is connecting Likelihood to attacker skill level and Impact to business consequences. A score without reasoning is just a guess.
- Save threat-model/risk-register.md.
- Preview the file with Cmd+Shift+V (macOS) or Ctrl+Shift+V (Windows). You should see a clean table with T1 and T2 at the top, both scored 25 as Critical priority.
Table not rendering in preview?
Markdown tables require the header separator row (the one with dashes and pipes). Check that the row with |---| separators exists directly below the header row.
Also confirm that every row has exactly the same number of | characters. A missing pipe in one row breaks the entire table.
Help me fix my Markdown table formatting.
Document concrete mitigations for the top 5 threats
A threat register without mitigations is a problem statement, not a solution. For each of your top 5 threats, you need to document the specific security control that neutralizes the risk.
- Add the following mitigations section to the bottom of threat-model/risk-register.md:
## Top 5 Threat Mitigations
### T1: SQL Injection (Score: 25 - Critical)
**Current state:** String concatenation builds SQL query directly from user input.
**Mitigation:** Replace string concatenation with parameterized queries (prepared statements). Use an ORM like Sequelize or Knex.js that enforces parameterization by default.
**Validation:** Semgrep rule detecting string concatenation in SQL patterns. Integration test sending `' OR 1=1 --` in category parameter.
---
### T2: Hardcoded Stripe API Key (Score: 25 - Critical)
**Current state:** Live Stripe secret key stored as plaintext constant in server.js.
**Mitigation:** Move all secrets to environment variables. Use GitHub Actions secrets for CI/CD. Implement secret rotation policy (90-day maximum lifetime). Add Gitleaks pre-commit hook to prevent future leaks.
**Validation:** Gitleaks scan on every push. Repository secret scanning alerts enabled.
---
### T3: Hardcoded Database Password (Score: 20 - Critical)
**Current state:** Database password stored as plaintext constant in server.js.
**Mitigation:** Store database credentials in a secrets manager (AWS Secrets Manager, HashiCorp Vault, or environment variables at minimum). Use connection pooling with short-lived credentials where possible.
**Validation:** Gitleaks detects hardcoded passwords. Application fails to start if DB_PASSWORD environment variable is not set.
---
### T4: Missing Authentication on Payments (Score: 20 - Critical)
**Current state:** The /api/payments endpoint accepts requests from any unauthenticated client.
**Mitigation:** Add OAuth2 bearer token validation middleware. Require authenticated session with verified payment authorization scope. Implement request signing for payment operations.
**Validation:** Integration test confirming 401 response without valid token. Load test confirming rate limiting under authentication.
---
### T5: Verbose Error Messages (Score: 16 - High)
**Current state:** Raw error messages including database connection strings sent directly to clients.
**Mitigation:** Implement a global error handler that returns generic error messages to clients (e.g., "An internal error occurred") while logging full details server-side. Never expose stack traces, connection strings, or internal paths.
**Validation:** Custom Semgrep rule detecting res.status(500).send(err) patterns. Manual test triggering errors and confirming only generic messages returned.
What makes these mitigations concrete?
Each mitigation follows a three-part structure: current state (what is wrong), mitigation (the specific control to implement), and validation (how you prove the fix works). Generic advice like "improve security" or "add authentication" would fail in an interview.
Notice that T5's validation references a custom Semgrep rule. You will build exactly that rule in the Secret Mission later in this project.
- Save threat-model/risk-register.md.
- Commit both new files to your repository by running these commands:
git add threat-model/attack-tree.md threat-model/risk-register.md
git commit -m "Add attack tree and risk register with mitigations"
git push
- Confirm the push succeeded. You should see output mentioning your main branch and no error messages.
Push rejected or failed?
If you see "rejected" in the output, run git pull --rebase first, then try git push again. This happens when the remote has commits your local branch does not.
If you see an authentication error, confirm you are logged into GitHub in VS Code. Open the terminal and run gh auth status if you have the GitHub CLI, or check your credential manager.
Help me push my threat model files to GitHub.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files and pushed them to GitHub.
ⓧ I'd like to double check the full code
Here is the complete threat-model/attack-tree.md:
# Attack Tree: Steal Payment Card Data
## Root Goal
**Attacker Objective:** Exfiltrate customer payment card numbers from the ShopSecure platform.
## Attack Tree Structure
```
Steal Payment Card Data [ROOT]
|
+-- 1. Compromise API Gateway
| |
| +-- 1.1 Exploit SQL Injection in /api/products
| | (Inject malicious query via 'category' parameter)
| |
| +-- 1.2 Exploit Missing Authentication on /api/payments
| | (Send forged payment requests without credentials)
| |
| +-- 1.3 Abuse Open Redirect for Credential Phishing
| (Redirect admin users to attacker-controlled login page)
|
+-- 2. Intercept Data in Transit
| |
| +-- 2.1 Man-in-the-Middle Between API Gateway and Payment Service
| | (Exploit missing mTLS between internal services)
| |
| +-- 2.2 Capture Card Data from Verbose Error Messages
| (Trigger errors that leak card details in stack traces)
|
+-- 3. Compromise Data at Rest
| |
| +-- 3.1 Access Database with Hardcoded Credentials
| | (Use DB_PASSWORD found in source code)
| |
| +-- 3.2 Exploit Broken Object-Level Authorization
| (Access other users' payment records via /api/users/:id)
|
+-- 4. Compromise Third-Party Integration
|
+-- 4.1 Steal Stripe API Key from Source Code
| (Use hardcoded sk_live_ key to access payment data)
|
+-- 4.2 Intercept Stripe Webhook Callbacks
(Forge webhook events to manipulate payment state)
```
## Leaf Node Details
| Leaf Node | Exploit Technique | STRIDE Category | Prerequisite |
|-----------|------------------|-----------------|---------------|
| 1.1 | String concatenation in SQL query | Tampering | Access to /api/products endpoint |
| 1.2 | No auth middleware on route | Elevation of Privilege | Network access to API |
| 1.3 | Unvalidated redirect URL | Spoofing | Knowledge of admin email |
| 2.1 | Network sniffing on internal traffic | Information Disclosure | Access to internal network |
| 2.2 | Trigger error via /api/users/error | Information Disclosure | Access to /api/users endpoint |
| 3.1 | Read hardcoded DB_PASSWORD in source | Information Disclosure | Access to source code or repo |
| 3.2 | Enumerate user IDs sequentially | Elevation of Privilege | Authenticated session |
| 4.1 | Read sk_live_ key in server.js | Information Disclosure | Access to source code or repo |
| 4.2 | Send forged POST to webhook endpoint | Spoofing | Knowledge of webhook URL |
## Chain Analysis
**Shortest path to goal:** Node 3.1 (hardcoded credentials give direct database access with one exploit step).
**Most likely path:** Node 1.1 → database access (SQL injection is remotely exploitable and requires no authentication).
**Highest impact path:** Node 4.1 (Stripe API key gives access to ALL customer payment data across the platform).
Here is the complete threat-model/risk-register.md:
# Risk Register: ShopSecure Threat Model
## Scoring Methodology
**Likelihood** (1-5): How probable is exploitation?
- 5 = Trivial (automated tools, no auth required)
- 4 = Easy (publicly known technique, low skill)
- 3 = Moderate (requires some access or knowledge)
- 2 = Difficult (requires privileged access or chaining)
- 1 = Unlikely (requires physical access or insider)
**Impact** (1-5): How severe is the damage?
- 5 = Critical (full data breach, regulatory fines, business closure risk)
- 4 = High (significant data loss, customer trust damaged)
- 3 = Moderate (limited data exposure, service degradation)
- 2 = Low (minimal data exposed, quick recovery)
- 1 = Negligible (no data loss, cosmetic issue)
**Risk Score** = Likelihood x Impact (max 25)
## Risk Matrix
| Score Range | Priority | Response |
|-------------|----------|----------|
| 20-25 | Critical | Immediate fix required |
| 15-19 | High | Fix within current sprint |
| 10-14 | Medium | Schedule for next sprint |
| 5-9 | Low | Add to backlog |
| 1-4 | Informational | Document and monitor |
## Threat Register
| # | Threat | Element | STRIDE | Likelihood | Impact | Score | Priority |
|---|--------|---------|--------|-----------|--------|-------|----------|
| T1 | SQL injection via category parameter | API Gateway /api/products | Tampering | 5 | 5 | 25 | Critical |
| T2 | Hardcoded Stripe API key in source | server.js | Info Disclosure | 5 | 5 | 25 | Critical |
| T3 | Hardcoded DB password in source | server.js | Info Disclosure | 5 | 4 | 20 | Critical |
| T4 | Missing authentication on payments endpoint | API Gateway /api/payments | Elevation of Privilege | 5 | 4 | 20 | Critical |
| T5 | Verbose error messages leak internals | API Gateway /api/users | Info Disclosure | 4 | 4 | 16 | High |
| T6 | Open redirect enables phishing | API Gateway /redirect | Spoofing | 4 | 3 | 12 | Medium |
| T7 | No input validation on payment data | API Gateway /api/payments | Tampering | 4 | 3 | 12 | Medium |
| T8 | Missing mTLS between internal services | Data flow: API to Payment | Info Disclosure | 3 | 4 | 12 | Medium |
| T9 | No rate limiting on endpoints | All processes | Denial of Service | 4 | 2 | 8 | Low |
| T10 | Missing audit logging | All processes | Repudiation | 3 | 2 | 6 | Low |
## Top 5 Threat Mitigations
### T1: SQL Injection (Score: 25 - Critical)
**Current state:** String concatenation builds SQL query directly from user input.
**Mitigation:** Replace string concatenation with parameterized queries (prepared statements). Use an ORM like Sequelize or Knex.js that enforces parameterization by default.
**Validation:** Semgrep rule detecting string concatenation in SQL patterns. Integration test sending `' OR 1=1 --` in category parameter.
---
### T2: Hardcoded Stripe API Key (Score: 25 - Critical)
**Current state:** Live Stripe secret key stored as plaintext constant in server.js.
**Mitigation:** Move all secrets to environment variables. Use GitHub Actions secrets for CI/CD. Implement secret rotation policy (90-day maximum lifetime). Add Gitleaks pre-commit hook to prevent future leaks.
**Validation:** Gitleaks scan on every push. Repository secret scanning alerts enabled.
---
### T3: Hardcoded Database Password (Score: 20 - Critical)
**Current state:** Database password stored as plaintext constant in server.js.
**Mitigation:** Store database credentials in a secrets manager (AWS Secrets Manager, HashiCorp Vault, or environment variables at minimum). Use connection pooling with short-lived credentials where possible.
**Validation:** Gitleaks detects hardcoded passwords. Application fails to start if DB_PASSWORD environment variable is not set.
---
### T4: Missing Authentication on Payments (Score: 20 - Critical)
**Current state:** The /api/payments endpoint accepts requests from any unauthenticated client.
**Mitigation:** Add OAuth2 bearer token validation middleware. Require authenticated session with verified payment authorization scope. Implement request signing for payment operations.
**Validation:** Integration test confirming 401 response without valid token. Load test confirming rate limiting under authentication.
---
### T5: Verbose Error Messages (Score: 16 - High)
**Current state:** Raw error messages including database connection strings sent directly to clients.
**Mitigation:** Implement a global error handler that returns generic error messages to clients (e.g., "An internal error occurred") while logging full details server-side. Never expose stack traces, connection strings, or internal paths.
**Validation:** Custom Semgrep rule detecting res.status(500).send(err) patterns. Manual test triggering errors and confirming only generic messages returned.
You now have two powerful artifacts: an attack tree that maps how vulnerabilities chain together, and a risk register that tells you exactly where to focus first. Next up, you will build a real vulnerable application so your security pipeline has something to detect.
Create the Vulnerable Sample App
Your threat model documented where attacks could happen across the ShopSecure architecture. Now you need real, vulnerable code to prove your security pipeline catches those weaknesses.
In this step, you'll create a minimal Node.js Express app with intentional vulnerabilities that map directly to your STRIDE findings. These vulnerabilities will give your automated scanners something concrete to detect in the next step.
In this step, get ready to:
- Create a Node.js Express app with intentional security vulnerabilities.
- Add a package.json defining your project dependencies.
- Commit the vulnerable code to your repository.
Build the vulnerable Express server
Each vulnerability in this file maps to a threat you identified in your STRIDE analysis. The inline comments label each one so you can trace findings back to your threat model later.
- In VS Code, create a new file called server.js in the root of your appsec-threat-model-lab repository folder.
- Copy the following code into server.js:
const express = require('express');
const app = express();
// VULNERABILITY: Hardcoded secret (Gitleaks will catch this)
const API_SECRET = 'sk_live_4eC39HqLyjWDarjtT1zdp7dc';
const DB_PASSWORD = 'super_secret_password_123';
app.use(express.json());
// VULNERABILITY: SQL Injection pattern (Semgrep will catch this)
app.get('/api/products', (req, res) => {
const category = req.query.category;
const query = `SELECT * FROM products WHERE category = '${category}'`;
// In a real app this would execute against a database
res.json({ query: query, message: 'Products endpoint' });
});
// VULNERABILITY: Information disclosure via error details
app.get('/api/users/:id', (req, res) => {
try {
const userId = req.params.id;
// Simulate a database lookup that fails
if (userId === 'error') {
throw new Error('Connection to PostgreSQL failed: host=db.internal port=5432 user=admin');
}
res.json({ id: userId, name: 'Test User' });
} catch (err) {
// VULNERABILITY: Leaking internal error details to client
res.status(500).send(err.message);
}
});
// VULNERABILITY: Missing input validation and authentication
app.post('/api/payments', (req, res) => {
const { amount, cardNumber } = req.body;
// No authentication check
// No input validation
console.log(`Processing payment: ${cardNumber} for $${amount}`);
res.json({ status: 'processed', amount: amount });
});
// VULNERABILITY: Open redirect
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.redirect(url);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
What vulnerabilities does this code contain?
- Hardcoded secrets in API_SECRET and DB_PASSWORD. These are stored directly in code. Gitleaks will flag these.
- SQL injection in the /api/products endpoint. It builds a query by concatenating user input directly into SQL. Semgrep will catch this pattern.
- Information disclosure in the /api/users/:id error handler. It sends raw error messages (including internal hostnames and ports) to the client.
- Missing authentication in the /api/payments endpoint. It processes payments with no auth check or input validation.
- Open redirect in the /redirect endpoint. It redirects to any URL without validation, enabling phishing attacks.
You won't run this file yet. You need to install its dependencies first.
Add the package.json and commit
Every Node.js project needs a package.json to define its dependencies and metadata. Your security pipeline will scan this file to identify dependency risks.
- Create a new file called package.json in the same appsec-threat-model-lab root folder.
- Copy the following into package.json:
{
"name": "shopsecure-vulnerable-app",
"version": "1.0.0",
"description": "Intentionally vulnerable app for AppSec training",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "4.21.2"
}
}
What does this file do?
The package.json tells Node.js what packages your app depends on. When you run npm install, Node.js reads this file and downloads Express 4.21.2 into a node_modules folder.
The security pipeline will also scan this file and its dependency tree for known vulnerabilities in third-party packages.
- Save both server.js and package.json.
- Confirm both files exist by running this in your terminal:
ls
You should see server.js and package.json listed alongside your threat-model folder.
Don't see both files?
Make sure you created both files in the repository root (appsec-threat-model-lab), not inside the threat-model subfolder. Check your terminal is in the correct directory with pwd.
help me find where my files were saved
- Install the project dependencies by running:
npm install
You should see output showing packages being added and a node_modules folder appearing in your project.
- Test that your server starts by running:
node server.js
You should see Server running on port 3000 in your terminal.
- Press Ctrl+C to stop the server.
Seeing a 'Cannot find module' error?
This means npm install did not run successfully. Make sure your terminal is in the same folder as package.json before running npm install.
help me fix the Cannot find module error when running my server
Now that the app works, commit everything to your repository. You need a .gitignore to keep node_modules out of version control.
- Create a .gitignore file and commit your vulnerable app by running these commands:
echo "node_modules/" > .gitignore
git add .
git commit -m "Add vulnerable sample app for security scanning"
git push
Why exclude node_modules?
The node_modules folder contains thousands of downloaded dependency files. Anyone who clones your repo can regenerate it by running npm install. Committing it would bloat your repository and slow down your pipeline.
- Verify the push succeeded by checking your appsec-threat-model-lab repository on GitHub.
- You should see server.js, package.json, and .gitignore listed alongside your threat-model folder.
Push rejected or authentication error?
If git push fails with an authentication error, make sure your GitHub credentials are configured. Try running git remote -v to verify your remote URL is correct.
help me fix my git push authentication error
✔️ Awesome, I've got everything!
Great. Double check you've saved all files and your push completed without errors.
ⓧ I'd like to double check the full code
Here is the complete server.js:
const express = require('express');
const app = express();
// VULNERABILITY: Hardcoded secret (Gitleaks will catch this)
const API_SECRET = 'sk_live_4eC39HqLyjWDarjtT1zdp7dc';
const DB_PASSWORD = 'super_secret_password_123';
app.use(express.json());
// VULNERABILITY: SQL Injection pattern (Semgrep will catch this)
app.get('/api/products', (req, res) => {
const category = req.query.category;
const query = `SELECT * FROM products WHERE category = '${category}'`;
// In a real app this would execute against a database
res.json({ query: query, message: 'Products endpoint' });
});
// VULNERABILITY: Information disclosure via error details
app.get('/api/users/:id', (req, res) => {
try {
const userId = req.params.id;
// Simulate a database lookup that fails
if (userId === 'error') {
throw new Error('Connection to PostgreSQL failed: host=db.internal port=5432 user=admin');
}
res.json({ id: userId, name: 'Test User' });
} catch (err) {
// VULNERABILITY: Leaking internal error details to client
res.status(500).send(err.message);
}
});
// VULNERABILITY: Missing input validation and authentication
app.post('/api/payments', (req, res) => {
const { amount, cardNumber } = req.body;
// No authentication check
// No input validation
console.log(`Processing payment: ${cardNumber} for $${amount}`);
res.json({ status: 'processed', amount: amount });
});
// VULNERABILITY: Open redirect
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.redirect(url);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Here is the complete package.json:
{
"name": "shopsecure-vulnerable-app",
"version": "1.0.0",
"description": "Intentionally vulnerable app for AppSec training",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "4.21.2"
}
}
Your vulnerable app is now live in your repository. Next up, you'll build a GitHub Actions security pipeline that automatically catches these vulnerabilities on every push.
Wire Up the DevSecOps Pipeline
You have a vulnerable sample app with hardcoded secrets, SQL injection patterns, and error disclosure. The problem is that manual code review doesn't scale.
In this step, you'll create a GitHub Actions workflow that automatically scans your code with two security tools on every push. This is policy-as-code in action: security checks that run without human intervention and block unsafe code from merging.
In this step, get ready to:
- Create a GitHub Actions workflow that runs Semgrep SAST.
- Add Gitleaks secret detection to the pipeline.
- Write a custom Semgrep rule connected to your threat model.
Create the security pipeline workflow with Semgrep
GitHub Actions workflows live in a special directory called .github/workflows/. Any YAML file in this folder becomes an automated pipeline that GitHub runs in response to events like pushes and pull requests.
- Create the workflow directory by running this command in your VS Code terminal:
mkdir -p .github/workflows
- Confirm the directory exists by running:
ls .github
You should see workflows in the output.
- Create a new file at .github/workflows/security.yml and paste the following code:
name: Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}
permissions:
contents: read
jobs:
semgrep:
name: SAST - Semgrep
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v6
- name: Run Semgrep scan
run: semgrep scan --config auto --config .semgrep/ --sarif --output semgrep.sarif .
- name: Upload SARIF
if: always()
uses: actions/upload-artifact@v4
with:
name: semgrep-results
path: semgrep.sarif
What does this workflow do?
- on: triggers the pipeline on every push to main, every pull request targeting main, and on manual dispatch from the GitHub UI.
- permissions: contents: read follows the principle of least privilege. The workflow only needs to read your code, nothing else.
- container: image: semgrep/semgrep runs the job inside Semgrep's official Docker image, so you don't need to install it separately.
- semgrep scan --config auto --config .semgrep/ runs both the community ruleset AND your custom rules from the .semgrep/ directory.
- The --sarif flag outputs results in SARIF format, a standard format for static analysis results that integrates with other tools.
How does this enforce policy-as-code?
Semgrep returns exit code 1 if blocking findings exist. GitHub Actions treats any non-zero exit code as a job failure. This means your pipeline automatically blocks merges when security issues are detected. No human has to remember to check.
- Save .github/workflows/security.yml.
You won't see a visible change yet. The workflow runs on GitHub's servers after you push, which you'll do at the end of this step.
Seeing YAML formatting errors in VS Code?
YAML is sensitive to indentation. Make sure you're using spaces (not tabs) and that each nested level is indented by exactly 2 spaces.
Help me fix my YAML syntax errors in security.yml
Add Gitleaks secret detection to the workflow
Semgrep catches code-level vulnerabilities, but it won't flag hardcoded secrets like API keys or passwords. Gitleaks fills that gap by scanning your entire Git history for credential patterns.
- In .github/workflows/security.yml, add the following job below the semgrep job (at the same indentation level as semgrep:):
gitleaks:
name: Secret Detection - Gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
What does the Gitleaks job do?
- fetch-depth: 0 clones the full Git history. Gitleaks scans every commit, not just the latest code. This catches secrets that were committed and later "deleted" (they're still in the history).
- gitleaks/gitleaks-action@v3 is the official Gitleaks GitHub Action. On personal accounts, no license key is needed.
- GITHUB_TOKEN is automatically provided by GitHub Actions. You don't need to create or configure it.
- Save .github/workflows/security.yml.
Your complete workflow now has two parallel jobs: one for SAST and one for secret detection.
Not sure where to add the gitleaks job?
The gitleaks: key should be at the same indent level as semgrep: inside the jobs: block. Both are children of jobs.
Help me understand YAML indentation for my workflow
Write a custom Semgrep rule for your threat model
Your workflow already uses --config auto to pull community rules. You can also write custom rules that catch vulnerability patterns specific to your threat model. This connects your STRIDE analysis directly to automated detection.
- Create the custom rules directory by running:
mkdir -p .semgrep
- Confirm the directory exists by running:
ls .semgrep
The directory should appear in the output (it will be empty for now).
- Create a new file at .semgrep/error-disclosure.yml and paste the following rule:
rules:
- id: express-error-disclosure
patterns:
- pattern: res.status(500).send($ERR)
message: >
Sending raw error details to the client may leak internal information
such as stack traces, database connection strings, or file paths.
Use a generic error message instead.
severity: WARNING
languages: [javascript]
metadata:
category: security
subcategory: information-disclosure
threat-model-ref: "STRIDE-InfoDisclosure-API-Gateway"
What does this rule detect?
This rule catches the exact pattern from your server.js vulnerability: res.status(500).send(err.message). The $ERR is a Semgrep metavariable that matches any argument passed to .send().
The threat-model-ref in the metadata ties this detection rule back to your STRIDE analysis. When the pipeline flags this pattern, you can trace it directly to the Information Disclosure threat you documented.
- Save .semgrep/error-disclosure.yml.
- Commit and push all pipeline files to GitHub by running:
git add .github/workflows/security.yml .semgrep/error-disclosure.yml
git commit -m "Add security pipeline with Semgrep and Gitleaks"
git push origin main
What happens after you push?
GitHub detects the workflow file and starts running it automatically. Navigate to your repository on GitHub and click the Actions tab to see the pipeline execute. You'll see two jobs running: SAST - Semgrep and Secret Detection - Gitleaks.
- Open your repository on GitHub.
- Click the Actions tab at the top of the repository.
- Confirm you see the Security Pipeline workflow running or recently completed.
Don't see the workflow running?
Make sure your push went to the main branch. The workflow only triggers on pushes to main. Run git branch to confirm you're on the correct branch.
Help me troubleshoot my GitHub Actions workflow
✔️ Awesome, I've got everything!
Great. Double check you've saved both files and pushed them to GitHub.
ⓧ I'd like to double check the full code
Here is the complete .github/workflows/security.yml:
name: Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}
permissions:
contents: read
jobs:
semgrep:
name: SAST - Semgrep
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v6
- name: Run Semgrep scan
run: semgrep scan --config auto --config .semgrep/ --sarif --output semgrep.sarif .
- name: Upload SARIF
if: always()
uses: actions/upload-artifact@v4
with:
name: semgrep-results
path: semgrep.sarif
gitleaks:
name: Secret Detection - Gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
And the complete .semgrep/error-disclosure.yml:
rules:
- id: express-error-disclosure
patterns:
- pattern: res.status(500).send($ERR)
message: >
Sending raw error details to the client may leak internal information
such as stack traces, database connection strings, or file paths.
Use a generic error message instead.
severity: WARNING
languages: [javascript]
metadata:
category: security
subcategory: information-disclosure
threat-model-ref: "STRIDE-InfoDisclosure-API-Gateway"
Your security pipeline is live. Every push to main now triggers automated vulnerability scanning. Next up, you'll watch the pipeline catch real issues in your vulnerable app and learn how to triage the findings like a security engineer.
Triage Findings and Plan Remediation
Your pipeline is built, your vulnerable app is committed, and your security tools are configured. Now it's time for the part that separates a real security engineer from someone who just runs tools.
Triaging findings means deciding what matters, how urgently it matters, and who should fix it. Interviewers test this skill directly because anyone can run a scanner. The value is in interpreting results, filtering noise, and building a plan that a development team can actually follow.
In this step, get ready to:
- Push the vulnerable code and observe the pipeline catching real issues.
- Triage each finding with severity classification and remediation response.
- Create a remediation plan with SLA deadlines and false positive rationale.
Push code and observe pipeline results
Your pipeline triggers on every push to main. You need to push your committed code to GitHub so the pipeline runs against the intentionally vulnerable app.
- Push your code to GitHub by running this command in your terminal:
git push origin main
What does this trigger?
Pushing to main triggers the Security Pipeline workflow you defined in .github/workflows/security.yml. Two jobs run in parallel: Semgrep scans your code for vulnerability patterns, and Gitleaks scans your commit history for leaked secrets.
- Open your repository on GitHub in your browser.
- Click the Actions tab at the top of your repository page.
- You should see a workflow run labelled Security Pipeline either in progress or completed.
- Click on the workflow run to see the details of both jobs.
The Semgrep job should show findings for SQL injection, error disclosure, and open redirect patterns. The Gitleaks job should flag the hardcoded secrets in server.js.
What should I see in the pipeline results?
Both jobs will likely show failure indicators (red X marks) because your code contains intentional vulnerabilities. This is expected. Semgrep returns exit code 1 when it finds blocking findings, and Gitleaks flags any secrets in your commit history.
Click into each job to see the specific findings in the logs. Take note of what each tool found. You will use these findings in the next substep.
Pipeline not running?
Make sure you pushed to the main branch. The workflow only triggers on pushes to main or pull requests targeting main.
If the workflow file has a YAML syntax error, GitHub won't run it at all. Check the Actions tab for any error banner at the top of the page.
help me debug why my GitHub Actions workflow isn't triggering
Triage each finding and classify severity
Triage is the process of examining each scanner finding and deciding how dangerous it actually is in context. A finding's severity depends on three factors: how severe the impact would be, how easy it is to exploit, and what business data is at risk.
What is vulnerability triage?
Security scanners produce raw findings. Triage converts those findings into actionable intelligence. You classify each one using severity (Critical, High, Medium, Low) based on:
- Impact: what happens if this vulnerability is exploited (data breach, service outage, reputation damage).
- Exploitability: how easy is it for an attacker to trigger this (unauthenticated, requires special access, needs insider knowledge).
- Business context: what data or systems are exposed (payment data vs. public product info).
Based on the pipeline results, here are the findings you should have observed. Each one needs a severity classification and a remediation response.
How do I decide the remediation response?
For each finding, assign one of four responses:
- Fix: write a code change to eliminate the vulnerability. Assign an SLA deadline.
- Mitigate: apply a compensating control (e.g., WAF rule) that reduces risk without fixing the code directly.
- Transfer: accept the risk with a documented risk owner who takes responsibility.
- Accept: the risk is too low to justify action. Document the business justification.
- Create a new folder called triage in your project by running:
mkdir triage
- Create the triage report file by running:
touch triage/triage-report.md
- Confirm the file was created by running:
ls triage/
You should see triage-report.md in the output.
- Open triage/triage-report.md in VS Code.
- Add the report header and your first finding (the hardcoded Stripe key detected by Gitleaks) by pasting the following:
# Vulnerability Triage Report - ShopSecure
## Scan Date
Pipeline run triggered by push to main branch.
## Tools Used
- Semgrep (SAST - static analysis)
- Gitleaks (secret detection)
- Custom rule: express-error-disclosure
---
## Finding 1: Hardcoded Stripe Live Secret Key
| Field | Value |
|-------|-------|
| Tool | Gitleaks |
| File | server.js |
| Line | 5 |
| Severity | **Critical** |
| Exploitability | High - key is in plaintext in a public/accessible repository |
| Business Context | Live payment API key enables unauthorized charges |
| Response | **Fix** |
| SLA | 24 hours |
| Owner | Developer |
| Fix | Rotate the key immediately. Move to environment variable. Add .env to .gitignore. |
Why is this Critical?
A live Stripe secret key in source code means anyone with repository access can make real charges. This is the highest severity because it combines immediate exploitability (no special tools needed) with catastrophic business impact (financial fraud). The 24-hour SLA reflects that key rotation should happen within minutes of discovery.
- Continue building the report by adding findings 2 through 5 below the first finding:
---
## Finding 2: Hardcoded Database Password
| Field | Value |
|-------|-------|
| Tool | Gitleaks |
| File | server.js |
| Line | 6 |
| Severity | **High** |
| Exploitability | Medium - requires network access to database host |
| Business Context | Database contains user PII and payment records |
| Response | **Fix** |
| SLA | 7 days |
| Owner | Platform team |
| Fix | Move to environment variable. Rotate credential. Restrict DB network access. |
---
## Finding 3: SQL Injection in Products Endpoint
| Field | Value |
|-------|-------|
| Tool | Semgrep |
| File | server.js |
| Line | 13 |
| Severity | **Critical** |
| Exploitability | High - unauthenticated endpoint, user input directly in query |
| Business Context | Full database access including payment and user tables |
| Response | **Fix** |
| SLA | 24 hours |
| Owner | Developer |
| Fix | Use parameterized queries. Add input validation on category parameter. |
---
## Finding 4: Information Disclosure via Error Messages
| Field | Value |
|-------|-------|
| Tool | Semgrep (custom rule: express-error-disclosure) |
| File | server.js |
| Line | 30 |
| Severity | **Medium** |
| Exploitability | Medium - requires triggering an error condition |
| Business Context | Leaks internal hostnames, ports, and database usernames |
| Response | **Fix** |
| SLA | 30 days |
| Owner | Developer |
| Fix | Return generic error message to client. Log full error server-side only. |
---
## Finding 5: Open Redirect
| Field | Value |
|-------|-------|
| Tool | Semgrep |
| File | server.js |
| Line | 42 |
| Severity | **Medium** |
| Exploitability | Medium - requires victim to click a crafted link |
| Business Context | Enables phishing attacks using trusted domain |
| Response | **Mitigate** |
| SLA | 30 days |
| Owner | Developer |
| Fix | Validate redirect URL against allowlist of approved domains. |
Why different severities?
Notice that SQL injection and the leaked Stripe key both get Critical because they allow direct, unauthenticated exploitation with catastrophic impact. The database password is High because exploitation requires additional network access. Information disclosure and open redirect are Medium because they require specific conditions to exploit and their direct impact is lower.
This contextual reasoning is exactly what interviewers test. They want to hear you explain WHY something is Critical versus High, not just that you can read a scanner label.
Document false positive rationale and commit
In real-world triage, not every scanner finding is a true positive. Some findings are technically correct but not exploitable in context. Documenting why a finding is not a real risk is one of the most important skills interviewers test.
- Add the false positive analysis and SLA summary sections at the bottom of your triage report:
---
## False Positive Analysis
### Payments Endpoint - Missing Authentication
Semgrep may flag the payments endpoint for missing authentication checks.
In this training context, this is a **true positive** (not a false positive)
because the endpoint genuinely lacks auth.
However, in a production context where this endpoint sits behind an API
gateway that enforces OAuth2 tokens before traffic reaches the service,
the finding would be a **false positive at the service level**. The rationale:
- The API gateway enforces authentication before requests reach this service
- Network policy restricts direct access to the payments service
- The finding is valid at the gateway level but not at the service level
This distinction matters because security teams waste time fixing findings
that are already mitigated by infrastructure controls.
---
## SLA Summary
| Severity | SLA Deadline | Count |
|----------|-------------|-------|
| Critical | 24 hours | 2 |
| High | 7 days | 1 |
| Medium | 30 days | 2 |
| Low | 90 days | 0 |
## Ownership Summary
| Owner | Findings |
|-------|----------|
| Developer | 4 (Findings 1, 3, 4, 5) |
| Platform team | 1 (Finding 2) |
| Security team | 0 |
Why document false positive rationale?
Interviewers specifically test whether you can explain WHY a finding is not exploitable. Saying "it's a false positive" without explanation shows you can dismiss findings but not reason about them. Documenting the infrastructure context (API gateway, network policy, compensating controls) proves you understand defense-in-depth.
In production triage, false positive documentation also prevents the same finding from being re-triaged every sprint. It saves the entire team time.
- Save triage/triage-report.md.
- Commit and push your triage report by running these commands:
git add triage/
git commit -m "Add vulnerability triage report with remediation plan"
git push origin main
Git push rejected?
If your push is rejected, it may be because the remote has changes you don't have locally (GitHub sometimes initializes with a README). Pull first with git pull origin main and then try pushing again.
help me fix a rejected git push to my GitHub repository
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and pushed to GitHub.
ⓧ I'd like to double check the full code
Here is the complete triage/triage-report.md for reference:
# Vulnerability Triage Report - ShopSecure
## Scan Date
Pipeline run triggered by push to main branch.
## Tools Used
- Semgrep (SAST - static analysis)
- Gitleaks (secret detection)
- Custom rule: express-error-disclosure
---
## Finding 1: Hardcoded Stripe Live Secret Key
| Field | Value |
|-------|-------|
| Tool | Gitleaks |
| File | server.js |
| Line | 5 |
| Severity | **Critical** |
| Exploitability | High - key is in plaintext in a public/accessible repository |
| Business Context | Live payment API key enables unauthorized charges |
| Response | **Fix** |
| SLA | 24 hours |
| Owner | Developer |
| Fix | Rotate the key immediately. Move to environment variable. Add .env to .gitignore. |
---
## Finding 2: Hardcoded Database Password
| Field | Value |
|-------|-------|
| Tool | Gitleaks |
| File | server.js |
| Line | 6 |
| Severity | **High** |
| Exploitability | Medium - requires network access to database host |
| Business Context | Database contains user PII and payment records |
| Response | **Fix** |
| SLA | 7 days |
| Owner | Platform team |
| Fix | Move to environment variable. Rotate credential. Restrict DB network access. |
---
## Finding 3: SQL Injection in Products Endpoint
| Field | Value |
|-------|-------|
| Tool | Semgrep |
| File | server.js |
| Line | 13 |
| Severity | **Critical** |
| Exploitability | High - unauthenticated endpoint, user input directly in query |
| Business Context | Full database access including payment and user tables |
| Response | **Fix** |
| SLA | 24 hours |
| Owner | Developer |
| Fix | Use parameterized queries. Add input validation on category parameter. |
---
## Finding 4: Information Disclosure via Error Messages
| Field | Value |
|-------|-------|
| Tool | Semgrep (custom rule: express-error-disclosure) |
| File | server.js |
| Line | 30 |
| Severity | **Medium** |
| Exploitability | Medium - requires triggering an error condition |
| Business Context | Leaks internal hostnames, ports, and database usernames |
| Response | **Fix** |
| SLA | 30 days |
| Owner | Developer |
| Fix | Return generic error message to client. Log full error server-side only. |
---
## Finding 5: Open Redirect
| Field | Value |
|-------|-------|
| Tool | Semgrep |
| File | server.js |
| Line | 42 |
| Severity | **Medium** |
| Exploitability | Medium - requires victim to click a crafted link |
| Business Context | Enables phishing attacks using trusted domain |
| Response | **Mitigate** |
| SLA | 30 days |
| Owner | Developer |
| Fix | Validate redirect URL against allowlist of approved domains. |
---
## False Positive Analysis
### Payments Endpoint - Missing Authentication
Semgrep may flag the payments endpoint for missing authentication checks.
In this training context, this is a **true positive** (not a false positive)
because the endpoint genuinely lacks auth.
However, in a production context where this endpoint sits behind an API
gateway that enforces OAuth2 tokens before traffic reaches the service,
the finding would be a **false positive at the service level**. The rationale:
- The API gateway enforces authentication before requests reach this service
- Network policy restricts direct access to the payments service
- The finding is valid at the gateway level but not at the service level
This distinction matters because security teams waste time fixing findings
that are already mitigated by infrastructure controls.
---
## SLA Summary
| Severity | SLA Deadline | Count |
|----------|-------------|-------|
| Critical | 24 hours | 2 |
| High | 7 days | 1 |
| Medium | 30 days | 2 |
| Low | 90 days | 0 |
## Ownership Summary
| Owner | Findings |
|-------|----------|
| Developer | 4 (Findings 1, 3, 4, 5) |
| Platform team | 1 (Finding 2) |
| Security team | 0 |
You now have a professional triage report that demonstrates your ability to think critically about security findings. Next, you will analyze supply chain risks to understand how attackers compromise the dependencies your application trusts.
Analyze Supply Chain Risks
Your pipeline catches vulnerabilities in your code. But what about vulnerabilities injected through your dependencies?
Supply chain attacks target the packages your application depends on, not your application itself. In this step, you'll document the three major attack patterns, analyze a real-world compromise, and build a defense checklist that protects ShopSecure from dependency-level threats.
In this step, get ready to:
- Document three supply chain attack patterns with real-world examples.
- Analyze the axios compromise (March 2026) as a detailed case study.
- Create a supply chain defense checklist for ShopSecure.
Document the three major supply chain attack patterns
Supply chain attacks exploit the trust relationship between your application and its dependencies. Each pattern targets a different link in the chain.
- Create a supply-chain directory in your appsec-threat-model-lab repository by running this command in your VS Code terminal:
mkdir supply-chain
- Create a new file called supply-chain-analysis.md inside the supply-chain folder by running:
touch supply-chain/supply-chain-analysis.md
- Confirm the file exists by running:
ls supply-chain
You should see supply-chain-analysis.md in the output.
- Open supply-chain/supply-chain-analysis.md in VS Code by clicking on it in the Explorer sidebar.
- Add the document header and the first section documenting the three attack patterns by pasting the following content:
# Supply Chain Risk Analysis - ShopSecure
## Overview
This document analyzes supply chain attack patterns relevant to the ShopSecure
e-commerce application and provides a defense strategy.
## Attack Pattern 1: Dependency Confusion
### How It Works
An attacker publishes a malicious package on a public registry (like npm) using
the same name as an internal private package but with a higher version number.
When a misconfigured package manager resolves dependencies, it pulls the public
(malicious) version instead of the private (legitimate) one.
### Real-World Example
In May 2026, Microsoft disclosed 33 malicious npm packages across 9
organizational scopes using dependency confusion. The packages contained
obfuscated postinstall stagers that connected to command-and-control servers,
profiling developer environments.
### Detection Signals
- Package name matches an internal scope but exists on the public registry
- Version number is abnormally high (e.g., 99.0.0)
- Package was recently published with no prior history
- postinstall scripts that execute network calls or download binaries
### ShopSecure Relevance
If ShopSecure uses any private packages (e.g., @shopsecure/payments-sdk),
an attacker could publish a higher-versioned package to npm with the same name.
---
## Attack Pattern 2: Typosquatting
### How It Works
Bad actors publish malicious modules to the npm registry with names that look
like existing popular modules. They exploit typing errors or visual similarities
to trick developers into installing the wrong package.
### Real-World Example
In May 2026, 14 malicious npm packages were discovered impersonating
OpenSearch and ElasticSearch libraries. These packages harvested AWS
credentials, Vault tokens, and npm publish tokens from developer machines.
### Detection Signals
- Package name is a slight misspelling of a popular package
- Package name uses character substitution (l vs 1, O vs 0)
- Package has very low download counts despite a popular-sounding name
- Package has no README or minimal documentation
### ShopSecure Relevance
ShopSecure depends on express. An attacker could publish "expresss" or
"expres" with malicious code. A single typo in package.json installs the
wrong package.
---
## Attack Pattern 3: Maintainer Account Takeover
### How It Works
An attacker compromises the npm account of a legitimate package maintainer
(via credential stuffing, phishing, or token theft). They then publish a new
version of the legitimate package containing malicious code. Because the
package name and publisher are trusted, the update propagates silently.
### Detection Signals
- New version published after a long period of inactivity
- New hidden dependencies added that did not exist before
- postinstall scripts introduced where none existed previously
- Package behavior changes without corresponding changelog or PR
What does this document cover so far?
Each pattern targets a different link in the supply chain. Dependency confusion exploits how package managers resolve names. Typosquatting exploits human error during installation. Account takeover exploits the trust placed in legitimate publishers.
The real-world examples are from 2026 disclosures by Microsoft, demonstrating that these are active threats, not theoretical risks.
Why document attack patterns in a threat model?
Your STRIDE analysis in earlier steps focused on threats to your application's own code. Supply chain analysis extends that thinking to your dependencies. Interviewers test whether you understand that your attack surface includes everything you import, not just what you write.
Analyze the axios compromise as a case study
The axios compromise in March 2026 is a textbook example of maintainer account takeover. It demonstrates exactly why traditional security tooling fails against supply chain attacks.
- In supply-chain/supply-chain-analysis.md, add the axios case study section below the text you just pasted:
---
## Case Study: Axios Compromise (March 31, 2026)
### What Happened
On March 31, 2026, the axios npm package was compromised via maintainer
account hijacking. The attacker published versions 1.14.1 and 0.30.4
containing a hidden dependency called `plain-crypto-js`. This dependency
deployed a Remote Access Trojan (RAT) via its postinstall script.
Axios is one of the most popular HTTP client libraries in the npm ecosystem.
Any project that auto-updated to the compromised versions was infected.
### Why npm audit Missed It
`npm audit` queries the advisory database for known CVEs. It returns a clean
result if no CVEs have been filed, regardless of other supply chain signals.
At the time of the compromise:
- No CVE existed for the backdoored versions
- The `plain-crypto-js` dependency had no advisories filed against it
- npm audit reported zero vulnerabilities
The attack existed for weeks before any advisory was published. This is the
fundamental limitation: npm audit is a compliance tool that checks for
*known* vulnerabilities. It cannot detect *unknown* supply chain attacks.
### What Controls Would Have Caught It
| Control | How It Would Help |
|---------|-------------------|
| Lockfile + `npm ci` | Prevents auto-upgrade to compromised version |
| `ignore-scripts=true` | Blocks the RAT's postinstall execution |
| Dependency diff review | New hidden dependency visible in PR |
| Provenance verification | Unsigned/untrusted build origin flagged |
| Network egress monitoring | RAT C2 connection detected post-install |
### Key Takeaway
The axios compromise proves that `npm audit` alone is insufficient for supply
chain security. Organizations need layered defenses: lockfiles prevent
unexpected upgrades, ignore-scripts blocks payload execution, and provenance
verification validates package origin.
What makes this case study valuable for interviews?
Interviewers want to hear you explain why a tool failed, not just that it failed. The axios case lets you articulate a clear gap: npm audit only detects known CVEs, so zero-day supply chain attacks bypass it entirely.
Then you pivot to the layered controls that would have caught it. That demonstrates defensive thinking, which is exactly what hiring managers look for.
Wondering about the npm audit limitation?
If you're thinking "but I always run npm audit", that's the point. Many teams treat a clean audit as proof of security. The axios case shows that a clean audit only means no known CVEs exist. The attack was active for weeks before any advisory was filed.
Help me understand the difference between npm audit and supply chain security.
Create a supply chain defense checklist for ShopSecure
Now that you understand the attack patterns, you need to translate them into concrete defenses. A checklist gives your team actionable controls to implement.
- In supply-chain/supply-chain-analysis.md, add the defense checklist section at the bottom of the file:
---
## ShopSecure Supply Chain Defense Checklist
### 1. Use Lockfiles (package-lock.json)
- **What**: Always commit `package-lock.json` to version control.
- **Why**: Lockfiles pin exact dependency versions and integrity hashes.
Without a lockfile, `npm install` resolves the latest compatible version,
which could be a compromised release.
- **Action**: Verify `package-lock.json` exists in the repository root and is
never listed in `.gitignore`.
### 2. Use `npm ci` Instead of `npm install`
- **What**: In CI/CD pipelines and production deployments, always use `npm ci`.
- **Why**: `npm ci` installs exactly what the lockfile specifies. It fails if
`package-lock.json` is out of sync with `package.json`, catching unreviewed
dependency changes. `npm install` can silently update the lockfile.
- **Action**: Replace all `npm install` commands in scripts, Dockerfiles, and
CI workflows with `npm ci`.
### 3. Set `ignore-scripts=true`
- **What**: Disable automatic execution of package lifecycle scripts.
- **Why**: Most supply chain attacks deliver their payload via `postinstall`
scripts. Disabling scripts blocks execution of malicious code during install.
The axios RAT and dependency confusion stagers both relied on postinstall.
- **Action**: Add `ignore-scripts=true` to `.npmrc` in the project root.
Run `npm rebuild` only for packages that genuinely need native compilation.
### 4. Verify Package Provenance
- **What**: Check that packages were built from their claimed source repository.
- **Why**: Provenance attestations cryptographically link a published package
to a specific source commit and build system. A hijacked maintainer account
cannot produce valid provenance if the malicious code was never in the
source repo.
- **Action**: Use `npm audit signatures` to verify registry signatures.
Investigate any package that lacks provenance attestation before adopting it.
### 5. Review Dependency Diffs in Pull Requests
- **What**: Require human review of lockfile changes in every PR.
- **Why**: New dependencies, version bumps, and integrity hash changes are
visible in lockfile diffs. The axios compromise would have been visible as
an unexpected new dependency (`plain-crypto-js`) in a PR diff.
- **Action**: Configure GitHub branch protection to require approval on PRs
that modify `package-lock.json`.
---
## Summary
Supply chain attacks bypass traditional application security tooling because
they compromise trusted dependencies, not your own code. The three patterns
(dependency confusion, typosquatting, account takeover) each require different
detection strategies, but share a common defense: minimize trust in the
package ecosystem through lockfiles, script blocking, provenance verification,
and human review of dependency changes.
What does this checklist do?
Each control targets a specific step in the attack chain. Lockfiles prevent silent version upgrades. npm ci enforces the lockfile in automated environments. ignore-scripts blocks payload execution. Provenance verification catches packages built outside their claimed source.
Together, these create defense in depth. No single control stops every attack, but layered together they make successful exploitation significantly harder.
- Save supply-chain/supply-chain-analysis.md.
- Commit and push your supply chain analysis by running these commands:
git add supply-chain/
git commit -m "Add supply chain risk analysis document"
git push
You should see the push succeed with a confirmation message showing your commit on the main branch.
Push rejected or authentication error?
Make sure you're in the appsec-threat-model-lab directory when running the git commands. If you get an authentication error, check that your GitHub credentials are configured correctly in VS Code.
Help me fix a git push error in my VS Code terminal.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and that the push succeeded.
ⓧ I'd like to double check the full code
Here is the complete supply-chain/supply-chain-analysis.md file for comparison:
# Supply Chain Risk Analysis - ShopSecure
## Overview
This document analyzes supply chain attack patterns relevant to the ShopSecure
e-commerce application and provides a defense strategy.
## Attack Pattern 1: Dependency Confusion
### How It Works
An attacker publishes a malicious package on a public registry (like npm) using
the same name as an internal private package but with a higher version number.
When a misconfigured package manager resolves dependencies, it pulls the public
(malicious) version instead of the private (legitimate) one.
### Real-World Example
In May 2026, Microsoft disclosed 33 malicious npm packages across 9
organizational scopes using dependency confusion. The packages contained
obfuscated postinstall stagers that connected to command-and-control servers,
profiling developer environments.
### Detection Signals
- Package name matches an internal scope but exists on the public registry
- Version number is abnormally high (e.g., 99.0.0)
- Package was recently published with no prior history
- postinstall scripts that execute network calls or download binaries
### ShopSecure Relevance
If ShopSecure uses any private packages (e.g., @shopsecure/payments-sdk),
an attacker could publish a higher-versioned package to npm with the same name.
---
## Attack Pattern 2: Typosquatting
### How It Works
Bad actors publish malicious modules to the npm registry with names that look
like existing popular modules. They exploit typing errors or visual similarities
to trick developers into installing the wrong package.
### Real-World Example
In May 2026, 14 malicious npm packages were discovered impersonating
OpenSearch and ElasticSearch libraries. These packages harvested AWS
credentials, Vault tokens, and npm publish tokens from developer machines.
### Detection Signals
- Package name is a slight misspelling of a popular package
- Package name uses character substitution (l vs 1, O vs 0)
- Package has very low download counts despite a popular-sounding name
- Package has no README or minimal documentation
### ShopSecure Relevance
ShopSecure depends on express. An attacker could publish "expresss" or
"expres" with malicious code. A single typo in package.json installs the
wrong package.
---
## Attack Pattern 3: Maintainer Account Takeover
### How It Works
An attacker compromises the npm account of a legitimate package maintainer
(via credential stuffing, phishing, or token theft). They then publish a new
version of the legitimate package containing malicious code. Because the
package name and publisher are trusted, the update propagates silently.
### Detection Signals
- New version published after a long period of inactivity
- New hidden dependencies added that did not exist before
- postinstall scripts introduced where none existed previously
- Package behavior changes without corresponding changelog or PR
---
## Case Study: Axios Compromise (March 31, 2026)
### What Happened
On March 31, 2026, the axios npm package was compromised via maintainer
account hijacking. The attacker published versions 1.14.1 and 0.30.4
containing a hidden dependency called `plain-crypto-js`. This dependency
deployed a Remote Access Trojan (RAT) via its postinstall script.
Axios is one of the most popular HTTP client libraries in the npm ecosystem.
Any project that auto-updated to the compromised versions was infected.
### Why npm audit Missed It
`npm audit` queries the advisory database for known CVEs. It returns a clean
result if no CVEs have been filed, regardless of other supply chain signals.
At the time of the compromise:
- No CVE existed for the backdoored versions
- The `plain-crypto-js` dependency had no advisories filed against it
- npm audit reported zero vulnerabilities
The attack existed for weeks before any advisory was published. This is the
fundamental limitation: npm audit is a compliance tool that checks for
*known* vulnerabilities. It cannot detect *unknown* supply chain attacks.
### What Controls Would Have Caught It
| Control | How It Would Help |
|---------|-------------------|
| Lockfile + `npm ci` | Prevents auto-upgrade to compromised version |
| `ignore-scripts=true` | Blocks the RAT's postinstall execution |
| Dependency diff review | New hidden dependency visible in PR |
| Provenance verification | Unsigned/untrusted build origin flagged |
| Network egress monitoring | RAT C2 connection detected post-install |
### Key Takeaway
The axios compromise proves that `npm audit` alone is insufficient for supply
chain security. Organizations need layered defenses: lockfiles prevent
unexpected upgrades, ignore-scripts blocks payload execution, and provenance
verification validates package origin.
---
## ShopSecure Supply Chain Defense Checklist
### 1. Use Lockfiles (package-lock.json)
- **What**: Always commit `package-lock.json` to version control.
- **Why**: Lockfiles pin exact dependency versions and integrity hashes.
Without a lockfile, `npm install` resolves the latest compatible version,
which could be a compromised release.
- **Action**: Verify `package-lock.json` exists in the repository root and is
never listed in `.gitignore`.
### 2. Use `npm ci` Instead of `npm install`
- **What**: In CI/CD pipelines and production deployments, always use `npm ci`.
- **Why**: `npm ci` installs exactly what the lockfile specifies. It fails if
`package-lock.json` is out of sync with `package.json`, catching unreviewed
dependency changes. `npm install` can silently update the lockfile.
- **Action**: Replace all `npm install` commands in scripts, Dockerfiles, and
CI workflows with `npm ci`.
### 3. Set `ignore-scripts=true`
- **What**: Disable automatic execution of package lifecycle scripts.
- **Why**: Most supply chain attacks deliver their payload via `postinstall`
scripts. Disabling scripts blocks execution of malicious code during install.
The axios RAT and dependency confusion stagers both relied on postinstall.
- **Action**: Add `ignore-scripts=true` to `.npmrc` in the project root.
Run `npm rebuild` only for packages that genuinely need native compilation.
### 4. Verify Package Provenance
- **What**: Check that packages were built from their claimed source repository.
- **Why**: Provenance attestations cryptographically link a published package
to a specific source commit and build system. A hijacked maintainer account
cannot produce valid provenance if the malicious code was never in the
source repo.
- **Action**: Use `npm audit signatures` to verify registry signatures.
Investigate any package that lacks provenance attestation before adopting it.
### 5. Review Dependency Diffs in Pull Requests
- **What**: Require human review of lockfile changes in every PR.
- **Why**: New dependencies, version bumps, and integrity hash changes are
visible in lockfile diffs. The axios compromise would have been visible as
an unexpected new dependency (`plain-crypto-js`) in a PR diff.
- **Action**: Configure GitHub branch protection to require approval on PRs
that modify `package-lock.json`.
---
## Summary
Supply chain attacks bypass traditional application security tooling because
they compromise trusted dependencies, not your own code. The three patterns
(dependency confusion, typosquatting, account takeover) each require different
detection strategies, but share a common defense: minimize trust in the
package ecosystem through lockfiles, script blocking, provenance verification,
and human review of dependency changes.
You now have a complete supply chain risk analysis that goes beyond what most junior security engineers produce. This document proves you understand that application security extends past your own code into your entire dependency tree. Ready for a bonus challenge? The Secret Mission lets you connect your threat model findings directly to automated detection.
Secret mission
Write a Custom Semgrep Rule From Your Threat Model
Your threat model identified risks. Your pipeline catches generic vulnerabilities. But what if you could connect the two? In this secret mission, you'll write a custom Semgrep rule that detects a specific vulnerability pattern from your STRIDE analysis, then watch your pipeline catch it automatically.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and on GitHub's free tier, so there are no ongoing costs. Decide whether to keep your resources as a portfolio piece or delete them entirely.
Resources you used:
- GitHub repository appsec-threat-model-lab (with GitHub Actions workflows).
- Local project folder appsec-threat-model-lab on your Desktop.
Keep everything running
No action needed. Choose this if you want to keep the repository as a portfolio piece demonstrating your threat modeling and DevSecOps skills.
- Your GitHub Actions pipeline continues to run on every push at no cost (public repos get unlimited minutes, private repos get 2,000 free minutes per month).
- Your local files stay on your machine and take up minimal disk space.
- Your threat model documents and triage reports remain accessible for interview prep.
Pause - I'll come back to this later
There are no running processes or billable services to pause. Your resources are already idle when you are not actively pushing code.
- If the Node.js server is still running in your terminal, press Ctrl+C to stop it.
- Everything else stays as-is until you come back.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow both steps below to clean up completely.
- Delete the GitHub repository:
- Open your appsec-threat-model-lab repository on GitHub.
- Click Settings in the repository navigation bar.
- Scroll down to the Danger Zone section and click Delete this repository.
- Click I want to delete this repository.
- Click I have read and understand these effects.
- Type appsec-threat-model-lab in the confirmation box and click Delete this repository.
Delete your local project folder by running the command for your operating system:
macOS
rm -rf ~/Desktop/appsec-threat-model-lab
Windows
rmdir /s /q "$HOME\Desktop\appsec-threat-model-lab"
Nice Work!
Nice Work!
You've just completed two core app sec workflows end-to-end. You now have a professional-grade threat model and an automated DevSecOps pipeline that proves you can both reason about risk and implement security automation.
You've learned how to:
- Threat modeled a complex microservices architecture using STRIDE per Element, complete with Data Flow Diagrams, attack trees, and a risk-scored threat register with prioritized mitigations.
- Built an automated security pipeline with Semgrep (SAST) and Gitleaks (secret detection) that catches real vulnerabilities on every push to GitHub.
- Triaged security findings, built a remediation plan with SLA deadlines, and analyzed real-world supply chain attacks including dependency confusion, typosquatting, and maintainer account takeover.
- Wrote a custom Semgrep rule that connects a specific threat model finding to automated pipeline detection.
Ready to quiz yourself?