Build a DevSecOps Pipeline
Build a CI/CD pipeline with SAST, SCA, secrets, and IaC security gates.
Introduction
30 Second Summary
Every team that ships software faces the same risk. Vulnerabilities sneak into application code, secrets leak into repositories, and misconfigured cloud infrastructure sits exposed until someone exploits it.
In this project, you will build a DevSecOps pipeline that runs four parallel security scanners every time you push code to GitHub, failing the build on critical findings. You also produce a professional gap analysis documenting what each scanner detects and what they all miss.
What You'll Build
You push a commit and within seconds, four parallel scanners flag SQL injection in your Python code, leaked credentials in your git history, outdated dependencies in your requirements file, and public S3 buckets in your Terraform. The pipeline goes red. Your gap analysis proves you know exactly why.
By the end of this project, you'll have:
- A GitHub Actions pipeline that runs Semgrep, Trivy, Gitleaks, and Checkov in parallel on every push, independently catching different vulnerability classes and failing the build on critical findings.
- A deliberately vulnerable Python FastAPI application and set of Terraform files where you can point to each planted flaw and explain which scanner detects it at the code level.
- A professional gap analysis document mapping scanner coverage against vulnerability classes, identifying blind spots no tool catches, and recommending compensating controls.
- Secret Mission: Write a custom Semgrep rule that catches a vulnerability class the default rulesets miss.
Are there any prerequisites?
You need a GitHub account, Docker installed on your machine, and Python 3 available locally. Familiarity with basic CI/CD concepts and Terraform syntax will help, but each concept is explained as it appears.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up the Project Repository and Local Tools
A DevSecOps pipeline needs two things before it can run: a repository where GitHub Actions will execute the workflow, and local copies of the security scanners so you can iterate quickly without waiting for CI.
In this step, you will create the repository structure, then install three command-line security tools locally. These tools will eventually run in the pipeline, but having them on your machine first lets you validate findings before pushing code.
In this step, get ready to:
- Create and clone the GitHub repository with a project directory structure.
- Install Semgrep, Trivy, and Checkov locally.
- Verify all three scanners respond to version checks.
Create the GitHub repository and project structure
Your security pipeline needs a public GitHub repository so that GitHub Actions can run the workflow for free.
- Open your browser and go to github.com/new.
- Enter devsecops-pipeline as the repository name.
- Select Public for visibility.
- Leave all other options unchecked (no README, no .gitignore, no license).
- Click Create repository.
Why a public repository?
GitHub Actions provides 2,000 free minutes per month for public repositories. A private repo would consume paid minutes from your account's allowance.
- Open your terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone the repository by running this command:
git clone https://github.com/[[GITHUB_USER="your-github-username"]]/devsecops-pipeline.git
- Move into the cloned repository by running:
cd devsecops-pipeline
- Create the project directory structure by running this command:
mkdir -p app terraform .github/workflows docs
What does this command do?
The -p flag tells mkdir to create parent directories as needed. Since .github/workflows is nested, the flag creates the .github parent automatically.
- Confirm the directories exist by running:
ls -la
You should see app, terraform, .github, and docs listed in the output.
Don't see the .github folder?
Directories starting with a dot are hidden by default. The -la flags on ls force hidden files to appear. If you ran ls without -la, try ls -la instead.
help me see hidden directories in my terminal
Install Semgrep and Trivy
Semgrep is a static analysis tool that scans source code for vulnerability patterns. It installs through pipx, a tool that runs Python applications in isolated environments.
- Install pipx by running these commands:
python3 -m pip install --user pipx
python3 -m pipx ensurepath
What does ensurepath do?
The ensurepath command adds the directory where pipx installs tools to your system PATH. Without it, your terminal would not find commands installed through pipx.
- Close and reopen your terminal so the PATH changes take effect.
- Navigate back to your project folder by running:
cd ~/Desktop/devsecops-pipeline
- Install Semgrep by running this command:
pipx install semgrep
- Verify Semgrep installed successfully by running:
semgrep --version
You should see a version number printed (e.g. 1.170.0 or similar).
Seeing 'command not found' for semgrep?
Make sure you restarted your terminal after running ensurepath. If you still see the error, try running python3 -m pipx ensurepath again, then open a fresh terminal window.
help me fix pipx or semgrep not found on my PATH
Next, install Trivy. Trivy handles software composition analysis by scanning your dependencies for known vulnerabilities.
- Install Trivy v0.72.0 by running the official install script:
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin v0.72.0
What does this command do?
- curl -sfL downloads the install script silently (no progress bar, follows redirects).
- The pipe (|) sends the script directly to sh for execution.
- -b /usr/local/bin tells the installer where to place the Trivy binary.
- v0.72.0 pins the exact version so your results match this project.
- Verify the Trivy installation by running:
trivy --version
✔️ I see version 0.72.0
Trivy is installed at the correct version. You are ready to continue.
ⓧ I see a different version
You may have a pre-existing Trivy installation that takes priority on your PATH.
- Check which binary is being used by running:
which trivy
If the output is NOT /usr/local/bin/trivy, remove or rename the other binary, then re-run the install script above.
- After fixing, confirm by running trivy --version again.
ⓧ Command not found
The install script may have failed or requires sudo permissions.
- Check that /usr/local/bin is on your PATH by running:
echo $PATH
- If /usr/local/bin does not appear in the output, add it to your shell profile (~/.bashrc or ~/.zshrc) and restart your terminal.
- If it does appear, re-run the install script with sudo and check for error messages in the output.
help me install Trivy on my Linux machine
Install Checkov
Checkov is an infrastructure-as-code scanner. It checks Terraform, CloudFormation, and Kubernetes manifests against over 1,000 security policies.
- Install Checkov by running:
pip3 install checkov
- Verify the installation by running:
checkov --version
You should see a version number in the output (e.g. 3.3.8 or similar).
Seeing an externally-managed-environment error?
Some Linux distributions restrict system-wide pip installs. If you see this error, create a virtual environment first:
python3 -m venv ~/checkov-env
source ~/checkov-env/bin/activate
pip3 install checkov
You will need to activate this environment (source ~/checkov-env/bin/activate) each time you open a new terminal to use checkov.
help me fix the externally-managed-environment error when installing Checkov
Before you move on, predict: if you run all three version commands one after another, what do you expect to see?
- Confirm all three tools are ready by running:
semgrep --version && trivy --version && checkov --version
You should see three version numbers printed, one per line. This confirms Semgrep, Trivy, and Checkov are all installed and accessible from your terminal.
✔️ Awesome, I've got everything!
All three scanners are installed. Your local environment is ready for security scanning.
ⓧ I'd like to double check the full code
At this point you should have:
- A cloned devsecops-pipeline repo at ~/Desktop/devsecops-pipeline.
- Four directories inside: app/, terraform/, .github/workflows/, docs/.
- semgrep --version prints a version number.
- trivy --version prints 0.72.0.
- checkov --version prints a version number.
If any of these are missing, scroll up to the relevant install section and follow the troubleshooting callout.
Your local environment is armed with three security scanners. Next up, you will build a deliberately vulnerable Python application that gives these scanners something real to detect.
Build the Deliberately Vulnerable Application
Your tools are installed and your repository structure is ready. Now you need something for those scanners to actually find.
Running security scanners against clean code proves nothing. In this step, you will build a FastAPI application with deliberately planted vulnerabilities that map to real-world OWASP Top 10 weaknesses. Building the vulnerabilities yourself proves you understand vulnerability classes at a code level.
In this step, get ready to:
- Create a FastAPI application with planted SQL injection, hardcoded credentials, and command injection vulnerabilities.
- Add an intentionally outdated dependency file for later SCA scanning.
- Run Semgrep locally to confirm it detects the planted vulnerabilities.
Create the vulnerable FastAPI application
You will plant three distinct vulnerability classes from the OWASP Top 10 into a single Python file. Each vulnerability maps to a specific CWE (Common Weakness Enumeration) identifier that security scanners use for classification.
- Create the app/main.py file with the application setup, hardcoded credentials, and a database helper by pasting this code:
import os
import sqlite3
from fastapi import FastAPI, Form, Query
app = FastAPI()
# Hardcoded credentials - CWE-798
DB_USER = "admin"
DB_PASSWORD = "SuperSecret123!"
DB_HOST = "prod-db.internal.company.com"
# Fake AWS key for Gitleaks to detect
# AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
def get_db():
return sqlite3.connect(":memory:")
What does this code do?
This sets up the first vulnerability: hardcoded credentials (CWE-798). The DB_USER, DB_PASSWORD, and DB_HOST constants store sensitive database connection details directly in source code.
The commented-out AWS key (AKIAIOSFODNN7EXAMPLE) is planted for Gitleaks to detect later. Even inside a comment, secret scanners flag patterns that match known credential formats.
- Save app/main.py.
- Confirm the file exists by running:
ls app/
You should see main.py listed in the output.
Don't see main.py?
Make sure you created the file inside the app/ directory, not in the repository root. Your terminal should be inside the devsecops-pipeline folder when you run ls app/.
help me find where my main.py file ended up
- Add the login endpoint with a SQL injection vulnerability below the get_db function in app/main.py:
@app.post("/login")
def login(username: str = Form(...), password: str = Form(...)):
"""SQL Injection - CWE-89"""
db = get_db()
cursor = db.cursor()
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
result = cursor.fetchone()
if result:
return {"status": "authenticated", "user": result[0]}
return {"status": "failed"}
What does this code do?
This is SQL injection (CWE-89). The f-string on the query line drops user input directly into the SQL statement without any sanitization or parameterized queries.
An attacker could type ' OR '1'='1 as the username to bypass authentication entirely. Semgrep's taint analysis rules detect this pattern because untrusted input flows directly into a database query.
- Save app/main.py.
- Run Semgrep against the app directory to see if it already catches your planted vulnerabilities:
semgrep scan --config auto app/
Semgrep should already report findings for the SQL injection and potentially the hardcoded credentials. You will see output listing the matched rules with severity levels.
Semgrep shows no findings?
Make sure you saved app/main.py before running the scan. Also confirm the f-string SQL query uses curly braces: f"SELECT * FROM users WHERE username = '{username}'". Without the f prefix, it is just a regular string and Semgrep will not flag it.
help me debug why Semgrep isn't finding my SQL injection
- Add the command injection endpoint and a health check endpoint below the login function in app/main.py:
@app.get("/ping")
def ping(host: str = Query(...)):
"""Command Injection - CWE-78"""
result = os.system(f"ping -c 1 {host}")
return {"status": "ok", "exit_code": result}
@app.get("/health")
def health():
return {"status": "healthy", "version": "1.0.0"}
What does this code do?
The /ping endpoint is command injection (CWE-78). It passes user-supplied input directly into os.system(), which executes shell commands. An attacker could send google.com; cat /etc/passwd to run arbitrary commands on the server.
The /health endpoint is clean. It demonstrates that not every endpoint needs to be vulnerable.
- Save app/main.py. Your complete application now contains all three vulnerability classes.
✔️ Awesome, I've got everything!
Great. Double check you have saved app/main.py with all three sections: the setup and credentials at the top, the login endpoint in the middle, and the ping and health endpoints at the bottom.
ⓧ I'd like to double check the full code
Your app/main.py should have these sections in order. Scroll up and confirm each one is present:
- Imports (os, sqlite3, FastAPI, Form, Query) and app = FastAPI() instance.
- Hardcoded credentials: DB_USER, DB_PASSWORD, DB_HOST, and the commented-out AWS key.
- The get_db() helper function returning an in-memory SQLite connection.
- The login endpoint with the f-string SQL query.
- The ping endpoint with os.system().
- The health endpoint returning a status JSON.
Add the dependency file
Security scanners also check your dependencies for known vulnerabilities. By pinning to intentionally outdated versions, you give Trivy something to flag during SCA (Software Composition Analysis) scanning later.
- Create app/requirements.txt with these pinned dependency versions:
fastapi==0.115.0
uvicorn==0.30.0
Why pin to older versions?
Version 0.115.0 of FastAPI and 0.30.0 of uvicorn are intentionally behind the latest releases. Trivy's SCA scanner compares your pinned versions against its vulnerability database and flags any packages with known CVEs.
In a real project, outdated dependencies are one of the most common sources of exploitable vulnerabilities.
- Save app/requirements.txt.
- Confirm both files exist by running:
ls app/
You should see both main.py and requirements.txt listed.
Verify vulnerability detection with Semgrep
Your application now has three planted vulnerability classes. Time to prove the scanner catches them.
Before you run the scan, predict: how many findings do you think Semgrep will report? Will it catch all three vulnerability types (SQL injection, hardcoded credentials, command injection), or will it miss some?
- Run a full Semgrep scan against the app directory:
semgrep scan --config auto app/
Semgrep reports at least 3 findings. You should see detections for SQL injection (the f-string query in login) and hardcoded credentials (the DB_PASSWORD constant). The command injection via os.system() in the ping endpoint should also appear.
What should I see?
Semgrep's output lists each finding with: the file path, the line number, the matched rule name, the severity level, and a snippet of the flagged code. Look for rules in the security category referencing CWE-89 (SQL injection), CWE-798 (hardcoded credentials), and CWE-78 (command injection).
Take note of which findings Semgrep catches and which it might miss. This observation feeds directly into the gap analysis you will write in Step 5.
Seeing fewer than 3 findings?
Semgrep's --config auto downloads rules from the Semgrep registry on first run, so you need an internet connection. If you see a network error, check your connectivity and try again.
If Semgrep runs but only shows 1-2 findings, confirm your app/main.py has all three endpoints (/login, /ping, /health) and that the f-strings use curly braces for variable interpolation.
help me understand my Semgrep scan results
Your vulnerable application is ready and confirmed detectable. Next, you will add misconfigured Terraform files to give Checkov something to flag on the infrastructure side.
Add Misconfigured Terraform and Trigger Checkov Failures
In the previous step, you proved that Semgrep catches application-level vulnerabilities in Python code. Now it is time to shift focus from application security to cloud infrastructure security.
AppSec engineers must also spot cloud infrastructure risk. In this step, you will create intentionally misconfigured Terraform files that represent common AWS anti-patterns. Then you will run Checkov against them to verify it catches the misconfigurations before they ever reach a real cloud account.
In this step, get ready to:
- Create a Terraform file with intentionally insecure AWS resources (public S3 bucket, overly permissive IAM, unencrypted RDS).
- Run Checkov locally and observe which misconfigurations it catches.
Create the insecure Terraform configuration
Infrastructure as Code lets teams define cloud resources in version-controlled files. That also means a misconfigured resource can be caught by a scanner before it is ever deployed.
- Create terraform/main.tf with the AWS provider, an intentionally insecure S3 bucket, and an overly permissive IAM policy by pasting the following code:
provider "aws" {
region = "us-east-1"
}
# Insecure S3 bucket - no encryption, no versioning, public ACL
resource "aws_s3_bucket" "data" {
bucket = "company-sensitive-data-bucket"
acl = "public-read"
tags = {
Environment = "production"
}
}
# Overly permissive IAM policy - CKV_AWS_62, CKV_AWS_63
resource "aws_iam_policy" "admin" {
name = "overly-permissive-policy"
description = "This policy grants full access to everything"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "*"
Resource = "*"
}
]
})
}
What does this code do?
The provider block tells Terraform you are targeting AWS in us-east-1.
The aws_s3_bucket resource uses acl = "public-read" which makes the bucket contents readable by anyone on the internet. It also has no encryption configuration and no versioning, both of which Checkov expects to see on production buckets.
The aws_iam_policy grants Action: * on Resource: *. This means full unrestricted access to every AWS service and resource.
- Save terraform/main.tf.
- Confirm the file exists by running:
ls terraform/
You should see main.tf listed in the output.
- Now add the insecure RDS instance below the IAM policy block in terraform/main.tf:
# RDS instance with no encryption and publicly accessible
resource "aws_db_instance" "database" {
allocated_storage = 20
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
db_name = "appdb"
username = "admin"
password = "plaintext-password-123"
skip_final_snapshot = true
publicly_accessible = true
storage_encrypted = false
}
What does this RDS resource do?
This aws_db_instance has three critical security problems. The password is stored in plaintext in the code. The database is publicly accessible from the internet. Storage encryption is explicitly disabled.
In a real environment, any one of these would be a critical finding. Together they represent the kind of shortcuts Checkov is designed to catch before deployment.
- Save terraform/main.tf.
✔️ Awesome, I've got everything!
Great. Double check you have saved terraform/main.tf and move on to running Checkov.
ⓧ I'd like to double check the full code
Scroll up and confirm your terraform/main.tf contains all three sections in this order:
- The provider "aws" block and the aws_s3_bucket "data" resource with acl = "public-read".
- The aws_iam_policy "admin" resource with Action = "*" and Resource = "*".
- The aws_db_instance "database" resource with publicly_accessible = true and storage_encrypted = false.
Run Checkov and observe the failures
With the Terraform file in place, you can now scan it with Checkov to see which misconfigurations it flags.
Before you run the scan, think about this: how many FAILED checks do you expect Checkov to report for these three resources?
- Run Checkov against the terraform/ directory by running:
checkov -d terraform/
Checkov reports multiple FAILED checks. You should see failures for the S3 bucket (public access, no versioning) and the IAM policy (wildcard actions), among others.
What are those CKV_AWS check IDs?
Each failed check has an ID like CKV_AWS_20 or CKV_AWS_21. These map to specific security policies that Checkov enforces. For example, CKV_AWS_20 checks that S3 buckets do not have a public ACL, and CKV_AWS_21 checks that versioning is enabled.
Take note of the specific check IDs and their descriptions in the output. You will reference these in the gap analysis document later.
Not seeing any FAILED checks?
Make sure you saved terraform/main.tf and that you are running the command from the root of your devsecops-pipeline project directory.
Confirm Checkov is installed by running checkov --version. If the command is not found, reinstall with pip3 install checkov.
Help me troubleshoot why Checkov is not reporting failures
Your infrastructure scanner is catching real misconfigurations before they ever touch AWS. Next up, you will wire Semgrep, Trivy, Gitleaks, and Checkov into a GitHub Actions pipeline that enforces security gates on every push.
Wire Up the GitHub Actions Pipeline with Security Gates
Running scanners locally proved that your planted vulnerabilities are detectable. The real value comes from automated enforcement on every commit.
In this step, you will create a GitHub Actions workflow that runs all four scanners in parallel and fails the build when it finds HIGH or CRITICAL issues. This is the pipeline architecture that interviewers ask about.
In this step, get ready to:
- Create a GitHub Actions workflow with four parallel security scanner jobs.
- Push all project code to trigger the pipeline automatically.
- Confirm that security gates fail the build on your planted vulnerabilities.
Create the security scanning workflow
GitHub Actions workflows live in YAML files inside the .github/workflows/ directory. When you push code, GitHub reads these files and runs the jobs you define.
- Create a new file at .github/workflows/security.yml and add the workflow trigger and first two scanner jobs:
name: DevSecOps Pipeline
on:
push:
branches: ["*"]
pull_request:
branches: [main]
jobs:
sast:
name: SAST - Semgrep
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v6
- run: semgrep scan --config auto --error .
secrets:
name: Secrets - 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 this code do?
The on: block triggers this workflow on every push to any branch and on pull requests to main.
- The sast job runs inside the official semgrep/semgrep container. The --error flag makes Semgrep exit with code 1 when it finds issues, which fails the GitHub Actions step.
- The secrets job uses fetch-depth: 0 to clone the full git history. Gitleaks scans all previous commits for leaked secrets, not just the latest snapshot.
- GITHUB_TOKEN is automatically provided by GitHub Actions. You do not need to create or configure it yourself.
- Now add the SCA and IaC scanner jobs below the secrets job in the same file:
sca:
name: SCA - Trivy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Run Trivy filesystem scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
scan-ref: .
exit-code: "1"
severity: HIGH,CRITICAL
iac:
name: IaC - Checkov
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Checkov
run: pip3 install checkov
- name: Run Checkov
run: checkov -d terraform/ --hard-fail-on HIGH
What does this code do?
- The sca job uses the Trivy GitHub Action to scan your filesystem for vulnerable dependencies. exit-code: "1" means it fails the build when HIGH or CRITICAL vulnerabilities are found.
- The iac job installs Checkov at runtime and scans the terraform/ directory. The --hard-fail-on HIGH flag only fails the build for HIGH severity and above, avoiding noise from low-priority findings.
- All four jobs run in parallel because they are defined at the same level under jobs:. This is faster than running them sequentially and matches production best practice.
Why parallel jobs instead of sequential steps?
If scanners ran one after another, a single scanner failure would block the rest from running. Parallel jobs ensure you get findings from ALL scanners on every push, even if one fails early. You see the full security picture in one pipeline run.
- Save .github/workflows/security.yml.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Your .github/workflows/security.yml should contain these sections in order. Scroll up and confirm each is present:
- The name: and on: trigger block at the top.
- The sast job using the semgrep/semgrep container with --config auto --error ..
- The secrets job using gitleaks/gitleaks-action@v3 with fetch-depth: 0.
- The sca job using aquasecurity/trivy-action@master with exit-code: "1" and severity: HIGH,CRITICAL.
- The iac job running checkov -d terraform/ --hard-fail-on HIGH.
YAML indentation issues?
YAML is whitespace-sensitive. Each job must be indented with exactly 2 spaces under jobs:. Steps are indented 6 spaces from the left margin. If your editor uses tabs instead of spaces, switch to spaces in your editor settings.
help me fix YAML indentation in my GitHub Actions workflow
Push the code to trigger the pipeline
Your app/main.py already contains a commented fake AWS key (AKIAIOSFODNN7EXAMPLE) from the previous step. Gitleaks will scan git history and detect this pattern as a potential leaked credential.
- Stage, commit, and push all project files to GitHub by running these commands:
git add .
git commit -m "Add security scanning pipeline"
git push origin main
What does this do?
This commits your vulnerable app, Terraform files, and the workflow YAML in one push. The moment GitHub receives the push, it reads .github/workflows/security.yml and starts running all four scanner jobs in parallel.
Push rejected or authentication failed?
- If you see a permission error, confirm you cloned with HTTPS and have a personal access token configured, or that you cloned with SSH and your key is added to your GitHub account.
- If the push is rejected because the remote has changes, run git pull --rebase origin main first, then push again.
help me fix git push authentication issues
Confirm the security gates failed the build
Before you check, predict what you expect to see. Do you think the pipeline will pass or fail? Which of the four jobs do you think will report findings?
- Open your repository on GitHub in your browser.
- Click the Actions tab at the top of the repository page.
- Click the latest workflow run titled Add security scanning pipeline.
You should see a red X next to the workflow run. All four jobs show as failed because your planted vulnerabilities and misconfigurations triggered the security gates.
- Click on the SAST - Semgrep job to see its output. You should see findings for SQL injection, hardcoded credentials, and command injection.
- Click on the IaC - Checkov job. You should see failed checks for the public S3 bucket, wildcard IAM policy, and unencrypted RDS instance.
Why does each job fail independently?
Because jobs run in parallel, each one exits with its own status code. Semgrep fails because --error triggers exit code 1 on findings. Trivy fails because exit-code: "1" is set for HIGH/CRITICAL. Checkov fails because --hard-fail-on HIGH exits non-zero for high-severity checks. These are your security gates. Code cannot merge while any gate is red.
Workflow not appearing in the Actions tab?
- Confirm the file is at exactly .github/workflows/security.yml. A misplaced directory (like github/ without the leading dot) will not trigger.
- Check that your push succeeded by running git log --oneline -1 and confirming the commit appears on GitHub.
- If the workflow appears but a job shows queued for more than a minute, GitHub Actions runners may be busy. Wait and refresh the page.
help me troubleshoot why my GitHub Actions workflow is not running
Every commit now runs through four layers of security scanning before it can pass. Next up, you will analyze what these scanners caught and, more importantly, what they missed.
Produce the Gap Analysis and Remediation Document
Your pipeline ran and failed. Every scanner caught the vulnerabilities you planted. That proves the tools work.
But any engineer can run a scanner. What separates AppSec engineers from scanner operators is knowing what the tools miss and recommending compensating controls. In this step, you produce the gap analysis document that demonstrates that security thinking.
In this step, get ready to:
- Map each vulnerability class to the scanner that detected it in a coverage matrix.
- Identify blind spots that no scanner caught and recommend compensating controls.
- Document your pipeline architecture decisions for interviewers.
Review scanner findings and create the coverage matrix
The coverage matrix is the centrepiece of your gap analysis. Each row is a vulnerability class you planted, and each column is one of the four scanners.
- Switch to the GitHub Actions tab in your browser.
- Click on the failed workflow run to review each scanner job's output.
- Note which scanner caught which vulnerability. Compare this with the local output you saw from semgrep scan --config auto app/ and checkov -d terraform/ in the previous steps.
What am I looking for?
You already know the answers because you planted the vulnerabilities yourself. The goal here is to confirm which scanner reported each one, so your matrix is backed by evidence from real pipeline output.
- Create docs/gap-analysis.md and add the file header and coverage matrix table by pasting this content:
# Security Scanner Gap Analysis
## Coverage Matrix
| Vulnerability Class | CWE | Semgrep (SAST) | Trivy (SCA) | Gitleaks (Secrets) | Checkov (IaC) |
|---|---|---|---|---|---|
| SQL Injection | CWE-89 | Detected | - | - | - |
| Hardcoded Credentials (code) | CWE-798 | Detected | - | - | - |
| Command Injection | CWE-78 | Detected | - | - | - |
| Leaked AWS Key | CWE-798 | - | - | Detected | - |
| Outdated Dependencies | - | - | Detected | - | - |
| Public S3 Bucket | - | - | - | - | Detected |
| Overly Permissive IAM | - | - | - | - | Detected |
| Unencrypted RDS | - | - | - | - | Detected |
| Broken Access Control | CWE-284 | Not Detected | - | - | - |
| Business Logic Flaws | - | Not Detected | - | - | - |
What does this table show?
Each row is a vulnerability class from your project. The columns show which scanner (if any) detected it. A dash means the scanner is not designed to catch that class.
The last two rows are the critical insight. "Not Detected" means you looked for detection and confirmed no scanner caught it. These become your identified gaps.
- Save docs/gap-analysis.md.
- Confirm the file was created by running:
cat docs/gap-analysis.md
You should see your full coverage matrix printed in the terminal.
File not found?
Make sure you created the file inside the docs/ directory that already exists from Step 1. If you accidentally placed it at the repo root, move it with mv gap-analysis.md docs/.
Help me find where my file ended up
Document blind spots with risk and recommendation pairs
The coverage matrix shows what your scanners caught. The gaps section shows what they cannot catch. This is the part interviewers care about most.
- Add the following gaps section below your coverage matrix in docs/gap-analysis.md:
## Identified Gaps
### Gap 1: Broken Access Control (CWE-284)
- Risk: No scanner detects missing authorization checks on endpoints.
- Recommendation: Add authenticated DAST scanning in a staging environment. Supplement with manual threat modeling.
### Gap 2: Business Logic Flaws
- Risk: Scanners cannot detect application-specific logic errors.
- Recommendation: Manual penetration testing, threat modeling workshops, and application-specific test cases.
### Gap 3: Insecure Cryptography Detection
- Risk: Default SAST rules may miss subtle crypto issues.
- Recommendation: Add custom Semgrep rules for crypto patterns. Consider Bandit for Python-specific crypto checks.
Why three gaps?
Gap 1 (broken access control) is the OWASP #1 risk that no static scanner can reliably detect. It requires runtime context. Gap 2 (business logic) is fundamentally beyond automated tooling. Gap 3 (crypto) shows you know the default rulesets have limits you can extend.
Each gap pairs a risk statement with a concrete recommendation. This structure shows an interviewer you think in terms of compensating controls, not just tool output.
- Save docs/gap-analysis.md.
- Verify the gaps section was added correctly by running:
grep -c "^### Gap" docs/gap-analysis.md
You should see 3 printed, confirming all three gap sections are present.
Add pipeline architecture decisions
The final section explains your engineering choices. Interviewers ask "why these tools?" and "what would you add in production?" This section pre-answers both.
- Add the architecture decisions section at the bottom of docs/gap-analysis.md:
## Pipeline Architecture Decisions
### Tool Selection Rationale
- Semgrep: Fast, low false-positive rate for SAST. Supports custom rules.
- Trivy: Single binary handling SCA, container scanning, and IaC.
- Gitleaks: Purpose-built for secret detection. Scans git history.
- Checkov: 1000+ built-in policies for AWS/Azure/GCP.
### Gate Strategy
- All scanners run in parallel for faster feedback and independent failures.
- Gates fail on HIGH/CRITICAL only to avoid blocking on low-severity noise.
### Production Enhancements
1. DAST (OWASP ZAP) against staging
2. SBOM generation (Trivy SBOM output)
3. Diff-aware scanning on PRs
4. Findings sent to triage dashboard (DefectDojo)
5. Runtime monitoring (Falco) for container workloads
What does this section communicate?
The tool selection rationale shows you evaluated alternatives, not just grabbed the first tool you found. The gate strategy explains a deliberate threshold choice. The production enhancements list shows you know this pipeline is a starting point, not the end state.
- Save docs/gap-analysis.md.
- Commit and push the gap analysis document by running:
git add docs/gap-analysis.md
git commit -m "Add security scanner gap analysis"
git push
Before you check GitHub, what do you expect to see when you open docs/gap-analysis.md in the repository? Think about how GitHub renders markdown tables.
- Open your devsecops-pipeline repository on GitHub.
- Navigate to the docs/ folder and click gap-analysis.md.
GitHub renders your coverage matrix as a formatted table. You should see all 10 vulnerability rows with their detection status, the three gaps with risk/recommendation pairs, and the architecture decisions section with the numbered production enhancements list.
Table not rendering correctly on GitHub?
Markdown tables require exactly matching pipe characters in each row. Check that every row in your coverage matrix has 6 columns (separated by 5 pipes). Also confirm the separator row uses |---|---|---|---|---|---| with no extra spaces.
Help me fix my markdown table formatting
✔️ Awesome, I've got everything!
Great. Double check you have saved docs/gap-analysis.md and pushed it to GitHub.
ⓧ I'd like to double check the full code
Scroll up and confirm your docs/gap-analysis.md contains all three sections in this order:
- The Coverage Matrix table (10 vulnerability rows, 4 scanner columns).
- The Identified Gaps section (3 gaps, each with a Risk and Recommendation bullet).
- The Pipeline Architecture Decisions section (Tool Selection Rationale, Gate Strategy, Production Enhancements).
You now have a complete, portfolio-ready gap analysis that proves you think beyond tool output. This document shows interviewers you understand scanner limitations and can recommend layered defences. Next up, the Secret Mission challenges you to extend your scanner coverage by writing a custom Semgrep rule.
Secret mission
Write a Custom Semgrep Rule
Default rulesets have blind spots. In this bonus challenge, you will add vulnerabilities that Semgrep's built-in rules miss, then author your own detection rules to close the gap. This is exactly what production AppSec engineers do daily.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on free-tier resources and local tools, so there are no ongoing costs.
Resources you used:
- devsecops-pipeline GitHub repository (public, free tier).
- devsecops-pipeline local project folder on your machine.
- Semgrep (installed via pipx).
- Trivy (installed to /usr/local/bin).
- Checkov (installed via pip3).
Keep everything running
No action needed. Choose this if you want to keep the repository as a portfolio piece demonstrating your DevSecOps skills.
- Your GitHub repository remains public at no cost. GitHub Actions free tier gives you 2,000 minutes per month on public repos.
- Semgrep, Trivy, and Checkov remain installed locally for future projects.
- Your local devsecops-pipeline folder stays on your Desktop.
Pause - I'll come back to this later
Make the repository private so it is hidden from public view while you decide what to do next. No running processes to stop.
- Go to your devsecops-pipeline repository on GitHub.
- Click the Settings tab.
- Scroll down to the Danger Zone section.
- Click Change visibility and select Private.
- Your local files and installed tools remain untouched for when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Delete the GitHub repository: go to your devsecops-pipeline repository, click Settings, scroll to Danger Zone, and click Delete this repository. Follow the confirmation prompts.
- Remove the local project folder by running:
cd ~/Desktop
rm -rf devsecops-pipeline
- Uninstall Semgrep by running:
pipx uninstall semgrep
- Remove Trivy by running:
sudo rm /usr/local/bin/trivy
- Uninstall Checkov by running:
pip3 uninstall checkov -y
Nice Work!
Nice Work!
You just built a production-style DevSecOps pipeline that catches real vulnerabilities before they reach production. Every commit to your repository now triggers multi-layer security scanning with enforced gates that fail the build on critical findings.
You've learned how to:
- Build a deliberately vulnerable FastAPI application with planted OWASP Top 10 vulnerabilities (SQL injection, hardcoded credentials, command injection) and intentionally misconfigured Terraform files representing common AWS security anti-patterns.
- Wire up a GitHub Actions pipeline with four parallel security scanners (Semgrep, Trivy, Gitleaks, Checkov) and enforced security gates that fail on HIGH/CRITICAL findings.
- Produce a professional gap analysis documenting scanner coverage, blind spots, and actionable remediation recommendations that demonstrate security thinking beyond tool operation.
- (Secret Mission) Authored a custom Semgrep rule extending detection beyond default rulesets to catch application-specific vulnerability patterns.
Ready to quiz yourself?