From 2f54b0e8a9b3b4b21cc9d8654ae02e6d8177ecee Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 27 Jun 2026 01:29:19 +0400 Subject: [PATCH 1/2] 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 0ceb9772782fd77ac0fa1f342a2a7d1291178d40 Mon Sep 17 00:00:00 2001 From: RII6 Date: Sun, 28 Jun 2026 17:09:40 +0300 Subject: [PATCH 2/2] feat(lab7): trivy + PSS restricted + conftest gate --- labs/lab7/k8s/deployment.yaml | 49 ++++++++ labs/lab7/k8s/namespace.yaml | 8 ++ labs/lab7/k8s/networkpolicy.yaml | 25 ++++ labs/lab7/k8s/serviceaccount.yaml | 6 + labs/lab7/policies/pod-hardening.rego | 38 ++++++ submissions/lab7.md | 160 ++++++++++++++++++++++++++ 6 files changed, 286 insertions(+) create mode 100644 labs/lab7/k8s/deployment.yaml create mode 100644 labs/lab7/k8s/namespace.yaml create mode 100644 labs/lab7/k8s/networkpolicy.yaml create mode 100644 labs/lab7/k8s/serviceaccount.yaml create mode 100644 labs/lab7/policies/pod-hardening.rego create mode 100644 submissions/lab7.md diff --git a/labs/lab7/k8s/deployment.yaml b/labs/lab7/k8s/deployment.yaml new file mode 100644 index 000000000..70f878922 --- /dev/null +++ b/labs/lab7/k8s/deployment.yaml @@ -0,0 +1,49 @@ + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: juice-shop + namespace: juice-shop +spec: + selector: + matchLabels: + app: juice-shop + template: + metadata: + labels: + app: juice-shop + spec: + serviceAccountName: juice-shop-sa + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: app + image: bkimminich/juice-shop@sha256:fd58bdc9745416afce8184ee0666278a436574633ea7880365153a63bfd418b0 + resources: + limits: + memory: "512Mi" + cpu: "500m" + requests: + memory: "256Mi" + cpu: "200m" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp-volume + mountPath: /tmp + - name: logs-volume + mountPath: /juice-shop/logs + volumes: + - name: tmp-volume + emptyDir: {} + - name: logs-volume + emptyDir: {} diff --git a/labs/lab7/k8s/namespace.yaml b/labs/lab7/k8s/namespace.yaml new file mode 100644 index 000000000..67b3c6d44 --- /dev/null +++ b/labs/lab7/k8s/namespace.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: juice-shop + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/audit: restricted diff --git a/labs/lab7/k8s/networkpolicy.yaml b/labs/lab7/k8s/networkpolicy.yaml new file mode 100644 index 000000000..dce5edda6 --- /dev/null +++ b/labs/lab7/k8s/networkpolicy.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: juice-shop-netpol + namespace: juice-shop +spec: + podSelector: + matchLabels: + app: juice-shop + policyTypes: + - Ingress + - Egress + ingress: + - {} # Allows incoming connections (e.g. for kubectl port-forward) + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - ports: + - protocol: TCP + port: 443 diff --git a/labs/lab7/k8s/serviceaccount.yaml b/labs/lab7/k8s/serviceaccount.yaml new file mode 100644 index 000000000..fc24f5a35 --- /dev/null +++ b/labs/lab7/k8s/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: juice-shop-sa + namespace: juice-shop +automountServiceAccountToken: false diff --git a/labs/lab7/policies/pod-hardening.rego b/labs/lab7/policies/pod-hardening.rego new file mode 100644 index 000000000..c221e8e0c --- /dev/null +++ b/labs/lab7/policies/pod-hardening.rego @@ -0,0 +1,38 @@ +package main +import rego.v1 + +# 1. spec.securityContext.runAsNonRoot != true +deny contains msg if { + input.kind == "Deployment" + pod_spec := input.spec.template.spec + not pod_spec.securityContext.runAsNonRoot == true + msg := "Pod securityContext must have runAsNonRoot set to true" +} + +# 2. (any container) spec.containers[_].securityContext.readOnlyRootFilesystem != true +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not container.securityContext.readOnlyRootFilesystem == true + msg := sprintf("Container '%v' must have readOnlyRootFilesystem set to true", [container.name]) +} + +# 3. (any container) spec.containers[_].securityContext.allowPrivilegeEscalation != false +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not container.securityContext.allowPrivilegeEscalation == false + msg := sprintf("Container '%v' must have allowPrivilegeEscalation set to false", [container.name]) +} + +# 4. (any container) spec.containers[_].securityContext.capabilities.drop missing "ALL" +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not has_drop_all(container) + msg := sprintf("Container '%v' must drop ALL capabilities", [container.name]) +} + +has_drop_all(container) if { + "ALL" in container.securityContext.capabilities.drop +} diff --git a/submissions/lab7.md b/submissions/lab7.md new file mode 100644 index 000000000..04e701958 --- /dev/null +++ b/submissions/lab7.md @@ -0,0 +1,160 @@ +# Lab 7 — Submission + +## Task 1: Trivy Image + Config Scan + +### Image scan severity breakdown +| Severity | Total | With fix available | +|----------|------:|------------------:| +| Critical | 5 | 4 | +| High | 43 | 42 | +| **Total** | 48 | 46 | + +### Top 10 CVEs with fixes +| CVE | Severity | Package | Installed | Fix | +|-----|----------|---------|-----------|-----| +| CVE-2023-46233 | CRITICAL | crypto-js | 3.3.0 | 4.2.0 | +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.1.0 | 4.2.2 | +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.4.0 | 4.2.2 | +| CVE-2019-10744 | CRITICAL | lodash | 2.4.2 | 4.17.12 | +| CVE-2026-45447 | HIGH | libssl3t64 | 3.5.5-1~deb13u2 | 3.5.6-1~deb13u2 | +| NSWG-ECO-428 | HIGH | base64url | 0.0.6 | >=3.0.0 | +| CVE-2020-15084 | HIGH | express-jwt | 0.1.3 | 6.0.0 | +| CVE-2022-25881 | HIGH | http-cache-semantics | 3.8.1 | 4.1.1 | +| CVE-2022-23539 | HIGH | jsonwebtoken | 0.1.0 | 9.0.0 | +| NSWG-ECO-17 | HIGH | jsonwebtoken | 0.1.0 | >=4.2.2 | + +> **Note regarding lab instructions error:** +> The `jq` command provided in step 7.3 of the lab instructions (`{cve: .VulnerabilityID, severity: .Severity, pkg: .PkgName, fix: .FixedVersion}`) omits the `InstalledVersion` field, making it impossible to fill out the 'Installed' column in the table above using only the provided command. The command had to be manually modified to include `installed: .InstalledVersion` to complete the assignment properly. + + +### Compared to Lab 4's Grype scan +1. **Found by both tools (CVE-2015-9235 / GHSA-c7hr-j4mj-j2w6 in `jsonwebtoken`)**: Both tools identified the vulnerability, but they represent it differently. Trivy outputs the standard `CVE-2015-9235`, whereas Grype prefers ecosystem-specific identifiers and outputs `GHSA-c7hr-j4mj-j2w6`. This illustrates that the tools use different identifier preferences and package matching rules (Grype favors GitHub Security Advisories for npm). +2. **Found by Trivy, missed by Grype (CVE-2025-57349 in `messageformat`)**: This recent vulnerability was flagged by Trivy but missed by Grype. This difference is primarily due to DB freshness and update cadence. Trivy's vulnerability database is likely updated more frequently, allowing it to ingest newer CVEs faster than the default Anchore snapshot used by Grype. + + + + +## Task 2: Kubernetes Hardening + +### Manifests (paste relevant snippets) +- `namespace.yaml` PSS labels: +```yaml + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/audit: restricted +``` +- `deployment.yaml` securityContext sections (pod + container): +```yaml + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault +... + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL +``` +- `networkpolicy.yaml` ingress + egress: +```yaml + ingress: + - {} # Allows incoming connections (e.g. for kubectl port-forward) + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - ports: + - protocol: TCP + port: 443 +``` + +### Pod is running +Output of `kubectl get pod -n juice-shop -l app=juice-shop`: +``` +NAME READY STATUS RESTARTS AGE +juice-shop-654c7d67fb-2hzs4 1/1 Running 0 42s +``` +> **Note:** The pod successfully passes the `kubectl wait` condition because it initially starts without crashing, but shortly after goes into a `CrashLoopBackOff` state. + +### Trivy K8s scan +> **Note regarding lab instructions error:** The command `trivy k8s --namespace juice-shop` is deprecated. I used `trivy k8s --include-namespaces juice-shop`. + +| Severity | Count | +|----------|------:| +| Critical | 5 | +| High | 43 | + +### What broke and how you fixed it (2-3 sentences) +`readOnlyRootFilesystem: true` breaks Juice Shop because it needs to write to `/tmp` and `/juice-shop/logs`, as well as its SQLite database and FTP directory. I fixed the initial crash by mounting `emptyDir` volumes at `/tmp` and `/juice-shop/logs` (although v20.0.0 also requires writable access to `/juice-shop/data`, but simply mounting an `emptyDir` there hides the required `static` directory unless advanced K8s patterns like initContainers are used). + + + +## Bonus: Conftest Policy + +### Policy (paste labs/lab7/policies/pod-hardening.rego) +```rego +package main +import rego.v1 + +# 1. spec.securityContext.runAsNonRoot != true +deny contains msg if { + input.kind == "Deployment" + pod_spec := input.spec.template.spec + not pod_spec.securityContext.runAsNonRoot == true + msg := "Pod securityContext must have runAsNonRoot set to true" +} + +# 2. (any container) spec.containers[_].securityContext.readOnlyRootFilesystem != true +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not container.securityContext.readOnlyRootFilesystem == true + msg := sprintf("Container '%v' must have readOnlyRootFilesystem set to true", [container.name]) +} + +# 3. (any container) spec.containers[_].securityContext.allowPrivilegeEscalation != false +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not container.securityContext.allowPrivilegeEscalation == false + msg := sprintf("Container '%v' must have allowPrivilegeEscalation set to false", [container.name]) +} + +# 4. (any container) spec.containers[_].securityContext.capabilities.drop missing "ALL" +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not has_drop_all(container) + msg := sprintf("Container '%v' must drop ALL capabilities", [container.name]) +} + +has_drop_all(container) if { + "ALL" in container.securityContext.capabilities.drop +} +``` + +### Output: PASS on hardened manifest +``` +4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions +``` + +### Output: FAIL on bad manifest +``` +FAIL - /tmp/bad-pod.yaml - main - Container 'app' must drop ALL capabilities +FAIL - /tmp/bad-pod.yaml - main - Container 'app' must have allowPrivilegeEscalation set to false +FAIL - /tmp/bad-pod.yaml - main - Container 'app' must have readOnlyRootFilesystem set to true +FAIL - /tmp/bad-pod.yaml - main - Pod securityContext must have runAsNonRoot set to true + +4 tests, 0 passed, 0 warnings, 4 failures, 0 exceptions +``` + +### What this prevents at CI time (2-3 sentences) +This policy catches insecure configuration bugs (like running as root or allowing privilege escalation) natively in the CI pipeline BEFORE `kubectl apply` is ever executed. By failing the pipeline early, developers get immediate feedback in their Pull Requests without waiting for the cluster's admission controller (like PSS) to reject the deployment. This represents true "shift-left" security, reducing friction and keeping the cluster entirely unaware of bad manifests. \ No newline at end of file