Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Goal
<!-- 1 sentence: what this PR delivers -->

## Changes
<!-- Bullet list of artifacts added/modified -->
-
-
-

## Testing
<!-- Commands + observed output -->
```bash
# <command>
# <output>
```

## Artifacts & Screenshots
<!-- Links to files in this PR, image embeds where useful -->
-

## Checklist
- [ ] Title is clear (`feat(labN): <topic>` style)
- [ ] No secrets/large temp files committed
- [ ] Submission file at `submissions/labN.md` exists
49 changes: 49 additions & 0 deletions .github/workflows/lab1-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Lab 1 — Juice Shop Smoke Test

on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
smoke-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Pull and run Juice Shop
run: |
docker run -d --name juice-shop \
-p 127.0.0.1:3000:3000 \
bkimminich/juice-shop:v20.0.0

- name: Wait for Juice Shop to be healthy
run: |
echo "Waiting for Juice Shop to start..."
for i in $(seq 1 30); do
if curl --silent --fail http://127.0.0.1:3000/rest/admin/application-version >/dev/null; then
echo "Juice Shop is up!"
exit 0
fi
echo "Attempt $i/30 — not ready yet, sleeping 2s..."
sleep 2
done
echo "Juice Shop failed to start within 60s"
docker logs juice-shop
exit 1

- name: Verify homepage returns HTTP 200
run: |
curl -I -s http://127.0.0.1:3000 | head -5
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://127.0.0.1:3000

- name: Verify product API
run: |
curl -s http://127.0.0.1:3000/api/Products | jq '.data | length'

- name: Verify version endpoint
run: |
curl -s http://127.0.0.1:3000/rest/admin/application-version | jq
161 changes: 161 additions & 0 deletions labs/lab7/k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: juice-shop
namespace: juice-shop
labels:
app: juice-shop
spec:
replicas: 1
progressDeadlineSeconds: 600
selector:
matchLabels:
app: juice-shop
template:
metadata:
labels:
app: juice-shop
spec:
serviceAccountName: juice-shop-sa
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault

# Writable application directories contain seed files in the image.
# The init container copies those files into emptyDir volumes before
# the main container starts with a read-only root filesystem.
initContainers:
- name: initialize-writable-directories
image: bkimminich/juice-shop@sha256:fd58bdc9745416afce8184ee0666278a436574633ea7880365153a63bfd418b0
imagePullPolicy: IfNotPresent
command:
- /nodejs/bin/node
args:
- -e
- |
const fs = require('fs');
const path = require('path');

const copies = [
['/juice-shop/data', '/writable/data'],
['/juice-shop/ftp', '/writable/ftp'],
['/juice-shop/i18n', '/writable/i18n'],
['/juice-shop/logs', '/writable/logs'],
['/juice-shop/frontend/dist', '/writable/frontend-dist'],
['/juice-shop/.well-known/csaf', '/writable/csaf']
];

for (const [source, destination] of copies) {
fs.mkdirSync(destination, { recursive: true });
if (!fs.existsSync(source)) {
continue;
}
for (const entry of fs.readdirSync(source)) {
fs.cpSync(
path.join(source, entry),
path.join(destination, entry),
{ recursive: true, force: true }
);
}
}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
volumeMounts:
- name: data
mountPath: /writable/data
- name: ftp
mountPath: /writable/ftp
- name: i18n
mountPath: /writable/i18n
- name: logs
mountPath: /writable/logs
- name: frontend-dist
mountPath: /writable/frontend-dist
- name: csaf
mountPath: /writable/csaf

containers:
- name: juice-shop
image: bkimminich/juice-shop@sha256:fd58bdc9745416afce8184ee0666278a436574633ea7880365153a63bfd418b0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 500m
memory: 1Gi
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 18
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
volumeMounts:
- name: tmp
mountPath: /tmp
- name: data
mountPath: /juice-shop/data
- name: ftp
mountPath: /juice-shop/ftp
- name: i18n
mountPath: /juice-shop/i18n
- name: logs
mountPath: /juice-shop/logs
- name: frontend-dist
mountPath: /juice-shop/frontend/dist
- name: csaf
mountPath: /juice-shop/.well-known/csaf

volumes:
- name: tmp
emptyDir: {}
- name: data
emptyDir: {}
- name: ftp
emptyDir: {}
- name: i18n
emptyDir: {}
- name: logs
emptyDir: {}
- name: frontend-dist
emptyDir: {}
- name: csaf
emptyDir: {}
8 changes: 8 additions & 0 deletions labs/lab7/k8s/namespace.yaml
Original file line number Diff line number Diff line change
@@ -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
42 changes: 42 additions & 0 deletions labs/lab7/k8s/networkpolicy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: juice-shop-restricted
namespace: juice-shop
spec:
podSelector:
matchLabels:
app: juice-shop
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
- ipBlock:
cidr: 127.0.0.1/32
ports:
- protocol: TCP
port: 3000
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
6 changes: 6 additions & 0 deletions labs/lab7/k8s/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: juice-shop-sa
namespace: juice-shop
automountServiceAccountToken: false
35 changes: 35 additions & 0 deletions labs/lab7/policies/pod-hardening.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

deny contains msg if {
input.kind == "Deployment"
pod_spec := input.spec.template.spec
pod_security_context := object.get(pod_spec, "securityContext", {})
object.get(pod_security_context, "runAsNonRoot", false) != true
msg := "pod must set spec.securityContext.runAsNonRoot to true"
}

deny contains msg if {
input.kind == "Deployment"
some container in input.spec.template.spec.containers
security_context := object.get(container, "securityContext", {})
object.get(security_context, "readOnlyRootFilesystem", false) != true
msg := sprintf("container %q must set readOnlyRootFilesystem to true", [container.name])
}

deny contains msg if {
input.kind == "Deployment"
some container in input.spec.template.spec.containers
security_context := object.get(container, "securityContext", {})
object.get(security_context, "allowPrivilegeEscalation", null) != false
msg := sprintf("container %q must set allowPrivilegeEscalation to false", [container.name])
}

deny contains msg if {
input.kind == "Deployment"
some container in input.spec.template.spec.containers
security_context := object.get(container, "securityContext", {})
capabilities := object.get(security_context, "capabilities", {})
drop_list := object.get(capabilities, "drop", [])
not "ALL" in drop_list
msg := sprintf("container %q must drop the ALL capability set", [container.name])
}
Loading
Loading