diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..0092ff1b1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +## Goal + + +## Changes + +- +- +- + +## Testing + +```bash +# +# +``` + +## Artifacts & Screenshots + +- + +## Checklist +- [ ] Title is clear (`feat(labN): ` style) +- [ ] No secrets/large temp files committed +- [ ] Submission file at `submissions/labN.md` exists diff --git a/.github/workflows/lab1-smoke.yml b/.github/workflows/lab1-smoke.yml new file mode 100644 index 000000000..0f1f17093 --- /dev/null +++ b/.github/workflows/lab1-smoke.yml @@ -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 diff --git a/labs/lab8/keys/cosign.pub b/labs/lab8/keys/cosign.pub new file mode 100644 index 000000000..717927fc6 --- /dev/null +++ b/labs/lab8/keys/cosign.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEivYGVAeU5OEVvX65MyCvUsfE+Mc9 +IfHqp9JH0MSBpSF8RtcSKiQ+HThwfhzr9EsbGVL9sCV//0YL+poVa+uJCg== +-----END PUBLIC KEY----- diff --git a/submissions/lab1.md b/submissions/lab1.md new file mode 100644 index 000000000..d7bb7a6c1 --- /dev/null +++ b/submissions/lab1.md @@ -0,0 +1,100 @@ +# Lab 1 — Submission + +## Triage Report: OWASP Juice Shop + +### Scope & Asset +- Asset: OWASP Juice Shop (local lab instance) +- Image: `bkimminich/juice-shop:v20.0.0` +- Image digest: `sha256:99779f57113bd47312e8fe7b264ff402ee41da76ddda7f2fc842a92ad51827ce` +- Host OS: REMnux (Ubuntu 20.04-based) +- Docker version: `Docker version 26.1.3, build 26.1.3-0ubuntu1~20.04.1` + +### Deployment Details +- Run command used: `docker run -d --name juice-shop -p 127.0.0.1:3000:3000 bkimminich/juice-shop:v20.0.0` +- Access URL: http://127.0.0.1:3000 +- Network exposure: 127.0.0.1 only? [x] Yes [ ] No +- Container restart policy: default `no` (no `--restart` flag used) + +### Health Check +- HTTP code on `/`: `200` +- API check (first 200 chars of `/api/Products`): + ```json + {"status":"success","data":[{"id":1,"name":"Apple Juice (1000ml)","description":"The all-time classic.","price":1.99,"deluxePrice":0.99,"image":"apple_juice.jpg","createdAt":"2026-06-12T10:31:40.266Z" + ``` +- Container uptime: 4bd57343c74b bkimminich/juice-shop:v20.0.0 "/nodejs/bin/node /j…" 14 minutes ago Up 14 minutes 127.0.0.1:3000->3000/tcp juice-shop + +### Initial Surface Snapshot (from browser exploration) +- Login/Registration visible: [x] Yes [ ] No — notes: Login and Sign Up buttons present +- Product listing/search present: [x] Yes [ ] No — notes: Product cards displayed on homepage, search field available +- Admin or account area discoverable: [ ] Yes [x] No — notes: No direct admin link on landing page; authentication required +- Client-side errors in DevTools console: [ ] Yes [x] No — notes: Console clean, no errors detected +- Pre-populated local storage / cookies: language (set to 'en'), token (empty until login) + + +### Security Headers (Quick Look) +HTTP/1.1 200 OK +Access-Control-Allow-Origin: * +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Feature-Policy: payment 'self' +X-Recruiting: /#/jobs +Accept-Ranges: bytes +Cache-Control: public, max-age=0 +Last-Modified: Fri, 12 Jun 2026 10:31:40 GMT +ETag: W/"26af-19ebb633283" +Content-Type: text/html; charset=UTF-8 +Content-Length: 9903 +Vary: Accept-Encoding +Date: Fri, 12 Jun 2026 10:34:55 GMT +Connection: keep-alive +Keep-Alive: timeout=5 + +Which of these are MISSING? (cross-reference Lecture 1 OWASP Top 10:2025 — A06) +- [x] `Content-Security-Policy` - MISSING +- [x] `Strict-Transport-Security` - MISSING +- [ ] `X-Content-Type-Options: nosniff` +- [ ] `X-Frame-Options` + +### Top 3 Risks Observed (2-3 sentences each, in your own words) +1. Broken Access Control (A01) — The API endpoint /api/Products//reviews returns data without authentication. This allows an unauthenticated attacker to read other users' reviews and potentially exfiltrate data, violating the principle of least privilege. +2. Cryptographic Failures (A02) — Absence of the HSTS header and operation over HTTP (in local environment) means that in production traffic could be intercepted. If the application transmits credentials or tokens without TLS, this leads to sensitive data exposure. +3. Security Misconfiguration (A05) — The complete absence of CSP and HSTS indicates a default 'open' configuration. Combined with intentionally vulnerable Juice Shop code, this creates a broad surface for XSS, clickjacking, and MIME-sniffing attacks. + +## PR Template Setup + +- File: `.github/PULL_REQUEST_TEMPLATE.md` +- Sections included: Goal / Changes / Testing / Artifacts & Screenshots +- Checklist items: + - Title is clear (`feat(labN): ` style) + - No secrets/large temp files committed + - Submission file at `submissions/labN.md` exists +- Auto-fill verified: [x] Yes — PR description showed my template (screenshot or link to draft PR) + +## GitHub Community + +### Actions Completed +- [x] Starred course repository +- [x] Starred [simple-container-com/api](https://github.com/simple-container-com/api) +- [x] Following Professor [@Cre-eD](https://github.com/Cre-eD) +- [x] Following TA [@Naghme98](https://github.com/Naghme98) +- [x] Following TA [@pierrepicaud](https://github.com/pierrepicaud) +- [x] Following 3+ classmates + +### Why Stars Matter in Open Source +Stars are the currency of attention in the open-source ecosystem. A repository with 1000+ stars attracts more contributors and sponsors than an equivalent one with 10 stars. + +## Bonus: CI Smoke Test + +- Workflow file: `.github/workflows/lab1-smoke.yml` +- Trigger: `pull_request` on main +- Run URL (must be green): https://github.com/raaller/DevSecOps-Intro/actions/runs/27413678469 +- Workflow run duration: 27s +- Curl response excerpt: + ``` + HTTP/1.1 200 OK + Access-Control-Allow-Origin: * + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + Feature-Policy: payment 'self' + HTTP Status: 200 + ``` diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..61fa24bee --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1 @@ +lab3 signing test diff --git a/submissions/lab8.md b/submissions/lab8.md new file mode 100644 index 000000000..214f8fbbf --- /dev/null +++ b/submissions/lab8.md @@ -0,0 +1,214 @@ +# Lab 8 — Submission + +## Environment + +- Docker: `27.5.1` +- Cosign: `v2.4.3` +- jq: `1.7.1` +- Branch: `feature/lab8` +- Source commit: `7f777bf2b97dee0362254be52e4d9aba16948893` + +## Task 1: Sign + Tamper Demo + +### Registry + image push + +- Registry container: `lab8-registry` on `localhost:5000` +- Image pushed: `localhost:5000/juice-shop:v20.0.0` +- Image digest: `localhost:5000/juice-shop@sha256:8c76bce948965bcb2ad33c24a659d58f307d679ff48ec253a3d29138329f3c0d` + +```text +localhost:5000/juice-shop@sha256:8c76bce948965bcb2ad33c24a659d58f307d679ff48ec253a3d29138329f3c0d +``` + +### Signing + +The image was signed by immutable registry digest using the generated Cosign private key. + +```text +Pushing signature to: localhost:5000/juice-shop +``` + +The private key is excluded from Git. The public key is committed as `labs/lab8/keys/cosign.pub`. + +### Verification — passed + +`cosign verify` validated both the Cosign claims and the signature against the public key: + +```text +Verification for localhost:5000/juice-shop@sha256:8c76bce948965bcb2ad33c24a659d58f307d679ff48ec253a3d29138329f3c0d -- +The following checks were performed on each of these signatures: + - The cosign claims were validated + - The signatures were verified against the specified public key +``` + +The verified claim binds the signature to the expected manifest digest: + +```json +[ + { + "critical": { + "identity": { + "docker-reference": "localhost:5000/juice-shop" + }, + "image": { + "docker-manifest-digest": "sha256:8c76bce948965bcb2ad33c24a659d58f307d679ff48ec253a3d29138329f3c0d" + }, + "type": "cosign container image signature" + }, + "optional": null + } +] +``` + +### Tamper demo — failed correctly + +Alpine `3.20` was pushed under a Juice Shop-looking tag. It resolved to a different digest: + +```text +localhost:5000/juice-shop@sha256:6c2a9711b0a9f32b0239d9222eb1072309cf46c6431d319ae249186d811a987c +``` + +Verification returned exit code `10` and failed because no signature existed for the substituted digest: + +```text +WARNING: Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the signature. +Error: no signatures found +error during command execution: no signatures found +``` + +### Sanity — original still verifies + +The original digest was verified again after the tamper test: + +```text +Verification for localhost:5000/juice-shop@sha256:8c76bce948965bcb2ad33c24a659d58f307d679ff48ec253a3d29138329f3c0d -- +The following checks were performed on each of these signatures: + - The cosign claims were validated + - The signatures were verified against the specified public key +``` + +### Why digest binding matters + +An OCI tag is a mutable reference and can be moved to a different manifest, while a digest is a content-addressed identifier for one exact manifest. The tampered tag resolved to `sha256:6c2a...`, but the signature covered only the original `sha256:8c76...` digest, so verification failed. If trust were attached only to the tag name, an attacker could retarget that tag to malicious content while preserving the apparently trusted image name. + +## Task 2: SBOM + Provenance Attestations + +### SBOM attestation + +- Attached with `cosign attest --type cyclonedx`: **yes** +- Verification with `cosign verify-attestation --type cyclonedx`: **passed** +- CycloneDX version: `1.6` +- Source component count: `3069` +- Attested component count: `3069` +- Component-count diff: empty +- Full normalized SBOM diff: empty + +Decoded attestation excerpt: + +```json +{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "localhost:5000/juice-shop", + "digest": { + "sha256": "8c76bce948965bcb2ad33c24a659d58f307d679ff48ec253a3d29138329f3c0d" + } + } + ], + "predicateType": "https://cyclonedx.org/bom", + "predicate": { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:389ef301-1899-4bd7-abc6-526cb7c33477", + "version": 1, + "components": [ + { + "name": "1to2", + "version": "1.0.0", + "type": "library", + "purl": "pkg:npm/1to2@1.0.0" + } + ] + } +} +``` + +The complete decoded evidence is saved in `labs/lab8/results/sbom-attestation-report-excerpt.json`, and the count comparison is saved in `labs/lab8/results/sbom-component-counts.txt`. + +### Provenance attestation + +- Attached with `cosign attest --type slsaprovenance`: **yes** +- Verification with `cosign verify-attestation --type slsaprovenance`: **passed** +- Builder ID: `https://localhost/lab8-artemii-mashanov` +- Build type: `https://example.com/lab8/local-build` +- Source URI: `https://github.com/raaller/DevSecOps-Intro.git` +- Source revision: `7f777bf2b97dee0362254be52e4d9aba16948893` + +Decoded provenance statement: + +```json +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", + "subject": [ + { + "name": "localhost:5000/juice-shop", + "digest": { + "sha256": "8c76bce948965bcb2ad33c24a659d58f307d679ff48ec253a3d29138329f3c0d" + } + } + ], + "predicate": { + "builder": { + "id": "https://localhost/lab8-artemii-mashanov" + }, + "buildType": "https://example.com/lab8/local-build", + "invocation": { + "configSource": { + "uri": "https://github.com/raaller/DevSecOps-Intro.git", + "digest": { + "sha1": "7f777bf2b97dee0362254be52e4d9aba16948893" + } + } + } + } +} +``` + +### What this gives a Lab 9 verifier + +A valid image signature proves that the admitted digest was signed by the expected key, but it does not reveal which libraries are present. A signed CycloneDX attestation gives an admission controller a verifiable predicate tied to the same image digest, allowing policy to require both authenticity and an SBOM. During a Log4Shell-style incident, the signed SBOM can be queried for the affected package and version; a signed image without an SBOM remains authentic but operationally opaque and requires rescanning before impact can be determined. + +## Bonus: Blob Signing — Codecov 2021 Mitigation + +### Sign + verify + +The release artifact and its Cosign bundle were created as: + +```text +labs/lab8/results/my-tool.tar.gz +labs/lab8/results/my-tool.tar.gz.bundle +``` + +Verification of the unchanged artifact succeeded: + +```text +WARNING: Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the blob. +Verified OK +``` + +### Tamper test — failed correctly + +After appending a malicious payload to the archive, `cosign verify-blob` returned exit code `1`: + +```text +WARNING: Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the blob. +Error: invalid signature when validating ASN.1 encoded signature +error during command execution: invalid signature when validating ASN.1 encoded signature +``` + +### Codecov 2021 mitigation + +The Codecov uploader attack depended on consumers executing remotely distributed content without first authenticating its exact bytes. A consumer that downloaded the uploader and ran `cosign verify-blob --key cosign.pub --bundle ` before execution would reject any attacker-modified payload because its bytes would no longer match the signed digest. The script would therefore stop before invoking the altered uploader. +