diff --git a/.gitignore b/.gitignore index b5172ad70..c6b279fad 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ labs/lab7/results/ labs/lab8/results/ labs/lab9/falco/logs/ labs/lab10/work/dd/ +labs/lab10/imports/import-*.json labs/lab11/logs/ labs/lab11/reverse-proxy/certs/ labs/lab12/results/ diff --git a/labs/lab1.md b/labs/lab1.md index f33e69842..c37b0b302 100644 --- a/labs/lab1.md +++ b/labs/lab1.md @@ -95,7 +95,7 @@ Open `http://127.0.0.1:3000` in your browser. Note what you see: Create `submissions/lab1.md` and **fill in the template below** with your real observations. Don't paraphrase — record what you actually saw. -```markdown +````markdown # Lab 1 — Submission ## Triage Report: OWASP Juice Shop @@ -115,7 +115,7 @@ Create `submissions/lab1.md` and **fill in the template below** with your real o ### Health Check - HTTP code on `/`: -- API check (first 200 chars of `/rest/products`): +- API check (first 200 chars of `/api/Products`): ``` ``` @@ -143,7 +143,7 @@ Which of these are MISSING? (cross-reference Lecture 1 OWASP Top 10:2025 — A06 1. **** — 2. **** — 3. **** — -``` +```` ### 1.4: Cleanup (when done) @@ -274,7 +274,7 @@ A "GitHub Community" section with 1-2 sentences explaining: ### B.3: Document in `submissions/lab1.md` -```markdown +````markdown ## Bonus: CI Smoke Test - Workflow file: `.github/workflows/lab1-smoke.yml` @@ -285,7 +285,7 @@ A "GitHub Community" section with 1-2 sentences explaining: ``` ``` -``` +```` --- @@ -316,7 +316,7 @@ PR checklist (paste this into your PR body): ### Task 1 (6 pts) - ✅ Juice Shop v20.0.0 container running on `127.0.0.1:3000` (proof: `docker ps` output in submission) -- ✅ Homepage returns HTTP 200; `/rest/products` returns a JSON list +- ✅ Homepage returns HTTP 200; `/api/Products` returns a JSON list - ✅ Triage report has all six sections filled in with **real** values (no template placeholders left) - ✅ At least three security headers are correctly identified as present or missing - ✅ Top 3 risks each mapped to an OWASP Top 10:2025 category (A01–A10) diff --git a/labs/lab10.md b/labs/lab10.md index 02eb6ec37..1e34b3b70 100644 --- a/labs/lab10.md +++ b/labs/lab10.md @@ -30,7 +30,7 @@ In this lab you will practice: **You should have from Labs 4-9** (regenerate if missing): - Lab 4: `juice-shop.cdx.json`, `grype-from-sbom.json`, `trivy.json` - Lab 5: `auth-report.json` (ZAP), `semgrep.json` -- Lab 6: `checkov-terraform/results_json.json`, `kics/results.json` +- Lab 6: `checkov-terraform/results_json.json`, `kics-ansible/results.json`, `kics-pulumi/results.json` - Lab 7: `trivy-image.json`, `trivy-k8s.json` - Lab 8: `verify-original.json` (Cosign verify output) - Lab 9: `falco/logs/falco.log` (custom alerts) @@ -58,8 +58,9 @@ mkdir -p labs/lab10/work ``` > **Plumbing provided** (in `labs/lab10/imports/`): -> - Import script that maps the Lab 4-9 file paths to DefectDojo scan-type names -> - Sample environment variables file +> - `run-imports.sh` — imports every Lab 4-7 report into DefectDojo (paths resolved +> from the repo root; portable to stock macOS bash 3.2) +> - `env.sample` — the environment variables the script reads (`DD_URL`, `DD_TOKEN`, …) --- @@ -132,7 +133,7 @@ echo "Engagement: $ENGAGEMENT_ID" ### 10.4: Import scan files -For each prior lab, run: +For each prior lab, run (from the repo root): ```bash # Template — repeat for each scan type @@ -140,7 +141,7 @@ curl -X POST "$DD_URL/api/v2/import-scan/" \ -H "Authorization: Token $DD_TOKEN" \ -F "scan_type=Trivy Scan" \ -F "engagement=$ENGAGEMENT_ID" \ - -F "file=@../../lab7/results/trivy-image.json" + -F "file=@labs/lab7/results/trivy-image.json" ``` Scan-type names to use: @@ -152,12 +153,18 @@ Scan-type names to use: | 5 | `semgrep.json` | `Semgrep JSON Report` | | 5 | `auth-report.json` | `ZAP Scan` | | 6 | `checkov-terraform/results_json.json` | `Checkov Scan` | -| 6 | `kics/results.json` | `KICS Scan` | +| 6 | `kics-ansible/results.json` | `KICS Scan` | +| 6 | `kics-pulumi/results.json` | `KICS Scan` | | 7 | `trivy-image.json` | `Trivy Scan` | | 7 | `trivy-k8s.json` | `Trivy Operator Scan` | | 9 | `falco/logs/falco.log` | (custom-format — skip if not supported, document instead) | -Use the importer script in `labs/lab10/imports/import-all.sh` to automate. +To automate all of the above, just run the importer — it reuses the `DD_URL` + `DD_TOKEN` +you exported in 10.2 (see `labs/lab10/imports/env.sample` for every variable it reads): + +```bash +bash labs/lab10/imports/run-imports.sh +``` ### 10.5: Verify import + dedup @@ -197,7 +204,8 @@ curl -s -H "Authorization: Token $DD_TOKEN" \ | 5 | Semgrep JSON Report | semgrep.json | | | 5 | ZAP Scan | auth-report.json | | | 6 | Checkov Scan | results_json.json | | -| 6 | KICS Scan | results.json | | +| 6 | KICS Scan | kics-ansible/results.json | | +| 6 | KICS Scan | kics-pulumi/results.json | | | 7 | Trivy Scan (image) | trivy-image.json | | | 7 | Trivy Operator Scan | trivy-k8s.json | | | **Total raw imports** | | | | diff --git a/labs/lab10/imports/env.sample b/labs/lab10/imports/env.sample new file mode 100644 index 000000000..ba4551f4a --- /dev/null +++ b/labs/lab10/imports/env.sample @@ -0,0 +1,14 @@ +# Lab 10 — DefectDojo importer environment +# Copy these into your shell (or `source labs/lab10/imports/env.sample`) before +# running run-imports.sh. + +# DefectDojo base URL — the importer derives the API base ($DD_URL/api/v2). +export DD_URL="http://localhost:8080" + +# API token: DefectDojo UI -> Profile -> API v2 Key. +export DD_TOKEN="paste-your-token-here" + +# Optional overrides (defaults shown — match step 10.3): +# export DD_PRODUCT_TYPE="Engineering" +# export DD_PRODUCT="OWASP Juice Shop" +# export DD_ENGAGEMENT="Course Semester Run" diff --git a/labs/lab10/imports/run-imports.sh b/labs/lab10/imports/run-imports.sh index 0f0e33c93..f4875323e 100644 --- a/labs/lab10/imports/run-imports.sh +++ b/labs/lab10/imports/run-imports.sh @@ -1,21 +1,27 @@ #!/usr/bin/env bash set -euo pipefail -# Batch import helper for Lab 10 -# - Auto-detects scan_type names from your Dojo instance -# - Imports whichever files exist among ZAP, Semgrep, Trivy, Nuclei (and optional Grype) +# Batch import helper for Lab 10. +# Imports every Lab 4-7 scan report that exists into DefectDojo, auto-detecting +# the scan_type names from your instance (with sane fallbacks if discovery fails). # # Usage: -# export DD_API="http://localhost:8080/api/v2" -# export DD_TOKEN="" -# # Optional overrides (defaults shown) -# export DD_PRODUCT_TYPE="${DD_PRODUCT_TYPE:-Engineering}" -# export DD_PRODUCT="${DD_PRODUCT:-Juice Shop}" -# export DD_ENGAGEMENT="${DD_ENGAGEMENT:-Labs Security Testing}" +# export DD_URL="http://localhost:8080" # base URL (same as lab10.md step 10.2) +# export DD_TOKEN="" # Profile -> API v2 Key in the UI # bash labs/lab10/imports/run-imports.sh +# +# Optional overrides (defaults shown): +# DD_API="$DD_URL/api/v2" +# DD_PRODUCT_TYPE="Engineering" +# DD_PRODUCT="OWASP Juice Shop" +# DD_ENGAGEMENT="Course Semester Run" +# +# File paths are resolved relative to the repo root, so the script runs from any +# directory. Portable to bash 3.2 (stock macOS) — no mapfile/readarray. here_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" out_dir="$here_dir" +repo_root="$(cd "$here_dir/../../.." && pwd)" # labs/lab10/imports -> repo root require_env() { local name="$1" @@ -25,12 +31,13 @@ require_env() { fi } -require_env DD_API require_env DD_TOKEN +DD_URL="${DD_URL:-http://localhost:8080}" +DD_API="${DD_API:-$DD_URL/api/v2}" DD_PRODUCT_TYPE="${DD_PRODUCT_TYPE:-Engineering}" -DD_PRODUCT="${DD_PRODUCT:-Juice Shop}" -DD_ENGAGEMENT="${DD_ENGAGEMENT:-Labs Security Testing}" +DD_PRODUCT="${DD_PRODUCT:-OWASP Juice Shop}" +DD_ENGAGEMENT="${DD_ENGAGEMENT:-Course Semester Run}" echo "Using context:" echo " DD_API=$DD_API" @@ -40,95 +47,86 @@ echo " DD_ENGAGEMENT=$DD_ENGAGEMENT" have_jq=true command -v jq >/dev/null 2>&1 || have_jq=false -if ! $have_jq; then - echo "WARN: jq not found; falling back to defaults for scan_type names." >&2 -fi - -# Discover scan type names from your instance if jq is available -SCAN_ZAP="${SCAN_ZAP:-}" -SCAN_SEMGREP="${SCAN_SEMGREP:-}" -SCAN_TRIVY="${SCAN_TRIVY:-}" -SCAN_NUCLEI="${SCAN_NUCLEI:-}" +$have_jq || echo "WARN: jq not found; using default scan_type names." >&2 +# Discover scan_type names from the instance. Fallbacks keep the script working +# even if discovery fails (no jq, instance not up, auth error). +types=() if $have_jq; then echo "Discovering importer names from /test_types/ ..." - mapfile -t types < <(curl -sS -H "Authorization: Token $DD_TOKEN" "$DD_API/test_types/?limit=2000" | jq -r '.results[].name') - choose_type() { - local pat="$1" - local fallback="$2" - local val="" + while IFS= read -r name; do + [[ -n "$name" ]] && types+=("$name") + done < <(curl -sS -H "Authorization: Token $DD_TOKEN" \ + "$DD_API/test_types/?limit=2000" 2>/dev/null | jq -r '.results[].name' 2>/dev/null) +fi + +choose_type() { + local pat="$1" fallback="$2" t + if [[ ${#types[@]} -gt 0 ]]; then for t in "${types[@]}"; do - if [[ "$t" =~ $pat ]]; then val="$t"; break; fi + if [[ "$t" =~ $pat ]]; then echo "$t"; return; fi done - if [[ -z "$val" ]]; then val="$fallback"; fi - echo "$val" - } - SCAN_ZAP="${SCAN_ZAP:-$(choose_type '^ZAP' 'ZAP Scan')}" - SCAN_SEMGREP="${SCAN_SEMGREP:-$(choose_type '^Semgrep' 'Semgrep JSON Report')}" - SCAN_TRIVY="${SCAN_TRIVY:-$(choose_type '^Trivy' 'Trivy Scan')}" - SCAN_NUCLEI="${SCAN_NUCLEI:-$(choose_type '^Nuclei' 'Nuclei Scan')}" - # Grype importer (commonly named "Anchore Grype") - if [[ -z "${SCAN_GRYPE:-}" ]]; then - SCAN_GRYPE=$(printf '%s\n' "${types[@]}" | grep -i '^Anchore Grype' | head -n1) - if [[ -z "$SCAN_GRYPE" ]]; then - SCAN_GRYPE=$(printf '%s\n' "${types[@]}" | grep -i 'Grype' | head -n1) - fi fi -else - SCAN_ZAP="${SCAN_ZAP:-ZAP Scan}" - SCAN_SEMGREP="${SCAN_SEMGREP:-Semgrep JSON Report}" - SCAN_TRIVY="${SCAN_TRIVY:-Trivy Scan}" - SCAN_NUCLEI="${SCAN_NUCLEI:-Nuclei Scan}" -fi -SCAN_GRYPE="${SCAN_GRYPE:-Anchore Grype}" + echo "$fallback" +} + +SCAN_GRYPE="$(choose_type '^Anchore Grype' 'Anchore Grype')" +SCAN_TRIVY="$(choose_type '^Trivy Scan$' 'Trivy Scan')" +SCAN_TRIVY_OP="$(choose_type '^Trivy Operator' 'Trivy Operator Scan')" +SCAN_SEMGREP="$(choose_type '^Semgrep' 'Semgrep JSON Report')" +SCAN_ZAP="$(choose_type '^ZAP' 'ZAP Scan')" +SCAN_CHECKOV="$(choose_type '^Checkov' 'Checkov Scan')" +SCAN_KICS="$(choose_type '^KICS' 'KICS Scan')" echo "Importer names:" -echo " ZAP = $SCAN_ZAP" -echo " Semgrep = $SCAN_SEMGREP" -echo " Trivy = $SCAN_TRIVY" -echo " Nuclei = $SCAN_NUCLEI" -echo " Grype = $SCAN_GRYPE" +echo " Grype = $SCAN_GRYPE" +echo " Trivy = $SCAN_TRIVY" +echo " Trivy Operator = $SCAN_TRIVY_OP" +echo " Semgrep = $SCAN_SEMGREP" +echo " ZAP = $SCAN_ZAP" +echo " Checkov = $SCAN_CHECKOV" +echo " KICS = $SCAN_KICS" import_scan() { - local scan_type="$1"; shift - local file="$1"; shift + local scan_type="$1" file="$2" + local rel="${file#"$repo_root"/}" if [[ ! -f "$file" ]]; then - echo "SKIP: $scan_type file not found: $file" + echo "SKIP: $scan_type — file not found: $rel" return 0 fi - local base out - base="$(basename "$file")" - out="$out_dir/import-${base//[^A-Za-z0-9_.-]/_}.json" - echo "Importing $scan_type from $file" - curl -sS -X POST "$DD_API/import-scan/" \ - -H "Authorization: Token $DD_TOKEN" \ - -F "scan_type=$scan_type" \ - -F "file=@$file" \ - -F "product_type_name=$DD_PRODUCT_TYPE" \ - -F "product_name=$DD_PRODUCT" \ - -F "engagement_name=$DD_ENGAGEMENT" \ - -F "auto_create_context=true" \ - -F "minimum_severity=Info" \ - -F "close_old_findings=false" \ - -F "push_to_jira=false" \ - | tee "$out" + local tag base out + tag="$(basename "$(dirname "$file")")"; tag="${tag//[^A-Za-z0-9_.-]/_}" + base="$(basename "$file")"; base="${base//[^A-Za-z0-9_.-]/_}" + out="$out_dir/import-${tag}-${base}" + echo "Importing $scan_type from $rel" + if ! curl -sS -X POST "$DD_API/import-scan/" \ + -H "Authorization: Token $DD_TOKEN" \ + -F "scan_type=$scan_type" \ + -F "file=@$file" \ + -F "product_type_name=$DD_PRODUCT_TYPE" \ + -F "product_name=$DD_PRODUCT" \ + -F "engagement_name=$DD_ENGAGEMENT" \ + -F "auto_create_context=true" \ + -F "minimum_severity=Info" \ + -F "close_old_findings=false" \ + -F "push_to_jira=false" \ + | tee "$out" >/dev/null; then + echo " WARN: import request failed for $scan_type ($base)" >&2 + fi } -# Candidate paths per tool -zap_file="labs/lab5/zap/zap-report-noauth.json" -semgrep_file="labs/lab5/semgrep/semgrep-results.json" -trivy_file="labs/lab4/trivy/trivy-vuln-detailed.json" -nuclei_file="labs/lab5/nuclei/nuclei-results.json" - -# Grype -grype_file="labs/lab4/syft/grype-vuln-results.json" - -import_scan "$SCAN_ZAP" "$zap_file" -import_scan "$SCAN_SEMGREP" "$semgrep_file" -import_scan "$SCAN_TRIVY" "$trivy_file" -import_scan "$SCAN_NUCLEI" "$nuclei_file" - -# Grype -import_scan "$SCAN_GRYPE" "$grype_file" +# Lab 4 — SCA (SBOM-derived) +import_scan "$SCAN_GRYPE" "$repo_root/labs/lab4/grype-from-sbom.json" +import_scan "$SCAN_TRIVY" "$repo_root/labs/lab4/trivy.json" +# Lab 5 — DAST + SAST +import_scan "$SCAN_SEMGREP" "$repo_root/labs/lab5/results/semgrep.json" +import_scan "$SCAN_ZAP" "$repo_root/labs/lab5/results/auth-report.json" +# Lab 6 — IaC +import_scan "$SCAN_CHECKOV" "$repo_root/labs/lab6/results/checkov-terraform/results_json.json" +import_scan "$SCAN_KICS" "$repo_root/labs/lab6/results/kics-ansible/results.json" +import_scan "$SCAN_KICS" "$repo_root/labs/lab6/results/kics-pulumi/results.json" +# Lab 7 — Container image + K8s +import_scan "$SCAN_TRIVY" "$repo_root/labs/lab7/results/trivy-image.json" +import_scan "$SCAN_TRIVY_OP" "$repo_root/labs/lab7/results/trivy-k8s.json" echo "Done. Import responses saved under $out_dir" diff --git a/labs/lab11.md b/labs/lab11.md index 54e4104d2..fb145dc9b 100644 --- a/labs/lab11.md +++ b/labs/lab11.md @@ -123,7 +123,7 @@ curl -skI https://localhost | tee labs/lab11/results/headers.txt ### 11.4: Document in `submissions/lab11.md` -```markdown +````markdown # Lab 11 — BONUS — Submission ## Task 1: TLS + Security Headers @@ -155,7 +155,7 @@ curl -skI https://localhost | tee labs/lab11/results/headers.txt - Referrer-Policy: ... - Permissions-Policy: ... - Content-Security-Policy: ... -``` +```` --- @@ -212,7 +212,7 @@ Write the 7-step runbook described in Reading 11 (Detect expiry → Order → Va ### 11.8: Document in `submissions/lab11.md` -```markdown +````markdown ## Task 2: Production Posture ### Rate limit proof @@ -243,7 +243,7 @@ Write the 7-step runbook described in Reading 11 (Detect expiry → Order → Va ### What OCSP stapling buys you (2-3 sentences, reference Reading 11) Why is OCSP stapling useful for production but not for a self-signed lab cert? -``` +```` --- @@ -313,7 +313,7 @@ docker compose exec waf cat /var/log/modsec/audit.log | tail -50 \ ### B.5: Document in `submissions/lab11.md` -```markdown +````markdown ## Bonus: WAF Sidecar with OWASP CRS ### Setup choice @@ -344,7 +344,7 @@ Rule ID: **** — OWASP CRS rule name: ** ⚠️ Common Pitfalls -- 🚨 **`zap-auth.yaml` "context not found"** — the YAML hardcodes Juice Shop running at `juice-shop:3000` (Docker network internal name). If you renamed the container or didn't put both on the same `lab5-net` network, ZAP can't reach it. +- 🚨 **`zap-auth.yaml` "context not found"** — the YAML targets `juice-shop:3000` (Docker network internal name). If you renamed the container or didn't attach both to the same `lab5-net` network, ZAP can't reach it. The container name in `docker run --name` must match exactly. +- 🚨 **Active scan dies silently (CPU 100% → container exit)** — ZAP's active scan is memory-hungry. The `_JAVA_OPTIONS="-Xmx512m"` flag in step 5.3 caps the JVM heap; omitting it lets ZAP consume all available RAM until the container is OOM-killed with no error message. - 🚨 **Auth scan finds 0 alerts** — the `loginRequestBody` in `zap-auth.yaml` ships with the default Juice Shop admin creds (`admin@juice-sh.op` / `admin123`). If you changed them, auth scan logs in as anonymous and only sees unauth surface. - 🚨 **`zap.sh -port 8090` instead of default 8080** — added in the plumbing because the previous lab version conflicted with users running things on 8080. Don't change it unless you also change the YAML. - 🚨 **Semgrep `Parse error: ...`** — Juice Shop's TS sources occasionally hit edge cases. Add `--exclude='**/test/**'` to skip test fixtures if a single parse error blocks the whole scan. diff --git a/labs/lab5/scripts/compare_zap.sh b/labs/lab5/scripts/compare_zap.sh index a32cbec09..f3bd7ee1a 100755 --- a/labs/lab5/scripts/compare_zap.sh +++ b/labs/lab5/scripts/compare_zap.sh @@ -3,10 +3,10 @@ set -e -NOAUTH="labs/lab5/zap/zap-report-noauth.json" -AUTH="labs/lab5/zap/zap-report-auth.json" -OUT="labs/lab5/analysis/zap-comparison.txt" -mkdir -p labs/lab5/analysis +NOAUTH="${1:-labs/lab5/results/baseline-report.json}" +AUTH="${2:-labs/lab5/results/auth-report.json}" +OUT="${3:-labs/lab5/results/zap-comparison.txt}" +mkdir -p "$(dirname "$OUT")" parse_report() { local file="$1" @@ -29,8 +29,6 @@ by_risk = {'3': 0, '2': 0, '1': 0, '0': 0} risk_names = {'3': 'High', '2': 'Medium', '1': 'Low', '0': 'Info'} for site in sites: - if 'localhost:3000' not in site.get('@name', ''): - continue for alert in site.get('alerts', []): risk = alert.get('riskcode', '0') by_risk[risk] = by_risk.get(risk, 0) + 1 @@ -43,8 +41,6 @@ for code in ['3','2','1','0']: # count unique URLs scanned urls = set() for site in sites: - if 'localhost:3000' not in site.get('@name', ''): - continue for alert in site.get('alerts', []): for inst in alert.get('instances', []): urls.add(inst.get('uri', '')) diff --git a/labs/lab5/scripts/zap-auth.yaml b/labs/lab5/scripts/zap-auth.yaml index 0501fffa6..1cbd7e14e 100644 --- a/labs/lab5/scripts/zap-auth.yaml +++ b/labs/lab5/scripts/zap-auth.yaml @@ -2,9 +2,9 @@ env: contexts: - name: "Juice Shop Auth" urls: - - "http://localhost:3000" + - "http://juice-shop:3000" includePaths: - - "http://localhost:3000.*" + - "http://juice-shop:3000.*" excludePaths: - ".*\\.js$" - ".*\\.css$" @@ -15,8 +15,8 @@ env: authentication: method: "json" parameters: - loginPageUrl: "http://localhost:3000/rest/user/login" - loginRequestUrl: "http://localhost:3000/rest/user/login" + loginPageUrl: "http://juice-shop:3000/rest/user/login" + loginRequestUrl: "http://juice-shop:3000/rest/user/login" loginRequestBody: '{"email":"{%username%}","password":"{%password%}"}' verification: method: "poll" @@ -24,7 +24,7 @@ env: loggedOutRegex: ".*\"error\".*" pollFrequency: 60 pollUnits: "requests" - pollUrl: "http://localhost:3000/rest/user/whoami" + pollUrl: "http://juice-shop:3000/rest/user/whoami" pollAdditionalHeaders: - header: "Authorization" value: "Bearer {%token%}" @@ -43,13 +43,13 @@ jobs: - type: "spider" parameters: maxDuration: 5 - url: "http://localhost:3000" + url: "http://juice-shop:3000" user: "admin" - type: "spiderAjax" parameters: maxDuration: 10 - url: "http://localhost:3000" + url: "http://juice-shop:3000" user: "admin" - type: "passiveScan-config" @@ -64,19 +64,19 @@ jobs: - type: "activeScan" parameters: user: "admin" - maxScanDurationInMins: 15 - maxRuleDurationInMins: 3 + maxScanDurationInMins: 10 + maxRuleDurationInMins: 2 - type: "report" parameters: template: "traditional-html" - reportDir: "/zap/wrk/zap/" - reportFile: "report-auth.html" + reportDir: "/zap/wrk/results/" + reportFile: "auth-report.html" reportTitle: "ZAP Authenticated Scan Report" - type: "report" parameters: template: "traditional-json" - reportDir: "/zap/wrk/zap/" - reportFile: "zap-report-auth.json" + reportDir: "/zap/wrk/results/" + reportFile: "auth-report.json" reportTitle: "ZAP Authenticated Scan (JSON)" diff --git a/labs/lab6.md b/labs/lab6.md index 52373d093..af7c44f16 100644 --- a/labs/lab6.md +++ b/labs/lab6.md @@ -5,7 +5,7 @@ ![points](https://img.shields.io/badge/points-10%2B2-orange) ![tech](https://img.shields.io/badge/tech-Checkov%20%2B%20KICS-informational) -> **Goal:** Scan vulnerable Terraform + Pulumi with Checkov, scan vulnerable Ansible with KICS, then (bonus) write a custom Checkov policy for a project-specific rule. +> **Goal:** Scan vulnerable Terraform with Checkov, scan vulnerable Ansible + Pulumi with KICS, then (bonus) write a custom Checkov policy for a project-specific rule. > **Deliverable:** A PR from `feature/lab6` with `submissions/lab6.md` (findings tables + analysis) and (bonus) a custom Checkov policy file. Submit PR link via Moodle. --- @@ -13,8 +13,8 @@ ## Overview In this lab you will practice: -- **Checkov 3.x** on Terraform + Pulumi (~2,500 built-in policies, including 800+ graph-based) — Lecture 6 -- **KICS** on Ansible (~2,400 Rego-based queries) — Lecture 6 +- **Checkov 3.x** on Terraform (~2,500 built-in policies, including 800+ graph-based) — Lecture 6 +- **KICS** on Ansible + Pulumi (~2,400 Rego-based queries) — Lecture 6 - **Triage at the module level** (Lecture 6 slide 17 — one fix at module level closes many findings) - (Bonus) Writing a **custom Checkov policy** in YAML for a project-specific rule @@ -59,10 +59,10 @@ mkdir -p labs/lab6/results ``` > **Plumbing provided** (in `labs/lab6/vulnerable-iac/`): -> - `terraform/` — deliberately misconfigured AWS resources (S3, IAM, RDS, EKS) -> - `pulumi/` — deliberately misconfigured GCP resources (Python Pulumi) +> - `terraform/` — deliberately misconfigured AWS resources (IAM, RDS, DynamoDB, security groups) +> - `pulumi/` — deliberately misconfigured AWS resources (Python + YAML) > - `ansible/` — deliberately misconfigured Linux hardening playbook -> - `README.md` — documents which CKV_* / KICS rules each file targets +> - `README.md` — documents the vulnerability classes each file targets --- @@ -82,50 +82,45 @@ checkov -d labs/lab6/vulnerable-iac/terraform \ ### 6.2: Triage by rule frequency +Checkov scans the directory with several frameworks at once (`terraform` and `secrets`), so +`results_json.json` is a JSON **array** — one object per framework. Open-source Checkov doesn't +assign severities (that's a Prisma Cloud feature), so you triage by **how often each rule fires**: +the most frequent rule is the one a single module-level fix can clear in bulk (Lecture 6 slide 17). + ```bash -# Top 5 rule IDs by count (Lecture 6 slide 17 — module-level leverage) -jq '[.results.failed_checks[].check_id] | group_by(.) | map({rule: .[0], count: length}) | - sort_by(-.count) | .[:5]' \ +# Top 5 rule IDs by count — the highest-leverage fixes +jq '[.[].results.failed_checks[]?.check_id] + | group_by(.) | map({rule: .[0], count: length}) + | sort_by(-.count) | .[:5]' \ labs/lab6/results/checkov-terraform/results_json.json -# Severity breakdown -jq '[.results.failed_checks[].severity] | group_by(.) | map({severity: .[0], count: length})' \ +# Passed / failed per framework +jq 'map({framework: .check_type, passed: .summary.passed, failed: .summary.failed})' \ labs/lab6/results/checkov-terraform/results_json.json ``` -### 6.4: Document in `submissions/lab6.md` +### 6.3: Document in `submissions/lab6.md` ```markdown # Lab 6 — Submission -## Task 1: Checkov on Terraform + Pulumi +## Task 1: Checkov on Terraform -### Terraform scan -- Total checks: -- Passed: -- Failed: - -| Severity | Count | -|----------|------:| -| Critical | | -| High | | -| Medium | | -| Low | | +### Terraform scan (passed/failed per framework) +| Framework | Passed | Failed | +|-----------|-------:|-------:| +| terraform | | | +| secrets | | | ### Top 5 rule IDs (by frequency) | Rule ID | Count | What it checks | |---------|------:|----------------| | | | <1-line description> | -### Pulumi scan -| Severity | Count | -|----------|------:| -| ... | - ### Module-leverage analysis (Lecture 6 slide 17) Looking at your top-5 Terraform rules, which ONE fix would eliminate the most findings if applied -at the module level? (2-3 sentences. e.g., "If the S3 module had `block_public_acls = true` as default, -the 8 findings of CKV_AWS_56 would all go away.") +at the module level? (2-3 sentences. e.g., "If the shared IAM policy dropped its `Resource: "*"` +wildcard, the CKV_AWS_355/289/290 findings on every policy would collapse into one fix.") ``` --- @@ -136,7 +131,7 @@ the 8 findings of CKV_AWS_56 would all go away.") **Objective:** Run KICS against the Ansible playbook AND the Pulumi source; see how Rego-based queries surface different findings than Checkov, and demonstrate KICS's broader format coverage. -### 6.5: Run KICS on Ansible +### 6.4: Run KICS on Ansible ```bash docker run --rm \ @@ -147,7 +142,7 @@ docker run --rm \ --report-formats json,sarif ``` -### 6.5b: Run KICS on Pulumi (natively supported) +### 6.4b: Run KICS on Pulumi (natively supported) ```bash docker run --rm \ @@ -158,25 +153,32 @@ docker run --rm \ --report-formats json,sarif ``` -### 6.6: Analyze +### 6.5: Analyze + +Each scan wrote its own `results.json` (`kics-ansible/` and `kics-pulumi/`). KICS reports a single +JSON object with a `.queries` array, and — unlike Checkov — it assigns severities, so here you can +triage by severity as well as frequency. ```bash -# Severity breakdown -jq '[.queries[].severity] | group_by(.) | map({severity: .[0], count: length})' \ - labs/lab6/results/kics/results.json - -# Top queries by impact -jq '[.queries[] | {query: .query_name, severity, count: (.files | length)}] | - sort_by(-.count) | .[:5]' \ - labs/lab6/results/kics/results.json +# Severity breakdown — for each scan +for scan in kics-ansible kics-pulumi; do + echo "== $scan ==" + jq '[.queries[].severity] | group_by(.) | map({severity: .[0], count: length})' \ + labs/lab6/results/$scan/results.json +done + +# Top queries by impact (Ansible shown; repeat for kics-pulumi) +jq '[.queries[] | {query: .query_name, severity, count: (.files | length)}] + | sort_by(-.count) | .[:5]' \ + labs/lab6/results/kics-ansible/results.json ``` -### 6.7: Document in `submissions/lab6.md` +### 6.6: Document in `submissions/lab6.md` ```markdown -## Task 2: KICS on Ansible +## Task 2: KICS on Ansible + Pulumi -### Severity breakdown +### Ansible — severity breakdown | Severity | Count | |----------|------:| | HIGH | | @@ -184,7 +186,16 @@ jq '[.queries[] | {query: .query_name, severity, count: (.files | length)}] | | LOW | | | INFO | | -### Top 5 KICS queries (by frequency) +### Pulumi — severity breakdown +| Severity | Count | +|----------|------:| +| CRITICAL | | +| HIGH | | +| MEDIUM | | +| LOW | | +| INFO | | + +### Top 5 KICS queries — Ansible (by frequency) | Query | Severity | Files | |-------|----------|------:| | | | | @@ -247,14 +258,15 @@ checkov -d labs/lab6/vulnerable-iac/terraform \ ### B.4: Verify your policy fires ```bash -# Look for your custom rule ID in the results -jq '.results.failed_checks[] | select(.check_id | startswith("CKV2_CUSTOM_"))' \ +# Look for your custom rule ID among the failed checks +jq '[.[].results.failed_checks[]?] + | map(select(.check_id | startswith("CKV2_CUSTOM_")))' \ labs/lab6/results/checkov-custom/results_json.json ``` ### B.5: Document in `submissions/lab6.md` -```markdown +````markdown ## Bonus: Custom Checkov Policy ### Policy file (paste full contents of labs/lab6/policies/my-custom-policy.yaml) @@ -263,7 +275,7 @@ jq '.results.failed_checks[] | select(.check_id | startswith("CKV2_CUSTOM_"))' \ ``` ### Rule fires -Output of `jq '.results.failed_checks[] | select(.check_id | startswith("CKV2_CUSTOM_"))'`: +Output of the B.4 jq (must show ≥1 failed check whose `check_id` starts with `CKV2_CUSTOM_`): ``` ``` @@ -271,7 +283,7 @@ Output of `jq '.results.failed_checks[] | select(.check_id | startswith("CKV2_CU ### Why this rule matters 2-3 sentences: what real-world incident or compliance requirement does your custom policy address? (References to specific incidents or NIST/CIS controls strengthen the answer.) -``` +```` --- @@ -289,8 +301,8 @@ git push -u origin feature/lab6 PR checklist body: ```text -- [x] Task 1 — Checkov on Terraform + Pulumi with top-5 rules and module-leverage analysis -- [ ] Task 2 — KICS on Ansible with Checkov-vs-KICS comparison +- [x] Task 1 — Checkov on Terraform with top-5 rules and module-leverage analysis +- [ ] Task 2 — KICS on Ansible + Pulumi with Checkov-vs-KICS comparison - [ ] Bonus — Custom Checkov policy demonstrably firing on the vulnerable sample ``` @@ -299,14 +311,14 @@ PR checklist body: ## Acceptance Criteria ### Task 1 (6 pts) -- ✅ Checkov runs complete for both Terraform and Pulumi -- ✅ Severity tables match actual JSON output (no placeholders) -- ✅ Top-5 rules table populated with real CKV_AWS_*/CKV_GCP_* IDs + descriptions +- ✅ Checkov scan completes on the Terraform sample +- ✅ Passed/failed table matches actual JSON output (no placeholders) +- ✅ Top-5 rules table populated with real CKV_AWS_* IDs + descriptions - ✅ Module-leverage analysis identifies ONE concrete fix with multi-finding impact ### Task 2 (4 pts) -- ✅ KICS scan completes on the Ansible sample -- ✅ Severity + top-5 tables populated with real query names +- ✅ KICS scan completes on both the Ansible and Pulumi samples +- ✅ Ansible + Pulumi severity tables and the Ansible top-5 table use real values - ✅ Checkov-vs-KICS comparison has substantive 2-3-sentence answers per question ### Bonus Task (2 pts) @@ -321,8 +333,8 @@ PR checklist body: | Task | Points | Criteria | |------|-------:|----------| -| **Task 1** — Checkov | **6** | Terraform + Pulumi scans + top-5 rules + module-leverage analysis | -| **Task 2** — KICS | **4** | Ansible scan + Checkov-vs-KICS comparison with concrete examples | +| **Task 1** — Checkov | **6** | Terraform scan + top-5 rules + module-leverage analysis | +| **Task 2** — KICS | **4** | Ansible + Pulumi scans + Checkov-vs-KICS comparison with concrete examples | | **Bonus Task** — Custom policy | **2** | Valid YAML schema + actually firing on vulnerable resource + business justification | | **Total** | **12** | 10 main + 2 bonus | @@ -344,12 +356,11 @@ PR checklist body:
⚠️ Common Pitfalls -- 🚨 **`pulumi preview --json` fails with "no Pulumi.yaml found"** — use the pre-rendered fallback `labs/lab6/vulnerable-iac/pulumi/pulumi-state-rendered.json` (shipped as plumbing). -- 🚨 **Checkov scans 0 files** — `-d` expects a DIRECTORY; `-f` expects a single file. Pulumi's rendered state is `-f`; Terraform is `-d`. -- 🚨 **KICS finds 0 issues on Ansible** — make sure the path includes the playbook YAML (`-p .../ansible/`). KICS sometimes silently skips files it doesn't recognize as Ansible. Check `kics list-platforms` to verify Ansible is in the supported list (it is). -- 🚨 **Custom policy doesn't fire** — the most common cause is `attribute` path typos. Test on a known-failing resource first (e.g., your custom S3 rule on a bucket without your required block). Add `severity: HIGH` even if you don't need it; Checkov is picky about required fields. -- 🚨 **`CKV_CUSTOM_*` ID conflicts with built-ins** — use `CKV2_CUSTOM_1+` (the `2` prefix marks graph rules and avoids collisions with the built-in numerical sequence). -- 💡 **Read the plumbing README** — `labs/lab6/vulnerable-iac/README.md` lists which Checkov/KICS rules each vulnerable file is designed to trigger. Useful for sanity-checking your scans found what they should. +- 🚨 **Checkov scans 0 files** — `-d` expects a DIRECTORY (the whole `terraform/` folder); `-f` expects a single file. This lab uses `-d`. +- 🚨 **KICS finds 0 issues on Ansible** — point `-p` at the playbook directory (`-p .../ansible/`). Run `kics list-platforms` to confirm Ansible is supported (it is). +- 🚨 **Custom policy doesn't fire** — usually an `attribute` path typo. Test on a known-failing resource first, and always include a `severity:` field; Checkov is strict about required fields. +- 🚨 **`CKV_CUSTOM_*` ID collides with a built-in** — use `CKV2_CUSTOM_1+`; the `2` prefix marks graph rules and stays clear of the built-in sequence. +- 💡 **Read the plumbing README** — `labs/lab6/vulnerable-iac/README.md` lists the vulnerability classes each file targets, so you can sanity-check that your scans found what they should.
diff --git a/labs/lab6/vulnerable-iac/README.md b/labs/lab6/vulnerable-iac/README.md index 65cc1e178..da6146e2b 100644 --- a/labs/lab6/vulnerable-iac/README.md +++ b/labs/lab6/vulnerable-iac/README.md @@ -191,61 +191,41 @@ vulnerable-iac/ --- -## 🛠️ Tools to Use +## 🛠️ Tools Used in This Lab -Students should scan this code with: +| Format | Tool | Why | +|--------|------|-----| +| **Terraform** | **Checkov 3.x** | ~2,500 built-in policies; native HCL support (Task 1) | +| **Pulumi** | **KICS (Checkmarx)** | First-class Pulumi YAML support; Checkov has no Pulumi framework (Task 2) | +| **Ansible** | **KICS (Checkmarx)** | Comprehensive Rego-based Ansible queries (Task 2) | +| **Policy-as-Code** | **Custom Checkov policy (YAML)** | Catch organization-specific rules the catalog doesn't ship (Bonus) | -### Terraform -- **tfsec**: Fast Terraform security scanner -- **Checkov**: Policy-as-code security scanner -- **Terrascan**: OPA-based compliance scanner - -### Pulumi -- **KICS (Checkmarx)**: Open-source scanner with first-class Pulumi YAML support - - Dedicated Pulumi queries catalog (AWS/Azure/GCP/Kubernetes) - - Auto-detects Pulumi platform - - Provides comprehensive security analysis - -### Ansible -- **KICS (Checkmarx)**: Open-source scanner with comprehensive Ansible security queries - - Dedicated Ansible queries catalog - - Auto-detects Ansible playbooks - - Provides comprehensive security analysis - -### Policy-as-Code -- **Conftest/OPA**: Custom policy enforcement for all IaC types +See `labs/lab6.md` for the exact commands. --- ## 📋 Expected Student Outcomes Students should: -1. Identify all 80+ security vulnerabilities across Terraform, Pulumi, and Ansible code - - Note: Pulumi code includes both Python and YAML formats for comprehensive analysis -2. Compare detection capabilities of different tools -3. Compare security issues between declarative (Terraform HCL) and programmatic (Pulumi Python/YAML) IaC +1. Surface the security vulnerabilities across the Terraform, Pulumi, and Ansible samples + - Note: Pulumi code includes both Python and YAML formats; KICS scans the YAML +2. Triage findings by rule frequency (Checkov) and severity (KICS) to find the highest-leverage fixes +3. Compare how Checkov (HCL) and KICS (Rego) surface different findings on the same resource types 4. Evaluate KICS's first-class Pulumi support and query catalog -5. Understand false positives vs true positives -6. Write custom policies to catch organizational-specific issues -7. Provide remediation steps for each vulnerability class -8. Recommend tool selection strategies for CI/CD pipelines +5. Write a custom Checkov policy to catch an organization-specific rule the catalog doesn't ship +6. Reason about tool selection (Checkov vs KICS) for a CI/CD pipeline --- ## 🔧 How to Use (Students) -```bash -# Copy vulnerable code to your lab directory -cp -r vulnerable-iac/terraform/* labs/lab6/terraform/ -cp -r vulnerable-iac/pulumi/* labs/lab6/pulumi/ -cp -r vulnerable-iac/ansible/* labs/lab6/ansible/ - -# Scan with multiple tools (see lab6.md for commands) -docker run --rm -v "$(pwd)/labs/lab6/terraform":/src aquasec/tfsec:latest /src -docker run --rm -v "$(pwd)/labs/lab6/terraform":/tf bridgecrew/checkov:latest -d /tf -docker run -t --rm -v "$(pwd)/labs/lab6/pulumi":/src checkmarx/kics:latest scan -p /src -o /src/kics-report --report-formats json,html -# ... and more -``` +Scan these samples in place — no copying needed. Follow `labs/lab6.md` step by step: + +- **Task 1** — `checkov -d labs/lab6/vulnerable-iac/terraform ...` +- **Task 2** — `kics scan -p .../ansible/` and `kics scan -p .../pulumi/` +- **Bonus** — re-run Checkov with `--external-checks-dir labs/lab6/policies` + +> Don't fix these files — analyze them. The findings are the deliverable. --- @@ -269,15 +249,12 @@ docker run -t --rm -v "$(pwd)/labs/lab6/pulumi":/src checkmarx/kics:latest scan ## ✅ Validation To verify students have completed the lab successfully, check that they: -- [ ] Identified at least 20 Terraform vulnerabilities -- [ ] Identified at least 15 Pulumi vulnerabilities -- [ ] Identified at least 15 Ansible vulnerabilities -- [ ] Compared at least 4 scanning tools (tfsec, Checkov for Terraform, KICS for Pulumi, Terrascan, ansible-lint) -- [ ] Analyzed differences between Terraform (HCL) and Pulumi (Python/YAML) security issues -- [ ] Evaluated KICS's Pulumi-specific query catalog and platform support -- [ ] Created at least 3 custom OPA policies -- [ ] Provided remediation guidance -- [ ] Explained tool selection rationale +- [ ] Ran Checkov on the Terraform sample and reported real findings (top-5 rules + passed/failed) +- [ ] Ran KICS on both the Ansible and Pulumi samples and reported real severities +- [ ] Compared Checkov (HCL) vs KICS (Rego) with concrete examples +- [ ] Identified a module-level fix that clears multiple Terraform findings at once +- [ ] (Bonus) Wrote a custom Checkov policy that demonstrably fires on the sample +- [ ] Explained Checkov-vs-KICS tool selection rationale --- diff --git a/labs/lab7.md b/labs/lab7.md index 74d991f74..4b1d52c44 100644 --- a/labs/lab7.md +++ b/labs/lab7.md @@ -99,7 +99,7 @@ trivy config /tmp/Dockerfile-bad --severity HIGH,CRITICAL --format table ```bash # Top 10 CVEs with fixes (Lecture 7 slide 9 — "fix available AND severity ≥ HIGH first") jq '[.Results[].Vulnerabilities[]? | select(.FixedVersion != null) | - {cve: .VulnerabilityID, severity: .Severity, pkg: .PkgName, fix: .FixedVersion}] | + {cve: .VulnerabilityID, severity: .Severity, pkg: .PkgName, installed: .InstalledVersion, fix: .FixedVersion}] | sort_by(.severity) | .[:10]' \ labs/lab7/results/trivy-image.json ``` @@ -215,18 +215,18 @@ kubectl -n juice-shop describe pod -l app=juice-shop | grep -A 3 -i "security co ### 7.7: Trivy K8s scan ```bash -trivy k8s --namespace juice-shop \ +trivy k8s --include-namespaces juice-shop \ --severity HIGH,CRITICAL \ --format json --output labs/lab7/results/trivy-k8s.json -trivy k8s --namespace juice-shop \ +trivy k8s --include-namespaces juice-shop \ --severity HIGH,CRITICAL \ --report=summary ``` ### 7.8: Document in `submissions/lab7.md` -```markdown +````markdown ## Task 2: Kubernetes Hardening ### Manifests (paste relevant snippets) @@ -258,7 +258,7 @@ Output of `kubectl get pod -n juice-shop -l app=juice-shop`: ### What broke and how you fixed it (2-3 sentences) `readOnlyRootFilesystem: true` likely broke Juice Shop. What paths did it need to write? How did you fix it (which emptyDir mounts)? -``` +```` --- @@ -316,7 +316,7 @@ conftest test /tmp/bad-pod.yaml --policy labs/lab7/policies ### B.3: Document in `submissions/lab7.md` -```markdown +````markdown ## Bonus: Conftest Policy ### Policy (paste labs/lab7/policies/pod-hardening.rego) @@ -337,7 +337,7 @@ conftest test /tmp/bad-pod.yaml --policy labs/lab7/policies ### What this prevents at CI time (2-3 sentences) Reference Lecture 7 slide 16 (admission control diagram). What Class of bug does this policy catch BEFORE `kubectl apply` runs? Why is catching at CI-time better than at admission-time? -``` +```` --- diff --git a/labs/lab8.md b/labs/lab8.md index 1d93c165a..e9222de92 100644 --- a/labs/lab8.md +++ b/labs/lab8.md @@ -160,7 +160,7 @@ cosign verify \ ### 8.6: Document in `submissions/lab8.md` -```markdown +````markdown # Lab 8 — Submission ## Task 1: Sign + Tamper Demo @@ -196,7 +196,7 @@ Output of `cosign verify` on tampered digest: ### Why digest binding matters (Lecture 8 slide 6) 2-3 sentences. The tampered re-tag pointed to a DIFFERENT digest; your signature was bound to the ORIGINAL digest. What would have broken if Cosign had signed the tag instead? -``` +```` --- @@ -271,7 +271,7 @@ cosign verify-attestation \ ### 8.9: Document in `submissions/lab8.md` -```markdown +````markdown ## Task 2: SBOM + Provenance Attestations ### SBOM attestation @@ -292,7 +292,7 @@ cosign verify-attestation \ Lecture 8 slide 12 + Lecture 9 slide 4 — at K8s admission time, a Kyverno verify-images policy can require BOTH signatures AND specific attestation predicates. What's the operational difference between a "signed but no SBOM" image and a "signed with SBOM" image when the next Log4Shell hits? -``` +```` --- @@ -374,7 +374,7 @@ cat /tmp/blob-tamper.txt # paste this into submission ### B.5: Document in `submissions/lab8.md` -```markdown +````markdown ## Bonus: Blob Signing (Codecov 2021 mitigation) ### Sign + verify @@ -394,7 +394,7 @@ Codecov's bash uploader was distributed via `curl | bash` without signature veri If their CI consumers had been running `cosign verify-blob` before `bash`-ing the script, how would the attack have failed? Reference Lecture 8 slide 14 + the specific cosign command that would have caught it. -``` +```` --- diff --git a/labs/lab9.md b/labs/lab9.md index 2575ac529..6f6a43ce2 100644 --- a/labs/lab9.md +++ b/labs/lab9.md @@ -41,7 +41,7 @@ You need: - **Docker** (Falco runs containerized) - **`jq`** - **`conftest`** v0.68.x — `brew install conftest` (Lab 7 bonus used this if you did it) -- **Linux host with kernel ≥ 5.8** (for modern eBPF). On macOS/Windows, Docker Desktop's Linux VM is fine +- **A Linux kernel with eBPF + BTF** (for Falco's modern driver). Native Linux and WSL2 (kernel ≥ 5.8) work out of the box. **macOS — including Apple Silicon — does NOT work through Docker Desktop**: its LinuxKit VM kernel ships without BTF, so Falco runs but detects nothing. Use **Colima** instead — see Common Pitfalls → "macOS / Apple Silicon" ```bash git switch main && git pull @@ -54,8 +54,8 @@ mkdir -p labs/lab9/{falco/{rules,logs},policies/extra,analysis} ``` > **Plumbing provided** (already in `labs/lab9/`): -> - [`labs/lab9/manifests/`](lab9/manifests/) — sample K8s manifests of varying compliance (pass + fail cases) -> - [`labs/lab9/policies/`](lab9/policies/) — starter Conftest policies (you'll extend them) +> - [`labs/lab9/manifests/`](lab9/manifests/) — `k8s/juice-{hardened,unhardened}.yaml` + `compose/juice-compose.yml` +> - [`labs/lab9/policies/`](lab9/policies/) — starter Conftest policies for both shapes (`k8s-security.rego`, `compose-security.rego`) > > Read these files before writing your own — they show the Rego style + sample manifest shape. @@ -102,12 +102,12 @@ sleep 5 # Trigger A: Terminal shell in container — built-in rule docker exec -it lab9-target /bin/sh -lc 'echo "shell-in-container test"' -# Trigger B: Container drift — write under /usr/local/bin -docker exec --user 0 lab9-target /bin/sh -lc 'echo "drift" > /usr/local/bin/drift.txt' +# Trigger B: Read a sensitive file — built-in "Read sensitive file untrusted" rule +docker exec lab9-target /bin/sh -lc 'cat /etc/shadow' # Wait a few seconds, then check Falco alerts sleep 3 -grep -E "(Terminal shell|Write below)" labs/lab9/falco/logs/falco.log | head -10 +grep -E "(Terminal shell|Read sensitive file)" labs/lab9/falco/logs/falco.log | head -10 ``` ### 9.4: Write 1 custom Falco rule @@ -123,9 +123,10 @@ Create `labs/lab9/falco/rules/custom-rules.yaml`: # - priority: WARNING # - tags: [container, drift] # -# Hint: existing default rules write under /usr/local/bin — yours is similar but -# /tmp is a different path. Look at /etc/falco/falco_rules.yaml for the macro -# open_write to understand the syntax. +# Hint: Falco ships the `open_write` macro — read it inside the container: +# docker exec falco cat /etc/falco/falco_rules.yaml | grep -A2 'macro: open_write' +# Your rule combines open_write + a container check (container.id != host) + +# fd.name startswith /tmp/. ``` Falco auto-reloads rules in `/etc/falco/rules.d/`. To force reload after editing: @@ -144,7 +145,7 @@ grep "Write to /tmp by container" labs/lab9/falco/logs/falco.log | head -5 ### 9.6: Document in `submissions/lab9.md` -```markdown +````markdown # Lab 9 — Submission ## Task 1: Runtime Detection with Falco @@ -155,7 +156,7 @@ JSON alert from Falco logs (paste the most relevant lines): ``` -### Baseline alert B — Container drift (write below binary dir) +### Baseline alert B — Read sensitive file untrusted (`cat /etc/shadow`) ```json ``` @@ -175,7 +176,7 @@ Falco log line showing your custom rule: Your custom "write to /tmp" rule will fire on legitimate uses too (logging frameworks often write to /tmp). What's your tuning approach? (2-3 sentences referencing the `exceptions:` block vs `and not proc.name=...` patterns from Lecture 9.) -``` +```` --- @@ -183,19 +184,22 @@ often write to /tmp). What's your tuning approach? (2-3 sentences referencing th > ⏭️ Optional. Skipping won't affect future labs. -**Objective:** Write Rego policies for Conftest that catch ≥3 K8s manifest hardening issues at CI time. +**Objective:** Write Rego policies for Conftest that catch ≥3 K8s manifest hardening issues at CI time, then run the shipped compose policy to see the same `deny[msg]` skill generalize to a second target shape. ### 9.7: Read the provided manifests + starter policies ```bash -ls labs/lab9/manifests/ -# Should show: good-pod.yaml, bad-pod-runasroot.yaml, bad-pod-no-resources.yaml, etc. +ls labs/lab9/manifests/k8s/ +# Should show: juice-hardened.yaml (compliant), juice-unhardened.yaml (non-compliant) ls labs/lab9/policies/ -# Should show: starter Rego files demonstrating Conftest patterns +# Two starter policies, one per target shape: +# k8s-security.rego (package k8s.security) — K8s Deployments (input.spec.template.spec) +# compose-security.rego (package compose.security) — docker-compose (input.services) cat labs/lab9/policies/*.rego -# Read the starter policies — your task extends them +# Read both — note how the SAME deny[msg] pattern adapts to two different input shapes. +# Task 2 has you EXTEND the K8s one (9.8) and RUN the compose one (9.9). ``` ### 9.8: Write your Conftest policies @@ -219,25 +223,47 @@ Add to `labs/lab9/policies/extra/`: # (requires Rego v1 — recent OPA/Conftest versions) ``` -### 9.9: Run Conftest against good + bad manifests +### 9.9: Run Conftest — your K8s policy + the shipped compose policy + +**A. Your K8s policy** (`policies/extra/`) against the shipped manifests: ```bash -# Good manifest — should PASS -conftest test labs/lab9/manifests/good-pod.yaml \ +# Compliant manifest — should PASS (0 failures) +conftest test labs/lab9/manifests/k8s/juice-hardened.yaml \ --policy labs/lab9/policies/extra/ -# Bad manifest #1 — should FAIL with deny messages -conftest test labs/lab9/manifests/bad-pod-runasroot.yaml \ +# Non-compliant manifest — should FAIL with multiple deny messages +# (juice-unhardened has no securityContext, no resources, and a :latest tag, +# so it trips several of your rules at once) +conftest test labs/lab9/manifests/k8s/juice-unhardened.yaml \ --policy labs/lab9/policies/extra/ +``` -# Bad manifest #2 — should FAIL -conftest test labs/lab9/manifests/bad-pod-no-resources.yaml \ - --policy labs/lab9/policies/extra/ +**B. The shipped compose policy** — same `deny[msg]` skill, a different target shape. +It declares `package compose.security`, so Conftest needs `--namespace compose.security` +to find its rules (Conftest defaults to the `main` namespace): + +```bash +# Shipped hardened compose — should PASS +conftest test labs/lab9/manifests/compose/juice-compose.yml \ + --policy labs/lab9/policies/compose-security.rego \ + --namespace compose.security + +# A deliberately unhardened compose — should FAIL (no user / read_only / cap_drop) +cat > /tmp/bad-compose.yml <<'EOF' +services: + app: + image: nginx:latest + ports: ["8080:80"] +EOF +conftest test /tmp/bad-compose.yml \ + --policy labs/lab9/policies/compose-security.rego \ + --namespace compose.security ``` ### 9.10: Document in `submissions/lab9.md` -```markdown +````markdown ## Task 2: Conftest Policy-as-Code ### My policy file (paste labs/lab9/policies/extra/hardening.rego) @@ -245,25 +271,27 @@ conftest test labs/lab9/manifests/bad-pod-no-resources.yaml \ ``` -### Good manifest passes +### Compliant manifest passes (juice-hardened.yaml) ``` ``` -### Bad manifest 1 fails (runAsRoot) +### Non-compliant manifest fails (juice-unhardened.yaml) ``` - + ``` -### Bad manifest 2 fails (no resources) +### Compose policy generalizes (shipped compose-security.rego) ``` - + ``` ### Why CI-time vs admission-time (Lecture 9 slide 9) 2-3 sentences. CI-time Conftest happens during PR review; admission-time Conftest happens at `kubectl apply`. What's the operational benefit of running BOTH (defense in depth)? -``` +```` --- @@ -279,7 +307,7 @@ Common cryptominer indicators (any 2 are sufficient for the rule): | Indicator | Pattern | |---|---| -| Connection to mining pool port | `tcp.dport in (3333, 4444, 5555, 7777, 14444, 19999, 45700)` | +| Connection to mining pool port | `fd.sport in (3333, 4444, 5555, 7777, 14444, 19999, 45700)` | | DNS query for known pool hostname | `evt.type=connect and fd.sockfamily=ip and fd.cip.name contains "minexmr"` | | Process name matches known miner | `proc.name in (xmrig, ethminer, cgminer, t-rex, claymore)` | | High CPU + low network ratio | (Out of scope — needs metrics) | @@ -311,7 +339,7 @@ grep "Cryptominer" labs/lab9/falco/logs/falco.log ### B.4: Document in `submissions/lab9.md` -```markdown +````markdown ## Bonus: Cryptominer Detection Rule ### Rule (paste) @@ -328,7 +356,7 @@ grep "Cryptominer" labs/lab9/falco/logs/falco.log - Which 2 indicators did you use and why? - What does this miss? (i.e., the false-negative case — e.g., obfuscated mining over HTTPS) - How would you combine this with the Lecture 9 SLA matrix? -``` +```` --- @@ -361,7 +389,7 @@ PR checklist body: ```text - [x] Task 1 — 2 baseline + 1 custom Falco alert with tuning discussion -- [ ] Task 2 — ≥3 Conftest rules, passing on good manifest, failing on bad +- [ ] Task 2 — ≥3 Conftest rules (K8s pass/fail) + shipped compose policy run - [ ] Bonus — Cryptominer detection rule with triggered alert ``` @@ -371,15 +399,16 @@ PR checklist body: ### Task 1 (6 pts) - ✅ Falco running with modern eBPF (verify with `docker logs falco | grep -i engine`) -- ✅ Both baseline alerts (Terminal shell + Container drift) appear in Falco logs +- ✅ Both baseline alerts (Terminal shell + Read sensitive file) appear in Falco logs - ✅ Custom rule `custom-rules.yaml` exists with required fields - ✅ Custom rule fires (visible in Falco log after the test trigger) - ✅ Tuning consideration mentions `exceptions:` block OR `and not` pattern with reasoning ### Task 2 (4 pts) - ✅ ≥3 Rego rules in `labs/lab9/policies/extra/` -- ✅ Good manifest PASSES (0 failures from conftest) -- ✅ ≥2 bad manifests FAIL with clear deny messages +- ✅ Compliant manifest (`juice-hardened.yaml`) PASSES (0 failures from conftest) +- ✅ Non-compliant manifest (`juice-unhardened.yaml`) FAILS with ≥2 distinct deny messages +- ✅ Shipped `compose-security.rego` run shown — PASS on `juice-compose.yml`, FAIL on a bad compose - ✅ CI-vs-admission answer demonstrates understanding of defense-in-depth ### Bonus Task (2 pts) @@ -394,7 +423,7 @@ PR checklist body: | Task | Points | Criteria | |------|-------:|----------| | **Task 1** — Falco runtime | **6** | 2 baseline + 1 custom alert + tuning discussion | -| **Task 2** — Conftest policies | **4** | 3+ Rego rules + good passes + bad fails + CI/admission reasoning | +| **Task 2** — Conftest policies | **4** | 3+ Rego rules + K8s good/bad + shipped compose policy run + CI/admission reasoning | | **Bonus Task** — Cryptominer rule | **2** | 2+ indicators + triggered alert + reflection on FN + SLA | | **Total** | **12** | 10 main + 2 bonus | @@ -417,6 +446,15 @@ PR checklist body: ⚠️ Common Pitfalls - 🚨 **Falco fails to start with "engine.kind not detected"** — your kernel might be < 5.8 (no modern eBPF). Fall back to the legacy eBPF driver: add `-e FALCO_BPF_PROBE=""` to the docker run command. +- 🚨 **macOS / Apple Silicon — Falco starts but detects nothing** — Docker Desktop runs a stripped LinuxKit VM whose kernel ships without BTF (and the raw tracepoints Falco's modern-eBPF probe attaches to), so Falco loads cleanly and stays blind. This is **not** a CPU/arch problem — the image is multi-arch (arm64 included) — it's the VM kernel. Fix: give Falco a real Ubuntu kernel via **Colima** (free, Homebrew, a Lima-based Docker backend whose VM has BTF): + ```bash + brew install colima docker + colima start --cpu 4 --memory 6 --disk 30 + # Gate check — must print "BTF OK" before you bother running Falco: + colima ssh -- test -f /sys/kernel/btf/vmlinux && echo "BTF OK" + ``` + Colima becomes your Docker backend, so steps 9.1–9.5 run **unchanged** (clone the repo under your home dir — Colima mounts `$HOME` into the VM). The universal 5-second check for *any* environment: `test -f /sys/kernel/btf/vmlinux` — present ⇒ modern eBPF attaches; absent (Docker Desktop) ⇒ Falco is blind. Task 2 (Conftest) is pure userspace — `brew install conftest` and run it natively on macOS, no VM needed. *(A plain Ubuntu VM via multipass/Lima/UTM works too; Colima is just the least-disruptive since it keeps the Docker CLI workflow.)* +- 🚨 **Falco dies with `could not initialize inotify handler`** — WSL2 / crowded-Docker hosts hit the default `fs.inotify.max_user_instances=128` (same wall as Lab 7). Two fixes: (a) as root, `sysctl -w fs.inotify.max_user_instances=1024`; or (b) if you can't sudo, append `-o watch_config_files=false` to the `falco ...` command — Falco then starts without the file watcher, and you reload rules manually with the `SIGHUP` step below (which doesn't need inotify). - 🚨 **No alerts fire after triggering** — Falco needs a few seconds to load rules. Wait 5+ seconds between starting Falco and triggering. Also confirm rules loaded with `docker logs falco | grep -i "loaded rule"`. - 🚨 **Custom rule has YAML parse error and silently doesn't load** — `docker logs falco | grep -i error` shows the parse error. Common cause: indentation. Validate with `yq eval . custom-rules.yaml`. - 🚨 **`docker kill --signal=SIGHUP falco`** — used to reload rules; if you instead `docker restart falco`, the log file gets truncated. diff --git a/labs/lab9/manifests/compose/juice-compose.yml b/labs/lab9/manifests/compose/juice-compose.yml index acfcb64bf..58f1ea645 100644 --- a/labs/lab9/manifests/compose/juice-compose.yml +++ b/labs/lab9/manifests/compose/juice-compose.yml @@ -1,6 +1,6 @@ services: juice: - image: bkimminich/juice-shop:v19.0.0 + image: bkimminich/juice-shop@sha256:2765a26de7647609099a338d5b7f61085d95903c8703bb70f03fcc4b12f0818d # juice-shop v19.0.0, pinned by digest ports: ["3006:3000"] user: "10001:10001" read_only: true diff --git a/labs/lab9/manifests/k8s/juice-hardened.yaml b/labs/lab9/manifests/k8s/juice-hardened.yaml index 105211966..37d153d2f 100644 --- a/labs/lab9/manifests/k8s/juice-hardened.yaml +++ b/labs/lab9/manifests/k8s/juice-hardened.yaml @@ -12,7 +12,7 @@ spec: spec: containers: - name: juice - image: bkimminich/juice-shop:v19.0.0 + image: bkimminich/juice-shop@sha256:2765a26de7647609099a338d5b7f61085d95903c8703bb70f03fcc4b12f0818d # juice-shop v19.0.0, pinned by digest securityContext: runAsNonRoot: true allowPrivilegeEscalation: false diff --git a/lectures/lec1.md b/lectures/lec1.md index 9bf2fe693..b51095fb4 100644 --- a/lectures/lec1.md +++ b/lectures/lec1.md @@ -70,10 +70,10 @@ graph LR ```mermaid flowchart LR - Dev[👩‍💻 Dev] -.--> Build[🏗️] - Ops[🖥️ Ops] -.--> Build - Sec[🛡️ Sec] -.--> Build - Build --> DevSecOps[🚀 DevSecOps
Continuous, automated security
at every stage] + Dev["👩‍💻 Dev"] -.-> Build["🏗️ Build"] + Ops["🖥️ Ops"] -.-> Build + Sec["🛡️ Sec"] -.-> Build + Build --> DevSecOps["🚀 DevSecOps
Continuous, automated security
at every stage"] style DevSecOps fill:#FF9800,color:#fff ``` diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 000000000..7c4c9884a --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,165 @@ +# Lab 11 — BONUS — Submission + +## Task 1: TLS + Security Headers + +### nginx.conf (paste the SSL + header sections only — not the whole file) +```nginx + ssl_certificate /etc/nginx/certs/localhost.crt; + ssl_certificate_key /etc/nginx/certs/localhost.key; + ssl_session_timeout 10m; + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1.3; + ssl_ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:EECDH+AESGCM:EDH+AESGCM"; + ssl_prefer_server_ciphers off; + ssl_stapling off; + # If using a publicly-trusted certificate, you may enable OCSP stapling: + # ssl_stapling on; + # ssl_stapling_verify on; + # resolver 1.1.1.1 8.8.8.8 valid=300s; + # resolver_timeout 5s; + # ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + + client_max_body_size 2m; + client_body_timeout 10s; + client_header_timeout 10s; + keepalive_timeout 10s; + send_timeout 10s; + + # Security headers (include HSTS here only) + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" always; +``` + +### A. HTTPS redirect proof +``` +HTTP/1.1 308 Permanent Redirect +Server: nginx +Date: Fri, 17 Jul 2026 18:04:03 GMT +Content-Type: text/html +Content-Length: 164 +Connection: keep-alive +Location: https://localhost/ +X-Frame-Options: DENY +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), microphone=(), geolocation=() +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' +``` + +### B. TLS 1.3 proof +``` +CONNECTION ESTABLISHED +Protocol version: TLSv1.3 +Ciphersuite: TLS_AES_256_GCM_SHA384 +Peer certificate: CN=juice.local +Hash used: SHA256 +Signature type: rsa_pss_rsae_sha256 +Verification error: self-signed certificate +Negotiated TLS1.3 group: X25519MLKEM768 +``` + +### C. Security headers proof (all 6 present) +``` +HTTP/1.1 200 OK +Server: nginx +Date: Fri, 17 Jul 2026 18:04:55 GMT +Content-Type: text/html; charset=UTF-8 +Content-Length: 9903 +Connection: keep-alive +Feature-Policy: payment 'self' +X-Recruiting: /#/jobs +Accept-Ranges: bytes +Cache-Control: public, max-age=0 +Last-Modified: Fri, 17 Jul 2026 18:01:41 GMT +ETag: W/"26af-19f713dc3d7" +Vary: Accept-Encoding +Strict-Transport-Security: max-age=63072000; includeSubDomains; preload +X-Frame-Options: DENY +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), microphone=(), geolocation=() +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' +``` + +### What each header defends against (1 sentence each) +- HSTS: Prevents downgrade attacks and cookie hijacking by forcing the browser to only connect via HTTPS. +- X-Content-Type-Options: nosniff: Prevents MIME-sniffing vulnerabilities, ensuring the browser strictly adheres to the provided Content-Type header. +- X-Frame-Options: DENY: Defends against clickjacking attacks by preventing the page from being rendered within a frame or iframe. +- Referrer-Policy: Protects sensitive information in URLs from being leaked in the Referer header to cross-origin requests. +- Permissions-Policy: Restricts which web features and APIs (like camera or geolocation) can be used, limiting the attack surface. +- Content-Security-Policy: Mitigates Cross-Site Scripting (XSS) and data injection attacks by defining which dynamic resources are allowed to load. + +## Task 2: Rate Limiting, Timeouts, Cipher Hardening, Cert Rotation + +### nginx.conf (Rate Limit & Connection Limit sections) +```nginx + # Rate limit zone for login + limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; + limit_req_status 429; + limit_conn_zone $binary_remote_addr zone=conn:10m; + + # Inside server { listen 443 ssl; ... } + limit_conn conn 50; + + # Inside location = /rest/user/login { ... } + limit_req zone=login burst=5 nodelay; +``` + +### Rate limit test proof (60 concurrent POSTs to login) +``` +Count Name +----- ---- + 3 500 + 57 429 +``` + +### Cert rotation runbook (7 steps) +1. **Detect expiry**: Monitor certificate expiration using synthetic checks (e.g., Datadog, Prometheus Blackbox Exporter) alerting at 30 days before expiration. +2. **Order new cert**: Automatically generate a new CSR and request a signed certificate from the CA (e.g., using Certbot/ACME or internal Vault PKI). +3. **Validate**: Verify the new certificate chain and private key match (`openssl x509 -noout -modulus`) and that it has the correct SANs and dates. +4. **Atomic swap**: Deploy the new certificates to the server (e.g., `/etc/nginx/certs/`) without overwriting old ones immediately, then gracefully reload Nginx (`nginx -s reload`). +5. **Verify**: Run automated post-deployment checks (`openssl s_client -connect ...`) to confirm the server is presenting the new, valid certificate. +6. **Rollback plan**: Keep the previous certificate files available and have a script ready to revert the symlinks/paths and reload Nginx if the verification fails. +7. **Audit**: Log the successful rotation event to a centralized auditing system for compliance tracking and to close the initial expiry alert. + +### What OCSP stapling buys you (2-3 sentences, reference Reading 11) +OCSP stapling improves privacy and performance by having the server fetch the certificate revocation status from the CA and "staple" it to the initial TLS handshake, rather than forcing every client to make a separate external network call to the CA. It is not useful for a self-signed lab cert because there is no external Certificate Authority to query for revocation status; the self-signed cert is inherently trusted (or untrusted) locally without an ongoing revocation infrastructure. + +## Bonus: WAF Sidecar with OWASP CRS + +### Setup choice +- WAF used: ModSecurity v3 (owasp/modsecurity-crs:nginx-alpine) +- OWASP CRS version: v3.3.10 (as bundled in image) +- Paranoia level: 1 + +### Attack payload sent +`GET /rest/products/search?q=' OR 1=1--` (URL-encoded) + +### Before WAF (Nginx alone) +``` +no-waf: HTTP 500 +``` + +### After WAF +``` +with-waf: HTTP 403 +``` + +### Audit log excerpt (the rule that fired) +``` +[error] 531#531: *1 [client 172.20.0.1] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `5' ) [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "81"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] +``` +Rule ID: **949110** (and implicitly the underlying SQLi rules like **942100**) — OWASP CRS rule name: **Inbound Anomaly Score Exceeded** + +### Tradeoff analysis (3 sentences) +Deploying a WAF with OWASP CRS provides real-time defense against zero-days and active exploitation attempts (like SQLi or XSS) that SAST/DAST might miss or can't patch immediately. However, it costs operational overhead, requires careful tuning to avoid false positives (especially at higher paranoia levels), and adds latency to requests. You would NOT deploy a WAF in front of internal microservices that only receive highly-trusted, authenticated traffic from within the cluster where the risk of injection is negligible and performance is paramount.