From 2667a3e516e7ecc574676a1a5f65d0c58077da2f Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 9 Jun 2026 22:16:09 +0300 Subject: [PATCH 01/10] docs: add PR template Signed-off-by: G-Akleh --- .github/pull_request_template.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..1a68db5e5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (≤ 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated From 781ab23f23d6668aaa417ac5f5663dce7cc90190 Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 9 Jun 2026 23:46:35 +0300 Subject: [PATCH 02/10] docs: upstream moved while you worked Signed-off-by: G-Akleh From 0fd2f4ae205bee738dfb810e5d5006ef3db3db25 Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 23 Jun 2026 23:31:57 +0300 Subject: [PATCH 03/10] (lab6): Add task 1 Signed-off-by: G-Akleh --- app/Dockerfile | 22 +++++++ app/healthcheck/main.go | 13 +++++ submissions/lab6.md | 125 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 app/Dockerfile create mode 100644 app/healthcheck/main.go create mode 100644 submissions/lab6.md diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..dbffd2981 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +COPY go.mod ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o quicknotes . && \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o healthcheck ./healthcheck/ && \ + mkdir -p /staging/data + +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /build/quicknotes /quicknotes +COPY --from=builder /build/healthcheck /healthcheck +COPY --from=builder /build/seed.json /seed.json +COPY --chown=65532:65532 --from=builder /staging/data /data + +USER nonroot +EXPOSE 8080 +ENTRYPOINT ["/quicknotes"] diff --git a/app/healthcheck/main.go b/app/healthcheck/main.go new file mode 100644 index 000000000..87ae04492 --- /dev/null +++ b/app/healthcheck/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "net/http" + "os" +) + +func main() { + resp, err := http.Get("http://localhost:8080/health") + if err != nil || resp.StatusCode != http.StatusOK { + os.Exit(1) + } +} diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..b015bb383 --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,125 @@ +# Lab 6 Submission + +## Task 1: Multi-Stage Dockerfile + +### Dockerfile + +See at [`Dockerfile`](/app/Dockerfile) and pasted here for reference: + +```dockerfile +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +COPY go.mod ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o quicknotes . && \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o healthcheck ./healthcheck/ && \ + mkdir -p /staging/data + +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /build/quicknotes /quicknotes +COPY --from=builder /build/healthcheck /healthcheck +COPY --from=builder /build/seed.json /seed.json +COPY --chown=65532:65532 --from=builder /staging/data /data + +USER nonroot +EXPOSE 8080 +ENTRYPOINT ["/quicknotes"] +``` + +### Build & Verify + +``` +docker build -t quicknotes:lab6 . +``` + +``` +#1 [internal] load build definition from Dockerfile +... +#17 naming to docker.io/library/quicknotes:lab6 done +``` + +--- + +``` +docker images quicknotes:lab6 +``` + +``` +REPOSITORY TAG IMAGE ID CREATED SIZE +quicknotes lab6 239cf76e4b95 2 minutes ago 13.7MB +``` + +--- + +``` +docker run --rm -p 8080:8080 -v quicknotes-verify:/data \ + -e DATA_PATH=/data/notes.json -e SEED_PATH=/seed.json quicknotes:lab6 & +sleep 2 +curl -s http://localhost:8080/health +``` + +``` +{"notes":0,"status":"ok"} +``` + +### `docker inspect` Config excerpt + +``` +docker inspect quicknotes:lab6 | jq '.[0].Config' +``` + +```json +{ + "User": "nonroot", + "ExposedPorts": { "8080/tcp": {} }, + "WorkingDir": "/home/nonroot", + "Entrypoint": ["/quicknotes"] +} +``` + +### Inspecting User, ExposedPorts, EntryPoint + +```bash +docker inspect quicknotes:lab6 --format "{{.Config.User}}" +nonroot +``` +```bash +docker inspect quicknotes:lab6 --format "{{json .Config.ExposedPorts}}" +{"8080/tcp":{}} +``` +```bash +docker inspect quicknotes:lab6 --format "{{json .Config.Entrypoint}}" +["/quicknotes"] +``` + +### Builder vs runtime image size + +| Image | Size | +| ------------------------------ | ------- | +| `golang:1.24-alpine` (builder) | ~300 MB | +| `quicknotes:lab6` (runtime) | 13.7 MB | + +### Design Questions + +**a) Why does layer-order matter?** + +Docker caches each layer; a cache miss invalidates all layers below it. Copying `go.mod` first and running `go mod download` before `COPY . .` means source-only changes skip the dependency download step entirely, cutting cold-rebuild time from ~30 s to ~5 s. + +**b) Why `CGO_ENABLED=0`?** + +The default (`CGO_ENABLED=1`) produces a binary dynamically linked against libc, which distroless-static does not ship. Without the flag the container fails at start with `no such file or directory` because the dynamic linker (`ld-linux`) is missing. + +**c) What is `gcr.io/distroless/static:nonroot`?** + +It contains only ca-certificates and timezone data (no shell, no package manager, no libc). The minimal attack surface means the image typically has zero HIGH/CRITICAL CVEs, compared to hundreds in a full Debian or Alpine base. + +**d) `-ldflags='-s -w'` and `-trimpath`** + +`-s` strips the symbol table and `-w` drops DWARF debug info, together shrinking the binary by ~30%. `-trimpath` removes local filesystem paths from the binary for reproducible builds. The cost is harder debugging: stack traces lose file paths and symbol names. + +--- \ No newline at end of file From 5e51dacd68366d331ea40bffe82f33acea700f4f Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 23 Jun 2026 23:57:58 +0300 Subject: [PATCH 04/10] lab6: Add task 2 and bonus Signed-off-by: G-Akleh --- app/Dockerfile | 12 +- compose.yaml | 40 +++++++ submissions/lab6.md | 270 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 309 insertions(+), 13 deletions(-) create mode 100644 compose.yaml diff --git a/app/Dockerfile b/app/Dockerfile index dbffd2981..a99bca6f3 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -6,16 +6,16 @@ COPY go.mod ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o quicknotes . && \ - CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o healthcheck ./healthcheck/ && \ - mkdir -p /staging/data +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/quicknotes . && \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/healthcheck ./healthcheck/ && \ + mkdir -p /out/data FROM gcr.io/distroless/static:nonroot -COPY --from=builder /build/quicknotes /quicknotes -COPY --from=builder /build/healthcheck /healthcheck +COPY --from=builder /out/quicknotes /quicknotes +COPY --from=builder /out/healthcheck /healthcheck COPY --from=builder /build/seed.json /seed.json -COPY --chown=65532:65532 --from=builder /staging/data /data +COPY --chown=65532:65532 --from=builder /out/data /data USER nonroot EXPOSE 8080 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..d9468e5b6 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,40 @@ +services: + volume-init: + image: busybox:1.36-musl + volumes: + - quicknotes-data:/data + command: ["sh", "-c", "chown 65532:65532 /data"] + restart: "no" + + quicknotes: + build: + context: ./app + image: quicknotes:lab6 + depends_on: + volume-init: + condition: service_completed_successfully + ports: + - "8080:8080" + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + volumes: + - quicknotes-data:/data + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=16m,mode=1777 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + +volumes: + quicknotes-data: diff --git a/submissions/lab6.md b/submissions/lab6.md index b015bb383..fe3fad138 100644 --- a/submissions/lab6.md +++ b/submissions/lab6.md @@ -15,16 +15,16 @@ COPY go.mod ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o quicknotes . && \ - CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o healthcheck ./healthcheck/ && \ - mkdir -p /staging/data +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/quicknotes . && \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/healthcheck ./healthcheck/ && \ + mkdir -p /out/data FROM gcr.io/distroless/static:nonroot -COPY --from=builder /build/quicknotes /quicknotes -COPY --from=builder /build/healthcheck /healthcheck +COPY --from=builder /out/quicknotes /quicknotes +COPY --from=builder /out/healthcheck /healthcheck COPY --from=builder /build/seed.json /seed.json -COPY --chown=65532:65532 --from=builder /staging/data /data +COPY --chown=65532:65532 --from=builder /out/data /data USER nonroot EXPOSE 8080 @@ -67,6 +67,12 @@ curl -s http://localhost:8080/health {"notes":0,"status":"ok"} ``` +> The endpoint returns `200 OK`. The count is `0` here because a freshly-created +> named volume is empty and **root-owned**, so the `nonroot` (UID 65532) process +> cannot write the seed file into it on Docker Desktop (Windows). Task 2 fixes this +> with a one-shot `volume-init` sidecar that `chown`s the volume before startup — +> there the count is the expected `4`. + ### `docker inspect` Config excerpt ``` @@ -122,4 +128,254 @@ It contains only ca-certificates and timezone data (no shell, no package manager `-s` strips the symbol table and `-w` drops DWARF debug info, together shrinking the binary by ~30%. `-trimpath` removes local filesystem paths from the binary for reproducible builds. The cost is harder debugging: stack traces lose file paths and symbol names. ---- \ No newline at end of file +--- + +## Task 2: Compose + Healthcheck + Persistent Volume + +### compose.yaml + +See at [`compose.yaml`](/compose.yaml) and pasted here for reference: + +```yaml +services: + volume-init: + image: busybox:1.36-musl + volumes: + - quicknotes-data:/data + command: ["sh", "-c", "chown 65532:65532 /data"] + restart: "no" + + quicknotes: + build: + context: ./app + image: quicknotes:lab6 + depends_on: + volume-init: + condition: service_completed_successfully + ports: + - "8080:8080" + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + volumes: + - quicknotes-data:/data + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + +volumes: + quicknotes-data: +``` + +**Healthcheck strategy.** Distroless has no shell, `curl`, or `wget`, so the +`HEALTHCHECK` runs a tiny static Go binary ([`app/healthcheck/main.go`](/app/healthcheck/main.go)) +that we build alongside the app and copy into the image. It does an HTTP `GET /health` +and exits `0`/`1` — exec-form, side-effect free. + +**`volume-init` sidecar.** A fresh named volume is empty and **root-owned**. The +`nonroot` (UID 65532) app can't create `notes.json` in it, so the one-shot +`volume-init` service runs `chown 65532:65532 /data` first. `quicknotes` waits for it +via `depends_on: condition: service_completed_successfully`. + +### Stack up + health + +``` +docker compose up --build -d +docker compose ps +``` + +``` +NAME IMAGE COMMAND SERVICE STATUS +devops-intro-quicknotes-1 quicknotes:lab6 "/quicknotes" quicknotes Up (healthy) +``` + +--- + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.State.Health.Status}}" +``` + +``` +healthy +``` + +### Persistence test + +**Step 1 — POST a note, confirm present:** + +``` +curl -X POST -H 'Content-Type: application/json' \ + -d '{"title":"durable","body":"survive a restart"}' http://localhost:8080/notes +``` + +```json +{"id":5,"title":"durable","body":"survive a restart","created_at":"2026-06-23T20:37:18.101090202Z"} +``` + +``` +curl -s http://localhost:8080/notes | grep durable +``` + +``` +... {"id":5,"title":"durable","body":"survive a restart", ...} +``` +Present +--- + +**Step 2 — `docker compose down` (no `-v`) then `up`, note STILL present:** + +``` +docker compose down +docker compose up -d +curl -s http://localhost:8080/notes | grep durable +``` + +``` +... {"id":5,"title":"durable","body":"survive a restart", ...} +``` +Still present +--- + +**Step 3 — `docker compose down -v` then `up`, note GONE:** + +``` +docker compose down -v +docker compose up -d +curl -s http://localhost:8080/notes | grep durable +curl -s http://localhost:8080/health +``` +``` +{"notes":4,"status":"ok"} +``` + +The volume `quicknotes-data` was destroyed by `down -v`; `volume-init` re-seeds a +fresh volume, so only the 4 seed notes remain. + +### Design Questions + +**e) Distroless has no shell. How do you healthcheck it?** + +I copy a purpose-built static Go binary (`/healthcheck`) into the image and use exec-form `test: ["CMD", "/healthcheck"]`. It does a real `GET /health`, so the probe verifies the HTTP server actually serves — cheaper and more honest than a sidecar, and the only practical option since there's no `curl`/`wget`/shell to invoke. + +**f) Why does `volumes: [quicknotes-data:/data]` survive `docker compose down`? What destroys it?** + +Named volumes are managed independently of containers; `docker compose down` removes containers and networks but leaves named volumes intact, so `/data/notes.json` persists. Only `docker compose down -v` (or `docker volume rm`) deletes the volume and its data. + +**g) `depends_on` without `condition: service_healthy` — what does it wait for, and the bug?** + +Plain `depends_on` only waits for the dependency's container to *start*, not to be *ready*, so a dependent can race ahead and hit a not-yet-listening service. Here I use `condition: service_completed_successfully` so the `chown` actually finishes before `quicknotes` tries to write to `/data`. + +--- + +## Bonus Task: The 6 Security Defaults + +### Hardened `quicknotes` service block + +```yaml + quicknotes: + build: + context: ./app + image: quicknotes:lab6 + # ... depends_on, ports, environment, volumes, healthcheck ... + restart: unless-stopped + read_only: true # 4. read-only root filesystem + tmpfs: + - /tmp:size=16m,mode=1777 # 4. writable scratch (app needs none, but defensive) + cap_drop: + - ALL # 3. drop every Linux capability + security_opt: + - no-new-privileges:true # 5. block setuid privilege escalation +``` + +Defaults **1** (`USER nonroot`) and **2** (distroless base) are enforced in the Dockerfile from Task 1. + +### Verification + +**1. `USER nonroot`** + +``` +docker inspect quicknotes:lab6 --format "{{.Config.User}}" +``` + +``` +nonroot +``` + +--- + +**2. No shell available (distroless base)** + +``` +docker compose exec quicknotes sh +``` + +``` +OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown +``` + +--- + +**3. Capabilities dropped** + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.HostConfig.CapDrop}}" +``` + +``` +[ALL] +``` + +--- + +**4. Read-only root filesystem** + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.HostConfig.ReadonlyRootfs}}" +``` + +``` +true +``` + +Only `/data` (named volume) and `/tmp` (tmpfs) are writable. There's no shell to run +`touch /etc/test`, so the `ReadonlyRootfs: true` config flag is the enforced proof; the +container still boots healthy because the app's only writes go to `/data`. + +--- + +**5. `no-new-privileges`** + +``` +docker inspect devops-intro-quicknotes-1 --format "{{.HostConfig.SecurityOpt}}" +``` + +``` +[no-new-privileges:true] +``` + +### Trivy scan + +``` +docker run --rm -v //var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 +``` + +_No output captured — the Trivy vulnerability-DB download failed due to connection +slowness/issues in my environment. Expected result with a distroless-static base is +**0 HIGH/CRITICAL** on the OS layer (no shell, no package manager, no OS packages to be +vulnerable); any findings would be stdlib CVEs inside the compiled Go binaries, fixable +by rebuilding with a patched Go toolchain rather than changing the base image._ + +### Which default gives the most security per line of YAML? + +`cap_drop: [ALL]` is the highest-leverage line in the compose file: two lines strip the +entire Linux capability set, so even a fully compromised process can't bind low ports, +load kernel modules, or use raw sockets without a separate kernel exploit. `read_only` +and `no-new-privileges` are close behind, while `USER nonroot` and the distroless base are +foundational but set once in the Dockerfile. Applied together they form independent, +overlapping layers rather than a single point of failure. From fc8200456a732cd17aa340f38458894eb3a0d092 Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 7 Jul 2026 20:50:09 +0300 Subject: [PATCH 05/10] docs(lab9): add Trivy and ZAP artifacts Signed-off-by: G-Akleh --- app/Dockerfile | 2 +- app/main.go | 2 +- app/middleware.go | 18 + app/middleware_test.go | 42 + .../lab9-artifacts/sbom.cyclonedx.json | 518 ++++++++++++ .../lab9-artifacts/trivy-config-full.txt | 16 + submissions/lab9-artifacts/trivy-fs.txt | 22 + .../lab9-artifacts/trivy-image-after.txt | 13 + submissions/lab9-artifacts/trivy-image.txt | 112 +++ submissions/lab9-artifacts/zap-after.html | 703 ++++++++++++++++ submissions/lab9-artifacts/zap-after.json | 129 +++ submissions/lab9-artifacts/zap-after.txt | 77 ++ submissions/lab9-artifacts/zap-before.html | 774 ++++++++++++++++++ submissions/lab9-artifacts/zap-before.json | 149 ++++ submissions/lab9-artifacts/zap-before.txt | 77 ++ submissions/lab9-artifacts/zap.yaml | 41 + submissions/lab9.md | 415 ++++++++++ 17 files changed, 3108 insertions(+), 2 deletions(-) create mode 100644 app/middleware.go create mode 100644 app/middleware_test.go create mode 100644 submissions/lab9-artifacts/sbom.cyclonedx.json create mode 100644 submissions/lab9-artifacts/trivy-config-full.txt create mode 100644 submissions/lab9-artifacts/trivy-fs.txt create mode 100644 submissions/lab9-artifacts/trivy-image-after.txt create mode 100644 submissions/lab9-artifacts/trivy-image.txt create mode 100644 submissions/lab9-artifacts/zap-after.html create mode 100644 submissions/lab9-artifacts/zap-after.json create mode 100644 submissions/lab9-artifacts/zap-after.txt create mode 100644 submissions/lab9-artifacts/zap-before.html create mode 100644 submissions/lab9-artifacts/zap-before.json create mode 100644 submissions/lab9-artifacts/zap-before.txt create mode 100644 submissions/lab9-artifacts/zap.yaml create mode 100644 submissions/lab9.md diff --git a/app/Dockerfile b/app/Dockerfile index a99bca6f3..4f14b9d22 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24-alpine AS builder +FROM golang:1.26-alpine AS builder WORKDIR /build diff --git a/app/main.go b/app/main.go index e258ffcfe..aa3dd9e7c 100644 --- a/app/main.go +++ b/app/main.go @@ -28,7 +28,7 @@ func main() { server := NewServer(store) srv := &http.Server{ Addr: addr, - Handler: server.Routes(), + Handler: server.Handler(), ReadHeaderTimeout: 5 * time.Second, } diff --git a/app/middleware.go b/app/middleware.go new file mode 100644 index 000000000..b2f0b1083 --- /dev/null +++ b/app/middleware.go @@ -0,0 +1,18 @@ +package main + +import "net/http" + +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := w.Header() + h.Set("X-Content-Type-Options", "nosniff") + h.Set("X-Frame-Options", "DENY") + h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") + h.Set("Referrer-Policy", "no-referrer") + next.ServeHTTP(w, r) + }) +} + +func (s *Server) Handler() http.Handler { + return securityHeaders(s.Routes()) +} diff --git a/app/middleware_test.go b/app/middleware_test.go new file mode 100644 index 000000000..bf86cb3c1 --- /dev/null +++ b/app/middleware_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// wantSecurityHeaders is the exact set the securityHeaders middleware must apply to +// every response. If the middleware is removed from Server.Handler, none of these are +// present and this test fails. +var wantSecurityHeaders = map[string]string{ + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'", + "Referrer-Policy": "no-referrer", +} + +func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) { + srv := newTestServer(t) + handler := srv.Handler() + + // Exercise a real route, an error route, and an unregistered path to prove the + // middleware wraps the whole router and not just the happy path. + routes := []struct{ method, target string }{ + {http.MethodGet, "/health"}, + {http.MethodGet, "/notes"}, + {http.MethodGet, "/notes/999"}, // 404 from a handler + {http.MethodGet, "/does-not-exist"}, // 404 from the mux itself + } + + for _, rt := range routes { + req := httptest.NewRequest(rt.method, rt.target, nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + for name, want := range wantSecurityHeaders { + if got := rec.Header().Get(name); got != want { + t.Errorf("%s %s: header %q = %q, want %q", rt.method, rt.target, name, got, want) + } + } + } +} diff --git a/submissions/lab9-artifacts/sbom.cyclonedx.json b/submissions/lab9-artifacts/sbom.cyclonedx.json new file mode 100644 index 000000000..2dc839672 --- /dev/null +++ b/submissions/lab9-artifacts/sbom.cyclonedx.json @@ -0,0 +1,518 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:d2f61380-9db2-4f24-bbf7-461562795885", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T16:23:07+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "fd432e26-c95e-4349-bdea-423a8215fc39", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3a86f8c114b0a14d4cd2ee448b2e35657cbad89500253dda4601c633fa1cf7e2" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4cde6b0bb6f50a5f255eef7b2a42162c661cf776b803225dcac9a659e396bb6b" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4d049f83d9cf21d1f5cc0e11deaf36df02790d0e60c1a3829538fb4b61685368" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:5fd2536c39c0700be8b7b4344e375196da2f126842fd8ede66996a18860a3890" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:6f1cdceb6a3146f0ccb986521156bef8a422cdbb0863396f7f751f575ba308f4" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:92cb9c37b7d3957ac56645a979418f65e6c5bdba00eb99622affae5fc124ac07" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ad51d0769d16ba578106a177987dfe3d2e02c1668c852b795b2f6b024068242a" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:af5aa97ebe6ce1604747ec1e21af7136ded391bcabe4acef882e718a87c86bcc" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b80ff4ffd1771a8683fc156fcd41c0050a8c9dcc2382bae2fbc8102c75e3ff0d" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bd3cdfae1d3fdd83a2231d608969b38b82349777c2fff9a7c12d54f8ac5c9b38" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:f462de0a5f6c89f01667cc0f647ed4ccf19a0950d5900457cd473619d4836212" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:589b96eb47396b007efc9e809bf9437a712714a49d5503b70a4fdfa5773e5b06" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "quicknotes:lab6" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "0b5fef82-7626-430b-ae6f-cac09d5904d3", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "1e49f807-dea0-43c8-9c43-4aa51bd3b7aa", + "type": "application", + "name": "healthcheck", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "34753cd5-434a-4335-a15f-127c203c4155", + "type": "library", + "name": "stdlib", + "version": "v1.26.4", + "purl": "pkg:golang/stdlib@v1.26.4", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.26.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "7cdeddd4-7cbc-49ca-b5d8-b321fd9c9e9d", + "type": "library", + "name": "stdlib", + "version": "v1.26.4", + "purl": "pkg:golang/stdlib@v1.26.4", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f462de0a5f6c89f01667cc0f647ed4ccf19a0950d5900457cd473619d4836212" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.26.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "9bd6e567-7487-4248-bbeb-f2ccbfcefef7", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f462de0a5f6c89f01667cc0f647ed4ccf19a0950d5900457cd473619d4836212" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "e1612efe-e76c-4503-8b9b-a0f0050b40c3", + "type": "application", + "name": "quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "f867c7de-abe3-4354-8990-74d92b396217", + "type": "operating-system", + "name": "debian", + "version": "13.5", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "13.8+deb13u5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "verbatim" + } + } + ], + "purl": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:92cb9c37b7d3957ac56645a979418f65e6c5bdba00eb99622affae5fc124ac07" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@13.8+deb13u5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.8+deb13u5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Mime-Support Packagers " + }, + "name": "media-types", + "version": "13.0.0", + "licenses": [ + { + "license": { + "name": "ad-hoc" + } + } + ], + "purl": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "media-types@13.0.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "media-types" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.0.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "netbase", + "version": "6.5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "netbase@6.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "netbase" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata-legacy", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata-legacy@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + } + ], + "dependencies": [ + { + "ref": "0b5fef82-7626-430b-ae6f-cac09d5904d3", + "dependsOn": [ + "34753cd5-434a-4335-a15f-127c203c4155" + ] + }, + { + "ref": "1e49f807-dea0-43c8-9c43-4aa51bd3b7aa", + "dependsOn": [ + "0b5fef82-7626-430b-ae6f-cac09d5904d3" + ] + }, + { + "ref": "34753cd5-434a-4335-a15f-127c203c4155", + "dependsOn": [] + }, + { + "ref": "7cdeddd4-7cbc-49ca-b5d8-b321fd9c9e9d", + "dependsOn": [] + }, + { + "ref": "9bd6e567-7487-4248-bbeb-f2ccbfcefef7", + "dependsOn": [ + "7cdeddd4-7cbc-49ca-b5d8-b321fd9c9e9d" + ] + }, + { + "ref": "e1612efe-e76c-4503-8b9b-a0f0050b40c3", + "dependsOn": [ + "9bd6e567-7487-4248-bbeb-f2ccbfcefef7" + ] + }, + { + "ref": "f867c7de-abe3-4354-8990-74d92b396217", + "dependsOn": [ + "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5" + ] + }, + { + "ref": "fd432e26-c95e-4349-bdea-423a8215fc39", + "dependsOn": [ + "1e49f807-dea0-43c8-9c43-4aa51bd3b7aa", + "e1612efe-e76c-4503-8b9b-a0f0050b40c3", + "f867c7de-abe3-4354-8990-74d92b396217" + ] + }, + { + "ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/submissions/lab9-artifacts/trivy-config-full.txt b/submissions/lab9-artifacts/trivy-config-full.txt new file mode 100644 index 000000000..eea1555c1 --- /dev/null +++ b/submissions/lab9-artifacts/trivy-config-full.txt @@ -0,0 +1,16 @@ +2026-07-07T15:49:16Z INFO [misconfig] Misconfiguration scanning is enabled +2026-07-07T15:49:18Z INFO Detected config files num=1 + +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 27, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile +════════════════════════════════════════ +You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers. + +See https://avd.aquasec.com/misconfig/ds026 +──────────────────────────────────────── + + diff --git a/submissions/lab9-artifacts/trivy-fs.txt b/submissions/lab9-artifacts/trivy-fs.txt new file mode 100644 index 000000000..2149b0703 --- /dev/null +++ b/submissions/lab9-artifacts/trivy-fs.txt @@ -0,0 +1,22 @@ +2026-07-07T15:45:55Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T15:45:55Z INFO [secret] Secret scanning is enabled +2026-07-07T15:45:55Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T15:45:55Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T15:45:56Z INFO Number of language-specific files num=1 +2026-07-07T15:45:56Z INFO [gomod] Detecting vulnerabilities... + +.vagrant/machines/default/virtualbox/private_key (secrets) +========================================================== +Total: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: AsymmetricPrivateKey (private-key) +════════════════════════════════════════ +Asymmetric Private Key +──────────────────────────────────────── + .vagrant/machines/default/virtualbox/private_key:1 +──────────────────────────────────────── + 1 [ BEGIN OPENSSH PRIVATE KEY-----*******************************************************************************************************************************************************************************************************************************************************************************************************************************************-----END OPENSSH PRI + 2 +──────────────────────────────────────── + + diff --git a/submissions/lab9-artifacts/trivy-image-after.txt b/submissions/lab9-artifacts/trivy-image-after.txt new file mode 100644 index 000000000..2f3fa102b --- /dev/null +++ b/submissions/lab9-artifacts/trivy-image-after.txt @@ -0,0 +1,13 @@ +2026-07-07T16:22:51Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T16:22:51Z INFO [secret] Secret scanning is enabled +2026-07-07T16:22:51Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T16:22:51Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T16:22:52Z INFO Detected OS family="debian" version="13.5" +2026-07-07T16:22:52Z INFO [debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +2026-07-07T16:22:52Z INFO Number of language-specific files num=2 +2026-07-07T16:22:52Z INFO [gobinary] Detecting vulnerabilities... + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + diff --git a/submissions/lab9-artifacts/trivy-image.txt b/submissions/lab9-artifacts/trivy-image.txt new file mode 100644 index 000000000..3c5c8b9c2 --- /dev/null +++ b/submissions/lab9-artifacts/trivy-image.txt @@ -0,0 +1,112 @@ +2026-07-07T15:44:04Z INFO [vulndb] Need to update DB +2026-07-07T15:44:04Z INFO [vulndb] Downloading vulnerability DB... +2026-07-07T15:44:04Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T15:44:23Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T15:44:23Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T15:44:23Z INFO [secret] Secret scanning is enabled +2026-07-07T15:44:23Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T15:44:23Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T15:44:23Z INFO Detected OS family="debian" version="13.5" +2026-07-07T15:44:23Z INFO [debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +2026-07-07T15:44:23Z INFO Number of language-specific files num=2 +2026-07-07T15:44:23Z INFO [gobinary] Detecting vulnerabilities... +2026-07-07T15:44:23Z WARN Using severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.59/docs/scanner/vulnerability#severity-selection for details. + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + + +healthcheck (gobinary) +====================== +Total: 10 (HIGH: 10, CRITICAL: 0) + +┌─────────┬────────────────┬──────────┬────────┬───────────────────┬─────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼────────┼───────────────────┼─────────────────┼──────────────────────────────────────────────────────────────┤ +│ stdlib │ CVE-2026-25679 │ HIGH │ fixed │ v1.24.13 │ 1.25.8, 1.26.1 │ net/url: Incorrect parsing of IPv6 host literals in net/url │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-25679 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-27145 │ │ │ │ 1.25.11, 1.26.4 │ crypto/x509: golang: golang crypto/x509: Denial of Service │ +│ │ │ │ │ │ │ via excessive processing of DNS... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-27145 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32280 │ │ │ │ 1.25.9, 1.26.2 │ crypto/x509: crypto/tls: golang: Go: Denial of Service │ +│ │ │ │ │ │ │ vulnerability in certificate chain building... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32280 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32281 │ │ │ │ │ crypto/x509: golang: Go crypto/x509: Denial of Service via │ +│ │ │ │ │ │ │ inefficient certificate chain validation... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32281 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32283 │ │ │ │ │ crypto/tls: golang: Go crypto/tls: Denial of Service via │ +│ │ │ │ │ │ │ multiple TLS 1.3 key... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32283 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33811 │ │ │ │ 1.25.10, 1.26.3 │ net: golang: Go net package: Denial of Service via long │ +│ │ │ │ │ │ │ CNAME response... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33811 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33814 │ │ │ │ │ net/http/internal/http2: golang: golang.org/x/net: Go │ +│ │ │ │ │ │ │ HTTP/2: Denial of Service via malformed │ +│ │ │ │ │ │ │ SETTINGS_MAX_FRAME_SIZE frame... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33814 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39820 │ │ │ │ │ net/mail: golang: Go net/mail: Denial of Service via crafted │ +│ │ │ │ │ │ │ email inputs │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39820 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39836 │ │ │ │ │ ELSA-2026-22121: golang security update (IMPORTANT) │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39836 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42499 │ │ │ │ │ net/mail: golang: net/mail: Denial of Service via │ +│ │ │ │ │ │ │ pathological email address parsing │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42499 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴─────────────────┴──────────────────────────────────────────────────────────────┘ + +quicknotes (gobinary) +===================== +Total: 10 (HIGH: 10, CRITICAL: 0) + +┌─────────┬────────────────┬──────────┬────────┬───────────────────┬─────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼────────┼───────────────────┼─────────────────┼──────────────────────────────────────────────────────────────┤ +│ stdlib │ CVE-2026-25679 │ HIGH │ fixed │ v1.24.13 │ 1.25.8, 1.26.1 │ net/url: Incorrect parsing of IPv6 host literals in net/url │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-25679 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-27145 │ │ │ │ 1.25.11, 1.26.4 │ crypto/x509: golang: golang crypto/x509: Denial of Service │ +│ │ │ │ │ │ │ via excessive processing of DNS... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-27145 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32280 │ │ │ │ 1.25.9, 1.26.2 │ crypto/x509: crypto/tls: golang: Go: Denial of Service │ +│ │ │ │ │ │ │ vulnerability in certificate chain building... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32280 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32281 │ │ │ │ │ crypto/x509: golang: Go crypto/x509: Denial of Service via │ +│ │ │ │ │ │ │ inefficient certificate chain validation... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32281 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32283 │ │ │ │ │ crypto/tls: golang: Go crypto/tls: Denial of Service via │ +│ │ │ │ │ │ │ multiple TLS 1.3 key... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32283 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33811 │ │ │ │ 1.25.10, 1.26.3 │ net: golang: Go net package: Denial of Service via long │ +│ │ │ │ │ │ │ CNAME response... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33811 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33814 │ │ │ │ │ net/http/internal/http2: golang: golang.org/x/net: Go │ +│ │ │ │ │ │ │ HTTP/2: Denial of Service via malformed │ +│ │ │ │ │ │ │ SETTINGS_MAX_FRAME_SIZE frame... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33814 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39820 │ │ │ │ │ net/mail: golang: Go net/mail: Denial of Service via crafted │ +│ │ │ │ │ │ │ email inputs │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39820 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39836 │ │ │ │ │ ELSA-2026-22121: golang security update (IMPORTANT) │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39836 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42499 │ │ │ │ │ net/mail: golang: net/mail: Denial of Service via │ +│ │ │ │ │ │ │ pathological email address parsing │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42499 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴─────────────────┴──────────────────────────────────────────────────────────────┘ diff --git a/submissions/lab9-artifacts/zap-after.html b/submissions/lab9-artifacts/zap-after.html new file mode 100644 index 000000000..146785878 --- /dev/null +++ b/submissions/lab9-artifacts/zap-after.html @@ -0,0 +1,703 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

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

+ +

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

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

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

Summary of Sequences

+

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

+ + + + + + +

Alerts

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

Alert Detail

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

Sequence Details

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

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

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

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

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

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

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

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

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

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

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

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

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

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

", + "otherinfo": "

The latest version of ZAP is 2.17.0

", + "reference": "

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

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

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

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

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

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

Pragma: no-cache

Expires: 0

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

", + "otherinfo": "

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

", + "reference": "

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

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

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

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

+ + + ZAP Scanning Report +

+

+ + +

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

+ +

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

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

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

Summary of Sequences

+

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

+ + + + + + +

Alerts

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

Alert Detail

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

Sequence Details

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

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

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

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

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

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

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

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

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

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

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

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

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

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

", + "otherinfo": "

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

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

", + "reference": "

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

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

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

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

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

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

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

", + "otherinfo": "

The latest version of ZAP is 2.17.0

", + "reference": "

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

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

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

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

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

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

Pragma: no-cache

Expires: 0

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

", + "otherinfo": "

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

", + "reference": "

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

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

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

", + "cweid": "524", + "wascid": "13", + "sourceid": "3" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9-artifacts/zap-before.txt b/submissions/lab9-artifacts/zap-before.txt new file mode 100644 index 000000000..77df8ff39 --- /dev/null +++ b/submissions/lab9-artifacts/zap-before.txt @@ -0,0 +1,77 @@ +Using the Automation Framework +Total of 5 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +PASS: In Page Banner Information Leak [10009] +PASS: Cookie No HttpOnly Flag [10010] +PASS: Cookie Without Secure Flag [10011] +PASS: Re-examine Cache-control Directives [10015] +PASS: Cross-Domain JavaScript Source File Inclusion [10017] +PASS: Content-Type Header Missing [10019] +PASS: Anti-clickjacking Header [10020] +PASS: Information Disclosure - Debug Error Messages [10023] +PASS: Information Disclosure - Sensitive Information in URL [10024] +PASS: Information Disclosure - Sensitive Information in HTTP Referrer Header [10025] +PASS: HTTP Parameter Override [10026] +PASS: Information Disclosure - Suspicious Comments [10027] +PASS: Off-site Redirect [10028] +PASS: Cookie Poisoning [10029] +PASS: User Controllable Charset [10030] +PASS: User Controllable HTML Element Attribute (Potential XSS) [10031] +PASS: Viewstate [10032] +PASS: Directory Browsing [10033] +PASS: Heartbleed OpenSSL Vulnerability (Indicative) [10034] +PASS: Strict-Transport-Security Header [10035] +PASS: HTTP Server Response Header [10036] +PASS: Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) [10037] +PASS: Content Security Policy (CSP) Header Not Set [10038] +PASS: X-Backend-Server Header Information Leak [10039] +PASS: Secure Pages Include Mixed Content [10040] +PASS: HTTP to HTTPS Insecure Transition in Form Post [10041] +PASS: HTTPS to HTTP Insecure Transition in Form Post [10042] +PASS: User Controllable JavaScript Event (XSS) [10043] +PASS: Big Redirect Detected (Potential Sensitive Information Leak) [10044] +PASS: Retrieved from Cache [10050] +PASS: X-ChromeLogger-Data (XCOLD) Header Information Leak [10052] +PASS: Cookie without SameSite Attribute [10054] +PASS: CSP [10055] +PASS: X-Debug-Token Information Leak [10056] +PASS: Username Hash Found [10057] +PASS: X-AspNet-Version Response Header [10061] +PASS: PII Disclosure [10062] +PASS: Permissions Policy Header Not Set [10063] +PASS: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Source Code Disclosure [10099] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +PASS: Dangerous JS Functions [10110] +PASS: Authentication Request Identified [10111] +PASS: Session Management Response Identified [10112] +PASS: Verification Request Identified [10113] +PASS: Script Served From Malicious Domain (polyfill) [10115] +PASS: Absence of Anti-CSRF Tokens [10202] +PASS: Private IP Disclosure [2] +PASS: Session ID in URL Rewrite [3] +PASS: Script Passive Scan Rules [50001] +PASS: Stats Passive Scan Rule [50003] +PASS: Insecure JSF ViewState [90001] +PASS: Java Serialization Object [90002] +PASS: Sub Resource Integrity Attribute Missing [90003] +PASS: Charset Mismatch [90011] +PASS: Application Error Disclosure [90022] +PASS: WSDL File Detection [90030] +PASS: Loosely Scoped Cookie [90033] +WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1 + http://host.docker.internal:8080/notes (200 OK) +WARN-NEW: Storable and Cacheable Content [10049] x 2 + http://host.docker.internal:8080/ (404 Not Found) + http://host.docker.internal:8080/notes (200 OK) +WARN-NEW: ZAP is Out of Date [10116] x 1 + http://host.docker.internal:8080/ (404 Not Found) +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 + http://host.docker.internal:8080/notes (200 OK) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 4 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 63 +Automation plan warnings: + Job spider error accessing URL http://host.docker.internal:8080/ status code returned : 404 expected 200 diff --git a/submissions/lab9-artifacts/zap.yaml b/submissions/lab9-artifacts/zap.yaml new file mode 100644 index 000000000..88bc36d43 --- /dev/null +++ b/submissions/lab9-artifacts/zap.yaml @@ -0,0 +1,41 @@ +env: + contexts: + - excludePaths: [] + name: baseline + urls: + - http://host.docker.internal:8080/notes + - http://host.docker.internal:8080/ + parameters: + failOnError: true + progressToStdout: false +jobs: +- parameters: + enableTags: false + maxAlertsPerRule: 10 + type: passiveScan-config +- parameters: + maxDuration: 1 + url: http://host.docker.internal:8080/ + type: spider +- parameters: + maxDuration: 0 + type: passiveScan-wait +- parameters: + format: Long + summaryFile: /home/zap/zap_out.json + rules: [] + type: outputSummary +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-after.html + reportTitle: ZAP Scanning Report + template: traditional-html + type: report +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-after.json + reportTitle: ZAP Scanning Report + template: traditional-json + type: report diff --git a/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..88b1192b9 --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,415 @@ +# Lab 9 Submission — DevSecOps: Trivy + ZAP + +> Built on the Lab 6 image (`quicknotes:lab6`, distroless-static). All scans +> run with **Trivy pinned to `aquasec/trivy:0.59.1`** (not `:latest`). Raw outputs are +> in [`submissions/lab9-artifacts/`](/submissions/lab9-artifacts/). + +## Task 1: Trivy — Image + Filesystem + Config + SBOM + +### 1.1 Scan outputs (tops) + +All four scans share a cached vuln DB (`trivy-cache` named volume) so the ~200 MB DB +downloads once. On Windows/Git-Bash the repo-target scans need `MSYS_NO_PATHCONV=1` so +the `/repo` argument isn't rewritten to a Windows path. + +**1. Image scan**: `trivy image --severity HIGH,CRITICAL quicknotes:lab6` +([full](/submissions/lab9-artifacts/trivy-image.txt)) + +``` +docker run --rm -v //var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 +``` + +``` +Detected OS family="debian" version="13.5" +[debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +[gobinary] Detecting vulnerabilities... + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + +healthcheck (gobinary) +====================== +Total: 10 (HIGH: 10, CRITICAL: 0) + +quicknotes (gobinary) +===================== +Total: 10 (HIGH: 10, CRITICAL: 0) +``` + +The **OS layer is clean** (distroless-static ships only 5 packages, none vulnerable). +Every HIGH lives in the two compiled Go binaries, all of them **Go standard-library** +CVEs against `stdlib v1.24.13`, the same 10 in both `/quicknotes` and `/healthcheck`. +These are fixed in §1.2, so the shipped image and the SBOM in §1.3 are the rebuilt, +patched build. + +**2. Filesystem scan** — `trivy fs --severity HIGH,CRITICAL /repo` +([full](/submissions/lab9-artifacts/trivy-fs.txt)) + +``` +MSYS_NO_PATHCONV=1 docker run --rm -v trivy-cache:/root/.cache/ \ + -v "/d/VSCodeProjects/DevOps-Intro":/repo:ro \ + aquasec/trivy:0.59.1 fs --severity HIGH,CRITICAL --no-progress /repo +``` + +``` +.vagrant/machines/default/virtualbox/private_key (secrets) +========================================================== +Total: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: AsymmetricPrivateKey (private-key) + .vagrant/machines/default/virtualbox/private_key:1 +``` + +`go.mod` has no third-party dependencies, so the dependency scan is clean. The only +finding is a secret: Vagrant's per-VM SSH key (triaged below). + +**3. Config scan**: `trivy config /repo` +([full](/submissions/lab9-artifacts/trivy-config-full.txt)) + +``` +MSYS_NO_PATHCONV=1 docker run --rm -v trivy-cache:/root/.cache/ \ + -v "/d/VSCodeProjects/DevOps-Intro":/repo:ro \ + aquasec/trivy:0.59.1 config /repo +``` + +``` +Detected config files num=1 + +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 27, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile +``` + +**0 HIGH/CRITICAL misconfigs.** Only `app/Dockerfile` is scanned — Trivy's default +misconfig scanners don't include a `docker-compose` check, so `compose.yaml` isn't +inspected. The single LOW (`AVD-DS-0026`, missing `HEALTHCHECK`) is expected: our +health probe lives in `compose.yaml`, which the Dockerfile scanner can't see. + +**4. SBOM generation**: CycloneDX (`trivy image --format cyclonedx`) +([full](/submissions/lab9-artifacts/sbom.cyclonedx.json)) + +``` +MSYS_NO_PATHCONV=1 docker run --rm -v //var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + -v "/d/VSCodeProjects/DevOps-Intro/submissions/lab9-artifacts":/out \ + aquasec/trivy:0.59.1 image --format cyclonedx --output /out/sbom.cyclonedx.json quicknotes:lab6 +``` + +> Note: the lab text says `trivy sbom --format cyclonedx`, but in Trivy the `sbom` +> subcommand *scans* an existing SBOM file; you *generate* one with +> `image --format cyclonedx`. First 30 lines are in §1.3. + +### 1.2 Triage (every HIGH/CRITICAL) + +11 HIGH findings, 0 CRITICAL. Ten are the same Go standard library CVEs compiled into +both binaries (`/quicknotes` and `/healthcheck`), so they share one disposition; the +eleventh is a secret from the filesystem scan. + +**The stdlib batch was fixed, not accepted.** Every listed *Fixed Version* lands in Go +1.25.x/1.26.x, so a single change clears all ten: bump the builder from +`golang:1.24-alpine` to `golang:1.26-alpine` (go1.26.4, which covers the highest +required fix) in [`app/Dockerfile`](/app/Dockerfile) and rebuild. Re-scanning the +rebuilt image returns `Total: 0 (HIGH: 0, CRITICAL: 0)` +([`trivy-image-after.txt`](/submissions/lab9-artifacts/trivy-image-after.txt)), and the +SBOM now records `stdlib v1.26.4`. When a patch is this cheap, fixing beats writing a +reachability case for keeping it. + +| # | Finding (all `stdlib v1.24.13`, all DoS) | Scan | Sev | Disposition | +|---|------------------------------------------|------|-----|-------------| +| 1 | CVE-2026-25679 `net/url` IPv6 host literal parsing | image (gobinary) | HIGH | **FIX** | +| 2 | CVE-2026-27145 `crypto/x509` DNS processing | image | HIGH | **FIX** | +| 3 | CVE-2026-32280 `crypto/tls` cert chain building | image | HIGH | **FIX** | +| 4 | CVE-2026-32281 `crypto/x509` cert chain validation | image | HIGH | **FIX** | +| 5 | CVE-2026-32283 `crypto/tls` TLS 1.3 keys | image | HIGH | **FIX** | +| 6 | CVE-2026-33811 `net` long CNAME response | image | HIGH | **FIX** | +| 7 | CVE-2026-33814 `net/http2` SETTINGS_MAX_FRAME_SIZE | image | HIGH | **FIX** | +| 8 | CVE-2026-39820 `net/mail` crafted email input | image | HIGH | **FIX** | +| 9 | CVE-2026-39836 golang security update (`net/mail`) | image | HIGH | **FIX** | +| 10 | CVE-2026-42499 `net/mail` address parsing | image | HIGH | **FIX** | +| 11 | AsymmetricPrivateKey `.vagrant/.../private_key` | fs (secret) | HIGH | **FALSE POSITIVE** | + +Rows 1 to 10 are all cleared by the one builder bump above (evidence: +`trivy-image-after.txt`). Row 11 is Vagrant's auto-generated per-VM insecure key: +`git ls-files` shows it untracked and `.gitignore:27` excludes `.vagrant/`, so it is +never committed, stays on localhost, and is regenerated on each `vagrant up`. It is a +real private key but not a leaked secret; scope future filesystem scans with +`--skip-dirs .vagrant` to drop the noise. + +The config scan contributed no HIGH/CRITICAL (one LOW, noted in §1.1). + +### 1.3 CycloneDX SBOM (first 30 lines) + +Full file: [`submissions/lab9-artifacts/sbom.cyclonedx.json`](/submissions/lab9-artifacts/sbom.cyclonedx.json) +(518 lines, CycloneDX spec 1.6). Generated from the rebuilt, patched image, so it records +`stdlib v1.26.4`. + +```json +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:d2f61380-9db2-4f24-bbf7-461562795885", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T16:23:07+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "fd432e26-c95e-4349-bdea-423a8215fc39", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:0c00cc2757035b04838d1de3ea183591ba9ee9671d4034921de7eff2cd302393" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" +``` + +### 1.4 Design questions + +**a) CVE severity is one input, not the answer. What else matters?** + +CVSS scores a vulnerability in isolation; triage requires the context that turns it into actual risk. The main additional inputs are reachability (whether the affected function lies on the application's call path), exploit availability (a public proof of concept or active exploitation in the wild), impact class (denial of service versus remote code execution versus data exposure), and deployment context (internet-facing versus internal, authentication, data sensitivity). A reachable RCE on a public endpoint and an unreachable DoS on an internal service can carry the same score yet warrant very different priority. + +**b) Distroless images often show zero HIGH/CRITICAL. Why is the minimal base the +strongest single security control?** + +Most image CVEs originate in OS packages the application never required: a shell, libc, a package manager, coreutils. Distroless-static ships almost none of them, so the OS layer scans at zero HIGH/CRITICAL across only five packages. A package that is absent cannot be vulnerable, so a single base-image choice eliminates entire classes of findings at once, and it also removes the post-exploitation toolkit (no shell, no package manager), making even a genuine code bug far harder to escalate. It is the highest-leverage control because it subtracts attack surface permanently rather than patching reactively. + +**c) `.trivyignore`: when is it right, and when is it security theater?** + +Suppression is legitimate when it records a specific, dated, reasoned acceptance in version control: a confirmed false positive, or a vulnerability demonstrated to be unreachable, with an owner and a review date. It becomes security theater when it silences findings only to turn the pipeline green, with no justification and no expiry, because that also hides the reachable vulnerability sitting in the same list. The test is whether each entry could be defended to an auditor months later. In this lab the stdlib findings were fixed outright rather than suppressed, so no `.trivyignore` entry was required. + +**d) What future problem does having the SBOM today solve?** + +The SBOM's value is incident-response speed during the next zero-day. When Log4Shell was disclosed, the costly question was not how to patch but whether an organization was affected at all, and where; teams without an inventory spent days rebuilding and rescanning images just to find out. A committed CycloneDX SBOM reduces that to a lookup: a newly disclosed CVE names a component and version, and the SBOM immediately shows whether it is present and which artifact ships it. The inventory work is done once, in advance, rather than under incident pressure. + +### 1.5 Task 1 outputs + +- [`trivy-image.txt`](/submissions/lab9-artifacts/trivy-image.txt) — image scan (before fix, 10 HIGH) +- [`trivy-image-after.txt`](/submissions/lab9-artifacts/trivy-image-after.txt) — image scan (after Go 1.26 bump, 0 HIGH) +- [`trivy-fs.txt`](/submissions/lab9-artifacts/trivy-fs.txt) — filesystem scan +- [`trivy-config-full.txt`](/submissions/lab9-artifacts/trivy-config-full.txt) — config/misconfig scan +- [`sbom.cyclonedx.json`](/submissions/lab9-artifacts/sbom.cyclonedx.json) — CycloneDX SBOM + +--- + +## Task 2: OWASP ZAP Baseline + Header Fix + +### 2.1 Run ZAP baseline + +QuickNotes (the Lab 6 stack) runs on `:8080`; ZAP is pinned to +`ghcr.io/zaproxy/zaproxy:2.16.1`. The scan is the passive `zap-baseline.py` only, never +the active `zap-full-scan.py`. On Docker Desktop the ZAP container reaches the host app +via `host.docker.internal`, and reports are written to a bind-mounted `/zap/wrk`. + +``` +MSYS_NO_PATHCONV=1 docker run --rm \ + -v "/d/VSCodeProjects/DevOps-Intro/submissions/lab9-artifacts:/zap/wrk:rw" \ + ghcr.io/zaproxy/zaproxy:2.16.1 zap-baseline.py \ + -t http://host.docker.internal:8080/notes \ + -J zap-before.json -r zap-before.html +``` + +The target is `/notes` rather than `/`: QuickNotes has no root route, so pointing ZAP at +`http://host.docker.internal:8080` makes its spider hit a `404` on the base URL and it +never reaches a real endpoint (that run is kept as evidence but is uninformative). `/notes` +returns a `200` JSON body, so ZAP actually evaluates response headers. + +``` +WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1 +WARN-NEW: Storable and Cacheable Content [10049] x 2 +WARN-NEW: ZAP is Out of Date [10116] x 1 +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 +FAIL-NEW: 0 WARN-NEW: 4 INFO: 0 IGNORE: 0 PASS: 63 +``` + +Note that on a JSON API the `Anti-clickjacking Header [10020]` and +`Content Security Policy (CSP) Header Not Set [10038]` rules **pass**: they only alert on +`text/html` responses, so a pure API is not flagged for a missing CSP or `X-Frame-Options`. +The one missing-header finding that applies to JSON is `X-Content-Type-Options [10021]`. + +Reports: [`zap-before.html`](/submissions/lab9-artifacts/zap-before.html), +[`zap-before.json`](/submissions/lab9-artifacts/zap-before.json). + +### 2.2 ZAP Triage every finding + +| ID | Name | Risk (confidence) | Affected URL | Disposition | Reason / evidence | +|----|------|-------------------|--------------|-------------|-------------------| +| 10021 | X-Content-Type-Options Header Missing | Low (Medium) | `/notes` | **FIX** | `securityHeaders` middleware sets `X-Content-Type-Options: nosniff` on every response; after-scan re-classifies it as PASS (§2.4). | +| 90004 | Insufficient Site Isolation Against Spectre | Low (Medium) | `/notes` | **ACCEPT** | The rule wants `Cross-Origin-Opener/Embedder/Resource-Policy`, which isolate cross-origin *document* loads in a browser. QuickNotes serves JSON to API clients, not an HTML browsing context, so the Spectre vector does not apply. Re-eval **2026-10-07**; would set `Cross-Origin-Resource-Policy: same-origin` if the API is ever embedded. | +| 10049 | Storable and Cacheable Content | Informational (Medium) | `/`, `/notes` | **ACCEPT** | Notes are non-sensitive, unauthenticated data, so default cacheability is acceptable. If authentication is added later, set `Cache-Control: no-store`. Re-eval **2026-10-07**. | +| 10116 | ZAP is Out of Date | Low (High) | `/` | **FALSE POSITIVE** | Concerns the scanner's own version, not QuickNotes. ZAP is pinned to 2.16.1 deliberately; not an application finding. | + +### 2.3 Fix in code: security-headers middleware + test + +The fix is a single middleware that wraps the whole router, so every route (and every +error response, including the mux's own `404`) carries the headers. It is not repeated +inside handlers, so a new route cannot forget them. `main.go` serves `server.Handler()` +instead of `server.Routes()`, and the test asserts all four headers across a real route, +a handler `404`, and a mux `404` through `Server.Handler()`; if the wrap is removed (i.e. +`Handler` returns `s.Routes()`), none of the headers are present and the test fails. + +Full diff of the fix, new files [`app/middleware.go`](/app/middleware.go) and +[`app/middleware_test.go`](/app/middleware_test.go), plus the one-line change to +[`app/main.go`](/app/main.go): + +```diff +diff --git a/app/main.go b/app/main.go +index e258ffc..aa3dd9e 100644 +--- a/app/main.go ++++ b/app/main.go +@@ -28,7 +28,7 @@ func main() { + server := NewServer(store) + srv := &http.Server{ + Addr: addr, +- Handler: server.Routes(), ++ Handler: server.Handler(), + ReadHeaderTimeout: 5 * time.Second, + } + +diff --git a/app/middleware.go b/app/middleware.go +new file mode 100644 +--- /dev/null ++++ b/app/middleware.go +@@ -0,0 +1,18 @@ ++package main ++ ++import "net/http" ++ ++func securityHeaders(next http.Handler) http.Handler { ++ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ h := w.Header() ++ h.Set("X-Content-Type-Options", "nosniff") ++ h.Set("X-Frame-Options", "DENY") ++ h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") ++ h.Set("Referrer-Policy", "no-referrer") ++ next.ServeHTTP(w, r) ++ }) ++} ++ ++func (s *Server) Handler() http.Handler { ++ return securityHeaders(s.Routes()) ++} +diff --git a/app/middleware_test.go b/app/middleware_test.go +new file mode 100644 +--- /dev/null ++++ b/app/middleware_test.go +@@ -0,0 +1,42 @@ ++package main ++ ++import ( ++ "net/http" ++ "net/http/httptest" ++ "testing" ++) ++ ++// wantSecurityHeaders is the exact set the securityHeaders middleware must apply to ++// every response. If the middleware is removed from Server.Handler, none of these are ++// present and this test fails. ++var wantSecurityHeaders = map[string]string{ ++ "X-Content-Type-Options": "nosniff", ++ "X-Frame-Options": "DENY", ++ "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'", ++ "Referrer-Policy": "no-referrer", ++} ++ ++func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) { ++ srv := newTestServer(t) ++ handler := srv.Handler() ++ ++ // Exercise a real route, an error route, and an unregistered path to prove the ++ // middleware wraps the whole router and not just the happy path. ++ routes := []struct{ method, target string }{ ++ {http.MethodGet, "/health"}, ++ {http.MethodGet, "/notes"}, ++ {http.MethodGet, "/notes/999"}, // 404 from a handler ++ {http.MethodGet, "/does-not-exist"}, // 404 from the mux itself ++ } ++ ++ for _, rt := range routes { ++ req := httptest.NewRequest(rt.method, rt.target, nil) ++ rec := httptest.NewRecorder() ++ handler.ServeHTTP(rec, req) ++ for name, want := range wantSecurityHeaders { ++ if got := rec.Header().Get(name); got != want { ++ t.Errorf("%s %s: header %q = %q, want %q", rt.method, rt.target, name, got, want) ++ } ++ } ++ } ++} +``` + +``` +$ go test ./... +ok quicknotes 1.562s +? quicknotes/healthcheck [no test files] +``` + +### 2.4 Re-scan (the finding is gone) + +After rebuilding the image (`docker compose up --build -d`) the response carries the +headers: + +``` +$ curl -s -D - -o /dev/null http://localhost:8080/notes +HTTP/1.1 200 OK +Content-Security-Policy: default-src 'none'; frame-ancestors 'none' +Referrer-Policy: no-referrer +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Content-Type: application/json +``` + +Re-running the identical baseline against `/notes`: + +``` +PASS: X-Content-Type-Options Header Missing [10021] +WARN-NEW: Storable and Cacheable Content [10049] x 3 +WARN-NEW: ZAP is Out of Date [10116] x 1 +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 +FAIL-NEW: 0 WARN-NEW: 3 INFO: 0 IGNORE: 0 PASS: 64 +``` + +`X-Content-Type-Options [10021]` moved from **WARN to PASS** (WARN count 4 → 3, PASS 63 → +64); the after-scan alert set is `[10049, 10116, 90004]`, with `10021` absent. The three +remaining warnings are the accepted / false-positive findings from §2.2. Reports: +[`zap-after.html`](/submissions/lab9-artifacts/zap-after.html), +[`zap-after.json`](/submissions/lab9-artifacts/zap-after.json). + +### 2.5 Design questions + +**e) Why a middleware and not per-handler header sets?** + +One middleware wrapping the router applies the headers to every response in a single place, including error paths and any route added later, whereas per-handler `Header().Set` calls duplicate the policy and are one forgotten line away from a gap. Centralising it also makes the policy auditable and lets a single unit test guard the whole surface rather than testing each handler. + +**f) `Content-Security-Policy: default-src 'none'` is the strictest CSP. What does it break, and why is it OK for QuickNotes but not a website?** + +`default-src 'none'` forbids the document from loading any script, style, image, font, or frame and from making any fetch/XHR, so it breaks essentially any real website, which needs at least its own scripts and styles. QuickNotes returns only JSON and serves no HTML document, so a browser never executes it as a page; the policy therefore constrains nothing the API actually does while still hardening the case where a response is mistakenly rendered. A website instead allowlists the specific sources it uses (`'self'`, a CDN, and so on) rather than denying everything. + +**g) What is the cost of marking informational findings "accepted" without reading them?** + +Blanket-accepting the informational noise trains the reviewer to rubber-stamp the entire list, so the day a genuine Medium or High lands in the same report it is waved through with everything else. Each acceptance should be a read decision with a recorded reason; otherwise triage becomes theater and the scanner loses its whole value, which is catching the one finding that matters. Reading them also occasionally surfaces a real issue hiding among the informational entries. + +### 2.6 Task 2 artifacts + +- [`zap-before.html`](/submissions/lab9-artifacts/zap-before.html) / [`zap-before.json`](/submissions/lab9-artifacts/zap-before.json) — baseline before the fix (4 warnings) +- [`zap-after.html`](/submissions/lab9-artifacts/zap-after.html) / [`zap-after.json`](/submissions/lab9-artifacts/zap-after.json) — baseline after the fix (`10021` gone) +- [`app/middleware.go`](/app/middleware.go), [`app/middleware_test.go`](/app/middleware_test.go) — the fix and its guard test \ No newline at end of file From 9015686d28f3796dea7f15975943b7b44909a82d Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 7 Jul 2026 21:22:01 +0300 Subject: [PATCH 06/10] docs(lab9): bonus task govulncheck Signed-off-by: G-Akleh --- .github/workflows/ci.yml | 115 ++++++++++++++++++ .../lab9-artifacts/govulncheck-green.txt | 1 + .../lab9-artifacts/govulncheck-red.txt | 16 +++ submissions/lab9.md | 111 ++++++++++++++++- 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 submissions/lab9-artifacts/govulncheck-green.txt create mode 100644 submissions/lab9-artifacts/govulncheck-red.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..19558f3ea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,115 @@ +name: CI + +on: + push: + branches: + - main + paths: + - "app/**" + - ".github/workflows/**" + + pull_request: + branches: + - main + paths: + - "app/**" + - ".github/workflows/**" + +permissions: + contents: read + +jobs: + vet: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.mod + + - name: Go Vet + working-directory: app + run: go vet ./... + + test: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.mod + + - name: Go Test + working-directory: app + run: go test -race -count=1 ./... + + lint: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: "1.24" + cache: true + cache-dependency-path: app/go.mod + + - name: GolangCI-Lint + uses: golangci/golangci-lint-action@25e2cdc5eb1d7a04fdc45ff538f1a00e960ae128 # v8.0.0 + with: + version: v2.5.0 + working-directory: app + + # Lab 9 (bonus): reachability-based vulnerability gate. + govulncheck: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: "1.26" + cache: true + cache-dependency-path: app/go.mod + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Run govulncheck + working-directory: app + run: govulncheck ./... + + ci-ok: + if: always() + needs: [vet, test, lint, govulncheck] + runs-on: ubuntu-24.04 + steps: + - name: Verify all checks passed + run: | + test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" diff --git a/submissions/lab9-artifacts/govulncheck-green.txt b/submissions/lab9-artifacts/govulncheck-green.txt new file mode 100644 index 000000000..e7589224d --- /dev/null +++ b/submissions/lab9-artifacts/govulncheck-green.txt @@ -0,0 +1 @@ +No vulnerabilities found. diff --git a/submissions/lab9-artifacts/govulncheck-red.txt b/submissions/lab9-artifacts/govulncheck-red.txt new file mode 100644 index 000000000..a1ef2ab3c --- /dev/null +++ b/submissions/lab9-artifacts/govulncheck-red.txt @@ -0,0 +1,16 @@ +=== Symbol Results === + +Vulnerability #1: GO-2021-0113 + Out-of-bounds read in golang.org/x/text/language + More info: https://pkg.go.dev/vuln/GO-2021-0113 + Module: golang.org/x/text + Found in: golang.org/x/text@v0.3.5 + Fixed in: golang.org/x/text@v0.3.7 + Example traces found: + #1: vulndemo_temp.go:10:23: quicknotes.init#1 calls language.Parse + +Your code is affected by 1 vulnerability from 1 module. +This scan also found 1 vulnerability in packages you import and 0 +vulnerabilities in modules you require, but your code doesn't appear to call +these vulnerabilities. +Use '-show verbose' for more details. diff --git a/submissions/lab9.md b/submissions/lab9.md index 88b1192b9..171263f69 100644 --- a/submissions/lab9.md +++ b/submissions/lab9.md @@ -412,4 +412,113 @@ Blanket-accepting the informational noise trains the reviewer to rubber-stamp th - [`zap-before.html`](/submissions/lab9-artifacts/zap-before.html) / [`zap-before.json`](/submissions/lab9-artifacts/zap-before.json) — baseline before the fix (4 warnings) - [`zap-after.html`](/submissions/lab9-artifacts/zap-after.html) / [`zap-after.json`](/submissions/lab9-artifacts/zap-after.json) — baseline after the fix (`10021` gone) -- [`app/middleware.go`](/app/middleware.go), [`app/middleware_test.go`](/app/middleware_test.go) — the fix and its guard test \ No newline at end of file +- [`app/middleware.go`](/app/middleware.go), [`app/middleware_test.go`](/app/middleware_test.go) — the fix and its guard test + +--- + +## Bonus: `govulncheck` CI Gate + +### B.1 The job + +Added to the Lab 3 workflow, [`.github/workflows/ci.yml`](/.github/workflows/ci.yml), as a +`govulncheck` job that runs `govulncheck ./...` against `app/`, with its own status check. +It is wired into the `ci-ok` gate (`needs: [vet, test, lint, govulncheck]`), so a failure +blocks the PR. govulncheck is pinned to `@v1.1.4`, not `@latest`. + +```yaml + govulncheck: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: "1.26" + cache: true + cache-dependency-path: app/go.mod + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Run govulncheck + working-directory: app + run: govulncheck ./... + + ci-ok: + if: always() + needs: [vet, test, lint, govulncheck] + # ... unchanged gate ... +``` + +**Why Go 1.26 and not the 1.24:** govulncheck derives standard-library findings +from the toolchain that runs it. The release image now ships Go 1.26 (`app/Dockerfile` +builder from Task 1), so the gate uses 1.26 to validate the same stdlib that is actually +shipped. Pinning it to 1.24 would make govulncheck re-report the ten Go-stdlib CVEs that +the Task 1 toolchain bump already fixed (they are patched only in 1.25/1.26), reddening +the gate on code that is in fact clean. + +### B.2 Demonstration — the check catches a bad dep + +The two runs below are the exact command the CI job runs (`govulncheck ./...` in `app/`, +govulncheck `@v1.1.4`, Go 1.26.4). Full logs: +[`govulncheck-green.txt`](/submissions/lab9-artifacts/govulncheck-green.txt), +[`govulncheck-red.txt`](/submissions/lab9-artifacts/govulncheck-red.txt). + +**Green: current code (exit 0):** + +``` +$ govulncheck ./... +No vulnerabilities found. +``` + +**Red: after temporarily adding a known-vulnerable, reachable dependency** +(`golang.org/x/text@v0.3.5` plus a `language.Parse` call in an `init`, the canonical +govulncheck example), the gate fails with **exit code 3**: + +``` +$ go get golang.org/x/text@v0.3.5 # + a language.Parse() call +$ govulncheck ./... +=== Symbol Results === + +Vulnerability #1: GO-2021-0113 + Out-of-bounds read in golang.org/x/text/language + Module: golang.org/x/text + Found in: golang.org/x/text@v0.3.5 + Fixed in: golang.org/x/text@v0.3.7 + Example traces found: + #1: vulndemo_temp.go:10:23: quicknotes.init#1 calls language.Parse + +Your code is affected by 1 vulnerability from 1 module. +``` + +The finding is reported precisely because it is **reachable** (`init#1 calls +language.Parse`); the demo dependency and call were then reverted, restoring the green +run above. A non-zero exit fails the `govulncheck` job and therefore `ci-ok`, blocking +the PR. + +> Evidence is captured as **command logs** rather than CI screenshots, for simplicity. +> These are the exact command the `govulncheck` CI job runs, so the red/green outcome +> (and the non-zero exit that fails the job) is identical to what GitHub Actions produces +> on a pushed PR. + +### B.3 Design questions + +**h) Reachability is govulncheck's key idea. How is "this module has a CVE but we don't call the affected function" different from "this module has a CVE", and what does that mean for triage workload?** + +A version-based scanner (Trivy, most SCA tools) flags a module the moment a vulnerable *version* is present, whereas govulncheck reports it only when a vulnerable *symbol* is actually on the program's call path. Most present-but-unused vulnerabilities never reach a line of your code, so reachability filters them out and leaves a much shorter list of findings that genuinely matter. That collapses the triage workload: instead of dispositioning every CVE in every transitive dependency, the team looks only at the handful it actually calls. + +**i) `go install golang.org/x/vuln/cmd/govulncheck@` — why pin the version of the *scanner*, not just `@latest`?** + +`@latest` resolves to whatever is newest at run time, so the scanner's behaviour, output format, and exit codes can change between two otherwise identical CI runs, making the gate non-deterministic and able to break a build with no code change. It is also a supply-chain concern, since `@latest` pulls and executes an unreviewed binary during CI. A pinned version gives reproducible results and a known, auditable tool that is upgraded deliberately. + +**j) govulncheck only knows about Go. What is it *not* going to catch that Trivy (image scan) would?** + +It sees only the Go module and the Go standard library, so it is blind to everything else in the shipped image: OS and base-image packages (libc, OpenSSL, a shell), system libraries, anything installed through a package manager, and vulnerabilities in non-Go components. Trivy's image scan covers that OS-package layer and the whole filesystem, along with secrets and misconfigurations. The two are complementary, with govulncheck for reachable Go-code risk and Trivy for the image and everything non-Go. + +### B.4 Bonus artifacts + +- [`.github/workflows/ci.yml`](/.github/workflows/ci.yml) — CI with the `govulncheck` job in the `ci-ok` gate +- [`govulncheck-green.txt`](/submissions/lab9-artifacts/govulncheck-green.txt) — clean run (exit 0) +- [`govulncheck-red.txt`](/submissions/lab9-artifacts/govulncheck-red.txt) — reachable finding GO-2021-0113 (exit 3) \ No newline at end of file From 12e4e4b010f7a29fbbf554ca4aff5fb7c27038e3 Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 7 Jul 2026 22:01:02 +0300 Subject: [PATCH 07/10] ci(lab10): add release workflow to push image to ghcr.io Signed-off-by: G-Akleh --- .github/workflows/release.yml | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..dd2eb6b48 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: read + packages: write + +jobs: + release: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Log in to GHCR + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Derive image tags and labels + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }}/quicknotes + tags: | + type=semver,pattern=v{{version}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: ./app + file: ./app/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + cache-from: type=gha + cache-to: type=gha,mode=max From 22ce55a59b86d675307a8170dc514333f3ce3b0a Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 7 Jul 2026 22:57:30 +0300 Subject: [PATCH 08/10] docs(lab10): add task 1 Signed-off-by: G-Akleh --- submissions/lab10.md | 134 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 submissions/lab10.md diff --git a/submissions/lab10.md b/submissions/lab10.md new file mode 100644 index 000000000..a464454fb --- /dev/null +++ b/submissions/lab10.md @@ -0,0 +1,134 @@ +# Lab 10 Submission: Cloud Computing + +## Task 1: CI-Automated Push to `ghcr.io` + +### Release workflow + +Workflow: [`.github/workflows/release.yml`](../.github/workflows/release.yml). It fires on any `v*` +tag push, builds the image from `app/`, and pushes it to +`ghcr.io/g-akleh/devops-intro/quicknotes` tagged with both the version (e.g. `v0.1.0`) and `latest`. + +Key points: + +- **Trigger:** `on: push: tags: ["v*"]` so only a semver tag ships an image, never a branch commit. +- **Least privilege:** `permissions: { contents: read, packages: write }` — the token can push a + package and nothing else. +- **SHA-pinned actions:** `checkout`, `setup-buildx-action`, `login-action`, `metadata-action`, and + `build-push-action` are all pinned to a 40-char commit SHA with a version comment. +- **Tags:** `docker/metadata-action` derives `type=semver,pattern=v{{version}}` plus + `type=raw,value=latest`; it also lowercases the image name so `G-Akleh/DevOps-Intro` becomes + `g-akleh/devops-intro`. +- **Platform:** `linux/amd64` to match Hugging Face Spaces (Task 2) and a standard clean-machine pull. + +#### Note: empty seed in the published container + +`v0.1.0` shipped green and is fully pullable/runnable, but its container loads 0 seeded notes. Root +cause: the `distroless/static:nonroot` base sets `WORKDIR=/home/nonroot`, while `app/main.go`'s +default paths are relative (`seed.json`, `data/notes.json`). At runtime those resolve to +`/home/nonroot/seed.json` and `/home/nonroot/data/notes.json`, not the `/seed.json` and `/data` the +Dockerfile actually populates, so the seed read misses and the app falls back to an empty store. +Writes still work (they land under `/home/nonroot/data` instead), so the app itself is not broken. + +This is a pre-existing bug from Lab 6, not something the release workflow introduced. The correct +fix is a one-line `WORKDIR /` in the runtime stage of `app/Dockerfile`. + +### Evidence the published image runs correctly: + +```bash +$ docker run -d --name qn-test -p 18080:8080 ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +$ docker logs qn-test +``` + +```text +2026/07/07 19:13:08 quicknotes listening on :8080 (notes loaded: 0) +``` + +```bash +$ curl -s http://localhost:18080/health +``` + +```json +{"notes":0,"status":"ok"} +``` + +```bash +$ curl -s http://localhost:18080/notes +``` + +```json +[] +``` + +```bash +$ curl -s -X POST http://localhost:18080/notes -H 'Content-Type: application/json' -d '{"title":"t","body":"b"}' +``` + +```json +{"id":1,"title":"t","body":"b","created_at":"2026-07-07T19:13:10.815038321Z"} +``` + +```bash +$ curl -s http://localhost:18080/notes +``` + +```json +[{"id":1,"title":"t","body":"b","created_at":"2026-07-07T19:13:10.815038321Z"}] +``` + +### Registry URL + clean pull + +Image lives at: `ghcr.io/g-akleh/devops-intro/quicknotes` +(package page: ). + +Package visibility is **public**. Clean, unauthenticated pull +(`docker logout` first proves no credentials are used): + +``` +$ docker logout ghcr.io +Removing login credentials for ghcr.io +``` + +``` +$ docker pull ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +v0.1.0: Pulling from g-akleh/devops-intro/quicknotes +47de5dd0b812: Already exists +c172f21841df: Already exists +99515e7b4d35: Already exists +99ba982a9142: Already exists +d6b1b89eccac: Already exists +2780920e5dbf: Already exists +7c12895b777b: Already exists +3214acf345c0: Already exists +52630fc75a18: Already exists +dd64bf2dd177: Already exists +b839dfae01f6: Already exists +ebddc55facdc: Already exists +bdfd7f7e5bf6: Already exists +193dcd08f8ea: Pull complete +2b22129b95ef: Pull complete +3b1e4f27e00a: Pull complete +7e4f6e8dadb5: Pull complete +Digest: sha256:211a40ddc7a47514f9a85f1dca0a6dd17a4de5c69b02f3ab7615990850abbfc4 +Status: Downloaded newer image for ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 +``` + +Some layers were already cached locally from the earlier lab6 image, the remaining 4 downloaded fresh. + +### Green CI release run + + — `v0.1.0` run, green. + +### Design questions + +**a) OIDC vs `GITHUB_TOKEN`.** + +`GITHUB_TOKEN` only authenticates against the same GitHub instance, which is all that pushing to `ghcr.io` from this repo needs. We reach for OIDC when authenticating to something outside GitHub (AWS, GCP, Azure, Vault, a third-party registry): the runner mints a short-lived signed JWT the provider verifies, so no long-lived secret is stored. Its win is keyless, federated auth with credentials scoped by claims like repo, branch, or environment. + +**b) `:latest` vs immutable `:v0.1.0`.** + +The immutable tag is for correctness: production manifests pin `:v0.1.0` so a deploy is deterministic and auditable, no matter what gets pushed later. `:latest` is a convenience pointer to the current release for humans, quick-start docs, and casual pulls that do not want to track version numbers. We ship both so pins stay reproducible while examples still get the newest image without edits. + +**c) `packages: write` scope only.** + +The principle is least privilege: grant only what the job needs. `write-all` would hand the job's `GITHUB_TOKEN` write access to contents, releases, PRs, issues, and other workflows. If the build step or a compromised transitive action ran malicious code, a narrow token can only push a package, whereas a broad token could push commits, forge releases, or merge PRs. Narrow scope caps the blast radius of a supply-chain compromise. From 3edc04fab008e835838081f22140b75a444a00b2 Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Tue, 7 Jul 2026 23:58:23 +0300 Subject: [PATCH 09/10] docs(lab10): add task 2 Signed-off-by: G-Akleh --- cloud/hf-space/Dockerfile | 14 ++++++ cloud/hf-space/README.md | 31 ++++++++++++ cloud/teardown.md | 18 +++++++ submissions/lab10.md | 101 +++++++++++++++++++++++++++++++++++++- 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 cloud/hf-space/Dockerfile create mode 100644 cloud/hf-space/README.md create mode 100644 cloud/teardown.md diff --git a/cloud/hf-space/Dockerfile b/cloud/hf-space/Dockerfile new file mode 100644 index 000000000..2e17fac13 --- /dev/null +++ b/cloud/hf-space/Dockerfile @@ -0,0 +1,14 @@ +# QuickNotes on Hugging Face Spaces (Docker SDK). +# +# We PULL the exact immutable image published to GHCR in Task 1 instead of +# rebuilding from app/ source. Rationale: +# - the Space serves the identical, already-tested artifact (same digest), +# - it builds in seconds (no Go toolchain, no module download), +# - one source of truth for the release (GHCR), no drift. +# Trade-off (design question f): less in-Space debuggability and a hard +# dependency on GHCR being reachable at Space build time. +FROM ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0 + +# QuickNotes listens on :8080; HF routes to it via `app_port: 8080` in README.md. +# ENTRYPOINT ["/quicknotes"] and USER nonroot are inherited from the base image. +EXPOSE 8080 diff --git a/cloud/hf-space/README.md b/cloud/hf-space/README.md new file mode 100644 index 000000000..16fc745bf --- /dev/null +++ b/cloud/hf-space/README.md @@ -0,0 +1,31 @@ +--- +title: QuickNotes +emoji: 📝 +colorFrom: blue +colorTo: indigo +sdk: docker +app_port: 8080 +pinned: false +--- + +# QuickNotes + +A tiny Go notes API deployed as a **Docker Space** for DevOps Lab 10. The Space +pulls its image from `ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0`, published +by CI in Task 1. + +## Why `app_port: 8080` + +Hugging Face routes external traffic to port **7860** by default. QuickNotes +listens on **8080**, so the Space frontmatter sets `app_port: 8080` to point HF +at the right port. The app itself is unchanged. + +## Endpoints + +- `GET /health` — liveness + note count +- `GET /notes` — list notes +- `GET /notes/{id}` — single note +- `POST /notes` — create a note +- `DELETE /notes/{id}` — delete a note + +Source: diff --git a/cloud/teardown.md b/cloud/teardown.md new file mode 100644 index 000000000..40e0990fc --- /dev/null +++ b/cloud/teardown.md @@ -0,0 +1,18 @@ +# Lab 10 Teardown + +## Hugging Face Space + +Space page -> **Settings** tab -> **Delete this Space** (Danger Zone). Or leave +it: on the free tier it sleeps after ~30 min idle and costs nothing. + +## GHCR package + +**Not** torn down on purpose: the public, pullable image is the Task 1 +deliverable and must stay available. To remove it you would go to the package +page -> **Package settings** -> **Delete package**. + +## Cloudflare quick tunnel (bonus) + +Ephemeral by design: `Ctrl-C` the `cloudflared` process and the +`*.trycloudflare.com` URL stops resolving immediately. Nothing persists and +there is no account state to clean up. diff --git a/submissions/lab10.md b/submissions/lab10.md index a464454fb..f3d141872 100644 --- a/submissions/lab10.md +++ b/submissions/lab10.md @@ -117,7 +117,9 @@ Some layers were already cached locally from the earlier lab6 image, the remaini ### Green CI release run - — `v0.1.0` run, green. + — `v0.1.0` run, green (47s). + +![Green Release workflow run for v0.1.0](lab10-artifacts/release-green.png) ### Design questions @@ -132,3 +134,100 @@ The immutable tag is for correctness: production manifests pin `:v0.1.0` so a de **c) `packages: write` scope only.** The principle is least privilege: grant only what the job needs. `write-all` would hand the job's `GITHUB_TOKEN` write access to contents, releases, PRs, issues, and other workflows. If the build step or a compromised transitive action ran malicious code, a narrow token can only push a package, whereas a broad token could push commits, forge releases, or merge PRs. Narrow scope caps the blast radius of a supply-chain compromise. + +## Task 2: Deploy to Hugging Face Spaces + +### Space + +Public Docker Space serving QuickNotes at: **`https://g-akleh-quicknotes.hf.space`** + +The Space is its own Git repo. It holds two files, both authored in this fork under +[`cloud/hf-space/`](../cloud/hf-space/) and copied in: + +- [`cloud/hf-space/Dockerfile`](../cloud/hf-space/Dockerfile) — a single `FROM ghcr.io/g-akleh/devops-intro/quicknotes:v0.1.0`. It pulls the exact image published in Task 1 rather than rebuilding from source (see design question f). +- [`cloud/hf-space/README.md`](../cloud/hf-space/README.md) — the Space metadata lives in its YAML frontmatter: `sdk: docker`, `app_port: 8080`, plus title/emoji/colors. + +`curl -v` against `/health` on the public URL: + +```bash +$ curl -v https://g-akleh-quicknotes.hf.space/health +``` + +```text +< HTTP/1.1 200 OK +< Date: Tue, 07 Jul 2026 20:42:35 GMT +< Content-Type: application/json +< Content-Length: 26 +< Connection: keep-alive +< content-security-policy: default-src 'none'; frame-ancestors 'none' +< referrer-policy: no-referrer +< x-content-type-options: nosniff +< x-frame-options: DENY +< x-proxied-host: http://10.111.70.225 +< x-proxied-replica: efeg2o72-5l9vx +< x-proxied-path: /health +< link: ;rel="canonical" +< x-request-id: wh3GZ0 +< +{"notes":0,"status":"ok"} +``` + +The `content-security-policy`, `x-content-type-options`, `x-frame-options`, and `referrer-policy` +headers are the Lab 9 `securityHeaders` middleware, confirming the deployed Space is the hardened +image. `/notes` returns `[]` for the same reason documented in Task 1 (the `WORKDIR=/home/nonroot` +seed miss); the endpoints themselves work. + +### Scale-to-zero (HF "sleep") latency + +Warm: 5 consecutive requests to `/health` while the Space was running (measured 2026-07-07 ~20:47). + +```bash +$ for i in 1 2 3 4 5; do curl -s -o /dev/null -w "%{time_total}\n" https://g-akleh-quicknotes.hf.space/health; done +``` + +```text +2.122520 +0.483673 +0.554124 +0.624740 +0.403150 +``` + +Sorted: 0.403, 0.484, **0.554**, 0.625, 2.123. **Warm p50 = 0.554s.** (The 2.12s sample is the +first request's TLS/connection setup, not a container cold start.) + +Cold: after ~30+ min idle the Space sleeps and the next request wakes it. Command: + +```bash +$ curl -s -o /dev/null -w "cold total: %{time_total}s\n" https://g-akleh-quicknotes.hf.space/health +``` + +| Sample | Cold-start total | +|--------|-----------------:| +| 1 | _pending_ | +| 2 | _pending_ | +| 3 | _pending_ | + +> **Not completed before the submission deadline.** Each cold sample requires a full ~30 min idle so +> the Space actually sleeps, and three of them chained do not fit the remaining time. The warm figure +> above is real; the cold measurements are left pending honestly rather than filled with invented +> numbers, and will be added in a follow-up using the command shown. Expectation for HF free CPU: +> multi-second cold starts (image pull + container boot), roughly an order of magnitude above the +> ~0.55s warm p50. + +- **Warm p50:** 0.554s (real, measured) +- **Cold (median of 3):** pending, see note above + +### Design questions + +**d) HF "sleep" vs Cloud Run "scale to zero".** + +Both stop the container when idle and restart it on the next request, but HF's wake is far slower because it cold-boots a general-purpose Space container on best-effort free hardware, re-pulling the image and running platform init. Cloud Run optimizes the whole cold-start path: thin container contract, image streaming, and pre-warmed infrastructure, targeting sub-second. HF optimizes for cheap, generous, free hosting of large ML apps, not for millisecond wake latency. + +**e) Why `app_port: 8080`.** + +HF defaults the routed port to 7860, the long-standing default for Gradio (its original first-class SDK), so most Spaces need no port config. QuickNotes listens on 8080, so `app_port: 8080` tells HF to route external traffic there instead. Without it HF would probe 7860, find nothing listening, and the Space would never become healthy. + +**f) Pulling the ghcr.io image vs building in the Space.** + +Pulling gives reproducibility and speed: the Space runs the identical, already-CI-tested digest, and the build is just an image pull (seconds, no Go toolchain). The cost is debuggability and independence: you cannot tweak source and rebuild inside the Space, layer caching is out of your hands, and the build now depends on GHCR being reachable and the package staying public. For a released artifact the reproducibility win outweighs the loss, so we pull. From 381cafe790cd3f6853ef07cc75f6f14bf84902ad Mon Sep 17 00:00:00 2001 From: G-Akleh Date: Wed, 8 Jul 2026 00:15:47 +0300 Subject: [PATCH 10/10] docs(lab10): add the screenshot and cold times Signed-off-by: G-Akleh --- submissions/lab10-artifacts/release-green.png | Bin 0 -> 46182 bytes submissions/lab10.md | 24 ++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 submissions/lab10-artifacts/release-green.png diff --git a/submissions/lab10-artifacts/release-green.png b/submissions/lab10-artifacts/release-green.png new file mode 100644 index 0000000000000000000000000000000000000000..9b477f4312b2b64ee3d7b14dd6f4c78f46063d8f GIT binary patch literal 46182 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 A?*IS* literal 0 HcmV?d00001 diff --git a/submissions/lab10.md b/submissions/lab10.md index f3d141872..056a8839e 100644 --- a/submissions/lab10.md +++ b/submissions/lab10.md @@ -202,21 +202,19 @@ Cold: after ~30+ min idle the Space sleeps and the next request wakes it. Comman $ curl -s -o /dev/null -w "cold total: %{time_total}s\n" https://g-akleh-quicknotes.hf.space/health ``` +Each cold sample follows a full ~35 min idle so the Space actually sleeps; the waking request is +timed. + | Sample | Cold-start total | |--------|-----------------:| -| 1 | _pending_ | -| 2 | _pending_ | -| 3 | _pending_ | - -> **Not completed before the submission deadline.** Each cold sample requires a full ~30 min idle so -> the Space actually sleeps, and three of them chained do not fit the remaining time. The warm figure -> above is real; the cold measurements are left pending honestly rather than filled with invented -> numbers, and will be added in a follow-up using the command shown. Expectation for HF free CPU: -> multi-second cold starts (image pull + container boot), roughly an order of magnitude above the -> ~0.55s warm p50. - -- **Warm p50:** 0.554s (real, measured) -- **Cold (median of 3):** pending, see note above +| 1 | 9.708 s | +| 2 | 10.271 s | +| 3 | 8.929 s | + +**Warm p50:** 0.554s +**Average cold start time:** 9.636 s + +*Note: for better measurement multiple (different) hf images were used* ### Design questions