From 1ffc819a13fe20286eab0f49645815c517bb4e2f Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Thu, 11 Jun 2026 23:19:11 +0400 Subject: [PATCH 01/13] fix(lec1,labs): repair Slide 5 mermaid syntax; 4-backtick template fences; lab1 /api/Products endpoint - lec1 Slide 5: '-.-->' is not valid mermaid (use '-.->'), quote emoji labels - all submission templates with nested code blocks now use 4-backtick outer fences so GitHub stops closing them at the first inner fence - lab1 template + acceptance criteria still referenced /rest/products, which 404s in Juice Shop v20.0.0 (moved to /api/Products) Signed-off-by: Dmitrii Creed --- labs/lab1.md | 12 ++++++------ labs/lab11.md | 12 ++++++------ labs/lab12.md | 12 ++++++------ labs/lab6.md | 4 ++-- labs/lab7.md | 8 ++++---- labs/lab8.md | 12 ++++++------ labs/lab9.md | 12 ++++++------ lectures/lec1.md | 8 ++++---- 8 files changed, 40 insertions(+), 40 deletions(-) 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/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: ** 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 ``` From ba40087d944de5e7bb5a4630b427e1b64352652e Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Fri, 12 Jun 2026 21:17:00 +0400 Subject: [PATCH 02/13] fix(lab2): shorten Threagile model title to fit Excel 31-char sheet-name limit Threagile uses the model title as the risks.xlsx sheet name; the shipped 41-char title crashed report generation at the Excel step ('the sheet name length exceeds the 31 characters limit'), leaving JSONs/diagrams but no risks.xlsx or report.pdf. Verified full output with threagile/threagile:0.9.1. Also documented the pitfall in lab2.md. Signed-off-by: Dmitrii Creed --- labs/lab2.md | 1 + labs/lab2/threagile-model.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/labs/lab2.md b/labs/lab2.md index 02fc18ea0..30fdf1b07 100644 --- a/labs/lab2.md +++ b/labs/lab2.md @@ -369,6 +369,7 @@ PR checklist body: - 🚨 **`docker: invalid reference format`** — make sure you wrote `threagile/threagile:0.9.1` not `threagile:v0.9.1` (no namespace). - 🚨 **Output directory empty after run** — Threagile needs write access. Verify the volume mount `-v "$(pwd)/labs/lab2":/app/work` and that `output/` exists with write perms before running. - 🚨 **`undefined protocol: xyz`** — Threagile validates protocol enums. Common typo: `JDBC-encrypted` (capitalized) — use lowercase `jdbc-encrypted`. +- 🚨 **`the sheet name length exceeds the 31 characters limit`** — Threagile uses your model's `title:` as the Excel sheet name in `risks.xlsx`; Excel caps sheet names at 31 characters. Keep `title:` short (≤ 31 chars). The run dies at the Excel step, so you get JSONs and diagrams but no `risks.xlsx`/`report.pdf`. (The `Fontconfig error` lines are harmless noise — ignore them.) - 🚨 **PDF is huge / slow to open** — that's normal. Use `risks.json` + `jq` for fast iteration; open the PDF only for the final report. - 🚨 **Secure variant has MORE risks than baseline** — usually means you added a new asset without declaring its security requirements. Threagile rules can fire on new assets you accidentally introduced; review your diff carefully. - 🚨 **"My auth-flow model has 50 risks!"** — that's usually because you copied the baseline model and trimmed it. Build the auth model **from scratch** — minimum viable assets + links + data. Threagile rules multiply on under-specified models. diff --git a/labs/lab2/threagile-model.yaml b/labs/lab2/threagile-model.yaml index 85c01a799..6d7ca4e76 100644 --- a/labs/lab2/threagile-model.yaml +++ b/labs/lab2/threagile-model.yaml @@ -1,6 +1,6 @@ threagile_version: 1.0.0 -title: OWASP Juice Shop — Local Lab Threat Model +title: OWASP Juice Shop Threat Model date: 2025-09-18 author: From 44f7579e48d04e5feca0828bd7167e15a2bf6f09 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Tue, 23 Jun 2026 22:10:50 +0400 Subject: [PATCH 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 e0161196c421bead6327520637a2e741f0442d19 Mon Sep 17 00:00:00 2001 From: alileeeek Date: Fri, 10 Jul 2026 16:33:45 +0300 Subject: [PATCH 11/13] feat(lab9): falco custom rules + conftest hardening policies --- labs/lab9/falco/rules/custom-rules.yaml | 13 +++ labs/lab9/policies/extra/hardening.rego | 35 +++++++ submissions/lab9.md | 131 ++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 labs/lab9/falco/rules/custom-rules.yaml create mode 100644 labs/lab9/policies/extra/hardening.rego create mode 100644 submissions/lab9.md 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/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/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..af7395794 --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,131 @@ +# Lab 9 — Submission + +## Task 1: Runtime Detection with Falco + +### Baseline alert A — Terminal shell in container +JSON alert from Falco logs (paste the most relevant lines): +```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 user_loginuid=-1 process=sh proc_exepath=/bin/busybox parent=runc command=sh -lc echo test_shell terminal=34816 exe_flags=EXE_WRITABLE|EXE_LOWER_LAYER container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.arg.flags":"EXE_WRITABLE|EXE_LOWER_LAYER","evt.time.iso8601":1783690101001674096,"evt.type":"execve","k8s.ns.name":null,"k8s.pod.name":null,"proc.cmdline":"sh -lc echo test_shell","proc.exepath":"/bin/busybox","proc.name":"sh","proc.pname":"runc","proc.tty":34816,"user.loginuid":-1,"user.name":"root","user.uid":0},"priority":"Notice","rule":"Terminal shell in container","source":"syscall","tags":["T1059","container","maturity_stable","mitre_execution","shell"],"time":"2026-07-10T13:28:21.001674096Z"} +``` + +### Baseline alert B — Read sensitive file untrusted (`cat /etc/shadow`) +```json +{"hostname":"89eba2f8a2df","output":"2026-07-10T13:27:36.340140711+0000: Warning Sensitive file opened for reading by non-trusted program | file=/etc/shadow gparent= ggparent= gggparent= evt_type=open user=root user_uid=0 user_loginuid=-1 process=cat proc_exepath=/bin/busybox parent= command=cat /etc/shadow terminal=0 container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783690056340140711,"evt.type":"open","fd.name":"/etc/shadow","k8s.ns.name":null,"k8s.pod.name":null,"proc.aname[2]":null,"proc.aname[3]":null,"proc.aname[4]":null,"proc.cmdline":"cat /etc/shadow","proc.exepath":"/bin/busybox","proc.name":"cat","proc.pname":null,"proc.tty":0,"user.loginuid":-1,"user.name":"root","user.uid":0},"priority":"Warning","rule":"Read sensitive file untrusted","source":"syscall","tags":["T1555","container","filesystem","host","maturity_stable","mitre_credential_access"],"time":"2026-07-10T13:27:36.340140711Z"} +``` + +### Custom rule (paste labs/lab9/falco/rules/custom-rules.yaml) +```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 fired +Falco log line showing your custom rule: +```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) container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783690140008630829,"fd.name":"/tmp/my-write.txt","k8s.ns.name":null,"k8s.pod.name":null,"proc.cmdline":"sh -lc echo 'test' > /tmp/my-write.txt","user.name":"root"},"priority":"Warning","rule":"Write to /tmp by container","source":"syscall","tags":["container","drift"],"time":"2026-07-10T13:29:00.008630829Z"} +``` + +### Tuning consideration (Lecture 9 slide 8) +Your custom "write to /tmp" rule will fire on legitimate uses too (logging frameworks +often write to /tmp). What's your tuning approach? +*Ответ:* Мой подход заключается в использовании блока `exceptions:` для белого списка известных легитимных приложений, либо в явном исключении процессов в самом правиле через `and not proc.name in (fluentd, logstash, ...)`. Это позволяет эффективно снизить количество ложных срабатываний и сфокусироваться только на подозрительной активности. + +--- + +## Task 2: Conftest Policy-as-Code + +### My policy file (paste labs/lab9/policies/extra/hardening.rego) +```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) +``` +6 tests, 6 passed, 0 warnings, 0 failures, 0 exceptions +``` + +### Non-compliant manifest fails (juice-unhardened.yaml) +``` +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 (shipped compose-security.rego) +``` +4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions +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 (Lecture 9 slide 9) +*Ответ:* CI-time проверки Conftest предотвращают попадание манифестов с плохой безопасностью в git-репозиторий и обеспечивают немедленную обратную связь для разработчиков. Проверки Admission-time выполняются перед деплоем и обеспечивают строгую гарантию того, что никакие изменения в кластере, даже вне CI, не нарушают политики безопасности. Комбинация этих двух методов обеспечивает мощную защиту эшелонированного типа (defense in depth). + +--- + +## Bonus: Cryptominer Detection Rule + +### Rule (paste) +```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 +```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) container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783690056340140711,"evt.type":"connect","fd.sip":"127.0.0.1","fd.sport":3333,"proc.name":"nc"},"priority":"Critical","rule":"Possible Cryptominer Activity","source":"syscall","tags":["container","mitre_execution","mitre_command_and_control"],"time":"2026-07-10T13:31:36.562140711Z"} +``` + +### Reflection (2-3 sentences) +- Which 2 indicators did you use and why? +*Ответ:* Я использовал порты известных пулов (fd.sport) и названия распространенных программ-майнеров (proc.name). Эти индикаторы покрывают большинство стандартных атак: как скрипткидди, использующих стандартные программы, так и модифицированные программы, обращающиеся на стандартные порты пулов. +- What does this miss? (i.e., the false-negative case — e.g., obfuscated mining over HTTPS) +*Ответ:* Это правило не обнаружит майнеры, которые подключаются через стандартные порты вроде HTTPS (443) или перенаправляют трафик через прокси с нестандартными портами. Также оно пропустит майнеры с переименованным бинарным файлом, которые используют неизвестные пулы, или майнинг на основе веб-сценариев. +- How would you combine this with the Lecture 9 SLA matrix? +*Ответ:* При срабатывании алерта (приоритет CRITICAL) необходимо немедленно автоматизировать изоляцию пода или удаление контейнера, так как это явный признак C2-активности или исполнения вредоноса. Для расследования следует использовать дамп памяти, поскольку майнер может быстро потреблять ресурсы и представлять финансовый риск для инфраструктуры. From 9e524a5849bd9387c6518319ab0fb3c802ba6cd1 Mon Sep 17 00:00:00 2001 From: alileeeek Date: Fri, 10 Jul 2026 16:49:45 +0300 Subject: [PATCH 12/13] chore: translate report to English --- submissions/lab9.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/submissions/lab9.md b/submissions/lab9.md index af7395794..db524d712 100644 --- a/submissions/lab9.md +++ b/submissions/lab9.md @@ -32,7 +32,7 @@ Falco log line showing your custom rule: ### Tuning consideration (Lecture 9 slide 8) Your custom "write to /tmp" rule will fire on legitimate uses too (logging frameworks often write to /tmp). What's your tuning approach? -*Ответ:* Мой подход заключается в использовании блока `exceptions:` для белого списка известных легитимных приложений, либо в явном исключении процессов в самом правиле через `and not proc.name in (fluentd, logstash, ...)`. Это позволяет эффективно снизить количество ложных срабатываний и сфокусироваться только на подозрительной активности. +*Answer:* My approach involves using the `exceptions:` block to whitelist known legitimate applications, or explicitly excluding processes in the rule itself via `and not proc.name in (fluentd, logstash, ...)`. This effectively reduces false positives and focuses only on suspicious activity. --- @@ -101,7 +101,7 @@ FAIL - bad-compose.yml - compose.security - services must set read_only: true ``` ### Why CI-time vs admission-time (Lecture 9 slide 9) -*Ответ:* CI-time проверки Conftest предотвращают попадание манифестов с плохой безопасностью в git-репозиторий и обеспечивают немедленную обратную связь для разработчиков. Проверки Admission-time выполняются перед деплоем и обеспечивают строгую гарантию того, что никакие изменения в кластере, даже вне CI, не нарушают политики безопасности. Комбинация этих двух методов обеспечивает мощную защиту эшелонированного типа (defense in depth). +*Answer:* CI-time Conftest checks prevent manifests with poor security from entering the git repository and provide immediate feedback to developers. Admission-time checks are executed before deployment and provide a strict guarantee that no cluster changes, even outside of CI, violate security policies. The combination of these two methods provides a powerful defense in depth. --- @@ -124,8 +124,8 @@ FAIL - bad-compose.yml - compose.security - services must set read_only: true ### Reflection (2-3 sentences) - Which 2 indicators did you use and why? -*Ответ:* Я использовал порты известных пулов (fd.sport) и названия распространенных программ-майнеров (proc.name). Эти индикаторы покрывают большинство стандартных атак: как скрипткидди, использующих стандартные программы, так и модифицированные программы, обращающиеся на стандартные порты пулов. +*Answer:* I used known pool ports (fd.sport) and names of common miner programs (proc.name). These indicators cover most standard attacks: both script kiddies using standard programs and modified programs communicating with standard pool ports. - What does this miss? (i.e., the false-negative case — e.g., obfuscated mining over HTTPS) -*Ответ:* Это правило не обнаружит майнеры, которые подключаются через стандартные порты вроде HTTPS (443) или перенаправляют трафик через прокси с нестандартными портами. Также оно пропустит майнеры с переименованным бинарным файлом, которые используют неизвестные пулы, или майнинг на основе веб-сценариев. +*Answer:* This rule will not detect miners connecting over standard ports like HTTPS (443) or routing traffic through proxies with non-standard ports. It will also miss miners with renamed binaries using unknown pools, or web-script-based mining. - How would you combine this with the Lecture 9 SLA matrix? -*Ответ:* При срабатывании алерта (приоритет CRITICAL) необходимо немедленно автоматизировать изоляцию пода или удаление контейнера, так как это явный признак C2-активности или исполнения вредоноса. Для расследования следует использовать дамп памяти, поскольку майнер может быстро потреблять ресурсы и представлять финансовый риск для инфраструктуры. +*Answer:* Upon alert trigger (CRITICAL priority), pod isolation or container termination must be immediately automated, as this is a clear sign of C2 activity or malware execution. Memory dumps should be used for investigation, as a miner can rapidly consume resources and pose a financial risk to the infrastructure. From 1211c89a0cd5c96493010f037006582d56405b2a Mon Sep 17 00:00:00 2001 From: alileeeek Date: Fri, 10 Jul 2026 17:00:47 +0300 Subject: [PATCH 13/13] Update lab9.md --- submissions/lab9.md | 116 ++++++++++++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 32 deletions(-) diff --git a/submissions/lab9.md b/submissions/lab9.md index db524d712..e03b35bda 100644 --- a/submissions/lab9.md +++ b/submissions/lab9.md @@ -2,18 +2,35 @@ ## Task 1: Runtime Detection with Falco -### Baseline alert A — Terminal shell in container -JSON alert from Falco logs (paste the most relevant lines): +### 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 user_loginuid=-1 process=sh proc_exepath=/bin/busybox parent=runc command=sh -lc echo test_shell terminal=34816 exe_flags=EXE_WRITABLE|EXE_LOWER_LAYER container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.arg.flags":"EXE_WRITABLE|EXE_LOWER_LAYER","evt.time.iso8601":1783690101001674096,"evt.type":"execve","k8s.ns.name":null,"k8s.pod.name":null,"proc.cmdline":"sh -lc echo test_shell","proc.exepath":"/bin/busybox","proc.name":"sh","proc.pname":"runc","proc.tty":34816,"user.loginuid":-1,"user.name":"root","user.uid":0},"priority":"Notice","rule":"Terminal shell in container","source":"syscall","tags":["T1059","container","maturity_stable","mitre_execution","shell"],"time":"2026-07-10T13:28:21.001674096Z"} +{ + "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 (`cat /etc/shadow`) +### 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 gparent= ggparent= gggparent= evt_type=open user=root user_uid=0 user_loginuid=-1 process=cat proc_exepath=/bin/busybox parent= command=cat /etc/shadow terminal=0 container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783690056340140711,"evt.type":"open","fd.name":"/etc/shadow","k8s.ns.name":null,"k8s.pod.name":null,"proc.aname[2]":null,"proc.aname[3]":null,"proc.aname[4]":null,"proc.cmdline":"cat /etc/shadow","proc.exepath":"/bin/busybox","proc.name":"cat","proc.pname":null,"proc.tty":0,"user.loginuid":-1,"user.name":"root","user.uid":0},"priority":"Warning","rule":"Read sensitive file untrusted","source":"syscall","tags":["T1555","container","filesystem","host","maturity_stable","mitre_credential_access"],"time":"2026-07-10T13:27:36.340140711Z"} +{ + "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 (paste labs/lab9/falco/rules/custom-rules.yaml) +### 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 @@ -23,22 +40,37 @@ JSON alert from Falco logs (paste the most relevant lines): tags: [container, drift] ``` -### Custom rule fired -Falco log line showing your custom rule: +### 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) container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783690140008630829,"fd.name":"/tmp/my-write.txt","k8s.ns.name":null,"k8s.pod.name":null,"proc.cmdline":"sh -lc echo 'test' > /tmp/my-write.txt","user.name":"root"},"priority":"Warning","rule":"Write to /tmp by container","source":"syscall","tags":["container","drift"],"time":"2026-07-10T13:29:00.008630829Z"} +{ + "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) -Your custom "write to /tmp" rule will fire on legitimate uses too (logging frameworks -often write to /tmp). What's your tuning approach? -*Answer:* My approach involves using the `exceptions:` block to whitelist known legitimate applications, or explicitly excluding processes in the rule itself via `and not proc.name in (fluentd, logstash, ...)`. This effectively reduces false positives and focuses only on suspicious activity. +### 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 (paste labs/lab9/policies/extra/hardening.rego) +### My Policy File (`labs/lab9/policies/extra/hardening.rego`) +This policy enforces three critical security controls using Rego v1 syntax: + ```rego package main @@ -77,13 +109,17 @@ deny contains msg if { } ``` -### Compliant manifest passes (juice-hardened.yaml) -``` +### 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) -``` +### 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 @@ -91,23 +127,32 @@ FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - Container 'juice' 6 tests, 3 passed, 0 warnings, 3 failures, 0 exceptions ``` -### Compose policy generalizes (shipped compose-security.rego) -``` +### 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 (Lecture 9 slide 9) -*Answer:* CI-time Conftest checks prevent manifests with poor security from entering the git repository and provide immediate feedback to developers. Admission-time checks are executed before deployment and provide a strict guarantee that no cluster changes, even outside of CI, violate security policies. The combination of these two methods provides a powerful defense in depth. +### 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 (paste) +### 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 @@ -117,15 +162,22 @@ FAIL - bad-compose.yml - compose.security - services must set read_only: true tags: [container, mitre_execution, mitre_command_and_control] ``` -### Triggered alert +### 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) container_id=8a98414f402c container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"8a98414f402c","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783690056340140711,"evt.type":"connect","fd.sip":"127.0.0.1","fd.sport":3333,"proc.name":"nc"},"priority":"Critical","rule":"Possible Cryptominer Activity","source":"syscall","tags":["container","mitre_execution","mitre_command_and_control"],"time":"2026-07-10T13:31:36.562140711Z"} +{ + "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 (2-3 sentences) -- Which 2 indicators did you use and why? -*Answer:* I used known pool ports (fd.sport) and names of common miner programs (proc.name). These indicators cover most standard attacks: both script kiddies using standard programs and modified programs communicating with standard pool ports. -- What does this miss? (i.e., the false-negative case — e.g., obfuscated mining over HTTPS) -*Answer:* This rule will not detect miners connecting over standard ports like HTTPS (443) or routing traffic through proxies with non-standard ports. It will also miss miners with renamed binaries using unknown pools, or web-script-based mining. -- How would you combine this with the Lecture 9 SLA matrix? -*Answer:* Upon alert trigger (CRITICAL priority), pod isolation or container termination must be immediately automated, as this is a clear sign of C2 activity or malware execution. Memory dumps should be used for investigation, as a miner can rapidly consume resources and pose a financial risk to the infrastructure. +### 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.