← Back to all topics
$ trivy fs . --severity HIGH,CRITICAL

DevSecOps
Instructor Guide

Shift security left — into the pipeline, into the Dockerfile, into git pre-commit hooks. Catch the bug before it ships.

01
Why DevSecOps? Shift-Left and the Cost-of-a-Bug Curve
A vulnerability caught in CI costs $10. Caught in production costs $10,000. The math drives the practice.

How to explain to students

Open with the cost-of-a-bug curve: the earlier you catch a security issue, the cheaper it is to fix. Caught at code-write time (IDE warning) → 5 minutes. Caught in CI → 30 minutes. Caught in production → days of incident response, customer notifications, possibly regulatory fines. "Shift-left" means moving security checks earlier in the pipeline, where they're cheap.

DevSecOps is not "now we have a security team". It's "every engineer owns security as part of their workflow." The DevOps engineer's job: make the right thing easy — pre-commit hooks that catch secrets, CI jobs that scan images, dashboards that show drift.

cost-of-a-bug
Time to fix Cost / impact
──────────── ───────────── ─────────────────────────
IDE / commit ~5 minutes $10
PR review ~30 minutes $50
CI ~1 hour $200
Staging ~1 day $1,000
Production ~1 week+ $10,000+ + reputation damage

# Where DevSecOps lives in the lifecycle
developer ─→ pre-commit ─→ PR ─→ CI ─→ CD ─→ prod ─→ monitoring
↑ ↑ ↑ ↑ ↑ ↑ ↑
IDE secrets/ SAST trivy policy WAF runtime
hints gitleaks review scan / OPA alerts detection
Shift-left
Pre-commit catches faster than CI. CI catches faster than prod. Move it left.
👥
Everyone owns security
Not "the security team's problem". Every engineer, every PR.
🤖
Make it automatic
Humans forget. Pipelines don't. Encode every rule as a CI check.
📐
Defense in depth
No single layer is foolproof. Stack pre-commit + CI + WAF + monitoring.

🎯 Practice Questions

Q1.
Define "shift-left" in security. List two practical examples a DevOps engineer can implement in one afternoon.
Show Answer
Shift-left = move security checks earlier in the development lifecycle, ideally pre-commit or CI, so issues are caught when they're cheap to fix.

Two one-afternoon implementations:
1. Add pre-commit hooks with gitleaks + trufflehog to block AWS keys / API tokens before they hit the repo. Free, ~10 lines of .pre-commit-config.yaml.
2. Add a Trivy step in CI that scans every Docker image build and fails the pipeline on HIGH/CRITICAL CVEs. Free, ~5 lines of GitHub Actions YAML.

Both prevent classes of incidents, both work the day they're added, both cost zero.
Q2.
A teammate says "let's set up DevSecOps when we have time." What's a 10-minute argument for doing one thing today?
Q3.
Explain "defense in depth" with a concrete example layered across pre-commit, CI, runtime, and monitoring.
02
Secrets Management — Stop Putting Keys in .env Files
AWS Secrets Manager, SSM Parameter Store, gitleaks — the trio that handles 90% of secret-related incidents

How to explain to students

Three rules, in order of impact: (1) Never commit a secret to git, (2) never bake a secret into a Docker image, (3) always rotate. .env files on a developer laptop are fine; in production, secrets live in AWS Secrets Manager or SSM Parameter Store, fetched at startup.

The first line of defense is a pre-commit hook that scans staged files. gitleaks and trufflehog both detect AWS keys, GitHub tokens, Stripe keys, etc. — install once, blocks 99% of accidental commits.

secrets — prevent, store, rotate
# 1. Pre-commit hook — block secrets at commit time
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks: [{ id: gitleaks }]

$ pre-commit install
$ git commit -m "test"
gitleaks ✗ — found AWS access key in src/config.ts:12
Commit blocked.

# 2. Already committed by accident? Rotate FIRST, then clean history
$ aws iam delete-access-key --access-key-id AKIA...
$ git filter-repo --path src/config.ts --invert-paths # nuke from history

# 3. Production secrets live in Secrets Manager
$ aws secretsmanager create-secret \
  --name myapp/prod/db \
  --secret-string '{"username":"app","password":"<random-32-char>"}'

# Read at app startup (Node.js)
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const sm = new SecretsManagerClient({});
const { SecretString } = await sm.send(new GetSecretValueCommand({
  SecretId: 'myapp/prod/db'
}));
const { username, password } = JSON.parse(SecretString);

# 4. Auto-rotation (Secrets Manager + Lambda)
$ aws secretsmanager rotate-secret \
  --secret-id myapp/prod/db \
  --rotation-lambda-arn arn:aws:lambda:eu-west-1:123:function:rotate-pg \
  --rotation-rules AutomaticallyAfterDays=30
🚫
Never git secrets
Pre-commit hooks block them. Every repo gets gitleaks.
🗝️
Secrets Manager > .env
Encrypted, audited, rotatable. App fetches at startup.
🔁
Rotate every 30–90d
Limits the blast radius of a stolen secret.
🚨
Already leaked?
Rotate FIRST. Then git history surgery. Then incident review.

🎯 Practice Questions

Q1.
An engineer pushes a commit with AWS_SECRET_ACCESS_KEY=abc123, force-pushes 30 seconds later. List the right order of operations.
Show Answer
1. Rotate first. Delete the leaked key in IAM and create a fresh one. Bots scrape GitHub commits within minutes — assume the key is already in someone's wallet.
2. Audit usage. CloudTrail → look for any API call from unfamiliar IPs in the last hour. Limit blast radius if any.
3. Then clean git history. git filter-repo or BFG to scrub the secret from all branches and force-push.
4. Add a pre-commit hook so it can't happen again.
5. Postmortem. Fix the process — why was the secret in code at all?

Force-pushing without rotating first is the #1 mistake. The window between commit and force-push is enough.
Q2.
When does it make sense to use AWS Secrets Manager vs SSM Parameter Store? Give one fit for each.
Q3.
Why is fetching secrets at app startup better than fetching on every request? Why is fetching at build time the worst option?
💡 Latency vs freshness vs blast radius.
Q4.
Sketch a GitHub Actions workflow step that scans the diff in each PR for secrets and fails if anything is found.
03
Container Image Scanning with Trivy
Free, fast, fast-fail. The single biggest container-security control you can deploy.

How to explain to students

Every Docker image is a stack of "someone else's code" — your base image (Ubuntu, Alpine), your language runtime (Node, Python), your dependencies (npm, pip). Each layer ages and accumulates known vulnerabilities (CVEs). Trivy compares the installed packages against a CVE database and reports what's affected.

Run Trivy twice: once locally before push, once in CI. CI is the gate — fail the build on HIGH/CRITICAL findings. The local run is for fast feedback while iterating.

trivy — local + CI
# Install (mac)
$ brew install trivy

# Scan an image
$ trivy image --severity HIGH,CRITICAL myapp:1.0.0
myapp:1.0.0 (alpine 3.19)
==========================
Total: 4 (HIGH: 3, CRITICAL: 1)
┌────────────┬────────────────┬──────────┬──────────────────┐
│ Library │ Vulnerability │ Severity │ Fixed Version │
├────────────┼────────────────┼──────────┼──────────────────┤
│ openssl │ CVE-2024-2511 │ HIGH │ 3.1.5-r0 │
│ libcrypto3 │ CVE-2024-2511 │ HIGH │ 3.1.5-r0 │
│ glibc │ CVE-2024-3596 │ CRITICAL │ no fix yet │
└────────────┴────────────────┴──────────┴──────────────────┘

# Scan project files (Dockerfile, terraform, code dependencies)
$ trivy fs --severity HIGH,CRITICAL .

# In GitHub Actions — fail the build on HIGH+ vulns
- name: Build image
run: docker build -t myapp:${{ github.sha }} .

- name: Trivy scan (fail on HIGH+)
uses: aquasecurity/trivy-action@0.20.0
with:
image-ref: myapp:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: 1 # fail build
ignore-unfixed: true # skip un-patchable CVEs

# Bonus: generate an SBOM for supply-chain audits
$ trivy image --format cyclonedx --output sbom.json myapp:1.0.0
trivy grype snyk SBOM ignore-unfixed aquasecurity/trivy-action

🎯 Practice Questions

Q1.
Your Trivy run reports 8 CRITICAL vulnerabilities, all in transitive deps with no fix yet. Should you ship? What flag changes the picture?
Show Answer
Two questions to answer first:
1. Are the vulnerabilities reachable in your code path? Many CVEs in transitive deps are in code you never call. Use --reachability-style tools (Snyk, Semgrep Pro) to triage.
2. Is there a workaround? Pin to an older patched version, swap the dep, sandbox the affected component.

If genuinely unfixable + reachable, the right call is risk-acceptance with documentation, not silent ignore.

The flag: --ignore-unfixed tells Trivy "skip CVEs with no available fix." Useful for keeping CI green while you wait for upstream patches — but document which ones you're skipping (use .trivyignore with comments and expiry dates).
Q2.
Why scan with trivy fs in addition to trivy image? What does each find that the other misses?
Q3.
An SBOM (Software Bill of Materials) is generated by Trivy. Why are SBOMs increasingly mandatory (e.g. US Executive Order 14028)?
Q4.
Add a pre-commit hook that runs trivy fs --severity CRITICAL . before every commit. Sketch the pre-commit config.
04
SAST & DAST in Pipelines — Static and Dynamic Code Analysis
SAST reads your code. DAST attacks it. You need both.

How to explain to students

SAST (Static Application Security Testing) = analyse source code without running it. Finds patterns: SQL string-concatenation, unescaped user input, weak crypto. Tools: Semgrep, CodeQL, language-specific linters (eslint-plugin-security for JS, bandit for Python).

DAST (Dynamic Application Security Testing) = attack the running app like a black-box pentester would. Sends malformed inputs, tries SQL injection, XSS, auth bypass. Tools: OWASP ZAP, Burp. Slower than SAST but finds real-world issues SAST misses (e.g. config-level holes).

sast + dast in CI
# SAST: Semgrep — runs in seconds, great default rules
$ semgrep scan --config=auto src/
findings: 3
src/api/login.ts:42 — javascript.express.security.injection.tainted-sql-string
src/utils/jwt.ts:18 — javascript.lang.security.audit.weak-crypto

# SAST in GitHub Actions
- uses: returntocorp/semgrep-action@v1
with:
config: p/javascript p/owasp-top-ten p/secrets

# DAST: OWASP ZAP baseline scan against staging
- name: Wait for staging
run: ./scripts/wait-for-staging.sh

- name: ZAP baseline scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: https://staging.example.com
fail_action: true  # gate the pipeline on findings

# SAST output sample — typical findings
[HIGH] SQL injection — string concat in DB query
const q = `SELECT * FROM users WHERE id = ${userInput}`; // 👀
Fix: use parameterised queries
const q = 'SELECT * FROM users WHERE id = $1';
await db.query(q, [userInput]);

# DAST output sample
[MEDIUM] Cookie without Secure flag — /login
[HIGH] X-Frame-Options missing — clickjacking risk
[INFO] Server header leaks: nginx/1.18.0

🎯 Practice Questions

Q1.
SAST or DAST? (a) "find SQL string concat in the code", (b) "send malicious payloads to /login", (c) "detect missing Content-Security-Policy header", (d) "find weak crypto.MD5 usage".
Show Answer
(a) SAST — pattern-matches source code; perfect for spotting string concat in queries.
(b) DAST — actively probes the running app's /login endpoint with malformed inputs.
(c) DAST — header presence is a runtime/config concern; SAST can't see what your reverse proxy or app emits in actual responses.
(d) SAST — looks at the import + call site of crypto.MD5 in source.

Rule of thumb: code-level pattern → SAST. Behaviour-level / response-header / config issue → DAST.
Q2.
Why does DAST need a deployed app while SAST doesn't? Why is DAST usually run against staging, not prod?
Q3.
Semgrep has a free + a paid tier. What's the additional value of the paid tier for an enterprise team?
💡 Cross-file taint analysis, custom rules, supply-chain reachability.
05
OWASP Top 10 — What Every DevOps Engineer Must Recognise
The web's most common bug classes. The DevOps engineer is often the last line of defence against them.

How to explain to students

The OWASP Top 10 is a regularly-updated list of the most prevalent web vulnerability classes. You don't have to be a pen-tester — but you must recognise these by name and know which DevOps controls mitigate each. A01 Broken Access Control is #1 for a reason: nearly every breach involves auth gone wrong.

owasp-top-10 (2021) + DevOps mitigation
A01 Broken Access Control → IAM least-privilege, JWT/RBAC review, integration tests
A02 Cryptographic Failures → HTTPS everywhere, modern TLS, HSTS, no MD5/SHA1
A03 Injection (SQL, OS, LDAP) → parameterised queries, input validation, SAST
A04 Insecure Design → threat modelling, security review of new features
A05 Security Misconfiguration → IaC + tfsec/checkov, hardened images, no defaults
A06 Vulnerable + Outdated Components → Dependabot, Trivy, npm audit in CI
A07 Identification + Auth Failures → MFA, password policy, lockouts, OIDC over passwords
A08 Software/Data Integrity Failures → signed commits, signed images, SBOM, verified deploys
A09 Security Logging Failures → CloudTrail, app audit logs, centralised SIEM
A10 SSRF (Server-Side Request Forgery)→ network egress allow-list, IMDSv2 on EC2

# Concrete examples in code

# A03 Injection — bad
db.query(`SELECT * FROM users WHERE email = '${input}'`);
# A03 Injection — good (parameterised)
db.query('SELECT * FROM users WHERE email = $1', [input]);

# A05 Misconfig — bad (S3 bucket policy)
"Principal": "*", "Effect": "Allow", "Action": "s3:*"
# A05 Misconfig — good
"Principal": { "AWS": "arn:aws:iam::123:role/specific-role" }, "Action": ["s3:GetObject"]

# A10 SSRF — bad (allow user URL → fetch internal resources)
const data = await fetch(req.body.url);
# A10 SSRF — good
if (!isExternalUrl(req.body.url)) return res.status(400).send('blocked');
🥇
A01 is #1
Broken access control. Every IAM review and JWT check matters.
🛠️
DevOps mitigations
IaC tfsec, Trivy, Dependabot, signed images, OIDC. Each maps to an OWASP item.
📚
Read the docs
owasp.org/Top10 — short, free, reread once a year.
🔁
List evolves
2017 → 2021 → 2025. Old "XSS" got merged into A03 Injection.

🎯 Practice Questions

Q1.
Map the following to OWASP categories: (a) "user can edit other users' rows by changing the URL ID", (b) "passwords stored as MD5", (c) "S3 bucket policy allows anyone to read".
Q2.
For A06 (vulnerable components) name three concrete DevOps controls that catch issues before they ship.
Show Answer
Three layered controls:
1. Dependabot (or Renovate) — opens PRs the moment a dependency has a new version with a security fix. Free on GitHub.
2. Trivy / Snyk in CI — fails the build if HIGH/CRITICAL CVEs land in package.json, requirements.txt, or the Docker image.
3. SBOM generation + storage — keep a record of every dependency shipped to prod (trivy --format cyclonedx). When a new CVE drops in 6 months, you can answer "are we affected?" in 30 seconds.

Bonus: npm audit / pip-audit as a pre-merge gate. Free, fast, catches the common case.
Q3.
SSRF (A10) is a top concern on cloud. Why is enabling IMDSv2 on EC2 a critical mitigation?
💡 IMDSv1 lets SSRF leak the EC2 instance role's credentials.
Q4.
A09 Security Logging — without centralised logs, what specific incident-response capability does a team lose?
06
Supply-Chain Security — Dependabot, Pinning, Signed Images
SolarWinds, log4shell, npm event-stream. The attack surface isn't your code — it's everyone you depend on.

How to explain to students

Modern apps depend on hundreds of packages, written by people you've never met, hosted on infra you don't control. Supply-chain attacks compromise that chain — a malicious commit in a package that 10,000 apps install. The classic examples: event-stream (npm, 2018), SolarWinds (2020), log4shell (2021). Defending against this is a DevOps responsibility.

Three control layers: (1) Pin versions in lock files — never use floating ranges in CI. (2) Auto-update via Dependabot / Renovate — security patches arrive within hours. (3) Verify integrity — signed commits (Sigstore), signed Docker images (Cosign), SBOM diffs in PR review.

supply-chain — three layers
# Layer 1 — Pin in CI
$ npm ci # not npm install — exact lock-file install, fails on drift

# Layer 2 — Dependabot config
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule: { interval: daily }
    groups:
      production: { dependency-type: production, update-types: [patch, minor] }
  - package-ecosystem: docker
    directory: /
    schedule: { interval: weekly }

# Layer 3 — Sign images with Cosign (keyless via OIDC)
$ cosign sign --yes 123.dkr.ecr.eu-west-1.amazonaws.com/myapp:1.0.0
Successfully signed. Signature pushed to: ECR registry.

# Verify before deploy (in GitOps / admission controller)
$ cosign verify \
  --certificate-identity-regexp '^https://github.com/myorg/myrepo' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  123.dkr.ecr.eu-west-1.amazonaws.com/myapp:1.0.0
✓ Signature verified — built by GitHub Actions in myorg/myrepo

# SBOM — keep around for audits
$ syft myapp:1.0.0 -o spdx-json > sbom.json
$ grype sbom:./sbom.json # query the SBOM, not the live image
Dependabot Renovate Cosign Sigstore SBOM log4shell

🎯 Practice Questions

Q1.
Why is npm ci in CI safer than npm install for supply-chain protection?
Q2.
Cosign keyless signing relies on OIDC tokens from the CI provider. What does this prevent that long-lived signing keys don't?
Show Answer
Long-lived signing keys must be stored somewhere — typically in secrets manager, attached to a CI runner, or held by a release engineer. Anyone who steals that key can sign images claiming to be your team — indistinguishable from real builds.

Keyless Cosign (Sigstore): at sign time, the CI runner asks the OIDC provider (e.g. GitHub Actions) for a short-lived token proving "this build ran in repo X on branch main." Sigstore mints a signing certificate bound to that identity, valid for ~10 minutes.

Result: there is no key to steal. To forge a signature, an attacker would need to compromise GitHub Actions and your repo — both of which have separate audit trails. This is the modern recommended practice.
Q3.
Walk through what happens after log4shell drops: the affected library is everywhere, your team needs to know "are we vulnerable?". Which artefact answers it in 5 minutes?
💡 SBOM.
07
Using AI for Threat Modelling & IAM Policy Review
AI is great at "find me 5 attack vectors against this design" — and not great at being the only reviewer

How to explain to students

AI is excellent at brainstorming attack scenarios ("here's my architecture, what could go wrong?"), reviewing IAM policies for over-permission, and explaining CVEs in plain language. It is not a substitute for a real security review on safety-critical changes — but it's a great force multiplier on day-to-day work.

Trap: AI sometimes invents CVE numbers or claims a non-existent attack works. Verify against MITRE / NVD before quoting a CVE in a postmortem. And never paste real production secrets, customer data, or unfixed-vuln details to a public AI tool.

AI prompts for security
# ✅ Threat-modelling prompt
"Here is the architecture (text diagram):
Cloudflare → ALB → ECS Fargate Node.js → RDS Postgres
+ S3 bucket for user uploads, signed URLs only
+ Cognito for auth
List 5 high-impact attack vectors against this design.
For each: which OWASP category, what's the worst case,
and the cheapest mitigation we can implement this sprint."

# ✅ IAM policy review prompt
"Review this IAM policy for over-permission. Suggest the minimum scope
for the use case: 'a Lambda that reads object X from one S3 bucket
and writes to one DynamoDB table'.
[paste policy JSON here]
Highlight any wildcard actions, broad resources, or missing conditions."

# Verify any CVE the AI quotes
$ curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-2511"
"vulnStatus": "Awaiting Analysis", "descriptions": [...]

# ❌ Never paste:
- real production secret values (rotate first if you must)
- customer PII
- unpatched vulnerability details from a private bug bounty
- internal architecture diagrams that name customers

🎯 Practice Questions

Q1.
Take "is my system secure?" and turn it into a concrete 5-bullet AI prompt for threat modelling.
Q2.
An AI tells you a vulnerability "doesn't really matter" because "exploitation requires admin access." Why is this a yellow flag, not a green light?
Show Answer
Two reasons:
1. Defence in depth — "requires admin" assumes nobody else gets admin. In a real breach, an attacker chains multiple bugs: the SSRF gives them an internal cred → which gives them admin → which lets them exploit "the bug that requires admin." Each layer matters.
2. AI is not your authoritative source. AI summarises training data; it doesn't have visibility into your specific deployment, threat model, or compliance constraints. Use the AI take as a hypothesis, not a verdict.

Always cross-check against the actual CVE description (CVSS score, attack vector, complexity) and your team's threat model.
Q3.
Why is pasting a production IAM policy (with the account ID + role names) to a public AI tool an unnecessary risk, even though it's not technically a secret?
08
Project: Add Security Scanning + Secrets Management to Your Pipeline
Take the Docker + CI/CD project and harden it end-to-end

How to explain to students

By this point, students have a Node app + Dockerfile + GitHub Actions pipeline (from earlier modules). Now they bolt on the full DevSecOps layer: pre-commit secret scan, npm audit gate, Trivy scan, Semgrep SAST, OWASP ZAP DAST, Cosign signing, and Secrets Manager for production credentials. The artefact: a hardened pipeline where pushing a known vulnerability is impossible.

.github/workflows/security.yml
name: Security
on: [push, pull_request]

permissions:
  contents: read
  id-token: write # Cosign keyless signing
  security-events: write # SARIF upload

jobs:

  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: gitleaks/gitleaks-action@v2

  deps:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm audit --audit-level=high

  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: returntocorp/semgrep-action@v1
        with: { config: p/javascript p/owasp-top-ten p/secrets }

  image:
    needs: [secrets, deps, sast]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t myapp:${{ github.sha }} .
      - uses: aquasecurity/trivy-action@0.20.0
        with: { image-ref: myapp:${{ github.sha }}, severity: HIGH,CRITICAL, exit-code: 1 }
      - run: cosign sign --yes myapp:${{ github.sha }}

  dast:
    needs: image
    runs-on: ubuntu-latest
    steps:
      - run: docker run -d -p 3000:3000 --name app myapp:${{ github.sha }}
      - uses: zaproxy/action-baseline@v0.12.0
        with: { target: http://localhost:3000, fail_action: true }
🛂
5 gates
secrets → deps → SAST → image scan → DAST. Each blocks before code goes live.
🤝
Parallel where possible
secrets/deps/sast run in parallel; image waits for all 3.
📥
SARIF upload
Findings show in GitHub's Security tab — no external dashboard needed.
🪪
Cosign keyless
Signs every prod image with no long-lived keys.
09
Quiz + Assignment — Audit & Harden a Real Repo
5 MCQs + 2 fill-ins + a graded "find and fix 3 vulns in your own project" exercise

Sample quiz questions (interactive)

Q1. The first thing to do after spotting a leaked AWS access key in a public repo:
A
Force-push to delete the commit
B
Rotate the key in IAM immediately, then clean history
C
Open an incident ticket
D
Make the repo private
Q2. SAST is best at finding:
A
Missing security headers in HTTP responses
B
Unsafe code patterns (SQL string concat, weak crypto)
C
Brute-force vulnerable endpoints
D
Network-level DDoS exposure
Q3. Trivy "ignore-unfixed" is appropriate when:
A
Always — it's the safer default
B
CVEs have no upstream patch and you've documented risk acceptance
C
Never — always fail on every CVE
D
Only in development environments
Q4. OWASP A01 is:
A
Broken Access Control
B
SQL Injection
C
XSS
D
CSRF
Q5. Cosign keyless signing relies on:
A
A long-lived signing key in Secrets Manager
B
A YubiKey
C
Short-lived OIDC tokens minted by the CI provider
D
A self-signed PKCS#12 cert

Fill-in-the-command

Fill 1: Trivy command — scan image myapp:1.0 and exit non-zero on HIGH or CRITICAL.
Fill 2: Read secret myapp/prod/db from AWS Secrets Manager via the CLI.

Assignment

📋 Assignment Requirements

  • Pick one of your earlier projects (Docker, CI/CD, AWS — any with code and a Dockerfile)
  • Add a .pre-commit-config.yaml running gitleaks; commit and verify it blocks a fake AWS key
  • Add a security workflow (.github/workflows/security.yml) with at minimum: gitleaks, npm audit (or pip-audit), Semgrep SAST, Trivy image scan
  • Each step must fail the build on findings of HIGH or above — no warnings-only passes
  • Move at least one production secret to AWS Secrets Manager (or SSM Parameter Store), document the rotation policy in the README
  • Add Dependabot config for the relevant ecosystem(s)
  • Run the workflow against a known-bad commit (paste a fake AWS key, install lodash@4.17.10) and screenshot the failed runs
  • Map the controls you added to OWASP categories — write a SECURITY.md table with at least 5 entries
  • Bonus: Add Cosign keyless signing to the image build job
  • Bonus: Add an OWASP ZAP baseline scan against a staging deploy
  • Bonus: Generate + commit an SBOM via Trivy or Syft, document how to query it for a future CVE
expected output of failing run
✗ secrets (gitleaks found 1 leak)
✗ deps (npm audit: 2 high vulnerabilities)
✗ sast (Semgrep: 1 SQL-injection finding)
✗ image (Trivy: 3 HIGH, 1 CRITICAL)
PR is BLOCKED. Fix findings + push again.
📊
Grading rubric
Pre-commit gitleaks: 15. CI workflow with 4+ gates: 30. Build fails on real findings: 25. Secrets moved + rotation doc: 15. SECURITY.md OWASP map: 15.
🎯
Common mistakes
Workflow runs but doesn't fail on findings (warnings-only); gitleaks scoped wrong; secret rotated but old commit still in git history.
💡
Stretch
Add an OPA/Conftest policy gate on Terraform (block public S3 buckets, missing encryption, etc.).