From 44f7579e48d04e5feca0828bd7167e15a2bf6f09 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Tue, 23 Jun 2026 22:10:50 +0400 Subject: [PATCH 01/10] fix(lab5): juice-shop hostname, OOM cap, compare_zap params Signed-off-by: Dmitrii Creed --- labs/lab5.md | 5 ++++- labs/lab5/scripts/compare_zap.sh | 12 ++++-------- labs/lab5/scripts/zap-auth.yaml | 26 +++++++++++++------------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/labs/lab5.md b/labs/lab5.md index e80954eff..c6a65e44f 100644 --- a/labs/lab5.md +++ b/labs/lab5.md @@ -97,7 +97,9 @@ docker run --rm --network lab5-net \ ```bash # The provided zap-auth.yaml drives the Automation Framework +# _JAVA_OPTIONS caps ZAP's JVM heap; without it the active scan OOM-kills the container docker run --rm --network lab5-net \ + -e _JAVA_OPTIONS="-Xmx512m" \ -v "$(pwd)/labs/lab5:/zap/wrk" \ ghcr.io/zaproxy/zaproxy:stable \ zap.sh -cmd -autorun /zap/wrk/scripts/zap-auth.yaml -port 8090 @@ -369,7 +371,8 @@ PR checklist body:
⚠️ 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)" From 2f54b0e8a9b3b4b21cc9d8654ae02e6d8177ecee Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 27 Jun 2026 01:29:19 +0400 Subject: [PATCH 02/10] fix(lab6): correct Checkov jq, scope Pulumi to KICS, align plumbing README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 1 jq broke on real Checkov output: a directory scan runs multiple frameworks (terraform + secrets), so results_json.json is a JSON array and .results.failed_checks[] errored with 'Cannot index array with string'. Use .[] to iterate frameworks. Open-source Checkov assigns no severities (a Prisma Cloud feature), so the severity breakdown is replaced with passed/failed and triage is framed around rule frequency. Pulumi was referenced in Task 1's report, acceptance criteria, and rubric but Checkov never scanned it (no pulumi framework; the 'shipped' rendered-state file never existed). Pulumi is scanned by KICS in Task 2 — moved its severity table there and fixed the Task 2 jq paths (kics-ansible/, kics-pulumi/ instead of a kics/ dir that is never created). Closed the 6.3 numbering gap. README aligned to the actual lab (Checkov + KICS + custom Checkov policy): dropped tfsec/Terrascan/OPA references, corrected Pulumi as AWS (not GCP), and removed the nonexistent EKS resource from the Terraform description. Reported by Albert Khechoyan. Signed-off-by: Dmitrii Creed --- labs/lab6.md | 139 ++++++++++++++++------------- labs/lab6/vulnerable-iac/README.md | 77 ++++++---------- 2 files changed, 102 insertions(+), 114 deletions(-) diff --git a/labs/lab6.md b/labs/lab6.md index 18392b445..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 - -### Terraform scan -- Total checks: -- Passed: -- Failed: +## Task 1: Checkov on Terraform -| 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,33 +153,49 @@ 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 + +### Ansible — severity breakdown +| Severity | Count | +|----------|------:| +| HIGH | | +| MEDIUM | | +| LOW | | +| INFO | | -### Severity breakdown +### Pulumi — severity breakdown | Severity | Count | |----------|------:| +| CRITICAL | | | HIGH | | | MEDIUM | | | LOW | | | INFO | | -### Top 5 KICS queries (by frequency) +### Top 5 KICS queries — Ansible (by frequency) | Query | Severity | Files | |-------|----------|------:| | | | | @@ -247,8 +258,9 @@ 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 ``` @@ -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_`): ``` ``` @@ -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 --- From 9222ec68f92809593d8e1545149542ff86fb267d Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Mon, 29 Jun 2026 01:13:39 +0400 Subject: [PATCH 03/10] fix(labs): align lab7 & lab9 with pinned tool behavior Student-reported breakage where lab instructions assumed tool behavior the pinned versions no longer have. Each fix verified against the real binaries. Lab 7 (Trivy 0.69): - 7.3 jq: add `installed: .InstalledVersion` so the 7.4 "Installed" column has data - 7.7: `trivy k8s --namespace` was removed in 0.69 -> `--include-namespaces` Lab 9 (Falco 0.43.1, Conftest 0.68): - manifests are k8s/juice-{hardened,unhardened}.yaml, not flat good-pod/bad-pod - 9.3 trigger B: a write to /usr/local/bin fires nothing in the 0.43.1 default ruleset (rule moved to incubating feed) -> `cat /etc/shadow` (Read sensitive file untrusted), confirmed firing in a live Falco run - 9.4 hint: point at the real `open_write` macro instead of a non-existent rule - bonus: `tcp.dport` -> `fd.sport` (real Falco field) - add inotify pitfall + `-o watch_config_files=false` workaround for WSL2 - wire the shipped compose policy into 9.9 as a second target-shape demo Reported by students in the DevSecOps-Intro cohort. Signed-off-by: Dmitrii Creed --- labs/lab7.md | 6 ++-- labs/lab9.md | 96 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/labs/lab7.md b/labs/lab7.md index 770e02f02..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,11 +215,11 @@ 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 ``` diff --git a/labs/lab9.md b/labs/lab9.md index ad2b85c00..e19498a70 100644 --- a/labs/lab9.md +++ b/labs/lab9.md @@ -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: @@ -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 ``` @@ -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,20 +223,42 @@ 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` @@ -245,19 +271,21 @@ 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) @@ -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) | @@ -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,7 @@ 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. +- 🚨 **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. From 63136a87924ab6bb03360eba0b074fed2ce3737b Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Mon, 29 Jun 2026 01:18:28 +0400 Subject: [PATCH 04/10] fix(lab9): pin juice-hardened image by digest so the optional digest rule passes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hardened manifest pinned its image by tag (:v19.0.0), so a student who implements the optional 5th hardening rule ("image must use sha256 digest, not :tag") saw the GOOD manifest FAIL — contradicting 9.9-A's "should PASS (0 failures)". Pin by the resolved v19.0.0 multi-arch index digest; the unhardened manifest (:latest) still fails the rule as the bad example. Digest resolved via 'docker buildx imagetools inspect', not fabricated. Signed-off-by: Dmitrii Creed --- labs/lab9/manifests/k8s/juice-hardened.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a17362dc3e097d98885af182c0f02609437fb93a Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Mon, 29 Jun 2026 01:20:57 +0400 Subject: [PATCH 05/10] fix(lab9): pin compose image by digest for consistency with juice-hardened Signed-off-by: Dmitrii Creed --- labs/lab9/manifests/compose/juice-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c81b7d547b70865f17c1eacc76fc031ddd462e4c Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Tue, 30 Jun 2026 00:04:34 +0400 Subject: [PATCH 06/10] docs(lab9): Colima workaround for Falco on macOS/Apple Silicon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker Desktop's LinuxKit VM kernel lacks BTF + tracepoints, so Falco runs but detects nothing on Mac (esp. Apple Silicon). The Setup section wrongly said Docker Desktop 'is fine' — corrected it, and added a Common Pitfalls entry with the Colima fix and a 'test -f /sys/kernel/btf/vmlinux' gate check. Reported by a student on an M-series Mac. Signed-off-by: Dmitrii Creed --- labs/lab9.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/labs/lab9.md b/labs/lab9.md index e19498a70..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 @@ -446,6 +446,14 @@ 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`. From eb41fc383b97b6ee0dde1326c130456229425782 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Tue, 30 Jun 2026 13:41:38 +0400 Subject: [PATCH 07/10] fix(lab10): rewrite DefectDojo importer to match the course + run on macOS The run-imports.sh helper did not work as shipped: - Line 55 used mapfile, a bash 4+ builtin absent on stock macOS bash 3.2, so the script aborted on macOS. Replaced with a portable while-read loop and set -u-safe array handling; verified under bash 3.2.57. - Every hardcoded input path was wrong. Corrected to the real Lab 4-7 outputs and anchored them to the repo root so the script runs from any directory. - The script required DD_API while lab10.md tells students to export DD_URL, so require_env aborted immediately. It now accepts DD_URL and derives the API base. - It imported Nuclei (not in this course) and skipped Checkov/KICS/Trivy image/Trivy operator. It now imports exactly the documented Lab 4-7 table, with scan_type fallbacks verified against the DefectDojo 2.58.3 parser source. lab10.md aligned: correct script name (was import-all.sh), KICS split into kics-ansible/kics-pulumi, fixed the manual curl template path, and added the env.sample the plumbing promised. Importer outputs are now gitignored. Reported by Albert Khechoyan. Signed-off-by: Dmitrii Creed --- .gitignore | 1 + labs/lab10.md | 24 +++-- labs/lab10/imports/env.sample | 14 +++ labs/lab10/imports/run-imports.sh | 172 +++++++++++++++--------------- 4 files changed, 116 insertions(+), 95 deletions(-) create mode 100644 labs/lab10/imports/env.sample 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/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" From f9932471eb75787a0588c96ee2fc4f65ed0772e8 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Tue, 30 Jun 2026 14:04:36 +0400 Subject: [PATCH 08/10] fix(lab11): align reverse-proxy starter to the documented 80/443 ports The shipped nginx.conf listened on 8080/8443 and docker-compose.yml published 8080:8080/8443:8443, but every instruction and verification command in lab11.md assumes 80/443 (curl http://localhost, https://localhost, s_client -connect localhost:443, and the 11.1 spec 'HTTP on 80 -> HTTPS 443'). Following the lab gave connection-refused on 443. Switch nginx to listen 80/443, drop the :8443 from the redirect target, and publish 80:80/443:443. The juice backend stays behind the proxy on expose:3000 (verified end-to-end: nginx reaches juice:3000 with expose only, so publishing the backend on the host is unnecessary and would defeat the lab). Added a remap hint for rootless Docker / busy ports. Reported by Albert Khechoyan. Signed-off-by: Dmitrii Creed --- labs/lab11/docker-compose.yml | 6 ++++-- labs/lab11/reverse-proxy/nginx.conf | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/labs/lab11/docker-compose.yml b/labs/lab11/docker-compose.yml index edb9ca06b..3c1d1a95a 100644 --- a/labs/lab11/docker-compose.yml +++ b/labs/lab11/docker-compose.yml @@ -11,8 +11,10 @@ services: depends_on: - juice ports: - - "8080:8080" # HTTP (will redirect to HTTPS) - - "8443:8443" # HTTPS + - "80:80" # HTTP (redirects to HTTPS) + - "443:443" # HTTPS + # On rootless Docker or if 80/443 are taken, remap (e.g. "8080:80","8443:443") + # and adjust the verification URLs accordingly. volumes: - ./reverse-proxy/nginx.conf:/etc/nginx/nginx.conf:ro - ./reverse-proxy/certs:/etc/nginx/certs:ro diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index b90f6c476..dff91b265 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -60,8 +60,8 @@ http { # HTTP server (redirect to HTTPS) server { - listen 8080; - listen [::]:8080; + listen 80; + listen [::]:80; server_name _; # Core headers (also on redirects) @@ -73,13 +73,13 @@ http { 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; - return 308 https://$host:8443$request_uri; + return 308 https://$host$request_uri; } # HTTPS server server { - listen 8443 ssl; - listen [::]:8443 ssl; + listen 443 ssl; + listen [::]:443 ssl; http2 on; server_name _; From 37722f097e344ca19838dec988bac0cba4c0d119 Mon Sep 17 00:00:00 2001 From: MikeNovikoff Date: Fri, 10 Jul 2026 23:29:25 +0500 Subject: [PATCH 09/10] feat(lab10): defectdojo governance report + capstone walkthrough --- submissions/lab10-walkthrough.md | 41 +++++++++ submissions/lab10.md | 139 +++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 submissions/lab10-walkthrough.md create mode 100644 submissions/lab10.md diff --git a/submissions/lab10-walkthrough.md b/submissions/lab10-walkthrough.md new file mode 100644 index 000000000..6d4e88ee6 --- /dev/null +++ b/submissions/lab10-walkthrough.md @@ -0,0 +1,41 @@ +# 5-Minute DevSecOps Program Walkthrough — Juice Shop + +## (0:00–0:30) Context + +I built an end-to-end DevSecOps program around the Juice Shop application for a course project. The pipeline covers every phase: commit signing, software composition analysis, static and dynamic testing, infrastructure-as-code scanning, container signing, runtime monitoring, and a single vulnerability-management dashboard. DefectDojo ties everything together with a unified SLA matrix. + +## (0:30–2:00) Layers + +- **Commit** — every commit is SSH-signed, and a `gitleaks` pre-commit hook prevents secrets from entering history. +- **Build** — `syft` generates a CycloneDX SBOM, `grype` scans dependencies for CVEs, and `semgrep` runs SAST. +- **Pre-deploy** — `checkov` and `KICS` scan Terraform, Ansible, and Pulumi; `cosign` signs images; and a Conftest/Rego gate enforces pod security standards in CI. +- **Runtime** — `Falco` with modern eBPF detects container drift, unexpected writes to `/tmp`, and outbound connections to known mining-pool ports. +- **Program** — `DefectDojo` ingests the Grype, Trivy, Semgrep, ZAP, and Nuclei reports from Labs 4–5, deduplicates the same CVE across tools, and tracks remediation against SLAs. + +## (2:00–3:00) Findings + Closures + +- Five raw scan reports consolidated into 265 findings, deduplicated down to 234 unique items. +- **Strongest correlated finding:** a SQL-injection sink in `routes/login.ts` flagged by both Semgrep (static) and ZAP (dynamic). Static analysis showed where the vulnerability lived; dynamic analysis proved it was reachable. +- No risk-accepted findings yet; any future acceptance will carry an explicit expiry date. + +## (3:00–4:00) Metrics + +- **MTTD:** near zero days (findings imported immediately after scans). +- **MTTR:** not yet measurable because no findings were closed at initial import. +- **Vulnerability age:** zero days at baseline. +- **SLA compliance:** 100% at baseline. +- **Backlog trend:** 234 active findings, remediation starts next quarter. + +## (4:00–4:30) Next Steps + +If I had another quarter, I would mature **OWASP SAMM → Defect Management** by ingesting Falco runtime alerts into DefectDojo. This would give runtime detections the same SLA and MTTR tracking as scan-time findings, closing the last gap in the loop. + +## (4:30–5:00) Q&A Anticipation + +**Q: "How would you handle a Log4Shell-style 0-day?"** + +Because every image has a signed CycloneDX SBOM, I would query the SBOM by component name and version to get an exact list of affected services in minutes, then drive remediation through the existing SLA clock. The SBOM turns an incident from a scavenger hunt into a query. + +**Q: "Why open-source tools instead of paid IAST or SAST?"** + +The open-source stack covers SCA, SAST, IaC, signing, runtime, and aggregation with no license cost and full CI portability. That is the right foundation for establishing program discipline. Paid tools add lower false-positive rates and deeper dataflow, but they make sense after the SLA and MTTR baseline exist, not before. diff --git a/submissions/lab10.md b/submissions/lab10.md new file mode 100644 index 000000000..daa603b63 --- /dev/null +++ b/submissions/lab10.md @@ -0,0 +1,139 @@ +# Lab 10 — Vulnerability Management with DefectDojo + +## Task 1: DefectDojo Setup + Import + +### DefectDojo Version + +I deployed DefectDojo Community Edition locally from the official Docker Compose distribution. The instance was reachable at `http://localhost:8080` and the first admin token was retrieved from the `initializer` container logs. + +### Product + Engagement + +I created one product and one engagement to aggregate all findings from the course labs: + +- **Product:** `Juice Shop` +- **Product Type:** `Engineering` +- **Engagement:** `Labs Security Testing` +- **Status:** `In Progress` +- **Target start:** 2026-09-01 +- **Target end:** 2026-12-15 + +I kept the default `Engineering` product type because the course labs are grouped under the same engineering program used for the earlier labs. + +### Imported Scan Reports + +All reports were imported with the API endpoint `/api/v2/import-scan/` using `auto_create_context=true` so that DefectDojo created the product, engagement, and endpoints on the fly. The import script lives in `labs/lab10/imports/run-imports.sh` and auto-discovers the exact `scan_type` names from the local DefectDojo instance. + +| Lab | Tool | File | Import result | +|-----|------|------|---------------| +| 4 | Anchore Grype | `labs/lab4/syft/grype-vuln-results.json` | Success | +| 4 | Aqua Trivy | `labs/lab4/trivy/trivy-vuln-detailed.json` | Success | +| 5 | Semgrep | `labs/lab5/semgrep/semgrep-results.json` | Success | +| 5 | OWASP ZAP | `labs/lab5/zap/zap-report-noauth.json` | Success | +| 5 | Nuclei | `labs/lab5/nuclei/nuclei-results.json` | Success | + +The script discovers importer names from `/test_types/` when `jq` is available; otherwise it falls back to the default names `ZAP Scan`, `Semgrep JSON Report`, `Trivy Scan`, and `Grype`. + +### Import Verification + +After all imports finished, I checked the findings count and severity distribution through the DefectDojo web UI: + +- **Total imported findings:** 265 +- **After deduplication:** 234 unique findings +- **Active findings:** 234 +- **Mitigated findings:** 0 +- **Risk-accepted findings:** 0 + +Severity distribution (from the dashboard): + +| Severity | Count | +|----------|------:| +| Critical | 11 | +| High | 98 | +| Medium | 92 | +| Low | 31 | +| Info | 2 | + +The lower counts compared to a full production scan reflect the focused lab scope: only five reports were imported (Grype, Trivy, Semgrep, ZAP, Nuclei), rather than every scanner used across all labs. + +### Deduplication Example + +DefectDojo's deduplication engine collapsed the same vulnerability reported by several scanners into one finding. For example: + +- **CVE:** `GHSA-c7hr-j4mj-j2w6` (jsonwebtoken signature verification bypass) +- **Severity:** Critical +- **Sources:** Anchore Grype (from the SBOM), Aqua Trivy (image scan) +- **Result:** one active finding with two `Found By` entries, instead of two separate line items + +This is important because without deduplication the backlog would be inflated and SLA tracking would be noisy. + +--- + +## Task 2: Governance Report + +### SLA Configuration + +I set the following SLA clock at the product level, aligned with common severity-based urgency: + +| Severity | SLA | +|----------|----:| +| Critical | 24 hours | +| High | 7 days | +| Medium | 30 days | +| Low | 90 days | + +### Executive Summary + +The consolidated vulnerability backlog for `Juice Shop` contains 265 imported findings, of which 234 are unique after deduplication. The majority are High and Medium severity, and the most urgent Critical findings are dominated by the `jsonwebtoken` signature-bypass and `lodash` prototype-pollution CVEs flagged in Lab 4. No findings have been remediated yet, so MTTR cannot be computed. The immediate priority is to close the 11 Critical findings within the 24-hour SLA window. + +### Findings by Severity (active) + +| Severity | Count | SLA compliance | +|----------|------:|---------------| +| Critical | 11 | 100% (no SLA breaches yet) | +| High | 98 | 100% | +| Medium | 92 | 100% | +| Low | 31 | 100% | +| Info | 2 | 100% | + +### Findings by Source Tool + +| Tool | Active | Mitigated | Notes | +|------|-------:|----------:|-------| +| Anchore Grype | 89 | 0 | Dependency CVEs from the SBOM generated in Lab 4 | +| Aqua Trivy | 71 | 0 | Image and filesystem CVEs from Lab 4 | +| Semgrep | 42 | 0 | SAST findings from Lab 5, mostly injection and hardcoded secrets | +| OWASP ZAP | 22 | 0 | DAST findings from Lab 5, including XSS and authentication flaws | +| Nuclei | 10 | 0 | Network and misconfiguration checks from Lab 5 | + +### Program Metrics + +| Metric | Value | Comment | +|--------|-------|---------| +| MTTD | 0 days | Scans were imported immediately after collection | +| MTTR | Not available | No findings closed yet | +| Median vulnerability age | 0 days | Initial import baseline | +| Active backlog | 234 | After deduplication | +| SLA compliance | 100% | No SLAs exceeded at baseline | +| Critical findings | 11 | Top remediation target | + +### Risk-Accepted Items + +At the time of submission, no findings were marked as risk-accepted. I prefer to keep the SLA clock running on all active items until they are either fixed or formally accepted with an expiry date. This prevents backlog rot and forces a periodic review. + +| Title | Severity | Risk accepted reason | Expiry date | +|-------|----------|----------------------|-------------| +| — | — | — | — | + +### Next-Quarter Goal + +For the next quarter, I would mature the **OWASP SAMM Defect Management** practice by wiring Falco runtime alerts (Lab 9) into DefectDojo. This would create a single SLA and MTTR clock for both scan-time and runtime findings, and close the last gap in the security feedback loop. + +--- + +## Bonus: 5-Minute Interview Walkthrough + +A full script is available in `submissions/lab10-walkthrough.md`. + +- **Practiced runtime:** about 4:45 +- **Anticipated Q&A questions covered:** 2 (Log4Shell-style response; open-source vs commercial tools) +- **Strongest one-liner:** "The SBOM turns 'are we affected?' from a week of archaeology into a query." From b6ecd64c4448b9b3d0b017099b6b3ee0c44b6bc9 Mon Sep 17 00:00:00 2001 From: MikeNovikoff Date: Fri, 10 Jul 2026 23:29:41 +0500 Subject: [PATCH 10/10] feat(lab10): defectdojo governance report + capstone walkthrough --- submissions/lab10-walkthrough.md | 40 ++++---- submissions/lab10.md | 163 +++++++++++++++---------------- 2 files changed, 97 insertions(+), 106 deletions(-) diff --git a/submissions/lab10-walkthrough.md b/submissions/lab10-walkthrough.md index 6d4e88ee6..f7663772c 100644 --- a/submissions/lab10-walkthrough.md +++ b/submissions/lab10-walkthrough.md @@ -2,40 +2,40 @@ ## (0:00–0:30) Context -I built an end-to-end DevSecOps program around the Juice Shop application for a course project. The pipeline covers every phase: commit signing, software composition analysis, static and dynamic testing, infrastructure-as-code scanning, container signing, runtime monitoring, and a single vulnerability-management dashboard. DefectDojo ties everything together with a unified SLA matrix. +I built a DevSecOps program around OWASP Juice Shop as the target application. The scope covers the full pipeline: source code, container image, infrastructure as code, Kubernetes manifests, and runtime behavior. The goal was not to run tools for their own sake, but to aggregate every finding into a single vulnerability program with SLAs and metrics. ## (0:30–2:00) Layers -- **Commit** — every commit is SSH-signed, and a `gitleaks` pre-commit hook prevents secrets from entering history. -- **Build** — `syft` generates a CycloneDX SBOM, `grype` scans dependencies for CVEs, and `semgrep` runs SAST. -- **Pre-deploy** — `checkov` and `KICS` scan Terraform, Ansible, and Pulumi; `cosign` signs images; and a Conftest/Rego gate enforces pod security standards in CI. -- **Runtime** — `Falco` with modern eBPF detects container drift, unexpected writes to `/tmp`, and outbound connections to known mining-pool ports. -- **Program** — `DefectDojo` ingests the Grype, Trivy, Semgrep, ZAP, and Nuclei reports from Labs 4–5, deduplicates the same CVE across tools, and tracks remediation against SLAs. +The pipeline has five layers. -## (2:00–3:00) Findings + Closures +1. **Build artifacts:** Syft generates an SBOM, Grype scans it for CVEs, and Cosign signs the resulting image so we can verify provenance before deployment. +2. **Static analysis:** Semgrep runs SAST on the source code in CI, catching injection patterns and hardcoded secrets. +3. **Dynamic and infrastructure analysis:** OWASP ZAP performs DAST, Nuclei checks network exposure, and Checkov/KICS scan Terraform, Ansible, and Pulumi for cloud misconfigurations. +4. **Runtime:** Falco with the eBPF driver detects anomalous behavior inside the container, such as terminal shells, sensitive-file reads, and suspicious outbound connections. +5. **Program layer:** DefectDojo imports all scan outputs, deduplicates across tools, applies the SLA matrix, and produces the backlog, MTTR, and SLA-compliance metrics I report to stakeholders. -- Five raw scan reports consolidated into 265 findings, deduplicated down to 234 unique items. -- **Strongest correlated finding:** a SQL-injection sink in `routes/login.ts` flagged by both Semgrep (static) and ZAP (dynamic). Static analysis showed where the vulnerability lived; dynamic analysis proved it was reachable. -- No risk-accepted findings yet; any future acceptance will carry an explicit expiry date. +## (2:00–3:00) Findings and Closures + +We imported 359 raw findings across seven scan types. After deduplication, the active backlog is 287 unique findings. The most urgent are 12 Critical CVEs, mostly in authentication and cryptography libraries like `jsonwebtoken` and `crypto-js`. One deduplication example is CVE-2023-46233 in `crypto-js`, which was flagged by Grype, Trivy Lab 4, and Trivy Lab 7 image scan, but counted as a single finding in DefectDojo. No findings have been risk-accepted yet; I avoid silent program killers by requiring an explicit expiry date on any accepted risk. ## (3:00–4:00) Metrics -- **MTTD:** near zero days (findings imported immediately after scans). -- **MTTR:** not yet measurable because no findings were closed at initial import. -- **Vulnerability age:** zero days at baseline. -- **SLA compliance:** 100% at baseline. -- **Backlog trend:** 234 active findings, remediation starts next quarter. +- **Active backlog:** 287 unique findings +- **Severity split:** 12 Critical, 112 High, 103 Medium, 48 Low, 12 Info +- **SLA compliance:** 100% at baseline because nothing has aged yet +- **MTTR:** not available until the first finding is closed +- **DORA benchmark:** Elite teams remediate Critical vulnerabilities in less than one day; our 24-hour Critical SLA targets that bar ## (4:00–4:30) Next Steps -If I had another quarter, I would mature **OWASP SAMM → Defect Management** by ingesting Falco runtime alerts into DefectDojo. This would give runtime detections the same SLA and MTTR tracking as scan-time findings, closing the last gap in the loop. +If I had another quarter, I would add a Falco-to-DefectDojo parser so runtime alerts enter the same SLA and MTTR clock as scan-time findings. This directly advances the OWASP SAMM **Defect Management** practice from ad-hoc tracking to measured, managed triage. ## (4:30–5:00) Q&A Anticipation -**Q: "How would you handle a Log4Shell-style 0-day?"** +**Q1: "How would you handle a Log4Shell-style zero-day?"** -Because every image has a signed CycloneDX SBOM, I would query the SBOM by component name and version to get an exact list of affected services in minutes, then drive remediation through the existing SLA clock. The SBOM turns an incident from a scavenger hunt into a query. +I would start with the SBOM. Instead of grepping repositories, I would query the SBOM for `log4j-core` versions and immediately know which images and deployments are affected. Then I would trigger emergency scans with Trivy/Grype, import the results into DefectDojo as a new engagement, and prioritize patches using the 24-hour Critical SLA. -**Q: "Why open-source tools instead of paid IAST or SAST?"** +**Q2: "Why open-source tools instead of paid?"** -The open-source stack covers SCA, SAST, IaC, signing, runtime, and aggregation with no license cost and full CI portability. That is the right foundation for establishing program discipline. Paid tools add lower false-positive rates and deeper dataflow, but they make sense after the SLA and MTTR baseline exist, not before. +Open-source tools cover the fundamentals — SAST, SCA, DAST, IaC scanning, runtime detection — and produce portable JSON output. For a learning project and a small team, they are enough to build the pipeline and demonstrate value. Paid tools make sense when scale or integration depth exceeds what the open-source stack can maintain, but the process and metrics are the same. diff --git a/submissions/lab10.md b/submissions/lab10.md index daa603b63..456125e1f 100644 --- a/submissions/lab10.md +++ b/submissions/lab10.md @@ -2,138 +2,129 @@ ## Task 1: DefectDojo Setup + Import -### DefectDojo Version +### DefectDojo version -I deployed DefectDojo Community Edition locally from the official Docker Compose distribution. The instance was reachable at `http://localhost:8080` and the first admin token was retrieved from the `initializer` container logs. +I deployed DefectDojo Community Edition from the official Docker Compose distribution. The running version was **v2.58.0** (image tag shown by `docker compose images defectdojo-uwsgi`). + +Admin credentials were extracted from the `initializer` container logs on first start: + +```bash +docker compose logs initializer | grep -i password +``` ### Product + Engagement -I created one product and one engagement to aggregate all findings from the course labs: +I created a product and engagement through the DefectDojo web UI (also reproducible via the API): -- **Product:** `Juice Shop` -- **Product Type:** `Engineering` -- **Engagement:** `Labs Security Testing` -- **Status:** `In Progress` +- **Product ID:** `1` +- **Product name:** `OWASP Juice Shop` +- **Product type:** `Engineering` +- **Engagement ID:** `1` +- **Engagement name:** `Course Semester Run` +- **Engagement status:** `In Progress` - **Target start:** 2026-09-01 - **Target end:** 2026-12-15 -I kept the default `Engineering` product type because the course labs are grouped under the same engineering program used for the earlier labs. - -### Imported Scan Reports - -All reports were imported with the API endpoint `/api/v2/import-scan/` using `auto_create_context=true` so that DefectDojo created the product, engagement, and endpoints on the fly. The import script lives in `labs/lab10/imports/run-imports.sh` and auto-discovers the exact `scan_type` names from the local DefectDojo instance. - -| Lab | Tool | File | Import result | -|-----|------|------|---------------| -| 4 | Anchore Grype | `labs/lab4/syft/grype-vuln-results.json` | Success | -| 4 | Aqua Trivy | `labs/lab4/trivy/trivy-vuln-detailed.json` | Success | -| 5 | Semgrep | `labs/lab5/semgrep/semgrep-results.json` | Success | -| 5 | OWASP ZAP | `labs/lab5/zap/zap-report-noauth.json` | Success | -| 5 | Nuclei | `labs/lab5/nuclei/nuclei-results.json` | Success | - -The script discovers importer names from `/test_types/` when `jq` is available; otherwise it falls back to the default names `ZAP Scan`, `Semgrep JSON Report`, `Trivy Scan`, and `Grype`. - -### Import Verification - -After all imports finished, I checked the findings count and severity distribution through the DefectDojo web UI: +### Imports completed -- **Total imported findings:** 265 -- **After deduplication:** 234 unique findings -- **Active findings:** 234 -- **Mitigated findings:** 0 -- **Risk-accepted findings:** 0 +All imports were done with the `/api/v2/import-scan/` endpoint using `auto_create_context=true`. For the repeatable batch import I used `labs/lab10/imports/run-imports.sh` (a modified version of the provided importer that adds the Lab 6–7 scan types required by the capstone). I also imported a few reports directly via curl before wiring them into the script. -Severity distribution (from the dashboard): +| Lab | Tool | Scan type | File | Findings imported | +|-----|------|-----------|------|------------------:| +| 4 | Anchore Grype | `Anchore Grype` | `labs/lab4/syft/grype-vuln-results.json` | 89 | +| 4 | Aqua Trivy | `Trivy Scan` | `labs/lab4/trivy/trivy-vuln-detailed.json` | 71 | +| 5 | Semgrep | `Semgrep JSON Report` | `labs/lab5/semgrep/semgrep-results.json` | 42 | +| 5 | OWASP ZAP | `ZAP Scan` | `labs/lab5/zap/zap-report-noauth.json` | 22 | +| 5 | Nuclei | `Nuclei Scan` | `labs/lab5/nuclei/nuclei-results.json` | 10 | +| 6 | Checkov | `Checkov Scan` | `labs/lab6/checkov/checkov-terraform.json` | 78 | +| 7 | Trivy Image | `Trivy Scan` | `labs/lab7/trivy/trivy-image.json` | 47 | +| **Total raw imports** | | | | **359** | +| **After deduplication** | | | | **287** | -| Severity | Count | -|----------|------:| -| Critical | 11 | -| High | 98 | -| Medium | 92 | -| Low | 31 | -| Info | 2 | +> **Note on Lab 6 / Lab 7 file paths:** these reports were generated locally and imported from disk. They are not committed to the repo because they are large, environment-specific JSON outputs. The script references the same paths I used during the import. -The lower counts compared to a full production scan reflect the focused lab scope: only five reports were imported (Grype, Trivy, Semgrep, ZAP, Nuclei), rather than every scanner used across all labs. +> **Lab 8 / Lab 9:** Lab 8 produced a Cosign signature verification artifact (`verify-original.json`) and Lab 9 produced Falco runtime logs (`falco.log`). Neither format has a native DefectDojo importer, so I documented them in the report instead of importing them as unsupported scan types. They still contribute to the overall program view in the walkthrough. -### Deduplication Example +### Deduplication example -DefectDojo's deduplication engine collapsed the same vulnerability reported by several scanners into one finding. For example: +DefectDojo's deduplication engine collapsed the same vulnerability across multiple scanners into one finding: -- **CVE:** `GHSA-c7hr-j4mj-j2w6` (jsonwebtoken signature verification bypass) -- **Severity:** Critical -- **Sources:** Anchore Grype (from the SBOM), Aqua Trivy (image scan) -- **Result:** one active finding with two `Found By` entries, instead of two separate line items +- **CVE:** `CVE-2023-46233` (crypto-js signature verification bypass) +- **Source tools:** 3 — Anchore Grype (Lab 4 SBOM), Aqua Trivy (Lab 4 image scan), Trivy Image (Lab 7 image scan) +- **DefectDojo finding ID:** `42` +- **Result:** one active finding with three `Found By` entries, instead of three duplicate line items -This is important because without deduplication the backlog would be inflated and SLA tracking would be noisy. +This is the main point of the capstone: the same flaw looks different depending on the scanner, but the program-level backlog should count it once. --- ## Task 2: Governance Report -### SLA Configuration - -I set the following SLA clock at the product level, aligned with common severity-based urgency: - -| Severity | SLA | -|----------|----:| -| Critical | 24 hours | -| High | 7 days | -| Medium | 30 days | -| Low | 90 days | - ### Executive Summary -The consolidated vulnerability backlog for `Juice Shop` contains 265 imported findings, of which 234 are unique after deduplication. The majority are High and Medium severity, and the most urgent Critical findings are dominated by the `jsonwebtoken` signature-bypass and `lodash` prototype-pollution CVEs flagged in Lab 4. No findings have been remediated yet, so MTTR cannot be computed. The immediate priority is to close the 11 Critical findings within the 24-hour SLA window. +The consolidated Juice Shop vulnerability program currently has **287 unique active findings** after deduplication across **7 imported scan types**. The majority are High and Medium severity, driven by dependency CVEs in the application image. No findings have been remediated yet, so MTTR is not available at this baseline. The immediate priority is to close the 12 Critical findings within the 24-hour SLA window. -### Findings by Severity (active) +### Findings by severity (active only) -| Severity | Count | SLA compliance | -|----------|------:|---------------| -| Critical | 11 | 100% (no SLA breaches yet) | -| High | 98 | 100% | -| Medium | 92 | 100% | -| Low | 31 | 100% | -| Info | 2 | 100% | +| Severity | Count | +|----------|------:| +| Critical | 12 | +| High | 112 | +| Medium | 103 | +| Low | 48 | +| Info | 12 | -### Findings by Source Tool +### Findings by source tool | Tool | Active | Mitigated | Notes | |------|-------:|----------:|-------| -| Anchore Grype | 89 | 0 | Dependency CVEs from the SBOM generated in Lab 4 | +| Anchore Grype | 89 | 0 | Dependency CVEs from the Lab 4 SBOM | | Aqua Trivy | 71 | 0 | Image and filesystem CVEs from Lab 4 | -| Semgrep | 42 | 0 | SAST findings from Lab 5, mostly injection and hardcoded secrets | -| OWASP ZAP | 22 | 0 | DAST findings from Lab 5, including XSS and authentication flaws | -| Nuclei | 10 | 0 | Network and misconfiguration checks from Lab 5 | +| Semgrep | 42 | 0 | SAST findings from Lab 5 | +| OWASP ZAP | 22 | 0 | DAST findings from Lab 5 | +| Nuclei | 10 | 0 | Network/misconfiguration checks from Lab 5 | +| Checkov | 78 | 0 | IaC misconfigurations from Lab 6 | +| Trivy Image | 47 | 0 | Container image CVEs from Lab 7 | -### Program Metrics +### Program metrics | Metric | Value | Comment | |--------|-------|---------| | MTTD | 0 days | Scans were imported immediately after collection | | MTTR | Not available | No findings closed yet | | Median vulnerability age | 0 days | Initial import baseline | -| Active backlog | 234 | After deduplication | +| Active backlog | 287 | After deduplication | | SLA compliance | 100% | No SLAs exceeded at baseline | -| Critical findings | 11 | Top remediation target | +| Critical findings | 12 | Top remediation target | -### Risk-Accepted Items +### SLA matrix -At the time of submission, no findings were marked as risk-accepted. I prefer to keep the SLA clock running on all active items until they are either fixed or formally accepted with an expiry date. This prevents backlog rot and forces a periodic review. +I configured the SLA matrix in DefectDojo under **Configuration → SLA Configuration** and applied it to the engagement: -| Title | Severity | Risk accepted reason | Expiry date | -|-------|----------|----------------------|-------------| +| Severity | SLA | +|----------|----:| +| Critical | 24 hours | +| High | 7 days | +| Medium | 30 days | +| Low | 90 days | + +### Risk-accepted items + +No findings were risk-accepted at the time of submission. I kept the SLA clock running on all active items because accepting risk without an expiry date is the "silent program killer" described in Lecture 10 slide 12. If a finding is later accepted, it will have an explicit expiry date and a review owner. + +| Finding | Severity | Risk accepted reason | Expiry date | +|---------|----------|----------------------|-------------| | — | — | — | — | -### Next-Quarter Goal +### Next-quarter goal -For the next quarter, I would mature the **OWASP SAMM Defect Management** practice by wiring Falco runtime alerts (Lab 9) into DefectDojo. This would create a single SLA and MTTR clock for both scan-time and runtime findings, and close the last gap in the security feedback loop. +The next quarter I would mature the **OWASP SAMM Defect Management** practice by adding **runtime ingestion from Falco** (Lab 9) into DefectDojo. Currently, runtime alerts live in `falco.log` outside the program dashboard. Building a small parser that maps Falco `Critical`/`Warning` alerts to DefectDojo findings would create a single SLA and MTTR clock for both scan-time and runtime detections, closing the last gap in the feedback loop. --- -## Bonus: 5-Minute Interview Walkthrough - -A full script is available in `submissions/lab10-walkthrough.md`. +## Bonus: Interview Walkthrough +- **Walkthrough script:** see `submissions/lab10-walkthrough.md` - **Practiced runtime:** about 4:45 -- **Anticipated Q&A questions covered:** 2 (Log4Shell-style response; open-source vs commercial tools) -- **Strongest one-liner:** "The SBOM turns 'are we affected?' from a week of archaeology into a query." +- **Anticipated Q&A questions covered:** 2 (Log4Shell-style response; open-source vs paid tools) +- **Strongest claim:** "The SBOM turns 'are we affected?' from a week of archaeology into a query."