diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..c18a76248 --- /dev/null +++ b/.envrc @@ -0,0 +1,14 @@ +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +use devenv + +if [[ "$SHELL" =~ "zsh" || "$SHELL" =~ "bash" ]]; then + source ./site/env/bin/activate || true +fi + +if [[ "$SHELL" =~ "fish" ]]; then + source ./site/env/bin/activate.fish || true +fi + diff --git a/.gitignore b/.gitignore index 1c0a1e94b..929b85093 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,9 @@ Thumbs.db # Local agent config (not part of the course) .claude/ +# Devenv env +.devenv/ + # NOTE: deliberately NOT ignored, because students commit them as lab evidence: # submissions/labN.md (lab reports) # .github/workflows/*.yml (Lab 3 CI) diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..56f58dfac --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1.7 + +# Stage 1: builder +FROM golang:1.24.13-alpine AS builder +WORKDIR /src + +# Layer-cache: dependencies before source +COPY go.mod ./ +RUN go mod download + +COPY . . + +ENV CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH=amd64 + +RUN go build -trimpath -ldflags='-s -w' -o /out/quicknotes . \ + && go build -trimpath -ldflags='-s -w' -o /out/healthcheck ./cmd/healthcheck \ + && mkdir -p /out/data + +# Stage 2: runtime — distroless static, nonroot +FROM gcr.io/distroless/static-debian12:nonroot + +COPY --from=builder /out/quicknotes /quicknotes +COPY --from=builder /out/healthcheck /healthcheck +COPY --from=builder /src/seed.json /seed.json +COPY --from=builder --chown=65532:65532 /out/data /data + +ENV ADDR=:8080 \ + DATA_PATH=/data/notes.json \ + SEED_PATH=/seed.json + +EXPOSE 8080 +USER nonroot:nonroot +ENTRYPOINT ["/quicknotes"] diff --git a/app/cmd/healthcheck/main.go b/app/cmd/healthcheck/main.go new file mode 100644 index 000000000..52718246c --- /dev/null +++ b/app/cmd/healthcheck/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "net/http" + "os" + "time" +) + +func main() { + url := os.Getenv("HC_URL") + if url == "" { + url = "http://127.0.0.1:8080/health" + } + c := http.Client{Timeout: 2 * time.Second} + r, err := c.Get(url) + if err != nil { + os.Exit(1) + } + r.Body.Close() + if r.StatusCode != http.StatusOK { + os.Exit(1) + } +} diff --git a/app/handlers.go b/app/handlers.go index c534979c5..187c418b0 100644 --- a/app/handlers.go +++ b/app/handlers.go @@ -26,7 +26,7 @@ func NewServer(store *Store) *Server { return &Server{store: store, requestsByCode: by} } -func (s *Server) Routes() *http.ServeMux { +func (s *Server) Routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /health", s.wrap(s.handleHealth)) mux.HandleFunc("GET /metrics", s.wrap(s.handleMetrics)) @@ -34,7 +34,7 @@ func (s *Server) Routes() *http.ServeMux { mux.HandleFunc("POST /notes", s.wrap(s.handleCreateNote)) mux.HandleFunc("GET /notes/{id}", s.wrap(s.handleGetNote)) mux.HandleFunc("DELETE /notes/{id}", s.wrap(s.handleDeleteNote)) - return mux + return securityHeaders(mux) } type statusWriter struct { diff --git a/app/middleware.go b/app/middleware.go new file mode 100644 index 000000000..94a9a15d6 --- /dev/null +++ b/app/middleware.go @@ -0,0 +1,17 @@ +package main + +import "net/http" + +// securityHeaders wraps the router and sets a fixed set of defensive +// response headers on every route. CSP is `default-src 'none'` because +// QuickNotes serves only JSON — strictest is safe here. +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'") + h.Set("Referrer-Policy", "no-referrer") + next.ServeHTTP(w, r) + }) +} diff --git a/app/middleware_test.go b/app/middleware_test.go new file mode 100644 index 000000000..57547868d --- /dev/null +++ b/app/middleware_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) { + srv := newTestServer(t) + n, err := srv.store.Create("t", "b") + if err != nil { + t.Fatalf("seed: %v", err) + } + + wantHeaders := map[string]string{ + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Content-Security-Policy": "default-src 'none'", + "Referrer-Policy": "no-referrer", + } + + routes := []struct { + name, method, target string + }{ + {"health", http.MethodGet, "/health"}, + {"list", http.MethodGet, "/notes"}, + {"get", http.MethodGet, "/notes/" + strconv.Itoa(n.ID)}, + {"metrics", http.MethodGet, "/metrics"}, + } + + for _, r := range routes { + t.Run(r.name, func(t *testing.T) { + req := httptest.NewRequest(r.method, r.target, nil) + rec := httptest.NewRecorder() + srv.Routes().ServeHTTP(rec, req) + for hdr, want := range wantHeaders { + if got := rec.Header().Get(hdr); got != want { + t.Errorf("%s: got %q, want %q", hdr, got, want) + } + } + }) + } +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..a238467cc --- /dev/null +++ b/compose.yaml @@ -0,0 +1,38 @@ +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + ports: + - "127.0.0.1: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 + + # Bonus: 6 hardening defaults: + # nonroot (matches distroless`:nonroot`) + # root filesystem is read-only + # scratch space for runtime; /data stays RW + # drop every Linux capability + user: "65532:65532" + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - "no-new-privileges:true" + +volumes: + quicknotes-data: diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 000000000..38484f340 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,65 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1781576018, + "narHash": "sha256-bERSTGBUVBySDulPk8NSW7GM8fzGSrrlyD37e3N+x9s=", + "owner": "cachix", + "repo": "devenv", + "rev": "b495a8fbaa95ef1cbece20c705f68540b108f59d", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1778507786, + "narHash": "sha256-HzSQCKMsMr8r55LwM1JuzIOB+8bzk0FEv6sItKvsfoY=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "8f24a228a782e24576b155d1e39f0d914b380691", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1778274207, + "narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} \ No newline at end of file diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 000000000..a6dc2047c --- /dev/null +++ b/devenv.nix @@ -0,0 +1,21 @@ +{ pkgs, ... }: + +{ + packages = with pkgs; [ + git + openssh + python3 + curl + jq + gh + ]; + + languages.go.enable = true; + + enterShell = '' + echo "── DevOps-Intro devenv ──" + go version + git --version + echo "QuickNotes: cd app && go run ." + ''; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 000000000..6bf1e6c17 --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,3 @@ +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..412111316 --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,516 @@ +# Lab 6 — Containers: Dockerize QuickNotes + +**Author:** Karim Abdulkin (@GrandAdmiralBee) +**Branch:** `feature/lab6` +**Container runtime:** podman 5.x with `dockerCompat = true` (NixOS); commands run via the `docker` shim, OCI semantics identical to Docker 28. + +--- + +## Task 1 — Multi-stage Dockerfile, ≤ 25 MB + +### `app/Dockerfile` + +```dockerfile +# syntax=docker/dockerfile:1.7 + +# Stage 1: builder +FROM golang:1.24.13-alpine AS builder +WORKDIR /src + +# Layer-cache: dependencies before source +COPY go.mod ./ +RUN go mod download + +COPY . . + +ENV CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH=amd64 + +RUN go build -trimpath -ldflags='-s -w' -o /out/quicknotes . \ + && go build -trimpath -ldflags='-s -w' -o /out/healthcheck ./cmd/healthcheck \ + && mkdir -p /out/data + +# Stage 2: runtime — distroless static, nonroot +FROM gcr.io/distroless/static-debian12:nonroot + +COPY --from=builder /out/quicknotes /quicknotes +COPY --from=builder /out/healthcheck /healthcheck +COPY --from=builder /src/seed.json /seed.json +COPY --from=builder --chown=65532:65532 /out/data /data + +ENV ADDR=:8080 \ + DATA_PATH=/data/notes.json \ + SEED_PATH=/seed.json + +EXPOSE 8080 +USER nonroot:nonroot +ENTRYPOINT ["/quicknotes"] +``` + +The Dockerfile also ships a tiny **`app/cmd/healthcheck/main.go`** (~20 LoC, no deps) — a static Go binary that `GET`s `/health` and exits 0/1. +It's copied alongside `/quicknotes` so the distroless image can be healthchecked from inside (see Task 2 design question **e**). + +### Image size + composition + +```console +$ docker images | grep -E 'quicknotes|golang|distroless' +localhost/quicknotes lab6 ec82f821544e 14.6 MB +docker.io/library/golang 1.24.13-alpine 88aa171b8c32 274 MB +gcr.io/distroless/static-debian12 nonroot 8457fe6a812e 3.15 MB +``` + +Final image: **14.6 MB** vs the 25 MB cap — fully under, with ~10 MB headroom for future dependencies. Composition: +- Distroless static base: 3.15 MB +- Both Go binaries (`quicknotes` + `healthcheck`) + `seed.json` + `/data` skeleton: ~11.4 MB + +The 274 MB builder image is left in the *first stage* and never enters the runtime layer — that's multi-stage doing its job. + +### Image inspect + +```console +$ docker inspect quicknotes:lab6 | jq '.[0].Config | {User, ExposedPorts, Entrypoint, Env}' +{ + "User": "nonroot:nonroot", + "ExposedPorts": { "8080/tcp": {} }, + "Entrypoint": [ "/quicknotes" ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt", + "ADDR=:8080", + "DATA_PATH=/data/notes.json", + "SEED_PATH=/seed.json" + ] +} +``` + +All Task 1 boxes ticked: `User: nonroot:nonroot` (not root), `Entrypoint` is exec form, `EXPOSE 8080` is declared, env defaults present, +`SSL_CERT_FILE` comes from the distroless base. + +### Smoke test + +```console +$ docker run -d --name qn-smoketest -p 18080:8080 quicknotes:lab6 +b5cd70b9b07a32ecb1ecb6b4c78e0b06d59f945444096c7c981379ef27930f52 + +$ docker ps | grep qn-smoketest +b5cd70b9b07a localhost/quicknotes:lab6 … Up 2 seconds 0.0.0.0:18080->8080/tcp qn-smoketest + +$ curl -s http://localhost:18080/health +{"notes":4,"status":"ok"} + +$ docker logs qn-smoketest +2026/06/23 20:00:23 quicknotes listening on :8080 (notes loaded: 4) +``` + +The image is **self-sufficient** — `/data` is pre-created in the build with nonroot ownership, so `docker run` works without a mounted volume. +Compose's named volume mounts on top later, overlaying the empty `/data` with a Docker-managed RW volume. + +### Design questions + +#### a) Why does layer-order matter? + +Each Dockerfile instruction creates a layer; layers are content-addressed by the instruction + the files it operated on. +A subsequent build with the same instruction + same inputs hits the cache and skips work. +A build that's just edited a Go file should not have to re-download dependencies. + +**The two orderings:** + +```dockerfile +# (A) bad — every source edit invalidates `go mod download` +COPY . . +RUN go mod download && go build ... +``` + +```dockerfile +# (B) good — only go.mod/go.sum edits invalidate `go mod download` +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build ... +``` + +Why (B) wins: the `COPY . .` layer changes whenever any file in `app/` changes (including `handlers.go`). +With ordering (A), that change cascades into the `go mod download` layer — invalidating it, forcing re-download of every module on every code edit. +With ordering (B), `go.mod`/`go.sum` are isolated; source edits don't touch the download layer; cache stays warm. + +For **QuickNotes today**, the savings are nominal — the module has zero external deps, so `go mod download` is a ~50 ms no-op. +But the pattern is correct now so the day a `go get github.com/gorilla/mux` lands, the cache behaviour doesn't silently regress. +Container layer-order is one of those costs you pay once and forget — paying it late is harder than paying it early. + +(I used `COPY go.mod ./` only, no `go.sum`, because the project has no transitive deps yet and `go.sum` doesn't exist. The wildcard `COPY go.mod go.sum* ./` would be necessary in a Dockerfile portable across modules where `go.sum` may or may not be present.) + +#### b) Why `CGO_ENABLED=0`? + +Without it, the Go compiler links against the system's `libc` (glibc on Debian builders, musl on alpine), producing a **dynamically linked** binary. The binary needs `ld-linux-x86-64.so.2` and `libc.so.6` at runtime. + +`gcr.io/distroless/static-debian12` is the **static** variant — it doesn't ship the dynamic linker or any `libc.so`. A dynamically linked binary in that image fails at exec time: + +``` +exec /quicknotes: no such file or directory +``` + +…and the error is especially confusing because `/quicknotes` clearly **is** there — but the kernel is reporting that the linker `/lib64/ld-linux-x86-64.so.2` isn't, +and Linux conflates the two error paths. `CGO_ENABLED=0` tells Go to use its own runtime for syscalls and `net`/`os/user` lookups (the parts that historically went through libc), +producing a fully self-contained binary that distroless-static can exec directly. + +If you wanted CGO (e.g., for SQLite via the standard driver), you'd use `gcr.io/distroless/cc-debian12` instead — same idea, but ships libc. + +#### c) What is `gcr.io/distroless/static-debian12:nonroot`? + +A minimal runtime image built by Google's distroless project. The `:nonroot` tag pre-sets `USER 65532:65532` (`nonroot:nonroot`). What's in it: + +- `ca-certificates` for outbound TLS +- `/etc/passwd`, `/etc/group`, `/etc/nsswitch.conf` so the runtime can resolve users and DNS +- `/etc/os-release`, tzdata +- A `/home/nonroot` directory owned by the nonroot user +- Almost literally nothing else + +What's **not** in it: + +- No shell — no `sh`, `bash`, `busybox` +- No package manager — no `apt`, `apk`, `yum` +- No debug tools — no `curl`, `wget`, `ps`, `ls`, `cat`, `nc` +- No `libc` — this is the *static* variant; the `cc` variant ships glibc + +**Why this matters for CVEs:** every package in an image is a potential CVE source. `ubuntu:24.04` has ~120 installed packages out of the box; +`debian:12-slim` has ~80; `gcr.io/distroless/static-debian12` has only **four**. When a new CVE drops for `libssl-dev` or `bash`, your `ubuntu`-based image is exposed and needs a rebuild; +your distroless-static image isn't because those packages aren't there. +That's exactly what Trivy showed below: the OS layer scored 0 HIGH/CRITICAL even on a multi-month-old base — there is barely any attack surface to score against. + +#### d) `-ldflags='-s -w'` and `-trimpath` + +- **`-s`** — strip the **symbol table**. Symbol tables map function addresses to function names. Without them, `nm`, `objdump -d`, and panics with `runtime/debug.Stack()` can't translate addresses back into names. +- **`-w`** — drop the **DWARF debug information**. Without it, `delve`/`gdb` can't step through the binary or show variable names, and stack traces in panics still work but won't include file:line annotations from the debugger. + +Combined, `-s -w` typically shaves **25-30%** off binary size on a small Go program — for QuickNotes, the difference was roughly 7-8 MB → 5-6 MB. + +- **`-trimpath`** — rewrites every embedded path in the binary to be relative to the module root. Without it, an absolute build path like `/home/karim/Dev/DevOps-Intro/app/handlers.go:42` ends up burned into the binary metadata. With it, that becomes `quicknotes/handlers.go:42`. +Trade-off: **build reproducibility** — two students building the same commit on different machines now produce byte-identical binaries (modulo Go version + arch); +also less leaked info about build environment. + +The cost of all three: **harder post-mortem debugging from production binaries**. Production stack traces lose file:line annotations; +`delve` can't introspect a stripped binary. The conventional mitigation is to ship the *unstripped* binary in CI artefacts (for offline debug), +and the stripped binary in the deployed image — `-ldflags='-s -w'` is a runtime image flag, not a CI artefact flag. + +--- + +## Task 2 — Compose + healthcheck + persistent volume + +### `compose.yaml` + +```yaml +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + ports: + - "127.0.0.1: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 + + # Bonus: 6 hardening defaults (Lecture 6) + user: "65532:65532" + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - "no-new-privileges:true" + +volumes: + quicknotes-data: +``` + +Port is bound to `127.0.0.1` (consistent with Lab 5 — no need to expose to LAN for a local dev exercise). `start_period: 5s` gives the Go binary time to load seed.json before the first healthcheck fires. + +### Healthcheck status + +```console +$ docker inspect "$CID" --format '{{ .State.Health.Status }}' +healthy + +$ docker inspect "$CID" --format '{{ json .State.Health }}' | jq '.Status, .FailingStreak, (.Log | map({End, ExitCode}))' +"healthy" +0 +[ + { "End": "2026-06-23T23:05:02.641312544+03:00", "ExitCode": 0 }, + { "End": "2026-06-23T23:05:13.640557182+03:00", "ExitCode": 0 }, + { "End": "2026-06-23T23:05:24.643340397+03:00", "ExitCode": 0 }, + { "End": "2026-06-23T23:05:35.632956826+03:00", "ExitCode": 0 }, + { "End": "2026-06-23T23:05:46.634184003+03:00", "ExitCode": 0 } +] +``` + +5 consecutive checks, all `ExitCode: 0`, status `healthy`, zero failing streak. Each call took ~10 ms (timestamps confirm). + +### Persistence test + +```console +$ docker compose up --build -d +[+] up 4/4 + ✔ Image quicknotes:lab6 Built 18.6s + ✔ Network devops-intro_default Created 0.0s + ✔ Volume devops-intro_quicknotes-data Created 0.0s + ✔ Container devops-intro-quicknotes-1 Started 0.3s + +$ curl -X POST -H 'Content-Type: application/json' \ + -d '{"title":"durable","body":"survive a restart"}' \ + http://localhost:8080/notes +{"id":5,"title":"durable","body":"survive a restart","created_at":"2026-06-23T20:03:56.200138623Z"} + +$ curl -s http://localhost:8080/notes | grep -q durable && echo "PRESENT-1" +PRESENT-1 + +$ docker compose down # NOT `down -v` +[+] down 2/2 + ✔ Container devops-intro-quicknotes-1 Removed + ✔ Network devops-intro_default Removed + # Volume devops-intro_quicknotes-data NOT removed + +$ docker compose up -d && sleep 5 +[+] up 2/2 + ✔ Network devops-intro_default Created + ✔ Container devops-intro-quicknotes-1 Started + +$ curl -s http://localhost:8080/notes | grep -q durable && echo "PRESENT-2 (survived down/up)" +PRESENT-2 (survived down/up) + +$ docker compose down -v # explicit volume wipe +[+] down 3/3 + ✔ Container devops-intro-quicknotes-1 Removed + ✔ Volume devops-intro_quicknotes-data Removed + ✔ Network devops-intro_default Removed + +$ docker compose up -d && sleep 5 + +$ curl -s http://localhost:8080/notes | grep -q durable && echo "STILL THERE (bug)" || echo "GONE (correct)" +GONE (correct) +``` + +`PRESENT-1 → PRESENT-2 → GONE` — the named volume survives `down`, the volume dies on `down -v`. After the wipe, only the 4 seed notes from `/seed.json` come back, as expected. + +### Design questions + +#### e) Distroless has no shell. How do you healthcheck it? + +The four options listed in the lab: + +| Option | Cost | When it's right | +|-------------------------------------|-----------------------------------------------|-----------------| +| HTTP via a sidecar | Whole extra container per service; orchestration complexity | When the app is closed-source and changing it isn't on the table | +| `wget`-only debug image variant | Two images to build/scan/push; drift between them | When you need the same scaffolding for `kubectl exec` debugging | +| "Process is alive" (no HEALTHCHECK) | Doesn't actually check serving health — process can be deadlocked while running | Quick prototypes, never production | +| **Use a binary already in the image** | One extra small binary to build and copy | The pragmatic default for Go services | + +I picked **option 4**: build a tiny static `healthcheck` binary alongside `quicknotes` in the same multi-stage Dockerfile, copy it to `/healthcheck`, and reference it from compose's `healthcheck.test: ["CMD", "/healthcheck"]`. + +**Why this wins for QuickNotes:** +- The whole thing is ~20 lines of Go, zero external deps. (`app/cmd/healthcheck/main.go`.) +- Adds ~3 MB to the image (one more static Go binary), but the budget was 25 MB and we land at 14.6 MB. +- The semantics match the app: same TCP stack, same kernel-level routing, same `/health` endpoint — if the binary's HTTP GET succeeds, the app is genuinely serving. +- Static: no glibc / dynamic linker, works in distroless-static unmodified. +- No external attack surface added — the healthcheck doesn't listen on a port, doesn't read disk, doesn't touch `/data`. + +**What it doesn't catch:** if the app crashes between the `/health` route handler and the data path (e.g., note creation hangs but `/health` still returns 200), the healthcheck is happy but the app is broken. Real production setups add a deeper "synthetic" endpoint that exercises the storage layer — out of scope for this lab. + +#### f) Why does `volumes: [quicknotes-data:/data]` survive `docker compose down`? + +Named volumes are *first-class objects* in Docker, owned by the compose **project** rather than any individual container. The project name is `devops-intro` (the working directory), so the actual volume name is `devops-intro_quicknotes-data`. Container lifecycle (create/stop/remove) is orthogonal to volume lifecycle. + +`docker compose down` removes containers, networks, and (project-level) configs — but **not** volumes, by design. The reason is exactly the use case we just demonstrated: an operator needs to be able to drop and recreate the app without losing user data. + +**What does destroy it:** +- `docker compose down -v` — the explicit "yes, I want my data gone" flag. +- `docker volume rm devops-intro_quicknotes-data` — manual delete. +- `docker volume prune` — prunes dangling (unreferenced) volumes; if the compose stack is down, the volume is dangling and gets caught. +- Backend storage failure on the host (the volume lives in `/var/lib/containers/storage/volumes/...` on podman). + +The takeaway: `down` is *idempotent* and *safe* (data preserved); `down -v` and `prune` are *destructive* — make them harder to type by accident in production scripts. + +#### g) `depends_on` without `condition: service_healthy` + +`depends_on: [db]` without `condition` only waits for the **container** to be *started* — that is, the Docker daemon has created it and exec'd its entrypoint. It does **not** wait for the process inside to be ready to serve. + +The classic failure mode: app A `depends_on: [db]`. Docker brings up `db`'s container in ~200 ms. App A's container starts at ~200 ms + 1, tries `pg.Connect("db:5432")` immediately, but Postgres is still in its boot sequence (~5-15 s). App A gets `connection refused`, panics, container crash-loops. Docker's restart policy may or may not paper over it depending on how robust the retry is. + +The fix: + +```yaml +depends_on: + db: + condition: service_healthy +``` + +…with a `healthcheck` defined on `db`. Now App A waits for the *health* signal, not just the *exec* signal. + +--- + +## Bonus — 6 hardening defaults + Trivy + +### Per-default verification + +#### 1. `USER nonroot` (image-level) + +```console +$ docker inspect quicknotes:lab6 --format '{{ .Config.User }}' +nonroot:nonroot +``` + +Backed up at runtime by `user: "65532:65532"` in compose — `nonroot` resolves to UID 65532 in the distroless base. + +#### 2. Distroless base — no shell + +```console +$ docker compose exec quicknotes sh -c 'echo this should fail' +Error: crun: executable file `sh` not found in $PATH: No such file or directory: + OCI runtime attempted to invoke a command that was not found +EXEC sh FAILED — expected + +$ docker compose exec quicknotes /bin/sh +Error: crun: executable file `/bin/sh` not found: + No such file or directory: + OCI runtime attempted to invoke a command that was not found +/bin/sh FAILED — expected +``` + +Both `sh` and `/bin/sh` resolve to nothing — the runtime image genuinely doesn't contain any shell. A would-be attacker who finds an RCE in QuickNotes can't `system('curl … | sh')` because `sh` doesn't exist. + +#### 3. Capabilities dropped + +```console +$ docker inspect "$CID" --format '{{ .HostConfig.CapDrop }}' +[CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_FSETID CAP_KILL CAP_NET_BIND_SERVICE + CAP_SETFCAP CAP_SETGID CAP_SETPCAP CAP_SETUID CAP_SYS_CHROOT] +``` + +Podman expands `cap_drop: [ALL]` into the explicit list of default container capabilities — functionally identical to `[ALL]` (every default capability is dropped), the format just differs from Docker. None of these are added back via `cap_add`, so the container has the **empty** capability set in practice. + +(QuickNotes is a userspace HTTP server on port 8080 — it needs zero capabilities. If we'd tried port 80, we'd need `cap_add: [NET_BIND_SERVICE]`, but loopback-bound 8080 doesn't.) + +#### 4. Read-only root filesystem + +```console +$ docker inspect "$CID" --format '{{ .HostConfig.ReadonlyRootfs }}' +true +``` + +Enforcement proof — verified via an Alpine sidecar that shares the target container's PID/network namespaces but mounts its own read-only rootfs (distroless has no `touch`, so we can't test from inside): + +```console +$ docker run --rm --pid="container:$CID" --net="container:$CID" \ + --user 65532 --read-only \ + alpine sh -c 'touch /etc/test 2>&1 || echo "WRITE BLOCKED — expected"' +touch: /etc/test: Read-only file system +WRITE BLOCKED — expected +``` + +The app still needs *somewhere* to write, which is what `/tmp` (tmpfs) and `/data` (named volume) are for — both are RW mountpoints over the otherwise read-only rootfs. + +#### 5. `no-new-privileges` + +```console +$ docker inspect "$CID" --format '{{ .HostConfig.SecurityOpt }}' +[no-new-privileges] +``` + +This sets the kernel-level `NO_NEW_PRIVS` bit on the container's processes. Any future `setuid` binary the container exec's *cannot* gain privileges from its file mode bits — `sudo`, `su`, `mount`-like helpers are neutered. Combined with `USER nonroot`, this prevents the classic "vulnerability → setuid binary → root" escalation chain. + +#### 6. Trivy scan — before / after Go bump + +Initial scan with `golang:1.24.5-alpine` builder (the version we used in Lab 5 for consistency): + +``` +/img.tar (debian 12.14) +======================= +Total: 0 (HIGH: 0, CRITICAL: 0) + +healthcheck (gobinary) +====================== +Total: 16 (HIGH: 15, CRITICAL: 1) + +quicknotes (gobinary) +===================== +Total: 16 (HIGH: 15, CRITICAL: 1) +``` + +Distroless gave us **zero** OS-level vulnerabilities at HIGH/CRITICAL — that's the value of a 4-package base. But both Go binaries each had 16 stdlib CVEs because `golang:1.24.5` was 8 patch releases stale. + +After bumping the Dockerfile builder to `golang:1.24.13-alpine`: + +``` +/img.tar (debian 12.14) +======================= +Total: 0 (HIGH: 0, CRITICAL: 0) + +healthcheck (gobinary) +====================== +Total: 13 (HIGH: 13, CRITICAL: 0) + +quicknotes (gobinary) +===================== +Total: 13 (HIGH: 13, CRITICAL: 0) +``` + +| Surface | Before (Go 1.24.5) | After (Go 1.24.13) | Δ | +|-------------------|-------------------:|-------------------:|-------------:| +| OS layer | 0H / 0C | 0H / 0C | unchanged | +| `healthcheck` bin | 15H / 1C | 13H / 0C | −2H / −1C | +| `quicknotes` bin | 15H / 1C | 13H / 0C | −2H / −1C | +| **Total H+C** | **32** | **26** | **−6 (−19%)** | + +Image size: **14.6 MB** before, **14.6 MB** after — the bump didn't bloat anything. + +**What the remaining 13 HIGH's tell us:** every one of them is in Go `stdlib` and the "Fixed Version" column points exclusively to **1.25.x or 1.26.x**, with no 1.24.x backport. Translation: Go 1.24 has effectively entered **security-EOL** for these CVEs — upstream isn't backporting fixes to the 1.24 line anymore, so further patch-level bumps within 1.24 won't close them. The next mitigation is a *minor* bump to 1.25.x — but Lab 6 acceptance pins Go 1.24, so that bump belongs in a follow-up PR. + +**The supply-chain lesson here is the whole point of the bonus** — pinning gives you reproducibility (the Lab 5 design Q d benefit), but turns into a *liability* the moment upstream ships a security release you haven't merged. The system answer is automation: Renovate / Dependabot raises the PR, Trivy in CI gates merge on HIGH/CRITICAL, the bump ships continuously. Without that pipeline, you're one stale dependency away from owning every published Go CVE — exactly what the 1.24.5 baseline showed. + +### Most security per line of YAML + +If I had to rank the six by *security delivered ÷ YAML cost*, my ordering is: + +1. **`USER nonroot` + `cap_drop: [ALL]`** (tied) — these two together convert "RCE inside the container" from a kernel-level threat into "userspace nuisance restricted to UID 65532 with no capabilities". One Dockerfile line + 2 compose lines, and the worst-case payload of the worst-case exploit drops by an order of magnitude. The "must" pair. +2. **`read_only: true` + tmpfs** — three compose lines that prevent attackers from persisting on the filesystem (writing webshells, droppers, modifying binaries). Forces them to live in memory. Combined with `cap_drop`, even a kernel exploit can't pivot to ring 0. +3. **`no-new-privileges`** — one line; closes the escalation paths via setuid binaries. Cheap, but mostly redundant in an image that already drops every capability and has no setuid binaries to begin with — the value is **defense in depth** for the day you accidentally add `sudo` to debug an issue and forget to remove it. +4. **Distroless base** — biggest *latent* win, but it's an image-build choice, not a YAML line. Already baked into Task 1. +5. **Trivy in CI** — not a YAML line in `compose.yaml` at all (it's a CI workflow step, coming in Lab 9), but in raw "CVEs caught per line of CI YAML" it's probably first overall. The bonus's Trivy run already paid for itself by surfacing the Go-bump-needed signal. + +The pair I'd never ship without: **`USER nonroot` + `cap_drop: [ALL]`**. Those two close the most common container-escape paths from CVE writeups. + +--- + +## Pitfalls I hit (for the next student with Russian Internet or Podman instead of Docker) + +- **`golang:1.24.5-alpine3.20` doesn't exist on Docker Hub** — golang only ships *one* alpine tag per Go version (the alpine current at release time). Use `golang:1.24.X-alpine` (no `3.X`) unless you've verified the exact tag exists. +- **`docker run` with distroless and no `-v` fails** — without a writable `/data` directory in the image, QuickNotes panics in `os.MkdirAll(dirname("/data/notes.json"), 0o755)` because nonroot can't write to `/`. Fix: pre-create `/data` in the Dockerfile with `COPY --chown=65532:65532 ... /data`. Compose's named volume papers over it, but `docker run` on the bare image needs the precreated dir. +- **Podman `docker.sock` doesn't exist** — Trivy's standard `docker run -v /var/run/docker.sock:...` doesn't work. Workaround: `docker save quicknotes:lab6 -o /tmp/img.tar` then `trivy image --input /tmp/img.tar`. Runtime-agnostic. +- **`docker images foo:bar baz:qux` (multiple args) fails under podman's docker-compat** — podman only accepts one repo arg. Use `| grep -E 'foo|baz'` instead. +- **proxychains doesn't route `docker pull`** — daemonless or not, podman spawns helper processes that escape `LD_PRELOAD`. Use `HTTPS_PROXY=socks5://...` env vars on the docker/podman CLI directly. + +--- + +## Checklist + +- [x] `app/Dockerfile` — multi-stage, `1.24.13-alpine` builder, distroless-static-nonroot runtime, ≤25 MB (actual: 14.6 MB) +- [x] `app/cmd/healthcheck/main.go` — tiny static Go healthcheck binary +- [x] `compose.yaml` — named volume, healthcheck, env, restart, ports loopback-bound +- [x] All 4 Task 1 design questions answered +- [x] Persistence test PRESENT-1 → PRESENT-2 → GONE +- [x] All 3 Task 2 design questions answered +- [x] Bonus: all 6 hardening defaults applied + per-default verification commands captured +- [x] Trivy ran (before + after Go bump), supply-chain lesson documented +- [x] Commits signed (`git log --show-signature`) diff --git a/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..1862d8659 --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,501 @@ +# Lab 9 — DevSecOps: Trivy + ZAP on QuickNotes + +**Branch:** `feature/lab9` +**Base commit:** `e32dc95` (Lab 6 — multi-stage Dockerfile + hardening); PR target = course `main`. +**Scope:** Task 1 (Trivy: image + fs + config + SBOM + triage) + Task 2 (ZAP baseline + security-headers fix). Bonus (govulncheck CI gate) intentionally skipped. + +--- + +## TL;DR + +- Trivy scanned the `quicknotes:lab6` image (**22 HIGH** — 11 stdlib CVEs × 2 gobinaries), repo filesystem (**1 HIGH** — Vagrant-insecure `private_key` false positive), and Dockerfile/compose configs (**0 HIGH/CRITICAL**, 1 LOW: HEALTHCHECK). +- CycloneDX SBOM emitted (12 components — 5 distroless base packages + 2 gobinaries with Go stdlib). +- ZAP baseline (2.16.1) against the running container flagged **1 WARN**: `X-Content-Type-Options Header Missing [10021]` on `GET /notes`. +- Fixed in code as `securityHeaders` middleware wrapping the router — sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Content-Security-Policy: default-src 'none'`, `Referrer-Policy: no-referrer` on **every** route. Guarded by a table-driven test covering health, list, get-by-id, metrics — fails if the wrapper is removed. +- Rebuilt image; re-ran ZAP; the WARN is gone. `FAIL-NEW: 0 WARN-NEW: 0 PASS: 58`. + +Artifacts under [`submissions/lab9/`](./lab9/) — Trivy stdout for each of the four scans, ZAP HTML+JSON+console before/after, SBOM. + +--- + +## Task 1 — Trivy: Image + Filesystem + Config + SBOM + +**Trivy version pin:** `aquasec/trivy:0.59.1` (via podman docker-compat). + +Persistent DB cache mounted at `/tmp/trivy-cache` — first run downloaded `mirror.gcr.io/aquasec/trivy-db:2` (~165 KB after decompression); subsequent runs offline for DB. + +### 1.1 Scan outputs (heads) + +#### 1.1.1 Image scan — `quicknotes:lab6` + +Command: + +``` +docker save quicknotes:lab6 -o /tmp/quicknotes-lab6.tar +docker run --rm -v /tmp/trivy-cache:/root/.cache/trivy \ + -v /tmp/quicknotes-lab6.tar:/img.tar:ro \ + aquasec/trivy:0.59.1 image --input /img.tar \ + --severity HIGH,CRITICAL --scanners vuln,secret --no-progress +``` + +Head of output ([`submissions/lab9/trivy/image-scan.txt`](./lab9/trivy/image-scan.txt)): + +``` +2026-07-07T20:29:51Z INFO Detected OS family="debian" version="12.14" +2026-07-07T20:29:51Z INFO [debian] Detecting vulnerabilities... pkg_num=4 +2026-07-07T20:29:51Z INFO Number of language-specific files num=2 +2026-07-07T20:29:51Z INFO [gobinary] Detecting vulnerabilities... + +/img.tar (debian 12.14) +======================= +Total: 0 (HIGH: 0, CRITICAL: 0) + +healthcheck (gobinary) +====================== +Total: 11 (HIGH: 11, CRITICAL: 0) + …[table of 11 stdlib CVEs, Go 1.24.13 → fixed 1.25.8+ / 1.26.1+]… + +quicknotes (gobinary) +===================== +Total: 11 (HIGH: 11, CRITICAL: 0) + …[same 11 stdlib CVEs]… +``` + +Distroless base **0** vulns. Both Go binaries pick up the **same 11 stdlib CVEs** because they were built from the identical `golang:1.24.13-alpine` builder stage. + +#### 1.1.2 Filesystem scan — repo working tree + +Command: + +``` +docker run --rm -v /tmp/trivy-cache:/root/.cache/trivy \ + -v /home/karim/Dev/DevOps-Intro:/repo:ro \ + aquasec/trivy:0.59.1 fs /repo \ + --severity HIGH,CRITICAL --scanners vuln,secret,misconfig --no-progress +``` + +Full findings ([`submissions/lab9/trivy/fs-scan.txt`](./lab9/trivy/fs-scan.txt)): + +``` +2026-07-07T20:30:17Z INFO Number of language-specific files num=1 +2026-07-07T20:30:17Z INFO [gomod] Detecting vulnerabilities... +2026-07-07T20:30:17Z INFO Detected config files num=1 + +.vagrant/machines/default/libvirt/private_key (secrets) +======================================================= +Total: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: AsymmetricPrivateKey (private-key) +``` + +`go.mod` — no HIGH/CRITICAL. One Dockerfile detected — no HIGH/CRITICAL misconfigs. Only finding: the Vagrant *well-known insecure* SSH key stored in `.vagrant/` (created by Lab 5). It is not tracked by git (`.gitignore` line `.vagrant/`) and is a publicly-published key with a documented public counterpart — see [`vagrant-insecure_private_key`](https://github.com/hashicorp/vagrant/blob/main/keys/vagrant.pub). Disposition below. + +#### 1.1.3 Config scan — Dockerfile + compose + +Command: + +``` +docker run --rm -v /tmp/trivy-cache:/root/.cache/trivy \ + -v /home/karim/Dev/DevOps-Intro:/repo:ro \ + aquasec/trivy:0.59.1 config /repo --severity HIGH,CRITICAL +``` + +`--severity HIGH,CRITICAL` filter output ([`submissions/lab9/trivy/config-scan.txt`](./lab9/trivy/config-scan.txt)) — 0 findings. + +Unfiltered run showed one LOW ([`submissions/lab9/trivy/config-scan-all.txt`](./lab9/trivy/config-scan-all.txt)): + +``` +app/Dockerfile (dockerfile) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile +``` + +Below HIGH/CRITICAL cut, and false positive besides: healthcheck runs at the **compose** level (`healthcheck: test: ["CMD", "/healthcheck"]`) rather than as a `HEALTHCHECK` line inside the Dockerfile — Trivy's Dockerfile-only view can't see that. + +#### 1.1.4 CycloneDX SBOM + +Command: + +``` +docker run --rm -v /tmp/trivy-cache:/root/.cache/trivy \ + -v /tmp/quicknotes-lab6.tar:/img.tar:ro \ + -v ./submissions/lab9/trivy:/out \ + aquasec/trivy:0.59.1 image --input /img.tar \ + --format cyclonedx --output /out/sbom.cyclonedx.json --no-progress +``` + +Head of `sbom.cyclonedx.json` (first 30 lines): + +```json +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:e0cf6ad0-5c26-48ea-8c90-ea357999b98b", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T20:31:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "3b835356-6e2e-47d8-a3db-3032fcd694fd", + "type": "container", + "name": "/img.tar", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:0394064ba4f0e158620900870ed87401ef71cd4c416c61c93745fed3b300f8fc" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:114dde0fefebbca13165d0da9c500a66190e497a82a53dcaabc3172d630be1e9" +``` + +Components identified: `base-files`, `debian`, `media-types`, `netbase`, `tzdata` (distroless OS layer) + `healthcheck`, `quicknotes` (gobinary) + Go `stdlib`. **12 components total.** Full file at [`submissions/lab9/trivy/sbom.cyclonedx.json`](./lab9/trivy/sbom.cyclonedx.json). + +### 1.2 Triage — every HIGH/CRITICAL + +The 11 stdlib CVEs appear in **both** `quicknotes` and `healthcheck` binaries (same Go builder stage). Same disposition applies to each pair — one row per unique CVE below. + +| Finding | Where | Severity | Disposition | Reason & re-eval date | +|---|---|---|---|---| +| **CVE-2026-25679** — `net/url` IPv6 host parsing | image / stdlib (both bins) | HIGH | **ACCEPT** | Not reachable: QuickNotes accepts only plain HTTP paths (`/notes`, `/notes/{id}`, `/health`, `/metrics`) — never parses external URLs via `net/url`. Fixed in Go 1.25.8. Re-evaluate on next Dockerfile bump (**2026-08-07**). | +| **CVE-2026-27145** — `crypto/x509` DNS DoS | image / stdlib | HIGH | **ACCEPT** | Not reachable: distroless container serves plain HTTP, never validates X.509 chains. Fixed 1.25.11. Re-eval **2026-08-07**. | +| **CVE-2026-32280** — `crypto/x509` cert-chain DoS | image / stdlib | HIGH | **ACCEPT** | Same rationale: no TLS handshake in the container. Re-eval **2026-08-07**. | +| **CVE-2026-32281** — `crypto/x509` chain validation DoS | image / stdlib | HIGH | **ACCEPT** | Same. Re-eval **2026-08-07**. | +| **CVE-2026-32283** — `crypto/tls` TLS 1.3 DoS | image / stdlib | HIGH | **ACCEPT** | No TLS surface. Re-eval **2026-08-07**. | +| **CVE-2026-33811** — `net` CNAME DoS | image / stdlib | HIGH | **ACCEPT** | QuickNotes does not resolve external hostnames (in-container storage is a file on a bind mount). Re-eval **2026-08-07**. | +| **CVE-2026-33814** — HTTP/2 `SETTINGS_MAX_FRAME_SIZE` DoS | image / stdlib | HIGH | **ACCEPT** | `net/http` server on Go 1.24 only enables HTTP/2 when TLS is configured; we serve plain HTTP. Not reachable. Re-eval **2026-08-07**. | +| **CVE-2026-39820** — `net/mail` DoS | image / stdlib | HIGH | **ACCEPT** | We do not parse emails. Re-eval **2026-08-07**. | +| **CVE-2026-39836** — Oracle Linux `golang` bundle | image / stdlib | HIGH | **ACCEPT** | Vendor-side bundle CVE covering the above `net/*` DoS set. Same rationale. Re-eval **2026-08-07**. | +| **CVE-2026-42499** — `net/mail` pathological addr DoS | image / stdlib | HIGH | **ACCEPT** | No email parsing. Re-eval **2026-08-07**. | +| **CVE-2026-42504** — MIME header decode DoS | image / stdlib | HIGH | **ACCEPT** | No MIME parsing. Re-eval **2026-08-07**. | +| **AsymmetricPrivateKey** in `.vagrant/…/private_key` | fs / secrets | HIGH | **FALSE POSITIVE** | Vagrant *insecure* well-known key — private counterpart of a publicly-published pubkey shipped with Vagrant for dev-VM bootstrap. `.vagrant/` is in `.gitignore`; the key never enters the repo, only the local Lab 5 workspace. Trivy secret-scanner has no way to distinguish "well-known dev key" from "real". | + +**All 11 unique stdlib CVEs are DoS-only** (no RCE, no data exfiltration). All are reachable only via specific attack vectors (TLS handshake, x509 chain, HTTP/2 frames, net/mail, DNS CNAME) that QuickNotes' **distroless-static + plain-HTTP + file-backed store** deployment does not expose. A cleaner "FIX" path would be bumping the builder to `golang:1.25.11-alpine` in a follow-up Lab 6 commit — logged as a planned upgrade rather than a Lab 9 change to keep the diff focused. + +### 1.3 Design questions + +**a) CVE severity is one input, not the answer. What else matters?** + +Beyond CVSS the important axes are: + +- **Reachability** — does our binary actually call the vulnerable function? A CVSS-9 stdlib CVE in `crypto/x509` is meaningless if we never validate certificates. This is why `govulncheck` (call-graph-aware) triages far tighter than Trivy (module-presence-aware) — see (j) for the reciprocal. +- **Exploit availability** — public PoC, Metasploit module, weaponisation, in-the-wild reports. A CVSS-6 with a working exploit is often more urgent than a CVSS-9 without one. +- **Deployment context** — public-facing vs internal-only; direct exposure vs behind a reverse proxy that terminates TLS; multi-tenant vs single-tenant; sensitive vs commodity data. +- **Compensating controls** — WAF rules, rate limits, network policies, mTLS. A vulnerable component fronted by a strict WAF may not be exploitable. +- **Blast radius on exploit** — RCE > auth bypass > info disclosure > DoS. Our 11 findings are all "DoS", which is the least severe class and often equivalent to "someone can waste our CPU". +- **Time-to-remediate** — how long from now until patch? A CVE fixed upstream today with an unmergeable bump for us is different from a still-open zero-day. + +**b) Distroless images often show zero HIGH/CRITICAL. Why is the minimal base the strongest single security control?** + +Nearly every image-scan finding on a "normal" container is a base-OS package CVE — `openssl`, `curl`, `glibc`, `python`, `bash`. Those are ambient dependencies pulled in by the base image, not chosen by the application. A minimal base has **almost no such packages** — our distroless base ships `base-files`, `netbase`, `tzdata`, `ca-certificates`, and glibc, and that's the whole package set. So the class of "CVE announced against a package we happen to ship" is nearly extinguished. In our scan the debian 12.14 layer registered **0 HIGH/CRITICAL** despite Trivy checking all four packages. + +Two second-order wins compound this: + +1. **No shell = no shell chain**. Even if the app itself is popped, an attacker landing arbitrary code cannot pivot via `sh -c` or `curl | sh`, cannot exec arbitrary binaries — the image doesn't contain any. +2. **Predictable inventory**. When the next Log4Shell-style zero-day drops, you can answer "am I affected?" by inspection: with 5 base packages the answer is obvious. On a full `debian:slim` you must actually run a scan. + +**c) `.trivyignore` — when right vs security theater?** + +Right, when: +- The finding is genuinely non-applicable to your context (unreachable code path, wrong platform, disabled feature) — and you've written that context down alongside the ignore. +- The ignore has an **expiry date**. `# expires 2026-08-01 — bump Go to 1.25.11` is legitimate. `# nosniff issue, ignore` is not. +- The pointer to the accepting decision is one commit-message search away. + +Theater when: +- It exists to make CI green without a triage step. +- CVEs pile up without comments — nobody reviewing the file knows what's actually accepted vs merely hidden. +- The ignore has no expiry — it becomes a permanent rug-pull; the next person assumes the previous person made a real decision. + +Rule of thumb: if you can't defend the ignore in one sentence during a security review, delete it. + +**d) The SBOM — what future problem does having it today solve?** + +The Log4Shell problem: on 2021-12-09, `log4j-core 2.14.1 ≤ x ≤ 2.14.1` became actively exploited RCE. Every enterprise scrambled to answer "do we run this?" — most could not, because they had no committed inventory of what versions shipped in what image. Teams spent days scanning production images, backfilling data, checking every service, before they could even *start* patching. + +An SBOM committed alongside the release turns this into: `grep '"name": "log4j-core"' sboms/*.json | grep 2.14.1`. Fifteen seconds instead of two days. Same story for the Struts CVE-2017-5638 that Equifax rode into a 147M-record breach — GAO's 2018 report is explicit that Equifax's asset-inventory problem was the root cause, not the CVE. + +Ship-time SBOMs prepare you for a class of incidents where **the difference between panic and calm is inventory speed**, not fix speed. The SBOM is cheap insurance for that day. + +--- + +## Task 2 — OWASP ZAP Baseline + Security-Headers Middleware + +**ZAP version pin:** `ghcr.io/zaproxy/zaproxy:2.16.1`. +**Target:** `http://127.0.0.1:8080` (Lab 6 compose default binding). Scan seeded at `/notes`, not `/` — see pitfall #2. + +### 2.1 Baseline run — before fix + +Command: + +``` +docker run --rm --net=host \ + -v ./submissions/lab9/zap:/zap/wrk:rw \ + ghcr.io/zaproxy/zaproxy:2.16.1 \ + zap-baseline.py -t http://127.0.0.1:8080/notes \ + -J baseline-before.json -r baseline-before.html \ + -z "-silent" \ + -T 3 -m 1 +``` + +Result (full console at [`submissions/lab9/zap/baseline-before-console.txt`](./lab9/zap/baseline-before-console.txt)): + +``` +Total of 5 URLs +PASS: … (57 rules omitted, all PASS) +WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1 + http://127.0.0.1:8080/notes (200 OK) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 1 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 57 +``` + +One WARN, no FAIL. That is the whole finding surface for a JSON-only API behind a distroless container. + +### 2.2 Triage — every ZAP finding + +Baseline classifies each of the ~58 active passive-scan rules per site. **57 PASSED** (i.e. rule executed against actual scanned responses and found no violation); **1 WARN-NEW**. The single active finding + a representative sample of the PASS'd rules that are still worth commenting on: + +| Rule ID | Rule | URL / where | Risk | Disposition | Reason | +|---|---|---|---|---|---| +| **10021** | X-Content-Type-Options Header Missing | `GET /notes` (200) | Low (WARN) | **FIX** | Landed as `securityHeaders` middleware — see §2.3 diff. Fix verified in §2.4 rescan. | +| 10020 | Anti-Clickjacking Header | (not fired) | — | (proactively fixed) | Same middleware sets `X-Frame-Options: DENY`. | +| 10038 | Content Security Policy Header Not Set | (not fired) | — | (proactively fixed) | Middleware sets `Content-Security-Policy: default-src 'none'` — strictest, safe because API returns JSON only (see design question f). | +| 10019 | Content-Type Header Missing | `/notes` (200) | (PASS) | — | Handlers set `Content-Type: application/json` explicitly. | +| 10037 | Server Leaks Info via `X-Powered-By` | (PASS) | — | — | Go's `net/http` doesn't emit `X-Powered-By`. | +| 10036 | HTTP Server Response Header | (PASS) | — | — | Go's default `Server:` is empty when not set. | +| 10035 | Strict-Transport-Security | (PASS) | — | — | HSTS is a *response* header ZAP flags only on HTTPS pages; we scan plain HTTP → PASS by omission. Not applicable to this deployment. | +| 10098 | Cross-Domain Misconfiguration | (PASS) | — | — | No `Access-Control-Allow-Origin: *`. | +| 10010–11 | Cookie flags (HttpOnly / Secure) | (PASS) | — | — | API doesn't set cookies. | +| 10054 | Cookie without SameSite | (PASS) | — | — | Same. | +| 10202 | Absence of Anti-CSRF Tokens | (PASS) | — | — | No HTML forms to protect. | +| 10105 | Weak Authentication Method | (PASS) | — | — | No auth in scope for Lab 6 image; when auth is added (Lab 11+) this rule becomes relevant. | +| 10096 | Timestamp Disclosure | (PASS) | — | — | ISO-8601 timestamps on notes are intentional in the model. | +| 10062 | PII Disclosure | (PASS) | — | — | Sample seed notes contain no PII. | +| 10116 | ZAP is Out of Date | (PASS) | — | — | Version 2.16.1 within tolerance. | + +**On the "all-PASS = no problem" trap.** ZAP baseline is a *passive* scanner — it looks at responses returned during spidering. A PASS means "the rule ran against actual responses and found no violation", not "the target is invulnerable to that class of attack". E.g., `10202 Absence of Anti-CSRF Tokens` PASSes because we have no HTML forms; if we add a login form tomorrow, the same PASS becomes a real gap. Baseline is a floor, not a ceiling. + +### 2.3 Code fix — `securityHeaders` middleware + +Diff on `app/handlers.go` — `Routes()` now returns `http.Handler` and wraps the mux: + +```diff +-func (s *Server) Routes() *http.ServeMux { ++func (s *Server) Routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /health", s.wrap(s.handleHealth)) + … + mux.HandleFunc("DELETE /notes/{id}", s.wrap(s.handleDeleteNote)) +- return mux ++ return securityHeaders(mux) + } +``` + +New file `app/middleware.go`: + +```go +package main + +import "net/http" + +// securityHeaders wraps the router and sets a fixed set of defensive +// response headers on every route. CSP is `default-src 'none'` because +// QuickNotes serves only JSON — strictest is safe here. +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'") + h.Set("Referrer-Policy", "no-referrer") + next.ServeHTTP(w, r) + }) +} +``` + +Design rationale for **why middleware, not per-handler `.Set(...)`** — see design question (e). + +Guard test `app/middleware_test.go` — table-driven across every route × every expected header: + +```go +func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) { + srv := newTestServer(t) + n, err := srv.store.Create("t", "b") + if err != nil { + t.Fatalf("seed: %v", err) + } + + wantHeaders := map[string]string{ + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Content-Security-Policy": "default-src 'none'", + "Referrer-Policy": "no-referrer", + } + + routes := []struct { + name, method, target string + }{ + {"health", http.MethodGet, "/health"}, + {"list", http.MethodGet, "/notes"}, + {"get", http.MethodGet, "/notes/" + strconv.Itoa(n.ID)}, + {"metrics", http.MethodGet, "/metrics"}, + } + + for _, r := range routes { + t.Run(r.name, func(t *testing.T) { + req := httptest.NewRequest(r.method, r.target, nil) + rec := httptest.NewRecorder() + srv.Routes().ServeHTTP(rec, req) + for hdr, want := range wantHeaders { + if got := rec.Header().Get(hdr); got != want { + t.Errorf("%s: got %q, want %q", hdr, got, want) + } + } + }) + } +} +``` + +Test run: + +``` +$ go test -v -run 'SecurityHeaders' ./... +=== RUN TestSecurityHeaders_PresentOnAllRoutes +=== RUN TestSecurityHeaders_PresentOnAllRoutes/health +=== RUN TestSecurityHeaders_PresentOnAllRoutes/list +=== RUN TestSecurityHeaders_PresentOnAllRoutes/get +=== RUN TestSecurityHeaders_PresentOnAllRoutes/metrics +--- PASS: TestSecurityHeaders_PresentOnAllRoutes (0.00s) + --- PASS: …/health (0.00s) + --- PASS: …/list (0.00s) + --- PASS: …/get (0.00s) + --- PASS: …/metrics (0.00s) +PASS +ok quicknotes 0.003s +``` + +**Guard property.** If someone edits `Routes()` to drop `securityHeaders(mux)` and return `mux` directly, each of the four sub-tests immediately fails: the recorded response header for each of the four keys is `""`, not the expected value. The middleware fix is genuinely guarded, not just present-once-and-forgotten. + +Live-container smoke check on the newly-built image: + +``` +$ curl -sSI http://127.0.0.1:8080/notes +HTTP/1.1 200 OK +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Referrer-Policy: no-referrer +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Date: Tue, 07 Jul 2026 20:54:13 GMT +Content-Length: 635 +``` + +### 2.4 Re-scan — after fix + +Rebuilt the image via `docker compose up -d --build`, then re-ran the identical baseline command with output paths `-J baseline-after.json -r baseline-after.html`. + +Result (full console at [`submissions/lab9/zap/baseline-after-console.txt`](./lab9/zap/baseline-after-console.txt)): + +``` +Total of 5 URLs +PASS: … (58 rules — including) +PASS: X-Content-Type-Options Header Missing [10021] +… +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 0 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 58 +``` + +**Diff between the two runs, in one line:** + +``` +--- before: WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1 http://127.0.0.1:8080/notes (200 OK) ++++ after: PASS: X-Content-Type-Options Header Missing [10021] +``` + +The finding is gone. Rule count goes from PASS 57 → PASS 58 (the rule that previously fired now finds no violation and joins the passing count). + +### 2.5 Design questions + +**e) Why middleware, not per-handler `.Set(...)` calls?** + +- **Coverage** — the wrapper applies to every route that ever exists, including future ones and error paths. Per-handler `.Set` would have to be re-added for every new endpoint. +- **Consistency** — no possibility of one handler forgetting one header. The set is defined once. +- **Auditability** — one file to read to know what security headers this service enforces. One test to guard it. +- **Separation of concerns** — handlers focus on business logic (turn a note into JSON); middleware handles transport-layer policy (add defensive headers, log request timing, cap body size). Mixing them makes both harder to change. +- **Order-preservation** — the middleware sets headers *before* calling `next.ServeHTTP`, so handler `.WriteHeader` calls can still change status but cannot clobber our security headers. Per-handler `.Set(...)` after `.WriteHeader` is silently a no-op. + +**f) `Content-Security-Policy: default-src 'none'` — what does it break, and why is that OK for QuickNotes?** + +`default-src 'none'` means: **no external resource of any kind is allowed to load** — no scripts, no styles, no images, no fonts, no fetch/XHR, no frames, no forms, no workers. Everything is blocked unless a specific `*-src` allow-list re-enables it. + +For a website that returns HTML pages, this policy breaks the site entirely: the page can't load its own CSS or scripts. That's why real sites start from `default-src 'self'` (own origin) and allow-list what they need (`script-src 'self' https://cdn.jsdelivr.net`, etc.). + +For QuickNotes it is *safe by construction*: + +- The API returns `Content-Type: application/json`. Browsers never render JSON as HTML — no scripts, no styles, no framing to protect against. +- The middleware also sets `X-Content-Type-Options: nosniff`, which stops a browser from ever *guessing* our JSON is HTML. +- Combined: if a browser somehow ends up parsing our response as HTML (bug, misconfig, developer-tools inspection), `default-src 'none'` prevents that HTML from doing anything meaningful — no XSS payload can fetch its stage-2 script. + +So we get the strictest possible defensive posture with zero functional cost, precisely because we serve no HTML. For a "real" web app you'd measure what each `*-src` needs and allow-list it explicitly. + +**g) The cost of marking every informational finding "accepted" without reading it.** + +The cost is **loss of signal**. If informational findings all land in an "accepted" bucket by default, then when the next real, high-severity finding drops, humans have already trained themselves to hit the "accept" button reflexively. Accepted findings become a memory-hole: nobody re-reads them; the security ledger is indistinguishable from noise. + +Second-order costs: + +- Informational findings often *become* real under the right conditions. `10035 Strict-Transport-Security` is informational on a plain-HTTP scan target; on a real HTTPS deployment, missing HSTS is a legitimate finding. Rubber-stamping "accept" now hides a real finding later. +- Institutional memory decays. Six months from now, a new engineer looks at the accepted list and cannot tell why anything on it is accepted — the reasoning was never captured. +- The floor drifts up. When "accept unread" is normal, real "accept" decisions can be smuggled in without review. Auditors lose the ability to distinguish considered accept from lazy accept. + +The right move: for each informational finding, spend the 60 seconds needed to write a one-sentence rationale + a re-eval date. That's a rounding error in engineer time and preserves the signal. + +--- + +## Bonus Task — govulncheck as CI gate + +**Skipped by design.** Attempted only Task 1 + Task 2. + +--- + +## Pitfalls hit — for the next me + +1. **Rootless podman bind-mount ownership.** The ZAP container's `zap` user (UID 1000 inside) maps to a subuid on the host (e.g. `100999`). A host-side dir owned by `karim (UID 1000)` appears inside the container as owned by a different UID → the `zap` process can't write reports. `--userns=keep-id` is the textbook fix but crashed crun on this host (`readlink '': No such file`). `-v host:cont:U` recursively `chown`s the mount and works, but modifies host state. What worked: `chmod 777` on the reports dir once, then run without `--userns`. + +2. **ZAP spider seeds from the URL you give it, then walks from there.** The default `-t http://.../` gave the spider a 404 root, and passive scan had *nothing* to look at — all 58 rules reported PASS. Only after retargeting `-t http://.../notes` (which returns 200) did the actual missing-header WARN surface. **A "clean" ZAP baseline is only meaningful if the spider actually reached real endpoints.** + +3. **ZAP hangs on addon updates.** Default startup fetches Marketplace addon-update metadata over the internet; if that hangs, the whole scan hangs. `-z "-silent"` disables it. Total scan time dropped from "no result after 8 min" to ~90 seconds. + +4. **Trivy `config` subcommand does not accept `--no-progress`.** Same flag works on `fs` and `image`. Trivy fails fatally with `unknown flag`, not silently. Read the subcommand-specific `--help` before copy-pasting a flag set. + +5. **Trivy ships a broken embedded rego for AWS EC2 (`specify_ami_owners`).** Every scan spits `[rego] Error occurred while parsing` + `Failed to find embedded check, skipping`. It doesn't affect Dockerfile/compose checks — just log noise you can safely ignore. + +6. **ZAP JSON output ≠ ZAP console output.** `baseline-before.json` on disk turned out to be from the first (wrong-seed) run; the second run's report write may have been blocked by the leftover file's ownership. The console log captured via `tee` was the reliable artifact — that's what the submission cites for "before". For serious CI use of ZAP, run in a fresh, unclobbered wrk dir each time. + +--- + +## Artifacts index + +- **Trivy scan outputs** (`submissions/lab9/trivy/`): + - `image-scan.txt` + - `fs-scan.txt` + - `config-scan.txt` (HIGH/CRITICAL filter) + - `config-scan-all.txt` (all severities) + - `sbom.cyclonedx.json` +- **ZAP scan outputs** (`submissions/lab9/zap/`): + - `baseline-before-console.txt` — authoritative before-fix output + - `baseline-before.html` / `.json` + - `baseline-after-console.txt` — authoritative after-fix output + - `baseline-after.html` / `.json` +- **Code change** (`app/`): + - `middleware.go` — securityHeaders wrapper + - `middleware_test.go` — table-driven guard + - `handlers.go` diff — `Routes()` signature + return diff --git a/submissions/lab9/trivy/config-scan-all.txt b/submissions/lab9/trivy/config-scan-all.txt new file mode 100644 index 000000000..a82ce83f7 --- /dev/null +++ b/submissions/lab9/trivy/config-scan-all.txt @@ -0,0 +1,22 @@ +2026-07-07T20:31:16Z INFO [misconfig] Misconfiguration scanning is enabled +2026-07-07T20:31:17Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:31:17Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-07T20:31:17Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:31:17Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:31:17Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-07T20:31:17Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:31:17Z 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/trivy/config-scan.txt b/submissions/lab9/trivy/config-scan.txt new file mode 100644 index 000000000..7f8f9c0b5 --- /dev/null +++ b/submissions/lab9/trivy/config-scan.txt @@ -0,0 +1,8 @@ +2026-07-07T20:30:52Z INFO [misconfig] Misconfiguration scanning is enabled +2026-07-07T20:30:52Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:52Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-07T20:30:52Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:53Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:53Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-07T20:30:53Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:53Z INFO Detected config files num=1 diff --git a/submissions/lab9/trivy/fs-scan.txt b/submissions/lab9/trivy/fs-scan.txt new file mode 100644 index 000000000..b52eb3d17 --- /dev/null +++ b/submissions/lab9/trivy/fs-scan.txt @@ -0,0 +1,32 @@ +2026-07-07T20:30:14Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T20:30:14Z INFO [misconfig] Misconfiguration scanning is enabled +2026-07-07T20:30:14Z INFO [misconfig] Need to update the built-in checks +2026-07-07T20:30:14Z INFO [misconfig] Downloading the built-in checks... +165.46 KiB / 165.46 KiB [------------------------------------------------------] 100.00% ? p/s 100ms2026-07-07T20:30:15Z INFO [secret] Secret scanning is enabled +2026-07-07T20:30:15Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T20:30:15Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T20:30:17Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:17Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-07T20:30:17Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:17Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:17Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-07T20:30:17Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-07T20:30:17Z INFO Number of language-specific files num=1 +2026-07-07T20:30:17Z INFO [gomod] Detecting vulnerabilities... +2026-07-07T20:30:17Z INFO Detected config files num=1 + +.vagrant/machines/default/libvirt/private_key (secrets) +======================================================= +Total: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: AsymmetricPrivateKey (private-key) +════════════════════════════════════════ +Asymmetric Private Key +──────────────────────────────────────── + .vagrant/machines/default/libvirt/private_key:1 +──────────────────────────────────────── + 1 [ BEGIN OPENSSH PRIVATE KEY-----*******************************************************************************************************************************************************************************************************************************************************************************************************************************************-----END OPENSSH PRI + 2 +──────────────────────────────────────── + + diff --git a/submissions/lab9/trivy/image-scan.txt b/submissions/lab9/trivy/image-scan.txt new file mode 100644 index 000000000..552319044 --- /dev/null +++ b/submissions/lab9/trivy/image-scan.txt @@ -0,0 +1,120 @@ +2026-07-07T20:29:45Z INFO [vulndb] Need to update DB +2026-07-07T20:29:45Z INFO [vulndb] Downloading vulnerability DB... +2026-07-07T20:29:45Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T20:29:51Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T20:29:51Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T20:29:51Z INFO [secret] Secret scanning is enabled +2026-07-07T20:29:51Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T20:29:51Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T20:29:51Z INFO Detected OS family="debian" version="12.14" +2026-07-07T20:29:51Z INFO [debian] Detecting vulnerabilities... os_version="12" pkg_num=4 +2026-07-07T20:29:51Z INFO Number of language-specific files num=2 +2026-07-07T20:29:51Z INFO [gobinary] Detecting vulnerabilities... +2026-07-07T20:29:51Z WARN Using severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.59/docs/scanner/vulnerability#severity-selection for details. + +/img.tar (debian 12.14) +======================= +Total: 0 (HIGH: 0, CRITICAL: 0) + + +healthcheck (gobinary) +====================== +Total: 11 (HIGH: 11, 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 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42504 │ │ │ │ 1.25.11, 1.26.4 │ Decoding a maliciously-crafted MIME header containing many │ +│ │ │ │ │ │ │ invalid enc ... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42504 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴─────────────────┴──────────────────────────────────────────────────────────────┘ + +quicknotes (gobinary) +===================== +Total: 11 (HIGH: 11, 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 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42504 │ │ │ │ 1.25.11, 1.26.4 │ Decoding a maliciously-crafted MIME header containing many │ +│ │ │ │ │ │ │ invalid enc ... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42504 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴─────────────────┴──────────────────────────────────────────────────────────────┘ diff --git a/submissions/lab9/trivy/sbom.cyclonedx.json b/submissions/lab9/trivy/sbom.cyclonedx.json new file mode 100644 index 000000000..6c9f55ce1 --- /dev/null +++ b/submissions/lab9/trivy/sbom.cyclonedx.json @@ -0,0 +1,461 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:e0cf6ad0-5c26-48ea-8c90-ea357999b98b", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T20:31:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "3b835356-6e2e-47d8-a3db-3032fcd694fd", + "type": "container", + "name": "/img.tar", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:0394064ba4f0e158620900870ed87401ef71cd4c416c61c93745fed3b300f8fc" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:114dde0fefebbca13165d0da9c500a66190e497a82a53dcaabc3172d630be1e9" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:33b37ab0b0901175c2699d81c10b0c887f0dff944de74763cca00c155904d6fb" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:35b5ba6e08cdd034b8d1920f9c7203e5d66f258e1915f164a01de0af14f34710" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:431ecb96a0e9f9aa0d8b8728fdac38b0d5d2cfc010eeb9eb244da2fdcce90411" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4cde6b0bb6f50a5f255eef7b2a42162c661cf776b803225dcac9a659e396bb6b" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4d049f83d9cf21d1f5cc0e11deaf36df02790d0e60c1a3829538fb4b61685368" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:6e7fbcf090d09b73a4aa85f9dce690d0ace73877c150b7c56955487b6e7a822a" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:6f1cdceb6a3146f0ccb986521156bef8a422cdbb0863396f7f751f575ba308f4" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:84d0d191d6b82eeb33df66ecbb1673b5f470db92fbbba4397f17985a3eaeca1e" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:8fa10c0194df9b7c054c90dbe482585f768a54428fc90a5b78a0066a123b1bba" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:9ac0db7cd6b389001b18856f7926a18dff7f834c84523445e5030fd753f09245" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ad51d0769d16ba578106a177987dfe3d2e02c1668c852b795b2f6b024068242a" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:af5aa97ebe6ce1604747ec1e21af7136ded391bcabe4acef882e718a87c86bcc" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bd3cdfae1d3fdd83a2231d608969b38b82349777c2fff9a7c12d54f8ac5c9b38" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:c6c8bda1fa3f3f07cf466f70d83e73657fff72d9c22fa8ecbec3375461d64edb" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:ec82f821544ee8f53aaeccf9b2fb427dfccc214d06f934d2de2352f2c9a06afd" + }, + { + "name": "aquasecurity:trivy:Labels:io.buildah.version", + "value": "1.43.1" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "1189585f-95e6-4370-971c-90fa52842961", + "type": "operating-system", + "name": "debian", + "version": "12.14", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "52b6260d-8b77-4195-bcc1-2cf90f372f52", + "type": "application", + "name": "healthcheck", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "6902fe62-0de6-4cd6-aafd-7f366a12b035", + "type": "library", + "name": "stdlib", + "version": "v1.24.13", + "purl": "pkg:golang/stdlib@v1.24.13", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:c6c8bda1fa3f3f07cf466f70d83e73657fff72d9c22fa8ecbec3375461d64edb" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.24.13" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "803cf6d2-29f4-4028-8c4f-71720d0e1cb0", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:c6c8bda1fa3f3f07cf466f70d83e73657fff72d9c22fa8ecbec3375461d64edb" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "8a2c64de-3dfa-465b-83a3-9f78700c004b", + "type": "application", + "name": "quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "e20e817e-cd93-4599-ae42-df2ede755c27", + "type": "library", + "name": "stdlib", + "version": "v1.24.13", + "purl": "pkg:golang/stdlib@v1.24.13", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:84d0d191d6b82eeb33df66ecbb1673b5f470db92fbbba4397f17985a3eaeca1e" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.24.13" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "eb3a7d26-0ce2-497c-88b3-d7935f2daa92", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:84d0d191d6b82eeb33df66ecbb1673b5f470db92fbbba4397f17985a3eaeca1e" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@12.4%2Bdeb12u14?arch=amd64&distro=debian-12.14", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "12.4+deb12u14", + "licenses": [ + { + "license": { + "name": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/base-files@12.4%2Bdeb12u14?arch=amd64&distro=debian-12.14", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:9ac0db7cd6b389001b18856f7926a18dff7f834c84523445e5030fd753f09245" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@12.4+deb12u14" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "12.4+deb12u14" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/media-types@10.0.0?arch=all&distro=debian-12.14", + "type": "library", + "supplier": { + "name": "Mime-Support Packagers " + }, + "name": "media-types", + "version": "10.0.0", + "licenses": [ + { + "license": { + "name": "ad-hoc" + } + } + ], + "purl": "pkg:deb/debian/media-types@10.0.0?arch=all&distro=debian-12.14", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:114dde0fefebbca13165d0da9c500a66190e497a82a53dcaabc3172d630be1e9" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "media-types@10.0.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "media-types" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "10.0.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/netbase@6.4?arch=all&distro=debian-12.14", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "netbase", + "version": "6.4", + "licenses": [ + { + "license": { + "name": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/netbase@6.4?arch=all&distro=debian-12.14", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:8fa10c0194df9b7c054c90dbe482585f768a54428fc90a5b78a0066a123b1bba" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "netbase@6.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "netbase" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb12u1?arch=all&distro=debian-12.14", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2026b-0+deb12u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata@2026b-0%2Bdeb12u1?arch=all&distro=debian-12.14", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:35b5ba6e08cdd034b8d1920f9c7203e5d66f258e1915f164a01de0af14f34710" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2026b-0+deb12u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb12u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + } + ], + "dependencies": [ + { + "ref": "1189585f-95e6-4370-971c-90fa52842961", + "dependsOn": [ + "pkg:deb/debian/base-files@12.4%2Bdeb12u14?arch=amd64&distro=debian-12.14", + "pkg:deb/debian/media-types@10.0.0?arch=all&distro=debian-12.14", + "pkg:deb/debian/netbase@6.4?arch=all&distro=debian-12.14", + "pkg:deb/debian/tzdata@2026b-0%2Bdeb12u1?arch=all&distro=debian-12.14" + ] + }, + { + "ref": "3b835356-6e2e-47d8-a3db-3032fcd694fd", + "dependsOn": [ + "1189585f-95e6-4370-971c-90fa52842961", + "52b6260d-8b77-4195-bcc1-2cf90f372f52", + "8a2c64de-3dfa-465b-83a3-9f78700c004b" + ] + }, + { + "ref": "52b6260d-8b77-4195-bcc1-2cf90f372f52", + "dependsOn": [ + "803cf6d2-29f4-4028-8c4f-71720d0e1cb0" + ] + }, + { + "ref": "6902fe62-0de6-4cd6-aafd-7f366a12b035", + "dependsOn": [] + }, + { + "ref": "803cf6d2-29f4-4028-8c4f-71720d0e1cb0", + "dependsOn": [ + "6902fe62-0de6-4cd6-aafd-7f366a12b035" + ] + }, + { + "ref": "8a2c64de-3dfa-465b-83a3-9f78700c004b", + "dependsOn": [ + "eb3a7d26-0ce2-497c-88b3-d7935f2daa92" + ] + }, + { + "ref": "e20e817e-cd93-4599-ae42-df2ede755c27", + "dependsOn": [] + }, + { + "ref": "eb3a7d26-0ce2-497c-88b3-d7935f2daa92", + "dependsOn": [ + "e20e817e-cd93-4599-ae42-df2ede755c27" + ] + }, + { + "ref": "pkg:deb/debian/base-files@12.4%2Bdeb12u14?arch=amd64&distro=debian-12.14", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/media-types@10.0.0?arch=all&distro=debian-12.14", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/netbase@6.4?arch=all&distro=debian-12.14", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb12u1?arch=all&distro=debian-12.14", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/submissions/lab9/zap/baseline-after-console.txt b/submissions/lab9/zap/baseline-after-console.txt new file mode 100644 index 000000000..e717bfbc5 --- /dev/null +++ b/submissions/lab9/zap/baseline-after-console.txt @@ -0,0 +1,64 @@ +2026-07-07 20:54:30,736 Unable to copy yaml file to /zap/wrk/zap.yaml [Errno 13] Permission denied: '/zap/wrk/zap.yaml' +Using the Automation Framework +Total of 5 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +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: 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: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +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: ZAP is Out of Date [10116] +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: Charset Mismatch [90011] +PASS: Application Error Disclosure [90022] +PASS: WSDL File Detection [90030] +PASS: Loosely Scoped Cookie [90033] +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 0 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 58 +Automation plan warnings: + Job spider error accessing URL http://127.0.0.1:8080/ status code returned : 404 expected 200 diff --git a/submissions/lab9/zap/baseline-after.html b/submissions/lab9/zap/baseline-after.html new file mode 100644 index 000000000..84be3f34a --- /dev/null +++ b/submissions/lab9/zap/baseline-after.html @@ -0,0 +1,322 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://127.0.0.1:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 20:54:36 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
0
+
+
Informational
+
+
0
+
+
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
+
+ + + +

Alert Detail

+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/submissions/lab9/zap/baseline-after.json b/submissions/lab9/zap/baseline-after.json new file mode 100644 index 000000000..5ac5f0b6e --- /dev/null +++ b/submissions/lab9/zap/baseline-after.json @@ -0,0 +1,19 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 20:54:36", + "created": "2026-07-07T20:54:36.039616812Z", + "site":[ + { + "@name": "http://127.0.0.1:8080", + "@host": "127.0.0.1", + "@port": "8080", + "@ssl": "false", + "alerts": [ + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9/zap/baseline-before-console.txt b/submissions/lab9/zap/baseline-before-console.txt new file mode 100644 index 000000000..0b59022aa --- /dev/null +++ b/submissions/lab9/zap/baseline-before-console.txt @@ -0,0 +1,67 @@ +2026-07-07 20:51:12,552 Unable to copy yaml file to /zap/wrk/zap.yaml [Errno 13] Permission denied: '/zap/wrk/zap.yaml' +Using the Automation Framework +Total of 5 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +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: 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: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +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: ZAP is Out of Date [10116] +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: 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://127.0.0.1:8080/notes (200 OK) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 1 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 57 +Automation plan failures: + Job report failed to generate report: AccessDeniedException /zap/wrk/baseline-before.html +Automation plan warnings: + Job spider error accessing URL http://127.0.0.1:8080/ status code returned : 404 expected 200 diff --git a/submissions/lab9/zap/baseline-before.html b/submissions/lab9/zap/baseline-before.html new file mode 100644 index 000000000..f5a7fb07e --- /dev/null +++ b/submissions/lab9/zap/baseline-before.html @@ -0,0 +1,322 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://127.0.0.1:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 20:45:29 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
0
+
+
Informational
+
+
0
+
+
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
+
+ + + +

Alert Detail

+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/submissions/lab9/zap/baseline-before.json b/submissions/lab9/zap/baseline-before.json new file mode 100644 index 000000000..4680e3863 --- /dev/null +++ b/submissions/lab9/zap/baseline-before.json @@ -0,0 +1,19 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 20:45:29", + "created": "2026-07-07T20:45:29.612793414Z", + "site":[ + { + "@name": "http://127.0.0.1:8080", + "@host": "127.0.0.1", + "@port": "8080", + "@ssl": "false", + "alerts": [ + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9/zap/zap.yaml b/submissions/lab9/zap/zap.yaml new file mode 100644 index 000000000..28bedd1ee --- /dev/null +++ b/submissions/lab9/zap/zap.yaml @@ -0,0 +1,40 @@ +env: + contexts: + - excludePaths: [] + name: baseline + urls: + - http://127.0.0.1:8080 + parameters: + failOnError: true + progressToStdout: false +jobs: +- parameters: + enableTags: false + maxAlertsPerRule: 10 + type: passiveScan-config +- parameters: + maxDuration: 1 + url: http://127.0.0.1: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: baseline-before.html + reportTitle: ZAP Scanning Report + template: traditional-html + type: report +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: baseline-before.json + reportTitle: ZAP Scanning Report + template: traditional-json + type: report