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/falco/rules/custom-rules.yaml b/labs/lab9/falco/rules/custom-rules.yaml new file mode 100644 index 000000000..84bf8ac1a --- /dev/null +++ b/labs/lab9/falco/rules/custom-rules.yaml @@ -0,0 +1,13 @@ +- rule: Write to /tmp by container + desc: Detects writes to /tmp inside any container + condition: open_write and container and container.id != "host" and fd.name startswith "/tmp/" + output: "Write to /tmp by container (container=%container.name user=%user.name file=%fd.name cmdline=%proc.cmdline)" + priority: WARNING + tags: [container, drift] + +- rule: Possible Cryptominer Activity + desc: Detects container connecting to common mining-pool ports or known miner processes + condition: container and evt.type=connect and (fd.sport in (3333, 4444, 5555, 7777, 14444, 19999, 45700) or proc.name in (xmrig, ethminer, cgminer, t-rex, claymore)) + output: "Possible Cryptominer Activity (container=%container.name process=%proc.name target=%fd.sip:%fd.sport)" + priority: CRITICAL + tags: [container, mitre_execution, mitre_command_and_control] 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/labs/lab9/policies/extra/hardening.rego b/labs/lab9/policies/extra/hardening.rego new file mode 100644 index 000000000..eba9864c4 --- /dev/null +++ b/labs/lab9/policies/extra/hardening.rego @@ -0,0 +1,35 @@ +package main + +import rego.v1 + +deny contains msg if { + input.kind == "Deployment" + pod_sec := object.get(input.spec.template.spec, "securityContext", {}) + not pod_sec.runAsNonRoot + + some container in input.spec.template.spec.containers + cont_sec := object.get(container, "securityContext", {}) + not cont_sec.runAsNonRoot + + msg := sprintf("Container '%v' must have runAsNonRoot=true", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + some container in input.spec.template.spec.containers + cont_sec := object.get(container, "securityContext", {}) + not cont_sec.allowPrivilegeEscalation == false + + msg := sprintf("Container '%v' must have allowPrivilegeEscalation=false", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + some container in input.spec.template.spec.containers + cont_sec := object.get(container, "securityContext", {}) + caps := object.get(cont_sec, "capabilities", {}) + drop := object.get(caps, "drop", []) + not "ALL" in drop + + msg := sprintf("Container '%v' must drop ALL capabilities", [container.name]) +} 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/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..e03b35bda --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,183 @@ +# Lab 9 — Submission + +## Task 1: Runtime Detection with Falco + +### Baseline Alert A — Terminal Shell in Container +Falco detected an interactive shell execution inside the target container. Note the `EXE_WRITABLE` flag and attached terminal (`proc.tty=34816`), which are strong indicators of manual intervention rather than automated orchestration. + +```json +{ + "hostname": "89eba2f8a2df", + "output": "2026-07-10T13:28:21.001674096+0000: Notice A shell was spawned in a container with an attached terminal | evt_type=execve user=root user_uid=0 process=sh parent=runc command=sh -lc echo test_shell container_id=8a98414f402c container_name=lab9-target", + "priority": "Notice", + "rule": "Terminal shell in container", + "tags": ["T1059", "container", "mitre_execution", "shell"] +} +``` + +### Baseline Alert B — Read Sensitive File Untrusted +Access to `/etc/shadow` by a non-trusted process (`cat`) triggered this warning. This maps directly to MITRE ATT&CK T1555 (Credentials from Password Stores). + +```json +{ + "hostname": "89eba2f8a2df", + "output": "2026-07-10T13:27:36.340140711+0000: Warning Sensitive file opened for reading by non-trusted program | file=/etc/shadow process=cat command=cat /etc/shadow container_id=8a98414f402c container_name=lab9-target", + "priority": "Warning", + "rule": "Read sensitive file untrusted", + "tags": ["T1555", "container", "filesystem", "mitre_credential_access"] +} +``` + +### Custom Rule: Write to /tmp by Container +This rule detects writes to `/tmp` specifically within containers (excluding host writes). It combines the built-in `open_write` macro with container context filtering. + +```yaml +- rule: Write to /tmp by container + desc: Detects writes to /tmp inside any container + condition: open_write and container and container.id != "host" and fd.name startswith "/tmp/" + output: "Write to /tmp by container (container=%container.name user=%user.name file=%fd.name cmdline=%proc.cmdline)" + priority: WARNING + tags: [container, drift] +``` + +### Custom Rule Triggered +The rule successfully fired when `echo "test" > /tmp/my-write.txt` was executed inside `lab9-target`. The alert captures the exact file path and command line used. + +```json +{ + "hostname": "89eba2f8a2df", + "output": "2026-07-10T13:29:00.008630829+0000: Warning Write to /tmp by container (container=lab9-target user=root file=/tmp/my-write.txt cmdline=sh -lc echo 'test' > /tmp/my-write.txt)", + "priority": "Warning", + "rule": "Write to /tmp by container", + "tags": ["container", "drift"] +} +``` + +### Tuning Consideration (Lecture 9 Slide 8) +Writing to `/tmp` is common behavior for legitimate applications (e.g., log rotation, temp file creation by web servers). To reduce false positives without disabling the rule entirely, I would use the `exceptions:` block in the Falco rule definition. For example: +```yaml +exceptions: + - name: trusted_procs + fields: [proc.name] + comps: [=] + values: [[fluentd, logstash, nginx]] +``` +Alternatively, using `and not proc.name in (...)` directly in the condition works but becomes hard to maintain as the allowlist grows. The `exceptions:` approach is preferred because it keeps the core logic clean and allows dynamic updates via configuration management without modifying the rule syntax itself. + +--- + +## Task 2: Conftest Policy-as-Code + +### My Policy File (`labs/lab9/policies/extra/hardening.rego`) +This policy enforces three critical security controls using Rego v1 syntax: + +```rego +package main + +import rego.v1 + +deny contains msg if { + input.kind == "Deployment" + pod_sec := object.get(input.spec.template.spec, "securityContext", {}) + not pod_sec.runAsNonRoot + + some container in input.spec.template.spec.containers + cont_sec := object.get(container, "securityContext", {}) + not cont_sec.runAsNonRoot + + msg := sprintf("Container '%v' must have runAsNonRoot=true", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + some container in input.spec.template.spec.containers + cont_sec := object.get(container, "securityContext", {}) + not cont_sec.allowPrivilegeEscalation == false + + msg := sprintf("Container '%v' must have allowPrivilegeEscalation=false", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + some container in input.spec.template.spec.containers + cont_sec := object.get(container, "securityContext", {}) + caps := object.get(cont_sec, "capabilities", {}) + drop := object.get(caps, "drop", []) + not "ALL" in drop + + msg := sprintf("Container '%v' must drop ALL capabilities", [container.name]) +} +``` + +### Compliant Manifest Passes (`juice-hardened.yaml`) +All 6 tests passed with zero failures, confirming the hardened manifest meets all policy requirements. + +```text +6 tests, 6 passed, 0 warnings, 0 failures, 0 exceptions +``` + +### Non-Compliant Manifest Fails (`juice-unhardened.yaml`) +Three distinct violations were detected, demonstrating the policy's ability to catch multiple security gaps simultaneously. + +```text +FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - Container 'juice' must drop ALL capabilities +FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - Container 'juice' must have allowPrivilegeEscalation=false +FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - Container 'juice' must have runAsNonRoot=true + +6 tests, 3 passed, 0 warnings, 3 failures, 0 exceptions +``` + +### Compose Policy Generalizes (`compose-security.rego`) +The same `deny[msg]` pattern adapts seamlessly to Docker Compose manifests by targeting `input.services` instead of K8s specs. + +**Hardened Compose (PASS):** +```text +4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions +``` + +**Unhardened Compose (FAIL):** +```text +FAIL - bad-compose.yml - compose.security - services must set an explicit non-root user +FAIL - bad-compose.yml - compose.security - services must set read_only: true + +4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions +``` + +### Why CI-Time vs Admission-Time (Defense in Depth) +CI-time Conftest provides **developer feedback loops**: violations appear in PR checks before code merges, enabling fast iteration and preventing insecure manifests from polluting the repository history. However, CI can be bypassed (e.g., direct `kubectl apply`, emergency hotfixes). Admission-time enforcement (via OPA Gatekeeper/Kyverno) acts as the **final safety net**, guaranteeing that *no* workload enters the cluster without meeting policies, regardless of origin. Running both creates defense-in-depth: CI shifts security left for velocity, while admission control ensures runtime compliance for safety. + +--- + +## Bonus: Cryptominer Detection Rule + +### Rule Definition +Combines network port detection and process name matching to catch both standard miners and custom binaries connecting to known pools. + +```yaml +- rule: Possible Cryptominer Activity + desc: Detects container connecting to common mining-pool ports or known miner processes + condition: container and evt.type=connect and (fd.sport in (3333, 4444, 5555, 7777, 14444, 19999, 45700) or proc.name in (xmrig, ethminer, cgminer, t-rex, claymore)) + output: "Possible Cryptominer Activity (container=%container.name process=%proc.name target=%fd.sip:%fd.sport)" + priority: CRITICAL + tags: [container, mitre_execution, mitre_command_and_control] +``` + +### Triggered Alert +Simulated connection to port 3333 via `nc` triggered the rule instantly. + +```json +{ + "hostname": "89eba2f8a2df", + "output": "2026-07-10T13:31:36.562140711+0000: Critical Possible Cryptominer Activity (container=lab9-target process=nc target=127.0.0.1:3333)", + "priority": "Critical", + "rule": "Possible Cryptominer Activity", + "tags": ["container", "mitre_execution", "mitre_command_and_control"] +} +``` + +### Reflection +**Indicators Used:** I combined `fd.sport` (mining pool ports like 3333) and `proc.name` (known miner binaries like xmrig). This dual-indicator approach catches both script-kiddie attacks using default tools and more sophisticated actors who might rename binaries but still connect to standard pool ports. + +**False Negatives:** This rule misses miners tunneling over HTTPS (port 443), using domain fronting, or employing web-based JavaScript miners (cryptojacking) that don't spawn separate processes. Renamed binaries connecting to obscure pools would also evade detection. + +**SLA Matrix Integration:** Given the CRITICAL priority and MITRE C2 tagging, this alert should trigger an automated response per the SLA matrix: immediate container isolation/termination + memory dump capture within 5 minutes. Financial impact from cryptomining escalates rapidly, so MTTD/MTTR targets must be aggressive compared to lower-severity drift alerts.