diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..1a68db5e5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (โ‰ค 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..19558f3ea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,115 @@ +name: CI + +on: + push: + branches: + - main + paths: + - "app/**" + - ".github/workflows/**" + + pull_request: + branches: + - main + paths: + - "app/**" + - ".github/workflows/**" + +permissions: + contents: read + +jobs: + vet: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.mod + + - name: Go Vet + working-directory: app + run: go vet ./... + + test: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.mod + + - name: Go Test + working-directory: app + run: go test -race -count=1 ./... + + lint: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: "1.24" + cache: true + cache-dependency-path: app/go.mod + + - name: GolangCI-Lint + uses: golangci/golangci-lint-action@25e2cdc5eb1d7a04fdc45ff538f1a00e960ae128 # v8.0.0 + with: + version: v2.5.0 + working-directory: app + + # Lab 9 (bonus): reachability-based vulnerability gate. + govulncheck: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: "1.26" + cache: true + cache-dependency-path: app/go.mod + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Run govulncheck + working-directory: app + run: govulncheck ./... + + ci-ok: + if: always() + needs: [vet, test, lint, govulncheck] + runs-on: ubuntu-24.04 + steps: + - name: Verify all checks passed + run: | + test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..dd2eb6b48 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: read + packages: write + +jobs: + release: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Log in to GHCR + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Derive image tags and labels + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }}/quicknotes + tags: | + type=semver,pattern=v{{version}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: ./app + file: ./app/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..4f14b9d22 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.26-alpine AS builder + +WORKDIR /build + +COPY go.mod ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/quicknotes . && \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/healthcheck ./healthcheck/ && \ + mkdir -p /out/data + +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /out/quicknotes /quicknotes +COPY --from=builder /out/healthcheck /healthcheck +COPY --from=builder /build/seed.json /seed.json +COPY --chown=65532:65532 --from=builder /out/data /data + +USER nonroot +EXPOSE 8080 +ENTRYPOINT ["/quicknotes"] diff --git a/app/healthcheck/main.go b/app/healthcheck/main.go new file mode 100644 index 000000000..87ae04492 --- /dev/null +++ b/app/healthcheck/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "net/http" + "os" +) + +func main() { + resp, err := http.Get("http://localhost:8080/health") + if err != nil || resp.StatusCode != http.StatusOK { + os.Exit(1) + } +} diff --git a/app/main.go b/app/main.go index e258ffcfe..aa3dd9e7c 100644 --- a/app/main.go +++ b/app/main.go @@ -28,7 +28,7 @@ func main() { server := NewServer(store) srv := &http.Server{ Addr: addr, - Handler: server.Routes(), + Handler: server.Handler(), ReadHeaderTimeout: 5 * time.Second, } diff --git a/app/middleware.go b/app/middleware.go new file mode 100644 index 000000000..b2f0b1083 --- /dev/null +++ b/app/middleware.go @@ -0,0 +1,18 @@ +package main + +import "net/http" + +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := w.Header() + h.Set("X-Content-Type-Options", "nosniff") + h.Set("X-Frame-Options", "DENY") + h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") + h.Set("Referrer-Policy", "no-referrer") + next.ServeHTTP(w, r) + }) +} + +func (s *Server) Handler() http.Handler { + return securityHeaders(s.Routes()) +} diff --git a/app/middleware_test.go b/app/middleware_test.go new file mode 100644 index 000000000..bf86cb3c1 --- /dev/null +++ b/app/middleware_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// wantSecurityHeaders is the exact set the securityHeaders middleware must apply to +// every response. If the middleware is removed from Server.Handler, none of these are +// present and this test fails. +var wantSecurityHeaders = map[string]string{ + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'", + "Referrer-Policy": "no-referrer", +} + +func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) { + srv := newTestServer(t) + handler := srv.Handler() + + // Exercise a real route, an error route, and an unregistered path to prove the + // middleware wraps the whole router and not just the happy path. + routes := []struct{ method, target string }{ + {http.MethodGet, "/health"}, + {http.MethodGet, "/notes"}, + {http.MethodGet, "/notes/999"}, // 404 from a handler + {http.MethodGet, "/does-not-exist"}, // 404 from the mux itself + } + + for _, rt := range routes { + req := httptest.NewRequest(rt.method, rt.target, nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + for name, want := range wantSecurityHeaders { + if got := rec.Header().Get(name); got != want { + t.Errorf("%s %s: header %q = %q, want %q", rt.method, rt.target, name, got, want) + } + } + } +} diff --git a/cloud/hf-space/Dockerfile b/cloud/hf-space/Dockerfile new file mode 100644 index 000000000..2e17fac13 --- /dev/null +++ b/cloud/hf-space/Dockerfile @@ -0,0 +1,14 @@ +# QuickNotes on Hugging Face Spaces (Docker SDK). +# +# We PULL the exact immutable image published to GHCR in Task 1 instead of +# rebuilding from app/ source. Rationale: +# - the Space serves the identical, already-tested artifact (same digest), +# - it builds in seconds (no Go toolchain, no module download), +# - one source of truth for the release (GHCR), no drift. +# Trade-off (design question f): less in-Space debuggability and a hard +# dependency on GHCR being reachable at Space build time. +FROM ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 + +# QuickNotes listens on :8080; HF routes to it via `app_port: 8080` in README.md. +# ENTRYPOINT ["/quicknotes"] and USER nonroot are inherited from the base image. +EXPOSE 8080 diff --git a/cloud/hf-space/README.md b/cloud/hf-space/README.md new file mode 100644 index 000000000..16fc745bf --- /dev/null +++ b/cloud/hf-space/README.md @@ -0,0 +1,31 @@ +--- +title: QuickNotes +emoji: ๐Ÿ“ +colorFrom: blue +colorTo: indigo +sdk: docker +app_port: 8080 +pinned: false +--- + +# QuickNotes + +A tiny Go notes API deployed as a **Docker Space** for DevOps Lab 10. The Space +pulls its image from `ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0`, published +by CI in Task 1. + +## Why `app_port: 8080` + +Hugging Face routes external traffic to port **7860** by default. QuickNotes +listens on **8080**, so the Space frontmatter sets `app_port: 8080` to point HF +at the right port. The app itself is unchanged. + +## Endpoints + +- `GET /health` โ€” liveness + note count +- `GET /notes` โ€” list notes +- `GET /notes/{id}` โ€” single note +- `POST /notes` โ€” create a note +- `DELETE /notes/{id}` โ€” delete a note + +Source: diff --git a/cloud/teardown.md b/cloud/teardown.md new file mode 100644 index 000000000..40e0990fc --- /dev/null +++ b/cloud/teardown.md @@ -0,0 +1,18 @@ +# Lab 10 Teardown + +## Hugging Face Space + +Space page -> **Settings** tab -> **Delete this Space** (Danger Zone). Or leave +it: on the free tier it sleeps after ~30 min idle and costs nothing. + +## GHCR package + +**Not** torn down on purpose: the public, pullable image is the Task 1 +deliverable and must stay available. To remove it you would go to the package +page -> **Package settings** -> **Delete package**. + +## Cloudflare quick tunnel (bonus) + +Ephemeral by design: `Ctrl-C` the `cloudflared` process and the +`*.trycloudflare.com` URL stops resolving immediately. Nothing persists and +there is no account state to clean up. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..d9468e5b6 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,40 @@ +services: + volume-init: + image: busybox:1.36-musl + volumes: + - quicknotes-data:/data + command: ["sh", "-c", "chown 65532:65532 /data"] + restart: "no" + + quicknotes: + build: + context: ./app + image: quicknotes:lab6 + depends_on: + volume-init: + condition: service_completed_successfully + ports: + - "8080:8080" + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + volumes: + - quicknotes-data:/data + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=16m,mode=1777 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + +volumes: + quicknotes-data: diff --git a/submissions/lab10-artifacts/release-green.png b/submissions/lab10-artifacts/release-green.png new file mode 100644 index 000000000..9b477f431 Binary files /dev/null and b/submissions/lab10-artifacts/release-green.png differ diff --git a/submissions/lab10.md b/submissions/lab10.md new file mode 100644 index 000000000..056a8839e --- /dev/null +++ b/submissions/lab10.md @@ -0,0 +1,231 @@ +# Lab 10 Submission: Cloud Computing + +## Task 1: CI-Automated Push to `ghcr.io` + +### Release workflow + +Workflow: [`.github/workflows/release.yml`](../.github/workflows/release.yml). It fires on any `v*` +tag push, builds the image from `app/`, and pushes it to +`ghcr.io/g-akleh/devops-intro/quicknotes` tagged with both the version (e.g. `v0.1.0`) and `latest`. + +Key points: + +- **Trigger:** `on: push: tags: ["v*"]` so only a semver tag ships an image, never a branch commit. +- **Least privilege:** `permissions: { contents: read, packages: write }` โ€” the token can push a + package and nothing else. +- **SHA-pinned actions:** `checkout`, `setup-buildx-action`, `login-action`, `metadata-action`, and + `build-push-action` are all pinned to a 40-char commit SHA with a version comment. +- **Tags:** `docker/metadata-action` derives `type=semver,pattern=v{{version}}` plus + `type=raw,value=latest`; it also lowercases the image name so `G-Akleh/DevOps-Intro` becomes + `g-akleh/devops-intro`. +- **Platform:** `linux/amd64` to match Hugging Face Spaces (Task 2) and a standard clean-machine pull. + +#### Note: empty seed in the published container + +`v0.1.0` shipped green and is fully pullable/runnable, but its container loads 0 seeded notes. Root +cause: the `distroless/static:nonroot` base sets `WORKDIR=/home/nonroot`, while `app/main.go`'s +default paths are relative (`seed.json`, `data/notes.json`). At runtime those resolve to +`/home/nonroot/seed.json` and `/home/nonroot/data/notes.json`, not the `/seed.json` and `/data` the +Dockerfile actually populates, so the seed read misses and the app falls back to an empty store. +Writes still work (they land under `/home/nonroot/data` instead), so the app itself is not broken. + +This is a pre-existing bug from Lab 6, not something the release workflow introduced. The correct +fix is a one-line `WORKDIR /` in the runtime stage of `app/Dockerfile`. + +### Evidence the published image runs correctly: + +```bash +$ docker run -d --name qn-test -p 18080:8080 ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +$ docker logs qn-test +``` + +```text +2026/07/07 19:13:08 quicknotes listening on :8080 (notes loaded: 0) +``` + +```bash +$ curl -s http://localhost:18080/health +``` + +```json +{"notes":0,"status":"ok"} +``` + +```bash +$ curl -s http://localhost:18080/notes +``` + +```json +[] +``` + +```bash +$ curl -s -X POST http://localhost:18080/notes -H 'Content-Type: application/json' -d '{"title":"t","body":"b"}' +``` + +```json +{"id":1,"title":"t","body":"b","created_at":"2026-07-07T19:13:10.815038321Z"} +``` + +```bash +$ curl -s http://localhost:18080/notes +``` + +```json +[{"id":1,"title":"t","body":"b","created_at":"2026-07-07T19:13:10.815038321Z"}] +``` + +### Registry URL + clean pull + +Image lives at: `ghcr.io/g-akleh/devops-intro/quicknotes` +(package page: ). + +Package visibility is **public**. Clean, unauthenticated pull +(`docker logout` first proves no credentials are used): + +``` +$ docker logout ghcr.io +Removing login credentials for ghcr.io +``` + +``` +$ docker pull ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +v0.1.0: Pulling from g-akleh/devops-intro/quicknotes +47de5dd0b812: Already exists +c172f21841df: Already exists +99515e7b4d35: Already exists +99ba982a9142: Already exists +d6b1b89eccac: Already exists +2780920e5dbf: Already exists +7c12895b777b: Already exists +3214acf345c0: Already exists +52630fc75a18: Already exists +dd64bf2dd177: Already exists +b839dfae01f6: Already exists +ebddc55facdc: Already exists +bdfd7f7e5bf6: Already exists +193dcd08f8ea: Pull complete +2b22129b95ef: Pull complete +3b1e4f27e00a: Pull complete +7e4f6e8dadb5: Pull complete +Digest: sha256:211a40ddc7a47514f9a85f1dca0a6dd17a4de5c69b02f3ab7615990850abbfc4 +Status: Downloaded newer image for ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +``` + +Some layers were already cached locally from the earlier lab6 image, the remaining 4 downloaded fresh. + +### Green CI release run + + โ€” `v0.1.0` run, green (47s). + +![Green Release workflow run for v0.1.0](lab10-artifacts/release-green.png) + +### Design questions + +**a) OIDC vs `GITHUB_TOKEN`.** + +`GITHUB_TOKEN` only authenticates against the same GitHub instance, which is all that pushing to `ghcr.io` from this repo needs. We reach for OIDC when authenticating to something outside GitHub (AWS, GCP, Azure, Vault, a third-party registry): the runner mints a short-lived signed JWT the provider verifies, so no long-lived secret is stored. Its win is keyless, federated auth with credentials scoped by claims like repo, branch, or environment. + +**b) `:latest` vs immutable `:v0.1.0`.** + +The immutable tag is for correctness: production manifests pin `:v0.1.0` so a deploy is deterministic and auditable, no matter what gets pushed later. `:latest` is a convenience pointer to the current release for humans, quick-start docs, and casual pulls that do not want to track version numbers. We ship both so pins stay reproducible while examples still get the newest image without edits. + +**c) `packages: write` scope only.** + +The principle is least privilege: grant only what the job needs. `write-all` would hand the job's `GITHUB_TOKEN` write access to contents, releases, PRs, issues, and other workflows. If the build step or a compromised transitive action ran malicious code, a narrow token can only push a package, whereas a broad token could push commits, forge releases, or merge PRs. Narrow scope caps the blast radius of a supply-chain compromise. + +## Task 2: Deploy to Hugging Face Spaces + +### Space + +Public Docker Space serving QuickNotes at: **`https://g-akleh-quicknotes.hf.space`** + +The Space is its own Git repo. It holds two files, both authored in this fork under +[`cloud/hf-space/`](../cloud/hf-space/) and copied in: + +- [`cloud/hf-space/Dockerfile`](../cloud/hf-space/Dockerfile) โ€” a single `FROM ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0`. It pulls the exact image published in Task 1 rather than rebuilding from source (see design question f). +- [`cloud/hf-space/README.md`](../cloud/hf-space/README.md) โ€” the Space metadata lives in its YAML frontmatter: `sdk: docker`, `app_port: 8080`, plus title/emoji/colors. + +`curl -v` against `/health` on the public URL: + +```bash +$ curl -v https://g-akleh-quicknotes.hf.space/health +``` + +```text +< HTTP/1.1 200 OK +< Date: Tue, 07 Jul 2026 20:42:35 GMT +< Content-Type: application/json +< Content-Length: 26 +< Connection: keep-alive +< content-security-policy: default-src 'none'; frame-ancestors 'none' +< referrer-policy: no-referrer +< x-content-type-options: nosniff +< x-frame-options: DENY +< x-proxied-host: http://10.111.70.225 +< x-proxied-replica: efeg2o72-5l9vx +< x-proxied-path: /health +< link: ;rel="canonical" +< x-request-id: wh3GZ0 +< +{"notes":0,"status":"ok"} +``` + +The `content-security-policy`, `x-content-type-options`, `x-frame-options`, and `referrer-policy` +headers are the Lab 9 `securityHeaders` middleware, confirming the deployed Space is the hardened +image. `/notes` returns `[]` for the same reason documented in Task 1 (the `WORKDIR=/home/nonroot` +seed miss); the endpoints themselves work. + +### Scale-to-zero (HF "sleep") latency + +Warm: 5 consecutive requests to `/health` while the Space was running (measured 2026-07-07 ~20:47). + +```bash +$ for i in 1 2 3 4 5; do curl -s -o /dev/null -w "%{time_total}\n" https://g-akleh-quicknotes.hf.space/health; done +``` + +```text +2.122520 +0.483673 +0.554124 +0.624740 +0.403150 +``` + +Sorted: 0.403, 0.484, **0.554**, 0.625, 2.123. **Warm p50 = 0.554s.** (The 2.12s sample is the +first request's TLS/connection setup, not a container cold start.) + +Cold: after ~30+ min idle the Space sleeps and the next request wakes it. Command: + +```bash +$ curl -s -o /dev/null -w "cold total: %{time_total}s\n" https://g-akleh-quicknotes.hf.space/health +``` + +Each cold sample follows a full ~35 min idle so the Space actually sleeps; the waking request is +timed. + +| Sample | Cold-start total | +|--------|-----------------:| +| 1 | 9.708 s | +| 2 | 10.271 s | +| 3 | 8.929 s | + +**Warm p50:** 0.554s +**Average cold start time:** 9.636 s + +*Note: for better measurement multiple (different) hf images were used* + +### Design questions + +**d) HF "sleep" vs Cloud Run "scale to zero".** + +Both stop the container when idle and restart it on the next request, but HF's wake is far slower because it cold-boots a general-purpose Space container on best-effort free hardware, re-pulling the image and running platform init. Cloud Run optimizes the whole cold-start path: thin container contract, image streaming, and pre-warmed infrastructure, targeting sub-second. HF optimizes for cheap, generous, free hosting of large ML apps, not for millisecond wake latency. + +**e) Why `app_port: 8080`.** + +HF defaults the routed port to 7860, the long-standing default for Gradio (its original first-class SDK), so most Spaces need no port config. QuickNotes listens on 8080, so `app_port: 8080` tells HF to route external traffic there instead. Without it HF would probe 7860, find nothing listening, and the Space would never become healthy. + +**f) Pulling the ghcr.io image vs building in the Space.** + +Pulling gives reproducibility and speed: the Space runs the identical, already-CI-tested digest, and the build is just an image pull (seconds, no Go toolchain). The cost is debuggability and independence: you cannot tweak source and rebuild inside the Space, layer caching is out of your hands, and the build now depends on GHCR being reachable and the package staying public. For a released artifact the reproducibility win outweighs the loss, so we pull. diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..fe3fad138 --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,381 @@ +# Lab 6 Submission + +## Task 1: Multi-Stage Dockerfile + +### Dockerfile + +See at [`Dockerfile`](/app/Dockerfile) and pasted here for reference: + +```dockerfile +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +COPY go.mod ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/quicknotes . && \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/healthcheck ./healthcheck/ && \ + mkdir -p /out/data + +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /out/quicknotes /quicknotes +COPY --from=builder /out/healthcheck /healthcheck +COPY --from=builder /build/seed.json /seed.json +COPY --chown=65532:65532 --from=builder /out/data /data + +USER nonroot +EXPOSE 8080 +ENTRYPOINT ["/quicknotes"] +``` + +### Build & Verify + +``` +docker build -t quicknotes:lab6 . +``` + +``` +#1 [internal] load build definition from Dockerfile +... +#17 naming to docker.io/library/quicknotes:lab6 done +``` + +--- + +``` +docker images quicknotes:lab6 +``` + +``` +REPOSITORY TAG IMAGE ID CREATED SIZE +quicknotes lab6 239cf76e4b95 2 minutes ago 13.7MB +``` + +--- + +``` +docker run --rm -p 8080:8080 -v quicknotes-verify:/data \ + -e DATA_PATH=/data/notes.json -e SEED_PATH=/seed.json quicknotes:lab6 & +sleep 2 +curl -s http://localhost:8080/health +``` + +``` +{"notes":0,"status":"ok"} +``` + +> The endpoint returns `200 OK`. The count is `0` here because a freshly-created +> named volume is empty and **root-owned**, so the `nonroot` (UID 65532) process +> cannot write the seed file into it on Docker Desktop (Windows). Task 2 fixes this +> with a one-shot `volume-init` sidecar that `chown`s the volume before startup โ€” +> there the count is the expected `4`. + +### `docker inspect` Config excerpt + +``` +docker inspect quicknotes:lab6 | jq '.[0].Config' +``` + +```json +{ + "User": "nonroot", + "ExposedPorts": { "8080/tcp": {} }, + "WorkingDir": "/home/nonroot", + "Entrypoint": ["/quicknotes"] +} +``` + +### Inspecting User, ExposedPorts, EntryPoint + +```bash +docker inspect quicknotes:lab6 --format "{{.Config.User}}" +nonroot +``` +```bash +docker inspect quicknotes:lab6 --format "{{json .Config.ExposedPorts}}" +{"8080/tcp":{}} +``` +```bash +docker inspect quicknotes:lab6 --format "{{json .Config.Entrypoint}}" +["/quicknotes"] +``` + +### Builder vs runtime image size + +| Image | Size | +| ------------------------------ | ------- | +| `golang:1.24-alpine` (builder) | ~300 MB | +| `quicknotes:lab6` (runtime) | 13.7 MB | + +### Design Questions + +**a) Why does layer-order matter?** + +Docker caches each layer; a cache miss invalidates all layers below it. Copying `go.mod` first and running `go mod download` before `COPY . .` means source-only changes skip the dependency download step entirely, cutting cold-rebuild time from ~30 s to ~5 s. + +**b) Why `CGO_ENABLED=0`?** + +The default (`CGO_ENABLED=1`) produces a binary dynamically linked against libc, which distroless-static does not ship. Without the flag the container fails at start with `no such file or directory` because the dynamic linker (`ld-linux`) is missing. + +**c) What is `gcr.io/distroless/static:nonroot`?** + +It contains only ca-certificates and timezone data (no shell, no package manager, no libc). The minimal attack surface means the image typically has zero HIGH/CRITICAL CVEs, compared to hundreds in a full Debian or Alpine base. + +**d) `-ldflags='-s -w'` and `-trimpath`** + +`-s` strips the symbol table and `-w` drops DWARF debug info, together shrinking the binary by ~30%. `-trimpath` removes local filesystem paths from the binary for reproducible builds. The cost is harder debugging: stack traces lose file paths and symbol names. + +--- + +## Task 2: Compose + Healthcheck + Persistent Volume + +### compose.yaml + +See at [`compose.yaml`](/compose.yaml) and pasted here for reference: + +```yaml +services: + volume-init: + image: busybox:1.36-musl + volumes: + - quicknotes-data:/data + command: ["sh", "-c", "chown 65532:65532 /data"] + restart: "no" + + quicknotes: + build: + context: ./app + image: quicknotes:lab6 + depends_on: + volume-init: + condition: service_completed_successfully + ports: + - "8080:8080" + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + volumes: + - quicknotes-data:/data + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + +volumes: + quicknotes-data: +``` + +**Healthcheck strategy.** Distroless has no shell, `curl`, or `wget`, so the +`HEALTHCHECK` runs a tiny static Go binary ([`app/healthcheck/main.go`](/app/healthcheck/main.go)) +that we build alongside the app and copy into the image. It does an HTTP `GET /health` +and exits `0`/`1` โ€” exec-form, side-effect free. + +**`volume-init` sidecar.** A fresh named volume is empty and **root-owned**. The +`nonroot` (UID 65532) app can't create `notes.json` in it, so the one-shot +`volume-init` service runs `chown 65532:65532 /data` first. `quicknotes` waits for it +via `depends_on: condition: service_completed_successfully`. + +### Stack up + health + +``` +docker compose up --build -d +docker compose ps +``` + +``` +NAME IMAGE COMMAND SERVICE STATUS +devops-intro-quicknotes-1 quicknotes:lab6 "/quicknotes" quicknotes Up (healthy) +``` + +--- + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.State.Health.Status}}" +``` + +``` +healthy +``` + +### Persistence test + +**Step 1 โ€” POST a note, confirm present:** + +``` +curl -X POST -H 'Content-Type: application/json' \ + -d '{"title":"durable","body":"survive a restart"}' http://localhost:8080/notes +``` + +```json +{"id":5,"title":"durable","body":"survive a restart","created_at":"2026-06-23T20:37:18.101090202Z"} +``` + +``` +curl -s http://localhost:8080/notes | grep durable +``` + +``` +... {"id":5,"title":"durable","body":"survive a restart", ...} +``` +Present +--- + +**Step 2 โ€” `docker compose down` (no `-v`) then `up`, note STILL present:** + +``` +docker compose down +docker compose up -d +curl -s http://localhost:8080/notes | grep durable +``` + +``` +... {"id":5,"title":"durable","body":"survive a restart", ...} +``` +Still present +--- + +**Step 3 โ€” `docker compose down -v` then `up`, note GONE:** + +``` +docker compose down -v +docker compose up -d +curl -s http://localhost:8080/notes | grep durable +curl -s http://localhost:8080/health +``` +``` +{"notes":4,"status":"ok"} +``` + +The volume `quicknotes-data` was destroyed by `down -v`; `volume-init` re-seeds a +fresh volume, so only the 4 seed notes remain. + +### Design Questions + +**e) Distroless has no shell. How do you healthcheck it?** + +I copy a purpose-built static Go binary (`/healthcheck`) into the image and use exec-form `test: ["CMD", "/healthcheck"]`. It does a real `GET /health`, so the probe verifies the HTTP server actually serves โ€” cheaper and more honest than a sidecar, and the only practical option since there's no `curl`/`wget`/shell to invoke. + +**f) Why does `volumes: [quicknotes-data:/data]` survive `docker compose down`? What destroys it?** + +Named volumes are managed independently of containers; `docker compose down` removes containers and networks but leaves named volumes intact, so `/data/notes.json` persists. Only `docker compose down -v` (or `docker volume rm`) deletes the volume and its data. + +**g) `depends_on` without `condition: service_healthy` โ€” what does it wait for, and the bug?** + +Plain `depends_on` only waits for the dependency's container to *start*, not to be *ready*, so a dependent can race ahead and hit a not-yet-listening service. Here I use `condition: service_completed_successfully` so the `chown` actually finishes before `quicknotes` tries to write to `/data`. + +--- + +## Bonus Task: The 6 Security Defaults + +### Hardened `quicknotes` service block + +```yaml + quicknotes: + build: + context: ./app + image: quicknotes:lab6 + # ... depends_on, ports, environment, volumes, healthcheck ... + restart: unless-stopped + read_only: true # 4. read-only root filesystem + tmpfs: + - /tmp:size=16m,mode=1777 # 4. writable scratch (app needs none, but defensive) + cap_drop: + - ALL # 3. drop every Linux capability + security_opt: + - no-new-privileges:true # 5. block setuid privilege escalation +``` + +Defaults **1** (`USER nonroot`) and **2** (distroless base) are enforced in the Dockerfile from Task 1. + +### Verification + +**1. `USER nonroot`** + +``` +docker inspect quicknotes:lab6 --format "{{.Config.User}}" +``` + +``` +nonroot +``` + +--- + +**2. No shell available (distroless base)** + +``` +docker compose exec quicknotes sh +``` + +``` +OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown +``` + +--- + +**3. Capabilities dropped** + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.HostConfig.CapDrop}}" +``` + +``` +[ALL] +``` + +--- + +**4. Read-only root filesystem** + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.HostConfig.ReadonlyRootfs}}" +``` + +``` +true +``` + +Only `/data` (named volume) and `/tmp` (tmpfs) are writable. There's no shell to run +`touch /etc/test`, so the `ReadonlyRootfs: true` config flag is the enforced proof; the +container still boots healthy because the app's only writes go to `/data`. + +--- + +**5. `no-new-privileges`** + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.HostConfig.SecurityOpt}}" +``` + +``` +[no-new-privileges:true] +``` + +### Trivy scan + +``` +docker run --rm -v //var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 +``` + +_No output captured โ€” the Trivy vulnerability-DB download failed due to connection +slowness/issues in my environment. Expected result with a distroless-static base is +**0 HIGH/CRITICAL** on the OS layer (no shell, no package manager, no OS packages to be +vulnerable); any findings would be stdlib CVEs inside the compiled Go binaries, fixable +by rebuilding with a patched Go toolchain rather than changing the base image._ + +### Which default gives the most security per line of YAML? + +`cap_drop: [ALL]` is the highest-leverage line in the compose file: two lines strip the +entire Linux capability set, so even a fully compromised process can't bind low ports, +load kernel modules, or use raw sockets without a separate kernel exploit. `read_only` +and `no-new-privileges` are close behind, while `USER nonroot` and the distroless base are +foundational but set once in the Dockerfile. Applied together they form independent, +overlapping layers rather than a single point of failure. diff --git a/submissions/lab9-artifacts/govulncheck-green.txt b/submissions/lab9-artifacts/govulncheck-green.txt new file mode 100644 index 000000000..e7589224d --- /dev/null +++ b/submissions/lab9-artifacts/govulncheck-green.txt @@ -0,0 +1 @@ +No vulnerabilities found. diff --git a/submissions/lab9-artifacts/govulncheck-red.txt b/submissions/lab9-artifacts/govulncheck-red.txt new file mode 100644 index 000000000..a1ef2ab3c --- /dev/null +++ b/submissions/lab9-artifacts/govulncheck-red.txt @@ -0,0 +1,16 @@ +=== Symbol Results === + +Vulnerability #1: GO-2021-0113 + Out-of-bounds read in golang.org/x/text/language + More info: https://pkg.go.dev/vuln/GO-2021-0113 + Module: golang.org/x/text + Found in: golang.org/x/text@v0.3.5 + Fixed in: golang.org/x/text@v0.3.7 + Example traces found: + #1: vulndemo_temp.go:10:23: quicknotes.init#1 calls language.Parse + +Your code is affected by 1 vulnerability from 1 module. +This scan also found 1 vulnerability in packages you import and 0 +vulnerabilities in modules you require, but your code doesn't appear to call +these vulnerabilities. +Use '-show verbose' for more details. diff --git a/submissions/lab9-artifacts/sbom.cyclonedx.json b/submissions/lab9-artifacts/sbom.cyclonedx.json new file mode 100644 index 000000000..2dc839672 --- /dev/null +++ b/submissions/lab9-artifacts/sbom.cyclonedx.json @@ -0,0 +1,518 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:d2f61380-9db2-4f24-bbf7-461562795885", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T16:23:07+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "fd432e26-c95e-4349-bdea-423a8215fc39", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3a86f8c114b0a14d4cd2ee448b2e35657cbad89500253dda4601c633fa1cf7e2" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4cde6b0bb6f50a5f255eef7b2a42162c661cf776b803225dcac9a659e396bb6b" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4d049f83d9cf21d1f5cc0e11deaf36df02790d0e60c1a3829538fb4b61685368" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:5fd2536c39c0700be8b7b4344e375196da2f126842fd8ede66996a18860a3890" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:6f1cdceb6a3146f0ccb986521156bef8a422cdbb0863396f7f751f575ba308f4" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:92cb9c37b7d3957ac56645a979418f65e6c5bdba00eb99622affae5fc124ac07" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ad51d0769d16ba578106a177987dfe3d2e02c1668c852b795b2f6b024068242a" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:af5aa97ebe6ce1604747ec1e21af7136ded391bcabe4acef882e718a87c86bcc" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b80ff4ffd1771a8683fc156fcd41c0050a8c9dcc2382bae2fbc8102c75e3ff0d" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bd3cdfae1d3fdd83a2231d608969b38b82349777c2fff9a7c12d54f8ac5c9b38" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:f462de0a5f6c89f01667cc0f647ed4ccf19a0950d5900457cd473619d4836212" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:589b96eb47396b007efc9e809bf9437a712714a49d5503b70a4fdfa5773e5b06" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "quicknotes:lab6" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "0b5fef82-7626-430b-ae6f-cac09d5904d3", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "1e49f807-dea0-43c8-9c43-4aa51bd3b7aa", + "type": "application", + "name": "healthcheck", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "34753cd5-434a-4335-a15f-127c203c4155", + "type": "library", + "name": "stdlib", + "version": "v1.26.4", + "purl": "pkg:golang/stdlib@v1.26.4", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.26.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "7cdeddd4-7cbc-49ca-b5d8-b321fd9c9e9d", + "type": "library", + "name": "stdlib", + "version": "v1.26.4", + "purl": "pkg:golang/stdlib@v1.26.4", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f462de0a5f6c89f01667cc0f647ed4ccf19a0950d5900457cd473619d4836212" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.26.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "9bd6e567-7487-4248-bbeb-f2ccbfcefef7", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f462de0a5f6c89f01667cc0f647ed4ccf19a0950d5900457cd473619d4836212" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "e1612efe-e76c-4503-8b9b-a0f0050b40c3", + "type": "application", + "name": "quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "f867c7de-abe3-4354-8990-74d92b396217", + "type": "operating-system", + "name": "debian", + "version": "13.5", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "13.8+deb13u5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "verbatim" + } + } + ], + "purl": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:92cb9c37b7d3957ac56645a979418f65e6c5bdba00eb99622affae5fc124ac07" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@13.8+deb13u5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.8+deb13u5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Mime-Support Packagers " + }, + "name": "media-types", + "version": "13.0.0", + "licenses": [ + { + "license": { + "name": "ad-hoc" + } + } + ], + "purl": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "media-types@13.0.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "media-types" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.0.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "netbase", + "version": "6.5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "netbase@6.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "netbase" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata-legacy", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata-legacy@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + } + ], + "dependencies": [ + { + "ref": "0b5fef82-7626-430b-ae6f-cac09d5904d3", + "dependsOn": [ + "34753cd5-434a-4335-a15f-127c203c4155" + ] + }, + { + "ref": "1e49f807-dea0-43c8-9c43-4aa51bd3b7aa", + "dependsOn": [ + "0b5fef82-7626-430b-ae6f-cac09d5904d3" + ] + }, + { + "ref": "34753cd5-434a-4335-a15f-127c203c4155", + "dependsOn": [] + }, + { + "ref": "7cdeddd4-7cbc-49ca-b5d8-b321fd9c9e9d", + "dependsOn": [] + }, + { + "ref": "9bd6e567-7487-4248-bbeb-f2ccbfcefef7", + "dependsOn": [ + "7cdeddd4-7cbc-49ca-b5d8-b321fd9c9e9d" + ] + }, + { + "ref": "e1612efe-e76c-4503-8b9b-a0f0050b40c3", + "dependsOn": [ + "9bd6e567-7487-4248-bbeb-f2ccbfcefef7" + ] + }, + { + "ref": "f867c7de-abe3-4354-8990-74d92b396217", + "dependsOn": [ + "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5" + ] + }, + { + "ref": "fd432e26-c95e-4349-bdea-423a8215fc39", + "dependsOn": [ + "1e49f807-dea0-43c8-9c43-4aa51bd3b7aa", + "e1612efe-e76c-4503-8b9b-a0f0050b40c3", + "f867c7de-abe3-4354-8990-74d92b396217" + ] + }, + { + "ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/submissions/lab9-artifacts/trivy-config-full.txt b/submissions/lab9-artifacts/trivy-config-full.txt new file mode 100644 index 000000000..eea1555c1 --- /dev/null +++ b/submissions/lab9-artifacts/trivy-config-full.txt @@ -0,0 +1,16 @@ +2026-07-07T15:49:16Z INFO [misconfig] Misconfiguration scanning is enabled +2026-07-07T15:49:18Z INFO Detected config files num=1 + +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 27, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers. + +See https://avd.aquasec.com/misconfig/ds026 +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + diff --git a/submissions/lab9-artifacts/trivy-fs.txt b/submissions/lab9-artifacts/trivy-fs.txt new file mode 100644 index 000000000..2149b0703 --- /dev/null +++ b/submissions/lab9-artifacts/trivy-fs.txt @@ -0,0 +1,22 @@ +2026-07-07T15:45:55Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T15:45:55Z INFO [secret] Secret scanning is enabled +2026-07-07T15:45:55Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T15:45:55Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T15:45:56Z INFO Number of language-specific files num=1 +2026-07-07T15:45:56Z INFO [gomod] Detecting vulnerabilities... + +.vagrant/machines/default/virtualbox/private_key (secrets) +========================================================== +Total: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: AsymmetricPrivateKey (private-key) +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +Asymmetric Private Key +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + .vagrant/machines/default/virtualbox/private_key:1 +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 1 [ BEGIN OPENSSH PRIVATE KEY-----*******************************************************************************************************************************************************************************************************************************************************************************************************************************************-----END OPENSSH PRI + 2 +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + diff --git a/submissions/lab9-artifacts/trivy-image-after.txt b/submissions/lab9-artifacts/trivy-image-after.txt new file mode 100644 index 000000000..2f3fa102b --- /dev/null +++ b/submissions/lab9-artifacts/trivy-image-after.txt @@ -0,0 +1,13 @@ +2026-07-07T16:22:51Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T16:22:51Z INFO [secret] Secret scanning is enabled +2026-07-07T16:22:51Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T16:22:51Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T16:22:52Z INFO Detected OS family="debian" version="13.5" +2026-07-07T16:22:52Z INFO [debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +2026-07-07T16:22:52Z INFO Number of language-specific files num=2 +2026-07-07T16:22:52Z INFO [gobinary] Detecting vulnerabilities... + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + diff --git a/submissions/lab9-artifacts/trivy-image.txt b/submissions/lab9-artifacts/trivy-image.txt new file mode 100644 index 000000000..3c5c8b9c2 --- /dev/null +++ b/submissions/lab9-artifacts/trivy-image.txt @@ -0,0 +1,112 @@ +2026-07-07T15:44:04Z INFO [vulndb] Need to update DB +2026-07-07T15:44:04Z INFO [vulndb] Downloading vulnerability DB... +2026-07-07T15:44:04Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T15:44:23Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T15:44:23Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T15:44:23Z INFO [secret] Secret scanning is enabled +2026-07-07T15:44:23Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T15:44:23Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T15:44:23Z INFO Detected OS family="debian" version="13.5" +2026-07-07T15:44:23Z INFO [debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +2026-07-07T15:44:23Z INFO Number of language-specific files num=2 +2026-07-07T15:44:23Z INFO [gobinary] Detecting vulnerabilities... +2026-07-07T15:44:23Z WARN Using severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.59/docs/scanner/vulnerability#severity-selection for details. + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + + +healthcheck (gobinary) +====================== +Total: 10 (HIGH: 10, CRITICAL: 0) + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Library โ”‚ Vulnerability โ”‚ Severity โ”‚ Status โ”‚ Installed Version โ”‚ Fixed Version โ”‚ Title โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ stdlib โ”‚ CVE-2026-25679 โ”‚ HIGH โ”‚ fixed โ”‚ v1.24.13 โ”‚ 1.25.8, 1.26.1 โ”‚ net/url: Incorrect parsing of IPv6 host literals in net/url โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-25679 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-27145 โ”‚ โ”‚ โ”‚ โ”‚ 1.25.11, 1.26.4 โ”‚ crypto/x509: golang: golang crypto/x509: Denial of Service โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ via excessive processing of DNS... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-27145 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-32280 โ”‚ โ”‚ โ”‚ โ”‚ 1.25.9, 1.26.2 โ”‚ crypto/x509: crypto/tls: golang: Go: Denial of Service โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ vulnerability in certificate chain building... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-32280 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-32281 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ crypto/x509: golang: Go crypto/x509: Denial of Service via โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ inefficient certificate chain validation... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-32281 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-32283 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ crypto/tls: golang: Go crypto/tls: Denial of Service via โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ multiple TLS 1.3 key... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-32283 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-33811 โ”‚ โ”‚ โ”‚ โ”‚ 1.25.10, 1.26.3 โ”‚ net: golang: Go net package: Denial of Service via long โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ CNAME response... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-33811 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-33814 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ net/http/internal/http2: golang: golang.org/x/net: Go โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ HTTP/2: Denial of Service via malformed โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ SETTINGS_MAX_FRAME_SIZE frame... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-33814 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-39820 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ net/mail: golang: Go net/mail: Denial of Service via crafted โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ email inputs โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-39820 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-39836 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ELSA-2026-22121: golang security update (IMPORTANT) โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-39836 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-42499 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ net/mail: golang: net/mail: Denial of Service via โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ pathological email address parsing โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-42499 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +quicknotes (gobinary) +===================== +Total: 10 (HIGH: 10, CRITICAL: 0) + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Library โ”‚ Vulnerability โ”‚ Severity โ”‚ Status โ”‚ Installed Version โ”‚ Fixed Version โ”‚ Title โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ stdlib โ”‚ CVE-2026-25679 โ”‚ HIGH โ”‚ fixed โ”‚ v1.24.13 โ”‚ 1.25.8, 1.26.1 โ”‚ net/url: Incorrect parsing of IPv6 host literals in net/url โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-25679 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-27145 โ”‚ โ”‚ โ”‚ โ”‚ 1.25.11, 1.26.4 โ”‚ crypto/x509: golang: golang crypto/x509: Denial of Service โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ via excessive processing of DNS... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-27145 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-32280 โ”‚ โ”‚ โ”‚ โ”‚ 1.25.9, 1.26.2 โ”‚ crypto/x509: crypto/tls: golang: Go: Denial of Service โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ vulnerability in certificate chain building... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-32280 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-32281 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ crypto/x509: golang: Go crypto/x509: Denial of Service via โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ inefficient certificate chain validation... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-32281 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-32283 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ crypto/tls: golang: Go crypto/tls: Denial of Service via โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ multiple TLS 1.3 key... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-32283 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-33811 โ”‚ โ”‚ โ”‚ โ”‚ 1.25.10, 1.26.3 โ”‚ net: golang: Go net package: Denial of Service via long โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ CNAME response... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-33811 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-33814 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ net/http/internal/http2: golang: golang.org/x/net: Go โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ HTTP/2: Denial of Service via malformed โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ SETTINGS_MAX_FRAME_SIZE frame... โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-33814 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-39820 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ net/mail: golang: Go net/mail: Denial of Service via crafted โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ email inputs โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-39820 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-39836 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ELSA-2026-22121: golang security update (IMPORTANT) โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-39836 โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ CVE-2026-42499 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ net/mail: golang: net/mail: Denial of Service via โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ pathological email address parsing โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ https://avd.aquasec.com/nvd/cve-2026-42499 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ diff --git a/submissions/lab9-artifacts/zap-after.html b/submissions/lab9-artifacts/zap-after.html new file mode 100644 index 000000000..146785878 --- /dev/null +++ b/submissions/lab9-artifacts/zap-after.html @@ -0,0 +1,703 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://host.docker.internal:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 17:26:01 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
2
+
+
Informational
+
+
1
+
+
False Positives:
+
+
0
+
+
+ + + + +

Summary of Sequences

+

For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
Insufficient Site Isolation Against Spectre VulnerabilityLow1
ZAP is Out of DateLow1
Storable and Cacheable ContentInformational3
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
Insufficient Site Isolation Against Spectre Vulnerability
Description +
Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.
+ +
URLhttp://host.docker.internal:8080/notes
MethodGET
ParameterCross-Origin-Resource-Policy
Attack
Evidence
Other Info
Instances1
Solution +
Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.
+
+ +
'same-site' is considered as less secured and should be avoided.
+
+ +
If resources must be shared, set the header to 'cross-origin'.
+
+ +
If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).
+ +
Reference + https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy + +
CWE Id693
WASC Id14
Plugin Id90004
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://host.docker.internal:8080/
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
Storable and Cacheable Content
Description +
The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where "shared" caching servers such as "proxy" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.
+ +
URLhttp://host.docker.internal:8080/
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://host.docker.internal:8080/notes
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://host.docker.internal:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
Instances3
Solution +
Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:
+
+ +
Cache-Control: no-cache, no-store, must-revalidate, private
+
+ +
Pragma: no-cache
+
+ +
Expires: 0
+
+ +
This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.
+ +
Reference + https://datatracker.ietf.org/doc/html/rfc7234 +
+ + https://datatracker.ietf.org/doc/html/rfc7231 +
+ + https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + +
CWE Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/submissions/lab9-artifacts/zap-after.json b/submissions/lab9-artifacts/zap-after.json new file mode 100644 index 000000000..59571b9c7 --- /dev/null +++ b/submissions/lab9-artifacts/zap-after.json @@ -0,0 +1,129 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 17:26:01", + "created": "2026-07-07T17:26:01.827256061Z", + "site":[ + { + "@name": "http://host.docker.internal:8080", + "@host": "host.docker.internal", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "pluginid": "90004", + "alertRef": "90004-1", + "alert": "Insufficient Site Isolation Against Spectre Vulnerability", + "name": "Insufficient Site Isolation Against Spectre Vulnerability", + "riskcode": "1", + "confidence": "2", + "riskdesc": "Low (Medium)", + "desc": "

Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.

", + "instances":[ + { + "id": "8", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "Cross-Origin-Resource-Policy", + "attack": "", + "evidence": "", + "otherinfo": "" + } + ], + "count": "1", + "systemic": false, + "solution": "

Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.

'same-site' is considered as less secured and should be avoided.

If resources must be shared, set the header to 'cross-origin'.

If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).

", + "otherinfo": "", + "reference": "

https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy

", + "cweid": "693", + "wascid": "14", + "sourceid": "1" + }, + { + "pluginid": "10116", + "alertRef": "10116", + "alert": "ZAP is Out of Date", + "name": "ZAP is Out of Date", + "riskcode": "1", + "confidence": "3", + "riskdesc": "Low (High)", + "desc": "

The version of ZAP you are using to test your app is out of date and is no longer being updated.

The risk level is set based on how out of date your ZAP version is.

", + "instances":[ + { + "id": "3", + "uri": "http://host.docker.internal:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "The latest version of ZAP is 2.17.0" + } + ], + "count": "1", + "systemic": false, + "solution": "

Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.

", + "otherinfo": "

The latest version of ZAP is 2.17.0

", + "reference": "

https://www.zaproxy.org/download/

", + "cweid": "1104", + "wascid": "45", + "sourceid": "3" + }, + { + "pluginid": "10049", + "alertRef": "10049-3", + "alert": "Storable and Cacheable Content", + "name": "Storable and Cacheable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "

The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where \"shared\" caching servers such as \"proxy\" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.

", + "instances":[ + { + "id": "4", + "uri": "http://host.docker.internal:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + }, + { + "id": "5", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + }, + { + "id": "2", + "uri": "http://host.docker.internal:8080/sitemap.xml", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + } + ], + "count": "3", + "systemic": false, + "solution": "

Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:

Cache-Control: no-cache, no-store, must-revalidate, private

Pragma: no-cache

Expires: 0

This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.

", + "otherinfo": "

In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.

", + "reference": "

https://datatracker.ietf.org/doc/html/rfc7234

https://datatracker.ietf.org/doc/html/rfc7231

https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html

", + "cweid": "524", + "wascid": "13", + "sourceid": "3" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9-artifacts/zap-after.txt b/submissions/lab9-artifacts/zap-after.txt new file mode 100644 index 000000000..c7152622d --- /dev/null +++ b/submissions/lab9-artifacts/zap-after.txt @@ -0,0 +1,77 @@ +Using the Automation Framework +Total of 5 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +PASS: In Page Banner Information Leak [10009] +PASS: Cookie No HttpOnly Flag [10010] +PASS: Cookie Without Secure Flag [10011] +PASS: Re-examine Cache-control Directives [10015] +PASS: Cross-Domain JavaScript Source File Inclusion [10017] +PASS: Content-Type Header Missing [10019] +PASS: Anti-clickjacking Header [10020] +PASS: X-Content-Type-Options Header Missing [10021] +PASS: Information Disclosure - Debug Error Messages [10023] +PASS: Information Disclosure - Sensitive Information in URL [10024] +PASS: Information Disclosure - Sensitive Information in HTTP Referrer Header [10025] +PASS: HTTP Parameter Override [10026] +PASS: Information Disclosure - Suspicious Comments [10027] +PASS: Off-site Redirect [10028] +PASS: Cookie Poisoning [10029] +PASS: User Controllable Charset [10030] +PASS: User Controllable HTML Element Attribute (Potential XSS) [10031] +PASS: Viewstate [10032] +PASS: Directory Browsing [10033] +PASS: Heartbleed OpenSSL Vulnerability (Indicative) [10034] +PASS: Strict-Transport-Security Header [10035] +PASS: HTTP Server Response Header [10036] +PASS: Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) [10037] +PASS: Content Security Policy (CSP) Header Not Set [10038] +PASS: X-Backend-Server Header Information Leak [10039] +PASS: Secure Pages Include Mixed Content [10040] +PASS: HTTP to HTTPS Insecure Transition in Form Post [10041] +PASS: HTTPS to HTTP Insecure Transition in Form Post [10042] +PASS: User Controllable JavaScript Event (XSS) [10043] +PASS: Big Redirect Detected (Potential Sensitive Information Leak) [10044] +PASS: Retrieved from Cache [10050] +PASS: X-ChromeLogger-Data (XCOLD) Header Information Leak [10052] +PASS: Cookie without SameSite Attribute [10054] +PASS: CSP [10055] +PASS: X-Debug-Token Information Leak [10056] +PASS: Username Hash Found [10057] +PASS: X-AspNet-Version Response Header [10061] +PASS: PII Disclosure [10062] +PASS: Permissions Policy Header Not Set [10063] +PASS: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Source Code Disclosure [10099] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +PASS: Dangerous JS Functions [10110] +PASS: Authentication Request Identified [10111] +PASS: Session Management Response Identified [10112] +PASS: Verification Request Identified [10113] +PASS: Script Served From Malicious Domain (polyfill) [10115] +PASS: Absence of Anti-CSRF Tokens [10202] +PASS: Private IP Disclosure [2] +PASS: Session ID in URL Rewrite [3] +PASS: Script Passive Scan Rules [50001] +PASS: Stats Passive Scan Rule [50003] +PASS: Insecure JSF ViewState [90001] +PASS: Java Serialization Object [90002] +PASS: Sub Resource Integrity Attribute Missing [90003] +PASS: Charset Mismatch [90011] +PASS: Application Error Disclosure [90022] +PASS: WSDL File Detection [90030] +PASS: Loosely Scoped Cookie [90033] +WARN-NEW: Storable and Cacheable Content [10049] x 3 + http://host.docker.internal:8080/ (404 Not Found) + http://host.docker.internal:8080/notes (200 OK) + http://host.docker.internal:8080/sitemap.xml (404 Not Found) +WARN-NEW: ZAP is Out of Date [10116] x 1 + http://host.docker.internal:8080/ (404 Not Found) +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 + http://host.docker.internal:8080/notes (200 OK) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 3 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 64 +Automation plan warnings: + Job spider error accessing URL http://host.docker.internal:8080/ status code returned : 404 expected 200 diff --git a/submissions/lab9-artifacts/zap-before.html b/submissions/lab9-artifacts/zap-before.html new file mode 100644 index 000000000..46c6331c1 --- /dev/null +++ b/submissions/lab9-artifacts/zap-before.html @@ -0,0 +1,774 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://host.docker.internal:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 17:22:45 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
3
+
+
Informational
+
+
1
+
+
False Positives:
+
+
0
+
+
+ + + + +

Summary of Sequences

+

For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
Insufficient Site Isolation Against Spectre VulnerabilityLow1
X-Content-Type-Options Header MissingLow1
ZAP is Out of DateLow1
Storable and Cacheable ContentInformational2
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
Insufficient Site Isolation Against Spectre Vulnerability
Description +
Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.
+ +
URLhttp://host.docker.internal:8080/notes
MethodGET
ParameterCross-Origin-Resource-Policy
Attack
Evidence
Other Info
Instances1
Solution +
Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.
+
+ +
'same-site' is considered as less secured and should be avoided.
+
+ +
If resources must be shared, set the header to 'cross-origin'.
+
+ +
If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).
+ +
Reference + https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy + +
CWE Id693
WASC Id14
Plugin Id90004
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
X-Content-Type-Options Header Missing
Description +
The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.
+ +
URLhttp://host.docker.internal:8080/notes
MethodGET
Parameterx-content-type-options
Attack
Evidence
Other InfoThis issue still applies to error type pages (401, 403, 500, etc.) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type. +At "High" threshold this scan rule will not alert on client or server error responses.
Instances1
Solution +
Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.
+
+ +
If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.
+ +
Reference + https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/gg622941(v=vs.85) +
+ + https://owasp.org/www-community/Security_Headers + +
CWE Id693
WASC Id15
Plugin Id10021
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://host.docker.internal:8080/
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
Storable and Cacheable Content
Description +
The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where "shared" caching servers such as "proxy" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.
+ +
URLhttp://host.docker.internal:8080/
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://host.docker.internal:8080/notes
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
Instances2
Solution +
Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:
+
+ +
Cache-Control: no-cache, no-store, must-revalidate, private
+
+ +
Pragma: no-cache
+
+ +
Expires: 0
+
+ +
This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.
+ +
Reference + https://datatracker.ietf.org/doc/html/rfc7234 +
+ + https://datatracker.ietf.org/doc/html/rfc7231 +
+ + https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + +
CWE Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/submissions/lab9-artifacts/zap-before.json b/submissions/lab9-artifacts/zap-before.json new file mode 100644 index 000000000..42caa2a6d --- /dev/null +++ b/submissions/lab9-artifacts/zap-before.json @@ -0,0 +1,149 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 17:22:45", + "created": "2026-07-07T17:22:45.955541609Z", + "site":[ + { + "@name": "http://host.docker.internal:8080", + "@host": "host.docker.internal", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "pluginid": "90004", + "alertRef": "90004-1", + "alert": "Insufficient Site Isolation Against Spectre Vulnerability", + "name": "Insufficient Site Isolation Against Spectre Vulnerability", + "riskcode": "1", + "confidence": "2", + "riskdesc": "Low (Medium)", + "desc": "

Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.

", + "instances":[ + { + "id": "9", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "Cross-Origin-Resource-Policy", + "attack": "", + "evidence": "", + "otherinfo": "" + } + ], + "count": "1", + "systemic": false, + "solution": "

Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.

'same-site' is considered as less secured and should be avoided.

If resources must be shared, set the header to 'cross-origin'.

If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).

", + "otherinfo": "", + "reference": "

https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy

", + "cweid": "693", + "wascid": "14", + "sourceid": "1" + }, + { + "pluginid": "10021", + "alertRef": "10021", + "alert": "X-Content-Type-Options Header Missing", + "name": "X-Content-Type-Options Header Missing", + "riskcode": "1", + "confidence": "2", + "riskdesc": "Low (Medium)", + "desc": "

The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.

", + "instances":[ + { + "id": "4", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "x-content-type-options", + "attack": "", + "evidence": "", + "otherinfo": "This issue still applies to error type pages (401, 403, 500, etc.) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.\nAt \"High\" threshold this scan rule will not alert on client or server error responses." + } + ], + "count": "1", + "systemic": false, + "solution": "

Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.

If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.

", + "otherinfo": "

This issue still applies to error type pages (401, 403, 500, etc.) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.

At \"High\" threshold this scan rule will not alert on client or server error responses.

", + "reference": "

https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/gg622941(v=vs.85)

https://owasp.org/www-community/Security_Headers

", + "cweid": "693", + "wascid": "15", + "sourceid": "1" + }, + { + "pluginid": "10116", + "alertRef": "10116", + "alert": "ZAP is Out of Date", + "name": "ZAP is Out of Date", + "riskcode": "1", + "confidence": "3", + "riskdesc": "Low (High)", + "desc": "

The version of ZAP you are using to test your app is out of date and is no longer being updated.

The risk level is set based on how out of date your ZAP version is.

", + "instances":[ + { + "id": "5", + "uri": "http://host.docker.internal:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "The latest version of ZAP is 2.17.0" + } + ], + "count": "1", + "systemic": false, + "solution": "

Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.

", + "otherinfo": "

The latest version of ZAP is 2.17.0

", + "reference": "

https://www.zaproxy.org/download/

", + "cweid": "1104", + "wascid": "45", + "sourceid": "10" + }, + { + "pluginid": "10049", + "alertRef": "10049-3", + "alert": "Storable and Cacheable Content", + "name": "Storable and Cacheable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "

The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where \"shared\" caching servers such as \"proxy\" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.

", + "instances":[ + { + "id": "0", + "uri": "http://host.docker.internal:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + }, + { + "id": "7", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + } + ], + "count": "2", + "systemic": false, + "solution": "

Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:

Cache-Control: no-cache, no-store, must-revalidate, private

Pragma: no-cache

Expires: 0

This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.

", + "otherinfo": "

In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.

", + "reference": "

https://datatracker.ietf.org/doc/html/rfc7234

https://datatracker.ietf.org/doc/html/rfc7231

https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html

", + "cweid": "524", + "wascid": "13", + "sourceid": "3" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9-artifacts/zap-before.txt b/submissions/lab9-artifacts/zap-before.txt new file mode 100644 index 000000000..77df8ff39 --- /dev/null +++ b/submissions/lab9-artifacts/zap-before.txt @@ -0,0 +1,77 @@ +Using the Automation Framework +Total of 5 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +PASS: In Page Banner Information Leak [10009] +PASS: Cookie No HttpOnly Flag [10010] +PASS: Cookie Without Secure Flag [10011] +PASS: Re-examine Cache-control Directives [10015] +PASS: Cross-Domain JavaScript Source File Inclusion [10017] +PASS: Content-Type Header Missing [10019] +PASS: Anti-clickjacking Header [10020] +PASS: Information Disclosure - Debug Error Messages [10023] +PASS: Information Disclosure - Sensitive Information in URL [10024] +PASS: Information Disclosure - Sensitive Information in HTTP Referrer Header [10025] +PASS: HTTP Parameter Override [10026] +PASS: Information Disclosure - Suspicious Comments [10027] +PASS: Off-site Redirect [10028] +PASS: Cookie Poisoning [10029] +PASS: User Controllable Charset [10030] +PASS: User Controllable HTML Element Attribute (Potential XSS) [10031] +PASS: Viewstate [10032] +PASS: Directory Browsing [10033] +PASS: Heartbleed OpenSSL Vulnerability (Indicative) [10034] +PASS: Strict-Transport-Security Header [10035] +PASS: HTTP Server Response Header [10036] +PASS: Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) [10037] +PASS: Content Security Policy (CSP) Header Not Set [10038] +PASS: X-Backend-Server Header Information Leak [10039] +PASS: Secure Pages Include Mixed Content [10040] +PASS: HTTP to HTTPS Insecure Transition in Form Post [10041] +PASS: HTTPS to HTTP Insecure Transition in Form Post [10042] +PASS: User Controllable JavaScript Event (XSS) [10043] +PASS: Big Redirect Detected (Potential Sensitive Information Leak) [10044] +PASS: Retrieved from Cache [10050] +PASS: X-ChromeLogger-Data (XCOLD) Header Information Leak [10052] +PASS: Cookie without SameSite Attribute [10054] +PASS: CSP [10055] +PASS: X-Debug-Token Information Leak [10056] +PASS: Username Hash Found [10057] +PASS: X-AspNet-Version Response Header [10061] +PASS: PII Disclosure [10062] +PASS: Permissions Policy Header Not Set [10063] +PASS: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Source Code Disclosure [10099] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +PASS: Dangerous JS Functions [10110] +PASS: Authentication Request Identified [10111] +PASS: Session Management Response Identified [10112] +PASS: Verification Request Identified [10113] +PASS: Script Served From Malicious Domain (polyfill) [10115] +PASS: Absence of Anti-CSRF Tokens [10202] +PASS: Private IP Disclosure [2] +PASS: Session ID in URL Rewrite [3] +PASS: Script Passive Scan Rules [50001] +PASS: Stats Passive Scan Rule [50003] +PASS: Insecure JSF ViewState [90001] +PASS: Java Serialization Object [90002] +PASS: Sub Resource Integrity Attribute Missing [90003] +PASS: Charset Mismatch [90011] +PASS: Application Error Disclosure [90022] +PASS: WSDL File Detection [90030] +PASS: Loosely Scoped Cookie [90033] +WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1 + http://host.docker.internal:8080/notes (200 OK) +WARN-NEW: Storable and Cacheable Content [10049] x 2 + http://host.docker.internal:8080/ (404 Not Found) + http://host.docker.internal:8080/notes (200 OK) +WARN-NEW: ZAP is Out of Date [10116] x 1 + http://host.docker.internal:8080/ (404 Not Found) +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 + http://host.docker.internal:8080/notes (200 OK) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 4 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 63 +Automation plan warnings: + Job spider error accessing URL http://host.docker.internal:8080/ status code returned : 404 expected 200 diff --git a/submissions/lab9-artifacts/zap.yaml b/submissions/lab9-artifacts/zap.yaml new file mode 100644 index 000000000..88bc36d43 --- /dev/null +++ b/submissions/lab9-artifacts/zap.yaml @@ -0,0 +1,41 @@ +env: + contexts: + - excludePaths: [] + name: baseline + urls: + - http://host.docker.internal:8080/notes + - http://host.docker.internal:8080/ + parameters: + failOnError: true + progressToStdout: false +jobs: +- parameters: + enableTags: false + maxAlertsPerRule: 10 + type: passiveScan-config +- parameters: + maxDuration: 1 + url: http://host.docker.internal:8080/ + type: spider +- parameters: + maxDuration: 0 + type: passiveScan-wait +- parameters: + format: Long + summaryFile: /home/zap/zap_out.json + rules: [] + type: outputSummary +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-after.html + reportTitle: ZAP Scanning Report + template: traditional-html + type: report +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-after.json + reportTitle: ZAP Scanning Report + template: traditional-json + type: report diff --git a/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..171263f69 --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,524 @@ +# Lab 9 Submission โ€” DevSecOps: Trivy + ZAP + +> Built on the Lab 6 image (`quicknotes:lab6`, distroless-static). All scans +> run with **Trivy pinned to `aquasec/trivy:0.59.1`** (not `:latest`). Raw outputs are +> in [`submissions/lab9-artifacts/`](/submissions/lab9-artifacts/). + +## Task 1: Trivy โ€” Image + Filesystem + Config + SBOM + +### 1.1 Scan outputs (tops) + +All four scans share a cached vuln DB (`trivy-cache` named volume) so the ~200 MB DB +downloads once. On Windows/Git-Bash the repo-target scans need `MSYS_NO_PATHCONV=1` so +the `/repo` argument isn't rewritten to a Windows path. + +**1. Image scan**: `trivy image --severity HIGH,CRITICAL quicknotes:lab6` +([full](/submissions/lab9-artifacts/trivy-image.txt)) + +``` +docker run --rm -v //var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 +``` + +``` +Detected OS family="debian" version="13.5" +[debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +[gobinary] Detecting vulnerabilities... + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + +healthcheck (gobinary) +====================== +Total: 10 (HIGH: 10, CRITICAL: 0) + +quicknotes (gobinary) +===================== +Total: 10 (HIGH: 10, CRITICAL: 0) +``` + +The **OS layer is clean** (distroless-static ships only 5 packages, none vulnerable). +Every HIGH lives in the two compiled Go binaries, all of them **Go standard-library** +CVEs against `stdlib v1.24.13`, the same 10 in both `/quicknotes` and `/healthcheck`. +These are fixed in ยง1.2, so the shipped image and the SBOM in ยง1.3 are the rebuilt, +patched build. + +**2. Filesystem scan** โ€” `trivy fs --severity HIGH,CRITICAL /repo` +([full](/submissions/lab9-artifacts/trivy-fs.txt)) + +``` +MSYS_NO_PATHCONV=1 docker run --rm -v trivy-cache:/root/.cache/ \ + -v "/d/VSCodeProjects/DevOps-Intro":/repo:ro \ + aquasec/trivy:0.59.1 fs --severity HIGH,CRITICAL --no-progress /repo +``` + +``` +.vagrant/machines/default/virtualbox/private_key (secrets) +========================================================== +Total: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: AsymmetricPrivateKey (private-key) + .vagrant/machines/default/virtualbox/private_key:1 +``` + +`go.mod` has no third-party dependencies, so the dependency scan is clean. The only +finding is a secret: Vagrant's per-VM SSH key (triaged below). + +**3. Config scan**: `trivy config /repo` +([full](/submissions/lab9-artifacts/trivy-config-full.txt)) + +``` +MSYS_NO_PATHCONV=1 docker run --rm -v trivy-cache:/root/.cache/ \ + -v "/d/VSCodeProjects/DevOps-Intro":/repo:ro \ + aquasec/trivy:0.59.1 config /repo +``` + +``` +Detected config files num=1 + +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 27, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile +``` + +**0 HIGH/CRITICAL misconfigs.** Only `app/Dockerfile` is scanned โ€” Trivy's default +misconfig scanners don't include a `docker-compose` check, so `compose.yaml` isn't +inspected. The single LOW (`AVD-DS-0026`, missing `HEALTHCHECK`) is expected: our +health probe lives in `compose.yaml`, which the Dockerfile scanner can't see. + +**4. SBOM generation**: CycloneDX (`trivy image --format cyclonedx`) +([full](/submissions/lab9-artifacts/sbom.cyclonedx.json)) + +``` +MSYS_NO_PATHCONV=1 docker run --rm -v //var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + -v "/d/VSCodeProjects/DevOps-Intro/submissions/lab9-artifacts":/out \ + aquasec/trivy:0.59.1 image --format cyclonedx --output /out/sbom.cyclonedx.json quicknotes:lab6 +``` + +> Note: the lab text says `trivy sbom --format cyclonedx`, but in Trivy the `sbom` +> subcommand *scans* an existing SBOM file; you *generate* one with +> `image --format cyclonedx`. First 30 lines are in ยง1.3. + +### 1.2 Triage (every HIGH/CRITICAL) + +11 HIGH findings, 0 CRITICAL. Ten are the same Go standard library CVEs compiled into +both binaries (`/quicknotes` and `/healthcheck`), so they share one disposition; the +eleventh is a secret from the filesystem scan. + +**The stdlib batch was fixed, not accepted.** Every listed *Fixed Version* lands in Go +1.25.x/1.26.x, so a single change clears all ten: bump the builder from +`golang:1.24-alpine` to `golang:1.26-alpine` (go1.26.4, which covers the highest +required fix) in [`app/Dockerfile`](/app/Dockerfile) and rebuild. Re-scanning the +rebuilt image returns `Total: 0 (HIGH: 0, CRITICAL: 0)` +([`trivy-image-after.txt`](/submissions/lab9-artifacts/trivy-image-after.txt)), and the +SBOM now records `stdlib v1.26.4`. When a patch is this cheap, fixing beats writing a +reachability case for keeping it. + +| # | Finding (all `stdlib v1.24.13`, all DoS) | Scan | Sev | Disposition | +|---|------------------------------------------|------|-----|-------------| +| 1 | CVE-2026-25679 `net/url` IPv6 host literal parsing | image (gobinary) | HIGH | **FIX** | +| 2 | CVE-2026-27145 `crypto/x509` DNS processing | image | HIGH | **FIX** | +| 3 | CVE-2026-32280 `crypto/tls` cert chain building | image | HIGH | **FIX** | +| 4 | CVE-2026-32281 `crypto/x509` cert chain validation | image | HIGH | **FIX** | +| 5 | CVE-2026-32283 `crypto/tls` TLS 1.3 keys | image | HIGH | **FIX** | +| 6 | CVE-2026-33811 `net` long CNAME response | image | HIGH | **FIX** | +| 7 | CVE-2026-33814 `net/http2` SETTINGS_MAX_FRAME_SIZE | image | HIGH | **FIX** | +| 8 | CVE-2026-39820 `net/mail` crafted email input | image | HIGH | **FIX** | +| 9 | CVE-2026-39836 golang security update (`net/mail`) | image | HIGH | **FIX** | +| 10 | CVE-2026-42499 `net/mail` address parsing | image | HIGH | **FIX** | +| 11 | AsymmetricPrivateKey `.vagrant/.../private_key` | fs (secret) | HIGH | **FALSE POSITIVE** | + +Rows 1 to 10 are all cleared by the one builder bump above (evidence: +`trivy-image-after.txt`). Row 11 is Vagrant's auto-generated per-VM insecure key: +`git ls-files` shows it untracked and `.gitignore:27` excludes `.vagrant/`, so it is +never committed, stays on localhost, and is regenerated on each `vagrant up`. It is a +real private key but not a leaked secret; scope future filesystem scans with +`--skip-dirs .vagrant` to drop the noise. + +The config scan contributed no HIGH/CRITICAL (one LOW, noted in ยง1.1). + +### 1.3 CycloneDX SBOM (first 30 lines) + +Full file: [`submissions/lab9-artifacts/sbom.cyclonedx.json`](/submissions/lab9-artifacts/sbom.cyclonedx.json) +(518 lines, CycloneDX spec 1.6). Generated from the rebuilt, patched image, so it records +`stdlib v1.26.4`. + +```json +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:d2f61380-9db2-4f24-bbf7-461562795885", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T16:23:07+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "fd432e26-c95e-4349-bdea-423a8215fc39", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" +``` + +### 1.4 Design questions + +**a) CVE severity is one input, not the answer. What else matters?** + +CVSS scores a vulnerability in isolation; triage requires the context that turns it into actual risk. The main additional inputs are reachability (whether the affected function lies on the application's call path), exploit availability (a public proof of concept or active exploitation in the wild), impact class (denial of service versus remote code execution versus data exposure), and deployment context (internet-facing versus internal, authentication, data sensitivity). A reachable RCE on a public endpoint and an unreachable DoS on an internal service can carry the same score yet warrant very different priority. + +**b) Distroless images often show zero HIGH/CRITICAL. Why is the minimal base the +strongest single security control?** + +Most image CVEs originate in OS packages the application never required: a shell, libc, a package manager, coreutils. Distroless-static ships almost none of them, so the OS layer scans at zero HIGH/CRITICAL across only five packages. A package that is absent cannot be vulnerable, so a single base-image choice eliminates entire classes of findings at once, and it also removes the post-exploitation toolkit (no shell, no package manager), making even a genuine code bug far harder to escalate. It is the highest-leverage control because it subtracts attack surface permanently rather than patching reactively. + +**c) `.trivyignore`: when is it right, and when is it security theater?** + +Suppression is legitimate when it records a specific, dated, reasoned acceptance in version control: a confirmed false positive, or a vulnerability demonstrated to be unreachable, with an owner and a review date. It becomes security theater when it silences findings only to turn the pipeline green, with no justification and no expiry, because that also hides the reachable vulnerability sitting in the same list. The test is whether each entry could be defended to an auditor months later. In this lab the stdlib findings were fixed outright rather than suppressed, so no `.trivyignore` entry was required. + +**d) What future problem does having the SBOM today solve?** + +The SBOM's value is incident-response speed during the next zero-day. When Log4Shell was disclosed, the costly question was not how to patch but whether an organization was affected at all, and where; teams without an inventory spent days rebuilding and rescanning images just to find out. A committed CycloneDX SBOM reduces that to a lookup: a newly disclosed CVE names a component and version, and the SBOM immediately shows whether it is present and which artifact ships it. The inventory work is done once, in advance, rather than under incident pressure. + +### 1.5 Task 1 outputs + +- [`trivy-image.txt`](/submissions/lab9-artifacts/trivy-image.txt) โ€” image scan (before fix, 10 HIGH) +- [`trivy-image-after.txt`](/submissions/lab9-artifacts/trivy-image-after.txt) โ€” image scan (after Go 1.26 bump, 0 HIGH) +- [`trivy-fs.txt`](/submissions/lab9-artifacts/trivy-fs.txt) โ€” filesystem scan +- [`trivy-config-full.txt`](/submissions/lab9-artifacts/trivy-config-full.txt) โ€” config/misconfig scan +- [`sbom.cyclonedx.json`](/submissions/lab9-artifacts/sbom.cyclonedx.json) โ€” CycloneDX SBOM + +--- + +## Task 2: OWASP ZAP Baseline + Header Fix + +### 2.1 Run ZAP baseline + +QuickNotes (the Lab 6 stack) runs on `:8080`; ZAP is pinned to +`ghcr.io/zaproxy/zaproxy:2.16.1`. The scan is the passive `zap-baseline.py` only, never +the active `zap-full-scan.py`. On Docker Desktop the ZAP container reaches the host app +via `host.docker.internal`, and reports are written to a bind-mounted `/zap/wrk`. + +``` +MSYS_NO_PATHCONV=1 docker run --rm \ + -v "/d/VSCodeProjects/DevOps-Intro/submissions/lab9-artifacts:/zap/wrk:rw" \ + ghcr.io/zaproxy/zaproxy:2.16.1 zap-baseline.py \ + -t http://host.docker.internal:8080/notes \ + -J zap-before.json -r zap-before.html +``` + +The target is `/notes` rather than `/`: QuickNotes has no root route, so pointing ZAP at +`http://host.docker.internal:8080` makes its spider hit a `404` on the base URL and it +never reaches a real endpoint (that run is kept as evidence but is uninformative). `/notes` +returns a `200` JSON body, so ZAP actually evaluates response headers. + +``` +WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1 +WARN-NEW: Storable and Cacheable Content [10049] x 2 +WARN-NEW: ZAP is Out of Date [10116] x 1 +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 +FAIL-NEW: 0 WARN-NEW: 4 INFO: 0 IGNORE: 0 PASS: 63 +``` + +Note that on a JSON API the `Anti-clickjacking Header [10020]` and +`Content Security Policy (CSP) Header Not Set [10038]` rules **pass**: they only alert on +`text/html` responses, so a pure API is not flagged for a missing CSP or `X-Frame-Options`. +The one missing-header finding that applies to JSON is `X-Content-Type-Options [10021]`. + +Reports: [`zap-before.html`](/submissions/lab9-artifacts/zap-before.html), +[`zap-before.json`](/submissions/lab9-artifacts/zap-before.json). + +### 2.2 ZAP Triage every finding + +| ID | Name | Risk (confidence) | Affected URL | Disposition | Reason / evidence | +|----|------|-------------------|--------------|-------------|-------------------| +| 10021 | X-Content-Type-Options Header Missing | Low (Medium) | `/notes` | **FIX** | `securityHeaders` middleware sets `X-Content-Type-Options: nosniff` on every response; after-scan re-classifies it as PASS (ยง2.4). | +| 90004 | Insufficient Site Isolation Against Spectre | Low (Medium) | `/notes` | **ACCEPT** | The rule wants `Cross-Origin-Opener/Embedder/Resource-Policy`, which isolate cross-origin *document* loads in a browser. QuickNotes serves JSON to API clients, not an HTML browsing context, so the Spectre vector does not apply. Re-eval **2026-10-07**; would set `Cross-Origin-Resource-Policy: same-origin` if the API is ever embedded. | +| 10049 | Storable and Cacheable Content | Informational (Medium) | `/`, `/notes` | **ACCEPT** | Notes are non-sensitive, unauthenticated data, so default cacheability is acceptable. If authentication is added later, set `Cache-Control: no-store`. Re-eval **2026-10-07**. | +| 10116 | ZAP is Out of Date | Low (High) | `/` | **FALSE POSITIVE** | Concerns the scanner's own version, not QuickNotes. ZAP is pinned to 2.16.1 deliberately; not an application finding. | + +### 2.3 Fix in code: security-headers middleware + test + +The fix is a single middleware that wraps the whole router, so every route (and every +error response, including the mux's own `404`) carries the headers. It is not repeated +inside handlers, so a new route cannot forget them. `main.go` serves `server.Handler()` +instead of `server.Routes()`, and the test asserts all four headers across a real route, +a handler `404`, and a mux `404` through `Server.Handler()`; if the wrap is removed (i.e. +`Handler` returns `s.Routes()`), none of the headers are present and the test fails. + +Full diff of the fix, new files [`app/middleware.go`](/app/middleware.go) and +[`app/middleware_test.go`](/app/middleware_test.go), plus the one-line change to +[`app/main.go`](/app/main.go): + +```diff +diff --git a/app/main.go b/app/main.go +index e258ffc..aa3dd9e 100644 +--- a/app/main.go ++++ b/app/main.go +@@ -28,7 +28,7 @@ func main() { + server := NewServer(store) + srv := &http.Server{ + Addr: addr, +- Handler: server.Routes(), ++ Handler: server.Handler(), + ReadHeaderTimeout: 5 * time.Second, + } + +diff --git a/app/middleware.go b/app/middleware.go +new file mode 100644 +--- /dev/null ++++ b/app/middleware.go +@@ -0,0 +1,18 @@ ++package main ++ ++import "net/http" ++ ++func securityHeaders(next http.Handler) http.Handler { ++ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ h := w.Header() ++ h.Set("X-Content-Type-Options", "nosniff") ++ h.Set("X-Frame-Options", "DENY") ++ h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") ++ h.Set("Referrer-Policy", "no-referrer") ++ next.ServeHTTP(w, r) ++ }) ++} ++ ++func (s *Server) Handler() http.Handler { ++ return securityHeaders(s.Routes()) ++} +diff --git a/app/middleware_test.go b/app/middleware_test.go +new file mode 100644 +--- /dev/null ++++ b/app/middleware_test.go +@@ -0,0 +1,42 @@ ++package main ++ ++import ( ++ "net/http" ++ "net/http/httptest" ++ "testing" ++) ++ ++// wantSecurityHeaders is the exact set the securityHeaders middleware must apply to ++// every response. If the middleware is removed from Server.Handler, none of these are ++// present and this test fails. ++var wantSecurityHeaders = map[string]string{ ++ "X-Content-Type-Options": "nosniff", ++ "X-Frame-Options": "DENY", ++ "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'", ++ "Referrer-Policy": "no-referrer", ++} ++ ++func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) { ++ srv := newTestServer(t) ++ handler := srv.Handler() ++ ++ // Exercise a real route, an error route, and an unregistered path to prove the ++ // middleware wraps the whole router and not just the happy path. ++ routes := []struct{ method, target string }{ ++ {http.MethodGet, "/health"}, ++ {http.MethodGet, "/notes"}, ++ {http.MethodGet, "/notes/999"}, // 404 from a handler ++ {http.MethodGet, "/does-not-exist"}, // 404 from the mux itself ++ } ++ ++ for _, rt := range routes { ++ req := httptest.NewRequest(rt.method, rt.target, nil) ++ rec := httptest.NewRecorder() ++ handler.ServeHTTP(rec, req) ++ for name, want := range wantSecurityHeaders { ++ if got := rec.Header().Get(name); got != want { ++ t.Errorf("%s %s: header %q = %q, want %q", rt.method, rt.target, name, got, want) ++ } ++ } ++ } ++} +``` + +``` +$ go test ./... +ok quicknotes 1.562s +? quicknotes/healthcheck [no test files] +``` + +### 2.4 Re-scan (the finding is gone) + +After rebuilding the image (`docker compose up --build -d`) the response carries the +headers: + +``` +$ curl -s -D - -o /dev/null http://localhost:8080/notes +HTTP/1.1 200 OK +Content-Security-Policy: default-src 'none'; frame-ancestors 'none' +Referrer-Policy: no-referrer +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Content-Type: application/json +``` + +Re-running the identical baseline against `/notes`: + +``` +PASS: X-Content-Type-Options Header Missing [10021] +WARN-NEW: Storable and Cacheable Content [10049] x 3 +WARN-NEW: ZAP is Out of Date [10116] x 1 +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 +FAIL-NEW: 0 WARN-NEW: 3 INFO: 0 IGNORE: 0 PASS: 64 +``` + +`X-Content-Type-Options [10021]` moved from **WARN to PASS** (WARN count 4 โ†’ 3, PASS 63 โ†’ +64); the after-scan alert set is `[10049, 10116, 90004]`, with `10021` absent. The three +remaining warnings are the accepted / false-positive findings from ยง2.2. Reports: +[`zap-after.html`](/submissions/lab9-artifacts/zap-after.html), +[`zap-after.json`](/submissions/lab9-artifacts/zap-after.json). + +### 2.5 Design questions + +**e) Why a middleware and not per-handler header sets?** + +One middleware wrapping the router applies the headers to every response in a single place, including error paths and any route added later, whereas per-handler `Header().Set` calls duplicate the policy and are one forgotten line away from a gap. Centralising it also makes the policy auditable and lets a single unit test guard the whole surface rather than testing each handler. + +**f) `Content-Security-Policy: default-src 'none'` is the strictest CSP. What does it break, and why is it OK for QuickNotes but not a website?** + +`default-src 'none'` forbids the document from loading any script, style, image, font, or frame and from making any fetch/XHR, so it breaks essentially any real website, which needs at least its own scripts and styles. QuickNotes returns only JSON and serves no HTML document, so a browser never executes it as a page; the policy therefore constrains nothing the API actually does while still hardening the case where a response is mistakenly rendered. A website instead allowlists the specific sources it uses (`'self'`, a CDN, and so on) rather than denying everything. + +**g) What is the cost of marking informational findings "accepted" without reading them?** + +Blanket-accepting the informational noise trains the reviewer to rubber-stamp the entire list, so the day a genuine Medium or High lands in the same report it is waved through with everything else. Each acceptance should be a read decision with a recorded reason; otherwise triage becomes theater and the scanner loses its whole value, which is catching the one finding that matters. Reading them also occasionally surfaces a real issue hiding among the informational entries. + +### 2.6 Task 2 artifacts + +- [`zap-before.html`](/submissions/lab9-artifacts/zap-before.html) / [`zap-before.json`](/submissions/lab9-artifacts/zap-before.json) โ€” baseline before the fix (4 warnings) +- [`zap-after.html`](/submissions/lab9-artifacts/zap-after.html) / [`zap-after.json`](/submissions/lab9-artifacts/zap-after.json) โ€” baseline after the fix (`10021` gone) +- [`app/middleware.go`](/app/middleware.go), [`app/middleware_test.go`](/app/middleware_test.go) โ€” the fix and its guard test + +--- + +## Bonus: `govulncheck` CI Gate + +### B.1 The job + +Added to the Lab 3 workflow, [`.github/workflows/ci.yml`](/.github/workflows/ci.yml), as a +`govulncheck` job that runs `govulncheck ./...` against `app/`, with its own status check. +It is wired into the `ci-ok` gate (`needs: [vet, test, lint, govulncheck]`), so a failure +blocks the PR. govulncheck is pinned to `@v1.1.4`, not `@latest`. + +```yaml + govulncheck: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: "1.26" + cache: true + cache-dependency-path: app/go.mod + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Run govulncheck + working-directory: app + run: govulncheck ./... + + ci-ok: + if: always() + needs: [vet, test, lint, govulncheck] + # ... unchanged gate ... +``` + +**Why Go 1.26 and not the 1.24:** govulncheck derives standard-library findings +from the toolchain that runs it. The release image now ships Go 1.26 (`app/Dockerfile` +builder from Task 1), so the gate uses 1.26 to validate the same stdlib that is actually +shipped. Pinning it to 1.24 would make govulncheck re-report the ten Go-stdlib CVEs that +the Task 1 toolchain bump already fixed (they are patched only in 1.25/1.26), reddening +the gate on code that is in fact clean. + +### B.2 Demonstration โ€” the check catches a bad dep + +The two runs below are the exact command the CI job runs (`govulncheck ./...` in `app/`, +govulncheck `@v1.1.4`, Go 1.26.4). Full logs: +[`govulncheck-green.txt`](/submissions/lab9-artifacts/govulncheck-green.txt), +[`govulncheck-red.txt`](/submissions/lab9-artifacts/govulncheck-red.txt). + +**Green: current code (exit 0):** + +``` +$ govulncheck ./... +No vulnerabilities found. +``` + +**Red: after temporarily adding a known-vulnerable, reachable dependency** +(`golang.org/x/text@v0.3.5` plus a `language.Parse` call in an `init`, the canonical +govulncheck example), the gate fails with **exit code 3**: + +``` +$ go get golang.org/x/text@v0.3.5 # + a language.Parse() call +$ govulncheck ./... +=== Symbol Results === + +Vulnerability #1: GO-2021-0113 + Out-of-bounds read in golang.org/x/text/language + Module: golang.org/x/text + Found in: golang.org/x/text@v0.3.5 + Fixed in: golang.org/x/text@v0.3.7 + Example traces found: + #1: vulndemo_temp.go:10:23: quicknotes.init#1 calls language.Parse + +Your code is affected by 1 vulnerability from 1 module. +``` + +The finding is reported precisely because it is **reachable** (`init#1 calls +language.Parse`); the demo dependency and call were then reverted, restoring the green +run above. A non-zero exit fails the `govulncheck` job and therefore `ci-ok`, blocking +the PR. + +> Evidence is captured as **command logs** rather than CI screenshots, for simplicity. +> These are the exact command the `govulncheck` CI job runs, so the red/green outcome +> (and the non-zero exit that fails the job) is identical to what GitHub Actions produces +> on a pushed PR. + +### B.3 Design questions + +**h) Reachability is govulncheck's key idea. How is "this module has a CVE but we don't call the affected function" different from "this module has a CVE", and what does that mean for triage workload?** + +A version-based scanner (Trivy, most SCA tools) flags a module the moment a vulnerable *version* is present, whereas govulncheck reports it only when a vulnerable *symbol* is actually on the program's call path. Most present-but-unused vulnerabilities never reach a line of your code, so reachability filters them out and leaves a much shorter list of findings that genuinely matter. That collapses the triage workload: instead of dispositioning every CVE in every transitive dependency, the team looks only at the handful it actually calls. + +**i) `go install golang.org/x/vuln/cmd/govulncheck@` โ€” why pin the version of the *scanner*, not just `@latest`?** + +`@latest` resolves to whatever is newest at run time, so the scanner's behaviour, output format, and exit codes can change between two otherwise identical CI runs, making the gate non-deterministic and able to break a build with no code change. It is also a supply-chain concern, since `@latest` pulls and executes an unreviewed binary during CI. A pinned version gives reproducible results and a known, auditable tool that is upgraded deliberately. + +**j) govulncheck only knows about Go. What is it *not* going to catch that Trivy (image scan) would?** + +It sees only the Go module and the Go standard library, so it is blind to everything else in the shipped image: OS and base-image packages (libc, OpenSSL, a shell), system libraries, anything installed through a package manager, and vulnerabilities in non-Go components. Trivy's image scan covers that OS-package layer and the whole filesystem, along with secrets and misconfigurations. The two are complementary, with govulncheck for reachable Go-code risk and Trivy for the image and everything non-Go. + +### B.4 Bonus artifacts + +- [`.github/workflows/ci.yml`](/.github/workflows/ci.yml) โ€” CI with the `govulncheck` job in the `ci-ok` gate +- [`govulncheck-green.txt`](/submissions/lab9-artifacts/govulncheck-green.txt) โ€” clean run (exit 0) +- [`govulncheck-red.txt`](/submissions/lab9-artifacts/govulncheck-red.txt) โ€” reachable finding GO-2021-0113 (exit 3) \ No newline at end of file