diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..5af91d954 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +## Goal + + +## Changes + + +## Testing + + +## Artifacts & Screenshots + + +## Checklist + +- [ ] PR has a clear, descriptive title +- [ ] Documentation has been updated if applicable +- [ ] No secrets or large temporary files are included in the changes + +--- + + + \ No newline at end of file diff --git a/labs/assets/homepage.jpg b/labs/assets/homepage.jpg new file mode 100644 index 000000000..d51895803 Binary files /dev/null and b/labs/assets/homepage.jpg differ diff --git a/labs/lab9/falco/rules/custom-rules.yaml b/labs/lab9/falco/rules/custom-rules.yaml new file mode 100644 index 000000000..feceed5bf --- /dev/null +++ b/labs/lab9/falco/rules/custom-rules.yaml @@ -0,0 +1,41 @@ +# Custom Falco rule — Lab 9 +# Detects writes to /tmp inside any container (not host) + +- rule: "Write to /tmp by container" + desc: "Detect any file write to /tmp inside a container" + condition: > + open_write + and container.id != host + and fd.name startswith /tmp/ + output: > + "Write to /tmp detected (container=%container.name user=%user.name + file=%fd.name cmdline=%proc.cmdline)" + priority: WARNING + tags: [container, drift] + +# Bonus: Detect cryptominer network/process patterns +# Combines 2 indicators: (1) process name matching known miner binaries, +# (2) network tools making connections to mining-pool ports. +# Uses spawned_process (execve-based) since connect tracepoints are not +# available on all kernels (e.g., Linux 7.x with modern eBPF). + +- rule: "Possible Cryptominer Activity" + desc: "Detect known miner processes or network tools connecting to mining-pool ports" + condition: > + spawned_process + and container.id != host + and (proc.name in (xmrig, ethminer, cgminer, t-rex, claymore) + or (proc.name in (nc, ncat, netcat, curl, wget) + and (proc.cmdline contains "3333" + or proc.cmdline contains "4444" + or proc.cmdline contains "5555" + or proc.cmdline contains "7777" + or proc.cmdline contains "14444" + or proc.cmdline contains "19999" + or proc.cmdline contains "45700"))) + output: > + "Possible cryptominer detected (container=%container.name + proc=%proc.name cmdline=%proc.cmdline + user=%user.name)" + priority: CRITICAL + tags: [container, mitre_execution, mitre_command_and_control] diff --git a/labs/lab9/manifests/compose/juice-compose.yml b/labs/lab9/manifests/compose/juice-compose.yml new file mode 100644 index 000000000..359eb75bf --- /dev/null +++ b/labs/lab9/manifests/compose/juice-compose.yml @@ -0,0 +1,9 @@ +services: + juice-shop: + image: bkimminich/juice-shop@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + user: "1000:1000" + read_only: true + cap_drop: + - ALL + ports: + - "3000:3000" diff --git a/labs/lab9/manifests/k8s/juice-hardened.yaml b/labs/lab9/manifests/k8s/juice-hardened.yaml new file mode 100644 index 000000000..4c1b75102 --- /dev/null +++ b/labs/lab9/manifests/k8s/juice-hardened.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: juice-shop-hardened + labels: + app: juice-shop +spec: + replicas: 1 + selector: + matchLabels: + app: juice-shop + template: + metadata: + labels: + app: juice-shop + spec: + securityContext: + runAsNonRoot: true + containers: + - name: juice-shop + image: bkimminich/juice-shop@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + ports: + - containerPort: 3000 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + limits: + memory: "512Mi" + cpu: "500m" + requests: + memory: "256Mi" + cpu: "250m" diff --git a/labs/lab9/manifests/k8s/juice-unhardened.yaml b/labs/lab9/manifests/k8s/juice-unhardened.yaml new file mode 100644 index 000000000..6b26c71ae --- /dev/null +++ b/labs/lab9/manifests/k8s/juice-unhardened.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: juice-shop-unhardened + labels: + app: juice-shop +spec: + replicas: 1 + selector: + matchLabels: + app: juice-shop + template: + metadata: + labels: + app: juice-shop + spec: + containers: + - name: juice-shop + image: bkimminich/juice-shop:latest + ports: + - containerPort: 3000 diff --git a/labs/lab9/policies/compose-security.rego b/labs/lab9/policies/compose-security.rego new file mode 100644 index 000000000..c00ba59ee --- /dev/null +++ b/labs/lab9/policies/compose-security.rego @@ -0,0 +1,26 @@ +package compose.security + +# Starter compose security policy — same deny[msg] pattern, different input shape. +# input is the parsed docker-compose YAML; use input.services to iterate. +# Uses `some name` to get object keys (Rego v1). + +deny contains msg if { + some name + svc := input.services[name] + not svc.user + msg := sprintf("service %q must specify a non-root user", [name]) +} + +deny contains msg if { + some name + svc := input.services[name] + not svc.read_only == true + msg := sprintf("service %q must set read_only: true", [name]) +} + +deny contains msg if { + some name + svc := input.services[name] + not svc.cap_drop + msg := sprintf("service %q must drop all capabilities (cap_drop: [ALL])", [name]) +} diff --git a/labs/lab9/policies/extra/hardening.rego b/labs/lab9/policies/extra/hardening.rego new file mode 100644 index 000000000..2cd63bed7 --- /dev/null +++ b/labs/lab9/policies/extra/hardening.rego @@ -0,0 +1,47 @@ +package main + +# Lab 9 — Conftest hardening policies for K8s manifests +# Each deny[msg] rule enforces one hardening requirement (Lecture 9 slide 10) +# Rego v1 syntax (deny contains msg if { ... }) + +# 1. runAsNonRoot must be true (pod-level OR container-level securityContext) +deny contains msg if { + container := input.spec.template.spec.containers[_] + not container.securityContext.runAsNonRoot == true + not input.spec.template.spec.securityContext.runAsNonRoot == true + msg := sprintf("container %q must set runAsNonRoot: true in securityContext", [container.name]) +} + +# 2. allowPrivilegeEscalation must be false for every container +deny contains msg if { + container := input.spec.template.spec.containers[_] + not container.securityContext.allowPrivilegeEscalation == false + msg := sprintf("container %q must set allowPrivilegeEscalation: false in securityContext", [container.name]) +} + +# 3. capabilities.drop must include "ALL" for every container +deny contains msg if { + container := input.spec.template.spec.containers[_] + not container.securityContext.capabilities.drop + msg := sprintf("container %q must drop all capabilities (capabilities.drop must include ALL)", [container.name]) +} + +deny contains msg if { + container := input.spec.template.spec.containers[_] + not "ALL" in container.securityContext.capabilities.drop + msg := sprintf("container %q capabilities.drop must include ALL", [container.name]) +} + +# 4. resources.limits.memory must be set for every container +deny contains msg if { + container := input.spec.template.spec.containers[_] + not container.resources.limits.memory + msg := sprintf("container %q must set resources.limits.memory", [container.name]) +} + +# 5. image must use sha256: digest, not a :tag (optional hardening) +deny contains msg if { + container := input.spec.template.spec.containers[_] + not contains(container.image, "@sha256:") + msg := sprintf("container %q image must use a sha256 digest, found %q", [container.name, container.image]) +} diff --git a/labs/lab9/policies/k8s-security.rego b/labs/lab9/policies/k8s-security.rego new file mode 100644 index 000000000..a8fe1a789 --- /dev/null +++ b/labs/lab9/policies/k8s-security.rego @@ -0,0 +1,12 @@ +package k8s.security + +# Starter K8s security policy — deny[msg] pattern (Lecture 9 slide 10) +# This checks the pod-level securityContext for runAsNonRoot. +# Extend this pattern for your own rules in policies/extra/hardening.rego. + +deny contains msg if { + container := input.spec.template.spec.containers[_] + not container.securityContext.runAsNonRoot == true + not input.spec.template.spec.securityContext.runAsNonRoot == true + msg := sprintf("container %q must run as non-root (runAsNonRoot: true)", [container.name]) +} diff --git a/labs/submission1.md b/labs/submission1.md new file mode 100644 index 000000000..a5c0e279f --- /dev/null +++ b/labs/submission1.md @@ -0,0 +1,52 @@ +# Triage Report — OWASP Juice Shop + +## Scope & Asset + +- Asset: OWASP Juice Shop (local lab instance) +- Image: bkimminich/juice-shop:v19.0.0 +- Release link/date: +- Image digest (optional): sha256:2765a26de7647609099a338d5b7f61085d95903c8703bb70f03fcc4b12f0818d + +## Environment + +- Host OS: Arch Linux (Linux kernel version: 6.16.8) +- Docker: Docker API version: 1.51, Docker client version: 28.4.0 + +## Deployment Details + +- Run command used: `docker run -d --name juice-shop -p 127.0.0.1:3000:3000 bkimminich/juice-shop:v19.0.0` +- Access URL: +- Network exposure: 127.0.0.1 only [x] Yes [ ] No (explain if No) + +## Health Check + +- Page load: +![homepage](assets/homepage.jpg) + +- API check: first 5–10 lines from `curl -s http://127.0.0.1:3000/rest/products | head` + +```html + + + + Error: Unexpected path: /rest/products +