diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..1a68db5e5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (≤ 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..9bfbdfc0a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.24.13 AS build + +RUN groupadd -r appgroup && useradd -r -g appgroup -u 65532 nonroot + +WORKDIR /app + +COPY --chown=nonroot:appgroup app /app/ + +RUN go mod download + +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o /bin/qn . + +FROM scratch + +COPY --from=build /bin/qn /bin/qn + +USER 65532 + +EXPOSE 8080 + +ENTRYPOINT ["/bin/qn"] diff --git a/app/main.go b/app/main.go index e258ffcfe..76b47bfff 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: SecurityHeaders(server.Routes()), ReadHeaderTimeout: 5 * time.Second, } diff --git a/app/security.go b/app/security.go new file mode 100644 index 000000000..fac2ffedb --- /dev/null +++ b/app/security.go @@ -0,0 +1,17 @@ +package main + +import "net/http" + +// SecurityHeaders wraps a handler and sets baseline security headers on every +// response. QuickNotes is a JSON API with no browsable UI, so the CSP is the +// strictest possible ('none') rather than a website-style allowlist. +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("Cross-Origin-Resource-Policy", "same-origin") + h.Set("Content-Security-Policy", "default-src 'none'") + h.Set("Cache-Control", "no-store") + next.ServeHTTP(w, r) + }) +} diff --git a/app/security_test.go b/app/security_test.go new file mode 100644 index 000000000..09b5fc6f4 --- /dev/null +++ b/app/security_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestSecurityHeaders_SetOnEveryRoute(t *testing.T) { + srv := newTestServer(t) + handler := SecurityHeaders(srv.Routes()) + + cases := []struct { + method, target string + }{ + {"GET", "/health"}, + {"GET", "/metrics"}, + {"GET", "/notes"}, + } + + wantHeaders := map[string]string{ + "X-Content-Type-Options": "nosniff", + "Cross-Origin-Resource-Policy": "same-origin", + "Content-Security-Policy": "default-src 'none'", + "Cache-Control": "no-store", + } + + for _, c := range cases { + req := httptest.NewRequest(c.method, c.target, nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + for name, want := range wantHeaders { + got := rec.Header().Get(name) + if got != want { + t.Errorf("%s %s: header %q = %q, want %q", c.method, c.target, name, got, want) + } + } + } +} + +func TestSecurityHeaders_WrapsHandler(t *testing.T) { + inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + rec := httptest.NewRecorder() + SecurityHeaders(inner).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) + + if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" { + t.Errorf("X-Content-Type-Options = %q, want %q", got, "nosniff") + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ef459d5d3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +services: + quicknotes: + build: + context: . + image: quicknotes:lab6 + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + ADDR: ":8080" + DATA_PATH: /data/notes.json + SEED_PATH: /seed.json + healthcheck: + test: ["NONE"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 5s + restart: unless-stopped + + prometheus: + image: prom/prometheus:v3.5.4 + volumes: + - ./monitoring/prometheus:/etc/prometheus:ro + ports: + - "9090:9090" + depends_on: + - quicknotes + + grafana: + image: grafana/grafana:11.5.2 + ports: + - "3000:3000" + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: quicknotes-dev + volumes: + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro + depends_on: + - prometheus + +volumes: + quicknotes-data: diff --git a/docs/runbook/high-error-rate.md b/docs/runbook/high-error-rate.md new file mode 100644 index 000000000..afdba7ee6 --- /dev/null +++ b/docs/runbook/high-error-rate.md @@ -0,0 +1,70 @@ +# Runbook: QuickNotes High Error Rate + +**Alert:** `QuickNotes High Error Rate` +**Severity:** page +**Condition:** HTTP error ratio (4xx + 5xx) > 5% sustained for 5 minutes + +--- + +## What this alert means + +More than 5% of requests to QuickNotes have been returning errors continuously for at least 5 minutes, meaning users are actively experiencing failures. + +--- + +## Triage steps + +1. **Check which status codes are spiking.** + Open Grafana → Golden Signals dashboard → Errors panel, or run: + ``` + curl -s http://localhost:9090/api/v1/query \ + --data-urlencode 'query=quicknotes_http_responses_by_code_total' \ + | jq '.data.result[] | {code: .metric.code, value: .value[1]}' + ``` + 5xx = server-side fault. 4xx = client or routing issue. + +2. **Check if the container is running and healthy.** + ``` + docker compose ps + docker compose logs quicknotes --tail=50 + ``` + Look for panics, `listen` errors, or `store:` failures. If the container has restarted, check `docker compose logs --since=10m`. + +3. **Check if the data volume is writable.** + QuickNotes writes `notes.json` on every `POST` and `DELETE`. If the named volume is full or permissions changed, writes return 500: + ``` + docker compose exec -it quicknotes df -h # won't work on scratch — check host instead + docker volume inspect quicknotes-data + ``` + On scratch images, check volume usage from the host: `df -h $(docker volume inspect quicknotes-data --format '{{ .Mountpoint }}')`. + +4. **Confirm Prometheus is still scraping.** + Open `http://localhost:9090/targets` — verify `quicknotes` shows `UP`. If `DOWN`, the metrics themselves may be stale and the alert could be a false positive from a scrape gap. + +--- + +## Mitigations + +1. **Restart the container** — if logs show a transient fault (SIGPIPE, temporary lock contention): + ``` + docker compose restart quicknotes + ``` + Verify traffic normalises within 30 seconds by watching the Errors panel. + +2. **Roll back to the last known-good image** — if the error spike started after a deploy: + ``` + docker compose down + # edit compose.yaml: change image: quicknotes:lab6 to the previous tag + docker compose up -d + ``` + +--- + +## Post-incident + +Once the alert resolves: + +1. Record the incident timeline: when the alert fired, when it was acknowledged, when it resolved. +2. Identify root cause from logs and metrics. +3. Write a postmortem following the Lecture 1 postmortem template — focus on what failed, what detected it, and what prevents recurrence. +4. If the alert fired on noise (e.g. a deploy rollout that resolved in <5 min), consider tightening `for:` duration or excluding specific codes (e.g. `404` from static asset probes). diff --git a/labs/lab1.md b/labs/lab1.md index bb4e226d9..eb319e50f 100644 --- a/labs/lab1.md +++ b/labs/lab1.md @@ -91,9 +91,20 @@ git config --global commit.gpgsign true git config --global tag.gpgsign true ``` -Tell the platform your SSH key is a **signing key**: -- GitHub: Settings → SSH and GPG keys → **New SSH key**, key type **Signing Key** -- GitLab: Profile → SSH Keys → tick "Usage type: Authentication & signing" +Now register the key on the platform. GitHub treats **Authentication** and **Signing** as *separate* roles for the same key, so you add it under both: + +- **Authentication Key** — lets you `clone` / `fetch` / `push` over SSH (`git@github.com:…`). If you cloned over HTTPS, or have never seen `ssh -T git@github.com` greet you by name, you don't have one configured yet — add it now or the `upstream` SSH remote will fail in Lab 2. +- **Signing Key** — gives your commits the **Verified** badge. + +- 🐙 GitHub: Settings → SSH and GPG keys → **New SSH key** → add the **same** `~/.ssh/id_ed25519.pub` **twice**, once with Key type **Authentication Key** and once with **Signing Key**. +- 🦊 GitLab: Profile → SSH Keys → a single key with **Usage type: Authentication & signing** covers both. + +Confirm authentication works before moving on: + +```bash +ssh -T git@github.com +# expect: Hi YOUR_USERNAME! You've successfully authenticated... +``` ### 1.4: Make a Signed Commit @@ -303,7 +314,8 @@ In `submissions/lab1.md`: ## Common Pitfalls - 🪤 **PR template doesn't auto-populate** — make sure the template is on `main` *before* opening the PR -- 🪤 **Commits show "Unverified"** — the SSH key must be added as a *Signing Key* on GitHub (not just an authentication key) +- 🪤 **Commits show "Unverified"** — the key must also be added as a **Signing Key** on GitHub; an Authentication Key alone won't verify commits (they're separate roles — see §1.3) +- 🪤 **`git@github.com: Permission denied (publickey)` on clone/fetch/push** — the *reverse* gap: your key is registered for signing but not as an **Authentication Key**. Add it as Authentication too (§1.3) and confirm with `ssh -T git@github.com`. Quick unblock for the *public* upstream: `git remote set-url upstream https://github.com/inno-devops-labs/DevOps-Intro.git` - 🪤 **`git push` rejected on `main`** — that's the bonus rule working as designed; push to `feature/lab1` instead - 🪤 **`gpg.format=ssh` ignored** — confirm Git ≥ 2.34: `git --version` - 🪤 **Pushed to the wrong branch** — `git switch feature/lab1` before `git push` diff --git a/labs/lab2.md b/labs/lab2.md index fca7b3f22..aae9acfb3 100644 --- a/labs/lab2.md +++ b/labs/lab2.md @@ -223,6 +223,7 @@ git bisect reset ## Common Pitfalls +- 🪤 **`git@github.com: Permission denied (publickey)` on `git fetch upstream`** — *not* a remote-config bug (the error is at the SSH layer, before Git reads the repo). Your key isn't registered for **authentication** on GitHub — and a **Signing Key** (Lab 1) does *not* count for auth, they're separate roles. Add the same `~/.ssh/id_ed25519.pub` as an **Authentication Key** (Lab 1 §1.3), verify with `ssh -T git@github.com`, then re-run. To unblock right now, the public upstream fetches over HTTPS with no key: `git remote set-url upstream https://github.com/inno-devops-labs/DevOps-Intro.git` - 🪤 **`reset --hard` without committing first** — your *uncommitted* edits really *are* gone (reflog only saves committed work). Always check `git status` first - 🪤 **`tag -v` says "no signature"** — you used `git tag NAME` instead of `git tag -a -s NAME -m "..."` - 🪤 **Rebase conflicts** — resolve, then `git rebase --continue`. Never `git rebase --skip` unless you know what you're skipping diff --git a/labs/lab3.md b/labs/lab3.md index 9f0970b20..87344cfb3 100644 --- a/labs/lab3.md +++ b/labs/lab3.md @@ -160,6 +160,23 @@ Tips: - GitLab: `parallel:matrix:` - Set `fail-fast: false` (GH) or equivalent so a single bad cell doesn't cancel the others — you want to *see* which combo broke +> ⚠️ **The matrix renames your checks — update branch protection (1.6) or your PR blocks forever.** A matrixed `test` job reports as `test (1.23)` and `test (1.24)`; the old required check named `test` will sit at *"Expected — Waiting for status to be reported"* indefinitely, even though every real check is green. Two fixes: +> +> 1. **Quick:** in the branch-protection rule, replace `vet`/`test` with the matrixed names (`vet (1.23)`, `vet (1.24)`, `test (1.23)`, `test (1.24)`). +> 2. **Robust (recommended):** add one aggregation job and require *only* it — then the matrix can change freely without touching protection settings: +> +> ```yaml +> ci-ok: +> if: always() +> needs: [vet, test, lint] +> runs-on: ubuntu-24.04 +> steps: +> - run: | +> test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" +> ``` +> +> The `if: always()` matters — without it, a failed `needs` job *skips* `ci-ok`, and a skipped required check lets the PR through on some configurations. + ### 2.3: Skip docs-only changes Edit your trigger so the pipeline runs **only** when something in `app/` or your CI config itself changes. README edits should not burn 4 minutes of CI time. @@ -179,6 +196,8 @@ Capture wall-clock times from the CI UI for three scenarios: > 💡 To get a clean baseline, temporarily disable each optimization with a commit, take a screenshot of the run time, then restore. +> 🧪 **Expect the cache rows to be boring — that's the finding, not a failure.** QuickNotes has **zero third-party dependencies** (look at `app/go.mod` — no `require` block, no `go.sum`), so the module cache has nothing to store and total wall-clock barely moves with `cache: true` vs `cache: false`. Most of your 60–80 s is runner provisioning, checkout, and the Go toolchain download — none of which `setup-go`'s cache touches. Report what you measured and *explain why* (that's design question **f** in disguise). To see where caching *would* pay, compare the **per-step** durations (`setup-go`, `go test`) instead of job totals, and note which step a real dependency-heavy project would save on. + ### 2.5: Document In `submissions/lab3.md`: @@ -284,6 +303,8 @@ Answer in 4-6 sentences: - 🪤 **Forgot `working-directory` (or `cd app`) for Go commands** — Go modules live in `app/`, not the repo root; commands run from the root will fail with "no Go files" - 🪤 **`fail-fast: true` (the GH Actions default) in a matrix** — one fail cancels the others; you can't see *which* combo broke - 🪤 **Branch protection set on someone else's fork's `main`** — you can only protect *your* fork's `main`. The upstream course repo has its own protection +- 🪤 **PR stuck on "Expected — Waiting for status to be reported" after adding the matrix** — the matrix renamed `test` → `test (1.23)`/`test (1.24)`, but branch protection still requires the old `test` context, which will never report again. Update the required-check names or switch to the `ci-ok` aggregation job (see §2.2) +- 🪤 **"Caching didn't speed anything up"** — on a zero-dependency module that's the *correct* result, not a mistake (see §2.4); don't pad the timing table with numbers you didn't observe - 🪤 **`golangci-lint` version not pinned** — "latest" pulls a new release tomorrow that may flag your code with new rules. Pin `v2.5.0` exactly - 🪤 **GitLab CI: incorrect anchor syntax** (`<<: *name`) — GitLab is strict; use the in-platform CI Lint tool (`Project → CI/CD → Editor → Validate`) - 🪤 **Cache hits expire after 7 days of inactivity on GH** — that's expected; the cache key is what protects you against poisoning diff --git a/monitoring/grafana/dashboards/golden-signals.json b/monitoring/grafana/dashboards/golden-signals.json new file mode 100644 index 000000000..bc60fdfd6 --- /dev/null +++ b/monitoring/grafana/dashboards/golden-signals.json @@ -0,0 +1,112 @@ +{ + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "reqps" }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "id": 1, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "single", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "rate(quicknotes_http_requests_total[1m])", + "legendFormat": "req/s", + "refId": "A" + } + ], + "title": "Traffic — Requests / s", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "percentunit", "min": 0, "max": 1 }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "id": 2, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "single", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "sum(rate(quicknotes_http_responses_by_code_total{code=~\"4..|5..\"}[1m])) / rate(quicknotes_http_requests_total[1m])", + "legendFormat": "error ratio", + "refId": "A" + } + ], + "title": "Errors — 4xx+5xx Ratio", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "description": "No latency histogram available; request rate (5m window) used as proxy per lab spec.", + "fieldConfig": { + "defaults": { "unit": "reqps" }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "id": 3, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "single", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "rate(quicknotes_http_requests_total[5m])", + "legendFormat": "req/s (5m avg)", + "refId": "A" + } + ], + "title": "Latency — Proxy: Request Rate (no histogram)", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "short" }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "id": 4, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "single", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "quicknotes_notes_total", + "legendFormat": "notes stored", + "refId": "A" + } + ], + "title": "Saturation — Notes Stored", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 38, + "tags": ["quicknotes", "sre"], + "time": { "from": "now-1h", "to": "now" }, + "timepicker": {}, + "timezone": "browser", + "title": "QuickNotes — Golden Signals", + "uid": "quicknotes-golden-signals", + "version": 1 +} diff --git a/monitoring/grafana/provisioning/alerting/alerts.yml b/monitoring/grafana/provisioning/alerting/alerts.yml new file mode 100644 index 000000000..a8c6cd5a6 --- /dev/null +++ b/monitoring/grafana/provisioning/alerting/alerts.yml @@ -0,0 +1,51 @@ +apiVersion: 1 + +groups: + - orgId: 1 + name: quicknotes + folder: QuickNotes + interval: 1m + rules: + - uid: quicknotes-high-error-rate + title: QuickNotes High Error Rate + condition: B + data: + - refId: A + relativeTimeRange: + from: 300 + to: 0 + datasourceUid: prometheus + model: + expr: > + sum(rate(quicknotes_http_responses_by_code_total{code=~"4..|5.."}[5m])) + / + sum(rate(quicknotes_http_requests_total[5m])) + instant: true + refId: A + - refId: B + datasourceUid: __expr__ + model: + type: threshold + refId: B + expression: A + conditions: + - evaluator: + type: gt + params: + - 0.05 + operator: + type: and + query: + params: + - A + reducer: + type: last + noDataState: NoData + execErrState: Error + for: 5m + labels: + severity: page + annotations: + summary: "QuickNotes error rate exceeded 5% for 5 minutes" + runbook_url: "docs/runbook/high-error-rate.md" + isPaused: false diff --git a/monitoring/grafana/provisioning/dashboards/dashboard.yml b/monitoring/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 000000000..32ac858a3 --- /dev/null +++ b/monitoring/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: default + orgId: 1 + folder: "" + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /var/lib/grafana/dashboards diff --git a/monitoring/grafana/provisioning/datasources/datasources.yml b/monitoring/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 000000000..8704d9e54 --- /dev/null +++ b/monitoring/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 000000000..ce26e0c99 --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'quicknotes' + static_configs: + - targets: ['quicknotes:8080'] + diff --git a/submissions/firing.png b/submissions/firing.png new file mode 100644 index 000000000..05b0f7ce3 Binary files /dev/null and b/submissions/firing.png differ diff --git a/submissions/grafana.png b/submissions/grafana.png new file mode 100644 index 000000000..94a4d63ef Binary files /dev/null and b/submissions/grafana.png differ diff --git a/submissions/lab1.md b/submissions/lab1.md new file mode 100644 index 000000000..3d61f8d7a --- /dev/null +++ b/submissions/lab1.md @@ -0,0 +1,74 @@ +# Lab 1 submission + +## `curl` outputs + +Input: + +`curl -s http://localhost:8080/health | python3 -m json.tool` + +Output: + +```sh +{ + "notes": 4, + "status": "ok" +} +``` + +Input: + +```sh +curl -s http://localhost:8080/notes | python3 -m json.tool +``` + +Output: + +```sh +[ + { + "id": 1, + "title": "Welcome to QuickNotes", + "body": "This is the project you'll containerize, deploy, monitor, and harden across all 10 labs.", + "created_at": "2026-01-15T10:00:00Z" + }, + { + "id": 2, + "title": "Read app/main.go first", + "body": "Start by understanding the entry point \u2014 env vars, signal handling, graceful shutdown.", + "created_at": "2026-01-15T10:05:00Z" + }, + { + "id": 3, + "title": "DevOps mantra", + "body": "If it hurts, do it more often.", + "created_at": "2026-01-15T10:10:00Z" + }, + { + "id": 4, + "title": "Endpoint cheat-sheet", + "body": "GET /notes GET /notes/{id} POST /notes DELETE /notes/{id} GET /health GET /metrics", + "created_at": "2026-01-15T10:15:00Z" + } +] +``` + +## Log outputs + +``` +commit 8821e5be0ef113c5d974ed6b182bd36ee4ab5482 (HEAD -> feature/lab1) +Good "git" signature for 4sitescarp@gmail.com with RSA key SHA256:L9YcU19uqANokeYEFcfoGcw1x+8iay2DQmOf7/L64lM +Author: ilnarkhasanov <4sitescarp@gmail.com> +Date: Sun Jun 7 14:54:44 2026 +0300 + + docs(lab1): replace submission directory + + Signed-off-by: ilnarkhasanov <4sitescarp@gmail.com> +``` + +## Signed commit proof + + + +## Explanation on why signed commits matter + +Based on `xz-utils`, the key thing is the following: signed commits can verify the origin of the commit. There can be no other evidence that commit was done by someone who is not a maintainer. In that case one persona was maintaining the repo for 2 years and got privileges. Then they added the vulnerability using signed commits. This case shows that signed commits can help to locate the origin of problem, but do not solve the problem itself. diff --git a/submissions/lab2.md b/submissions/lab2.md new file mode 100644 index 000000000..e469a75ab --- /dev/null +++ b/submissions/lab2.md @@ -0,0 +1,339 @@ +# Lab 2 submission + +## Explore your repo's plumbing + +Input: + +```sh +git rev-parse HEAD +``` + +Output: + +```sh +3fcf44f1df0624f972eca042c289435a025c1486 +``` + +Input: +```sh +git cat-file -t HEAD +``` + +Output: +```sh +commit +``` + +Input: +```sh +git cat-file -p HEAD +``` + +Output: +```sh +tree c75b34af8f9e1b6ae8bba1321cddd2abf05e0643 +parent b40e1a714757082ec39bd3eb0983049d6f4b21a6 +author ilnarkhasanov <4sitescarp@gmail.com> 1780949484 +0300 +committer ilnarkhasanov <4sitescarp@gmail.com> 1780950131 +0300 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMeIjD2FLzl5UlxMInYLwB + B7/iLF7TmgVHEcuTg7NUnjjv3B1o0IJOg9W9wLZBlywz5balP1SS+JqVm2+bjXPWyxLUo2 + bd08CKwJb3jjxgfJY51dI+JHVz4kVnt/Xf6YB7xnsFRjC07Qv2Cnwj9nHuzbGXYz+Wm0ra + 4TALoq3rKSwroZKkuWoPIORelWQU2SlcY83IB2CuMAikeoiAzrJqsjVW7uTuNENViFRdKH + f1Cgg5VJ0QBFz7LMS555FuxQLGHt822ZBtaidgQT0MaR93YJz3VPOvBxb9of9Bls3QiK+T + Esone9Fg5UlTIvfIP9N/hQ208Vs9e4YbtEfXO366PJamrD/sjJX9zyi5eOcZ7Lnfpj7fC3 + 6R1icDm0P9RV+fDgROJolFtU4Ibcqnajzz8iCdDHof6PRmjqsdbUxZnYkIr4H/wFnuO6AR + ol2h39SedwpG2fRXcwWeHH8XjJQ80lQWZ3On4+cFLQp+Cp3P1PuhPoq0kMZupZbAVsCY/0 + QQAAAANnaXQAAAAAAAAABnNoYTUxMgAAAZQAAAAMcnNhLXNoYTItNTEyAAABgI1SRIUej7 + Tx5BMjbL5/ykS2iu0Y4Cs0m/JZ3gcCLuuFzx8HY335YiF50eBiz292cobkyrSKeIfhs1uV + pvtgmnJt2fDZ0UsW2xrD7TuXx5dMT3kNV9onybD1nXzybnawztJMd/Y+pRBKBvoDy63edw + K2GKNYqy90hfef3IFkNeEXORttMwGmHiNp/0Kfqt6qey1rLZyrAss41aCoXMfHfnpswLJn + JErwtvhdgaREzM7nU8CNVBLHByvz1Z5j4lCTNUtJX5sSjLrWMVAVrtCat6GxxHwEQ1TGJE + 6mKUT+IS8pshph3RyT3WTnhBsWoQ/q47hl+c2b4ePBcm0wDy24AIU7FJeGVnZLUZJ0zuWf + 8ZUzUWMl+Y0KHz6CQrcUEoWWJD7GsOHnvS6/dYYnco7WSTUNba+dCwEwMkrLnWZtZ3Oyhx + 9fF7FLo8d0ueNeZBAOmrPIixuPj/FgjGo0UquGcC5tPcTLoKMElPCi+hoQu8yDBAfsbCSJ + fzTAolOZCxSdOw== + -----END SSH SIGNATURE----- + +docs(lab1): finish submission + +Signed-off-by: ilnarkhasanov <4sitescarp@gmail.com> +``` + +Input: + +```sh +git cat-file -p c75b34af8f9e1b6ae8bba1321cddd2abf05e0643 +``` + +Output: +```sh +040000 tree 1d07791eee3c3dd0955a02402b05b3a357816d8d .github +100644 blob 1c0a1e94b7bbdd951f456cda51af6b8484cc3cee .gitignore +100644 blob d10c04c6e7e0014f4fe883599c11747c15012d4e README.md +040000 tree 7d0898a908e274ea809722844cdbd836f3b1c05a app +040000 tree 6db686e340ecdd318fa43375e26254293371942a labs +040000 tree 3f11973a71be5915539cb53313149aa319d69cb5 lectures +040000 tree df98acc9e5305af9e23497a8bbdedcd1c993be44 submissions +``` + +Input: +```sh +git cat-file -p 1c0a1e94b7bbdd951f456cda51af6b8484cc3cee +``` + +Output: +``` +# ⚠️ KEEP THIS FILE MINIMAL. +# +# This .gitignore is inherited by every student fork. Anything listed here +# is something a student CANNOT `git add` without `-f`. So this file must +# ONLY contain: +# (a) instructor-only paths (refs/), and +# (b) machine-generated junk that NOBODY should ever commit. +# +# Do NOT add lab DELIVERABLES here (scan reports, SBOMs, go.sum, k8s +# manifests, CI workflows, Dockerfiles, playbooks, dashboards, …). Students +# are told to commit those in their submission PRs — ignoring them upstream +# silently breaks the lab. When in doubt, leave it OUT of this file. + +# ── Instructor-only ───────────────────────────────────────────── +# Reference submissions (dry-run worked examples). Never pushed upstream; +# students never see these. This is the one path that is intentionally hidden. +refs/ + +# ── Machine-generated junk (no one commits these) ─────────────── +# Compiled binaries / local runtime state +app/quicknotes +app/data/ +/quicknotes +*.exe + +# Vagrant runtime state (Lab 5) — the Vagrantfile IS committed; .vagrant/ is not +.vagrant/ + +# Nix build symlinks (Lab 11) — flake.nix + flake.lock ARE committed; result is not +result +result-* + +# Terraform state — MUST never be committed (can contain secrets) +*.tfstate +*.tfstate.backup +.terraform/ + +# Python virtualenvs / caches +.venv/ +__pycache__/ +*.pyc + +# Editor / IDE +.vscode/ +.idea/ +*.swp + +# OS noise +.DS_Store +Thumbs.db + +# Local agent config (not part of the course) +.claude/ + +# NOTE: deliberately NOT ignored, because students commit them as lab evidence: +# submissions/labN.md (lab reports) +# .github/workflows/*.yml (Lab 3 CI) +# Dockerfile, compose.yaml (Lab 6) +# ansible/ (Lab 7) +# monitoring/ (Lab 8) +# *.sbom.cdx.json, zap-*.html/json, trivy-*.txt (Lab 9 scan evidence) +# flake.nix, flake.lock (Lab 11) +# wasm/main.go, spin.toml, go.sum (Lab 12) +``` + +## 1.2: Look inside `.git/` + +Explanation: + +Input: + +```sh +ls -la .git +``` + +Output: + +```sh +total 64 +drwxr-xr-x 15 ilnarkhasanov staff 480 Jun 9 19:38 . +drwxr-xr-x@ 10 ilnarkhasanov staff 320 Jun 9 19:38 .. +-rw-r--r-- 1 ilnarkhasanov staff 75 Jun 8 23:13 COMMIT_EDITMSG +-rw-r--r-- 1 ilnarkhasanov staff 97 Jun 8 23:22 FETCH_HEAD +-rw-r--r-- 1 ilnarkhasanov staff 29 Jun 9 19:38 HEAD +-rw-r--r-- 1 ilnarkhasanov staff 41 Jun 8 23:22 ORIG_HEAD +-rw-r--r-- 1 ilnarkhasanov staff 451 Jun 8 23:11 config +-rw-r--r-- 1 ilnarkhasanov staff 73 Jun 6 10:35 description +drwxr-xr-x 16 ilnarkhasanov staff 512 Jun 6 10:35 hooks +-rw-r--r-- 1 ilnarkhasanov staff 3411 Jun 9 19:38 index +drwxr-xr-x 3 ilnarkhasanov staff 96 Jun 6 10:35 info +drwxr-xr-x 4 ilnarkhasanov staff 128 Jun 6 10:35 logs +drwxr-xr-x 28 ilnarkhasanov staff 896 Jun 8 23:22 objects +-rw-r--r-- 1 ilnarkhasanov staff 112 Jun 6 10:35 packed-refs +drwxr-xr-x 5 ilnarkhasanov staff 160 Jun 6 10:35 refs +``` + +Input: + +```sh +cat .git/HEAD +``` + +Output: + +```sh +ref: refs/heads/feature/lab1 +``` + +Input: + +```sh +ls .git/refs/heads/ +``` + +Output: + +```sh +feature main +``` + +Input: +```sh +ls .git/objects/ | head +``` + +Output: + +```sh +04 +1a +1d +27 +31 +38 +3d +3f +6f +79 +``` + +Input: +```sh +find .git/objects -type f | wc -l +``` + +Output: +```sh + 28 +``` + +Interpretation: + +This shows the internals of my git repo. Specifially, I see where my HEAD is located, what branches I have, what objects I have and what loose objects I have. + +## 1.3: Simulate disaster + recover + +Input: + +```sh +git reflog +``` + +Output: + +```sh +60f56cf HEAD@{0}: reset: moving to 60f56cf +3fcf44f HEAD@{1}: reset: moving to HEAD~2 +60f56cf HEAD@{2}: commit: wip(lab2): more progress +8c67dae HEAD@{3}: commit: wip(lab2): start +3fcf44f HEAD@{4}: checkout: moving from feature/lab1 to feature/lab2 +3fcf44f HEAD@{5}: checkout: moving from main to feature/lab1 +0478a0f HEAD@{6}: checkout: moving from feature/lab1 to main +3fcf44f HEAD@{7}: pull -r origin main (finish): returning to refs/heads/feature/lab1 +3fcf44f HEAD@{8}: pull -r origin main (pick): docs(lab1): finish submission +b40e1a7 HEAD@{9}: pull -r origin main (pick): docs(lab1): replace submission directory +d0fa48b HEAD@{10}: pull -r origin main (pick): docs(lab1): start submission +0478a0f HEAD@{11}: pull -r origin main (start): checkout 0478a0fe61ea6e5f6ff80aca2709e909b2a23db2 +b4b6473 HEAD@{12}: checkout: moving from main to feature/lab1 +0478a0f HEAD@{13}: commit: docs: add PR template +66bbd4d HEAD@{14}: checkout: moving from feature/lab1 to main +b4b6473 HEAD@{15}: commit: docs(lab1): finish submission +8821e5b HEAD@{16}: commit: docs(lab1): replace submission directory +d7652de HEAD@{17}: commit: docs(lab1): start submission +66bbd4d HEAD@{18}: reset: moving to HEAD~1 +31fd148 HEAD@{19}: commit: docs(lab1): start submission +66bbd4d HEAD@{20}: reset: moving to HEAD~1 +6fb82e5 HEAD@{21}: commit: docs(lab1): start submission +66bbd4d HEAD@{22}: reset: moving to HEAD~1 +f333a1c HEAD@{23}: commit: docs(lab1): start submission +66bbd4d HEAD@{24}: checkout: moving from main to feature/lab1 +66bbd4d HEAD@{25}: clone: from github.com:ilnarkhasanov/DevOps-Intro.git +``` + +Input: +```sh +git reset --hard 60f56cf +``` + +Output: +```sh +HEAD is now at 60f56cf wip(lab2): more progress +``` + +Explanation: + +If I run `git gc` before hard reseting I can lose the possibility to restore my commits since this command prunes orphan commits. + +## Task 2 — Tag a Release & Rebase a Feature (4 pts) + +### 2.1: Annotated, signed release tag + +The signed tag verification output: + +```sh +git tag -v "v0.1.0-lab2-${USER}" +``` + +Output: + +```sh +object 60f56cf7757414521b9286a4959d2c2d45b31d99 +type commit +tag v0.1.0-lab2-ilnarkhasanov +tagger ilnarkhasanov <4sitescarp@gmail.com> 1781025354 +0300 + +Lab 2 milestone — version control deep dive +Good "git" signature for 4sitescarp@gmail.com with RSA key SHA256:L9YcU19uqANokeYEFcfoGcw1x+8iay2DQmOf7/L64lM +``` + +#### Your branch's `git log --oneline --graph` before and after rebase: + +Before: +``` +* fadce66 (HEAD -> feature/lab2) docs(lab2): add solution for task 1 +* 60f56cf (tag: v0.1.0-lab2-ilnarkhasanov) wip(lab2): more progress +* 8c67dae wip(lab2): start +* 3fcf44f (origin/feature/lab1, feature/lab1) docs(lab1): finish submission +``` + +After: +``` +* 0f25773 (HEAD -> feature/lab2) docs(lab2): add solution for task 1 +* e4b6109 wip(lab2): more progress +* 5fbb5e0 wip(lab2): start +* f1054c2 docs(lab1): finish submission +* cb8d82b docs(lab1): replace submission directory +* 32961cd docs(lab1): start submission +* 9c9e541 (origin/main, origin/HEAD, main) docs: upstream moved while you worked +``` + +#### - A brief reflection on *when* you'd choose merge vs rebase + +I would use rebase on a project where linear project is required. On a project there several people use one branch merge can be better. diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..8dcc15912 --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,206 @@ +# Task 1 + +## Design questions + +1. Why does layer-order matter? + +Answer: + +`COPY . . && go mod download && go build` built for 4.8s, rebuilt for 4.3s without caching `go mod download`. + +`COPY go.mod go.sum ./ && go mod download && COPY . . && go build` built for 4.3s, rebuilt for 4.1s with cached `go mod download`. + +Layer-order matter since if `go mod download` is time-consuming operation, we can skip it if we've done it before, the result is the same, and it is separated operation in Dockerfile. + +0.4s + +2. Why CGO_ENABLED=0? What happens in distroless-static if you forget it? + +Answer: Setting `CGO_ENABLED=0` forces Go to produce a statically linked binary with zero external shared library dependencies. If you forget it on `distroless/static`, the container fails with `exec /bin/qn: no such file or directory` — the binary exists, but the dynamic linker (`ld-linux`) it needs does not, since `distroless/static` ships no libc. + +3. What is gcr.io/distroless/static:nonroot? What's in it, what isn't, and why does that matter for CVEs? + +Answer: + +What it is: A Google-maintained minimal base image containing only what a statically linked binary needs to run — nothing more. + +What's in it: CA certificates, tzdata, /etc/passwd and /etc/group with a nonroot user (uid 65532). + +What isn't in it: shell, libc, dynamic linker, package manager, or any standard OS utilities. + +Why does that matter for CVEs: +Every package in a container image is a potential CVE surface. Scanners like Trivy or Grype report vulnerabilities per installed package. + +Alpine ships ~20 packages by default. debian:slim ships ~80+. Each one can have CVEs. + +distroless/static ships essentially zero userspace packages. + +4. -ldflags='-s -w' and -trimpath: what does each flag do, and what's the cost? + +Answer: +- `-s` strips the symbol table — removes function names and addresses used by debuggers. Cost: `pprof` symbol lookup breaks; stack traces lose function names. +- `-w` strips DWARF debug info — removes line numbers and type info used by `dlv` / `gdb`. Cost: debugger attach no longer works. +- `-trimpath` removes absolute build-machine paths from the binary — stack traces show `app/main.go` instead of `/home/user/projects/app/main.go`. Cost: stack traces are less specific (no absolute path), but builds become reproducible and paths aren't leaked into the binary. + +Combined, `-s -w` reduce binary size by ~20–30%. `-trimpath` adds negligible size change. + +## Building and running container + +Input: `docker images quicknotes:lab6` + +Output: +``` +quicknotes lab6 470bbef4ae24 About an hour ago 5.51MB +``` + +Input: `docker inspect quicknotes:lab6 | jq '.[0].Config'` + +Output: +``` +{ + "Cmd": null, + "Entrypoint": [ + "/bin/qn" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": { + "8080/tcp": {} + }, + "Labels": null, + "OnBuild": null, + "User": "", + "Volumes": null, + "WorkingDir": "/" +} +``` + +Input: `docker images golang:1.24.5` + +``` +REPOSITORY TAG IMAGE ID CREATED SIZE +golang 1.24.5 27480458d896 11 months ago 860MB +``` + +## Dockerfile + +```dockerfile +FROM golang:1.24.5 AS build + +RUN groupadd -r appgroup && useradd -r -g appgroup -u 65532 nonroot + +WORKDIR /app + +COPY --chown=nonroot:appgroup app /app/ + +RUN go mod download + +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o /bin/qn . + +FROM scratch + +COPY --from=build /bin/qn /bin/qn + +USER 65532 + +EXPOSE 8080 + +ENTRYPOINT ["/bin/qn"] +``` + +# Task 2 + +## Design questions + +1. Distroless has no shell. How do you healthcheck it? + +`scratch` contains only `/bin/qn`. There is no shell, no wget, no curl, no nc — nothing to probe the HTTP endpoint from inside the container. + +Setting test: ["NONE"] disables the test entirely. Docker marks the container healthy as soon as the process is running, and restart: unless-stopped handles the crash case — if /bin/qn exits, Docker restarts it automatically. + +2. Why does volumes: [quicknotes-data:/data] survive docker compose down? And what does destroy it? + +Answer: `docker compose down` does not remove volumes, therefore data is persistent. If we run `docker compose down -v`, volumes will be deleted and data will be lost. + +3. `depends_on` without `condition: service_healthy` — what does it actually wait for? What's the bug it can cause? + +Answer: without a condition, depends_on only waits for the dependent container to start — meaning Docker has created the container and the process has launched. It does not wait for the service to be ready to accept connections. +The bug: if service B depends_on service A (e.g. a database), B starts immediately after A's process launches. A may still be initializing — running migrations, loading data, binding its port. B tries to connect, gets a connection refused, and crashes or enters a broken state. + +The fix: +``` +depends_on: + quicknotes: + condition: service_healthy +``` +This makes Docker wait until the healthcheck passes before starting the dependent service. It only works if the dependency actually has a healthcheck defined — which is one more reason a real healthcheck matters beyond just monitoring. + +## Persistence + +Input: +``` +docker compose up --build -d +sleep 3 +curl -X POST -H 'Content-Type: application/json' \ + -d '{"title":"durable","body":"survive a restart"}' \ + http://localhost:8080/notes +curl -s http://localhost:8080/notes | grep durable +``` + +Output: +``` +{"id":1,"title":"durable","body":"survive a restart","created_at":"2026-06-23T17:40:29.479735512Z"} +[{"id":1,"title":"durable","body":"survive a restart","created_at":"2026-06-23T17:40:29.479735512Z"}] +``` + +Input: +``` +docker compose down # NOT `down -v` +docker compose up -d +sleep 3 +curl -s http://localhost:8080/notes | grep durable +``` + +Output: +``` +[{"id":1,"title":"durable","body":"survive a restart","created_at":"2026-06-23T17:40:29.479735512Z"}] +``` + +Input: +``` +docker compose down -v # NOW the volume dies +docker compose up -d +sleep 3 +curl -s http://localhost:8080/notes | grep durable +``` + +Output: nothing + +## docker-compose + +```yaml +services: + quicknotes: + build: + context: . + image: quicknotes:lab6 + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + ADDR: ":8080" + DATA_PATH: /data/notes.json + SEED_PATH: /seed.json + healthcheck: + test: ["NONE"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 5s + restart: unless-stopped + +volumes: + quicknotes-data: +``` diff --git a/submissions/lab8.md b/submissions/lab8.md new file mode 100644 index 000000000..ddda4c240 --- /dev/null +++ b/submissions/lab8.md @@ -0,0 +1,233 @@ +# Task 1 + +## Design questions + +1. Pull vs push: which side needs to be reachable? + +Prometheus pulls — it initiates the HTTP request to /metrics. This means QuickNotes must be reachable from Prometheus, not the other way around. QuickNotes doesn't need to know Prometheus exists. + +Failure mode: if Prometheus can't reach QuickNotes (container down, wrong hostname, wrong port), the target shows DOWN in /targets and the scrape fails silently — no metrics are recorded for that interval. The app keeps running; only observability is lost. You won't know until you notice missing data in dashboards. + +2. scrape_interval: 5s vs 5m — what problems do each cause? + +5s — high cardinality write load on Prometheus storage, TSDB churn, and rate() windows become harder to reason about. A rate(metric[1m]) over 5s scrapes has only 12 data points — statistically fine, but you burn disk and CPU for marginal resolution gain on a low-traffic app. + +5m — rate() requires at least 2 data points in its window, so the window must be strictly greater than one scrape interval (>5m). A spike that lasts 3 minutes is invisible. Alert `for:` gates become unreliable because a single missed scrape covers the entire evaluation window. + +3. rate() vs irate() vs delta() for the Traffic panel + +Use rate(). It calculates the per-second average rate over the full range window, smoothing out spikes — the right signal for a traffic trend panel where you want to see sustained load. + +irate() uses only the last two data points, making it highly sensitive to single-interval spikes. It is suited for fast-moving counters where you want the instantaneous rate; wrong for traffic where you want trend visibility. + +delta() returns the absolute change in value over the window, not a per-second rate. It's for gauges (e.g. memory), not counters like quicknotes_http_requests_total. + +4. Why provision Grafana from files instead of clicking through the UI? + +Clicking through the UI stores state only in Grafana's SQLite database inside the container. docker compose down destroys the container and the dashboard is gone. File-based provisioning means the dashboard is version-controlled, reproducible on every docker compose up, and reviewable in a PR — the same reasons you don't configure CI by clicking buttons in the Jenkins UI. + +## Config files + +**monitoring/prometheus/prometheus.yml** + +```yaml +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'quicknotes' + static_configs: + - targets: ['quicknotes:8080'] +``` + +**monitoring/grafana/provisioning/datasources/datasources.yml** + +```yaml +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false +``` + +**monitoring/grafana/provisioning/dashboards/dashboard.yml** + +```yaml +apiVersion: 1 + +providers: + - name: default + orgId: 1 + folder: "" + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /var/lib/grafana/dashboards +``` + +Full dashboard JSON: [monitoring/grafana/dashboards/golden-signals.json](../monitoring/grafana/dashboards/golden-signals.json) + +## Grafana screenshot + + + +## Prometheus + +Input: `curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[].health'` +Output: +``` + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 613 100 613 0 0 115k 0 --:--:-- --:--:-- --:--:-- 119k +"up" +``` + +# Task 2 + +## Firing + + + +## Alert rule definition + +**monitoring/grafana/provisioning/alerting/alerts.yml** + +```yaml +apiVersion: 1 + +groups: + - orgId: 1 + name: quicknotes + folder: QuickNotes + interval: 1m + rules: + - uid: quicknotes-high-error-rate + title: QuickNotes High Error Rate + condition: B + data: + - refId: A + relativeTimeRange: + from: 300 + to: 0 + datasourceUid: prometheus + model: + expr: > + sum(rate(quicknotes_http_responses_by_code_total{code=~"4..|5.."}[5m])) + / + sum(rate(quicknotes_http_requests_total[5m])) + instant: true + refId: A + - refId: B + datasourceUid: __expr__ + model: + type: threshold + refId: B + expression: A + conditions: + - evaluator: + type: gt + params: + - 0.05 + operator: + type: and + query: + params: + - A + reducer: + type: last + noDataState: NoData + execErrState: Error + for: 5m + labels: + severity: page + annotations: + summary: "QuickNotes error rate exceeded 5% for 5 minutes" + runbook_url: "docs/runbook/high-error-rate.md" + isPaused: false +``` + +## Runbook + +**docs/runbook/high-error-rate.md** + +**Alert:** `QuickNotes High Error Rate` +**Severity:** page +**Condition:** HTTP error ratio (4xx + 5xx) > 5% sustained for 5 minutes + +### What this alert means + +More than 5% of requests to QuickNotes have been returning errors continuously for at least 5 minutes, meaning users are actively experiencing failures. + +### Triage steps + +1. **Check which status codes are spiking.** + Open Grafana → Golden Signals dashboard → Errors panel, or run: + ``` + curl -s http://localhost:9090/api/v1/query \ + --data-urlencode 'query=quicknotes_http_responses_by_code_total' \ + | jq '.data.result[] | {code: .metric.code, value: .value[1]}' + ``` + 5xx = server-side fault. 4xx = client or routing issue. + +2. **Check if the container is running and healthy.** + ``` + docker compose ps + docker compose logs quicknotes --tail=50 + ``` + Look for panics, `listen` errors, or `store:` failures. If the container has restarted, check `docker compose logs --since=10m`. + +3. **Check if the data volume is writable.** + QuickNotes writes `notes.json` on every `POST` and `DELETE`. If the named volume is full or permissions changed, writes return 500: + ``` + docker volume inspect quicknotes-data + df -h $(docker volume inspect quicknotes-data --format '{{ .Mountpoint }}') + ``` + +4. **Confirm Prometheus is still scraping.** + Open `http://localhost:9090/targets` — verify `quicknotes` shows `UP`. If `DOWN`, the metrics themselves may be stale and the alert could be a false positive from a scrape gap. + +### Mitigations + +1. **Restart the container** — if logs show a transient fault (SIGPIPE, temporary lock contention): + ``` + docker compose restart quicknotes + ``` + Verify traffic normalises within 30 seconds by watching the Errors panel. + +2. **Roll back to the last known-good image** — if the error spike started after a deploy: + ``` + docker compose down + # edit compose.yaml: change image tag to the previous version + docker compose up -d + ``` + +### Post-incident + +Once the alert resolves: + +1. Record the incident timeline: when the alert fired, when it was acknowledged, when it resolved. +2. Identify root cause from logs and metrics. +3. Write a postmortem following the Lecture 1 postmortem template — focus on what failed, what detected it, and what prevents recurrence. +4. If the alert fired on noise (e.g. a deploy rollout that resolved in <5 min), consider tightening `for:` duration or excluding specific codes. + +## Design questions + +e) **Why "sustained for 5 minutes" instead of firing immediately?** + +A single bad request — one malformed JSON body, one 404 — would fire the alert immediately and wake someone up at 3 AM for a non-event. The `for: 5m` gate requires the error ratio to stay above 5% continuously. That filters out transient blips (a deploy rollout, a single misbehaving client) and only pages when there is a real, ongoing user-facing problem worth waking someone up for. + +f) **Symptom alert vs cause alert — example and why cause is worse** + +A cause alert for QuickNotes: `quicknotes_notes_total > 1000` (too many notes stored) or alerting on container CPU > 80%. + +Cause alerts are worse because they fire on internal state that may or may not affect users. CPU at 80% might mean the app is healthy and busy. A full notes store might be fine if requests still succeed. Cause alerts produce noise without actionable signal — you're paged for something that isn't hurting users yet, and often never will. The error-rate alert fires only when users are already seeing failures. + +g) **Quantitative threshold for "alert is too noisy"** + +From Google SRE practice (Rob Ewaschuk, "My Philosophy on Alerting", also covered in the SRE Workbook Chapter 5): if the alert fires and fewer than 50% of pages result in a human taking action, the alert is too noisy. A tighter threshold used in practice: if the user-facing error rate shows no impact in more than 10% of firings, the precision is too low and the alert should be tightened or demoted to a warning. diff --git a/submissions/lab9-scans/sbom-cyclonedx.json b/submissions/lab9-scans/sbom-cyclonedx.json new file mode 100644 index 000000000..e74f8faf8 --- /dev/null +++ b/submissions/lab9-scans/sbom-cyclonedx.json @@ -0,0 +1,137 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "serialNumber": "urn:uuid:08cddf6a-9627-4f99-a3d7-2900ea1fbdd3", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T06:59:12+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "0.72.0" + } + ] + }, + "component": { + "bom-ref": "4ef8bb1e-50c8-40e2-8094-ff6b93e59122", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:70a952b1d983fbc9a4af340c4493196b673e1f6e085e4dcbd5b8fb791f3662ab" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:4118e243661f88fa122cd23925d8746c38f13fbed92a67b50ca6f94b53ef0db3" + }, + { + "name": "aquasecurity:trivy:Reference", + "value": "quicknotes:lab6" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "quicknotes:lab6" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:Size", + "value": "5507584" + } + ] + } + }, + "components": [ + { + "bom-ref": "2eccdeb1-1d38-4f4c-a940-23562fd45682", + "type": "application", + "name": "bin/qn", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/quicknotes", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:70a952b1d983fbc9a4af340c4493196b673e1f6e085e4dcbd5b8fb791f3662ab" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/stdlib@v1.24.5", + "type": "library", + "name": "stdlib", + "version": "v1.24.5", + "purl": "pkg:golang/stdlib@v1.24.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:70a952b1d983fbc9a4af340c4493196b673e1f6e085e4dcbd5b8fb791f3662ab" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.24.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + } + ], + "dependencies": [ + { + "ref": "2eccdeb1-1d38-4f4c-a940-23562fd45682", + "dependsOn": [ + "pkg:golang/quicknotes" + ] + }, + { + "ref": "4ef8bb1e-50c8-40e2-8094-ff6b93e59122", + "dependsOn": [ + "2eccdeb1-1d38-4f4c-a940-23562fd45682" + ] + }, + { + "ref": "pkg:golang/quicknotes", + "dependsOn": [ + "pkg:golang/stdlib@v1.24.5" + ] + }, + { + "ref": "pkg:golang/stdlib@v1.24.5", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/submissions/lab9-scans/trivy-config.txt b/submissions/lab9-scans/trivy-config.txt new file mode 100644 index 000000000..2a345ac54 --- /dev/null +++ b/submissions/lab9-scans/trivy-config.txt @@ -0,0 +1,38 @@ +2026-07-07T09:56:08+03:00 INFO [misconfig] Misconfiguration scanning is enabled +2026-07-07T09:56:08+03:00 INFO [checks-client] Need to update the checks bundle +2026-07-07T09:56:08+03:00 INFO [checks-client] Downloading the checks bundle... +2026-07-07T09:56:11+03:00 INFO Detected config files num=1 + +Report Summary + +┌────────────┬────────────┬───────────────────┐ +│ Target │ Type │ Misconfigurations │ +├────────────┼────────────┼───────────────────┤ +│ Dockerfile │ dockerfile │ 1 │ +└────────────┴────────────┴───────────────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +Dockerfile (dockerfile) +======================= +Tests: 27 (SUCCESSES: 26, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +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/ds-0026 + +──────────────────────────────────────── + + + + + diff --git a/submissions/lab9-scans/trivy-fs.txt b/submissions/lab9-scans/trivy-fs.txt new file mode 100644 index 000000000..c5648112c --- /dev/null +++ b/submissions/lab9-scans/trivy-fs.txt @@ -0,0 +1,59 @@ +2026-07-07T09:55:56+03:00 INFO [vuln] Vulnerability scanning is enabled +2026-07-07T09:55:56+03:00 INFO [secret] Secret scanning is enabled +2026-07-07T09:55:56+03:00 INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T09:55:56+03:00 INFO [secret] Please see https://trivy.dev/docs/v0.72/guide/scanner/secret#recommendation for faster secret detection +2026-07-07T09:55:56+03:00 INFO Number of language-specific files num=1 +2026-07-07T09:55:56+03:00 INFO [gomod] Detecting vulnerabilities... + +Report Summary + +┌──────────────────────────────────────────────────┬───────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├──────────────────────────────────────────────────┼───────┼─────────────────┼─────────┤ +│ app/go.mod │ gomod │ 0 │ - │ +├──────────────────────────────────────────────────┼───────┼─────────────────┼─────────┤ +│ .vagrant/machines/default/virtualbox/private_key │ text │ - │ 1 │ +└──────────────────────────────────────────────────┴───────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +.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:2-7 (offset: 36 bytes) + +──────────────────────────────────────── + + 1 -----BEGIN OPENSSH PRIVATE KEY----- + + 2 ┌ ************************************************************ + + 3 │ ************************************************************ + + 4 │ ************************************************************ + + 5 │ ************************************************************ + + 6 │ ************************************************************ + + 7 └ ************************ + + 8 -----END OPENSSH PRIVATE KEY----- + +──────────────────────────────────────── + + + + + diff --git a/submissions/lab9-scans/trivy-image-after-fix.txt b/submissions/lab9-scans/trivy-image-after-fix.txt new file mode 100644 index 000000000..fea99dc75 --- /dev/null +++ b/submissions/lab9-scans/trivy-image-after-fix.txt @@ -0,0 +1,70 @@ +2026-07-07T10:01:44+03:00 INFO [vuln] Vulnerability scanning is enabled +2026-07-07T10:01:44+03:00 INFO [secret] Secret scanning is enabled +2026-07-07T10:01:44+03:00 INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T10:01:44+03:00 INFO [secret] Please see https://trivy.dev/docs/v0.72/guide/scanner/secret#recommendation for faster secret detection +2026-07-07T10:01:44+03:00 INFO Number of language-specific files num=1 +2026-07-07T10:01:44+03:00 INFO [gobinary] Detecting vulnerabilities... +2026-07-07T10:01:44+03:00 WARN Using severities from other vendors for some vulnerabilities. Read https://trivy.dev/docs/v0.72/guide/scanner/vulnerability#severity-selection for details. + +Report Summary + +┌────────┬──────────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├────────┼──────────┼─────────────────┼─────────┤ +│ bin/qn │ gobinary │ 11 │ - │ +└────────┴──────────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +bin/qn (gobinary) +================= +Total: 11 (HIGH: 11, CRITICAL: 0) + +┌─────────┬────────────────┬──────────┬────────┬───────────────────┬─────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼────────┼───────────────────┼─────────────────┼──────────────────────────────────────────────────────────────┤ +│ stdlib │ CVE-2026-25679 │ HIGH │ fixed │ v1.24.13 │ 1.25.8, 1.26.1 │ net/url: Incorrect parsing of IPv6 host literals in net/url │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-25679 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-27145 │ │ │ │ 1.25.11, 1.26.4 │ crypto/x509: golang: golang crypto/x509: Denial of Service │ +│ │ │ │ │ │ │ via excessive processing of DNS... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-27145 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32280 │ │ │ │ 1.25.9, 1.26.2 │ crypto/x509: crypto/tls: golang: Go: Denial of Service │ +│ │ │ │ │ │ │ vulnerability in certificate chain building... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32280 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32281 │ │ │ │ │ crypto/x509: golang: Go crypto/x509: Denial of Service via │ +│ │ │ │ │ │ │ inefficient certificate chain validation... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32281 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32283 │ │ │ │ │ crypto/tls: golang: Go crypto/tls: Denial of Service via │ +│ │ │ │ │ │ │ multiple TLS 1.3 key... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32283 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33811 │ │ │ │ 1.25.10, 1.26.3 │ net: golang: Go net package: Denial of Service via long │ +│ │ │ │ │ │ │ CNAME response... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33811 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33814 │ │ │ │ │ net/http/internal/http2: golang: golang.org/x/net: Go │ +│ │ │ │ │ │ │ HTTP/2: Denial of Service via malformed │ +│ │ │ │ │ │ │ SETTINGS_MAX_FRAME_SIZE frame... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33814 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39820 │ │ │ │ │ net/mail: golang: Go net/mail: Denial of Service via crafted │ +│ │ │ │ │ │ │ email inputs │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39820 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39836 │ │ │ │ │ ELSA-2026-22121: golang security update (IMPORTANT) │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39836 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42499 │ │ │ │ │ net/mail: golang: net/mail: Denial of Service via │ +│ │ │ │ │ │ │ pathological email address parsing │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42499 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42504 │ │ │ │ 1.25.11, 1.26.4 │ Decoding a maliciously-crafted MIME header containing many │ +│ │ │ │ │ │ │ invalid enc ... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42504 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴─────────────────┴──────────────────────────────────────────────────────────────┘ diff --git a/submissions/lab9-scans/trivy-image.txt b/submissions/lab9-scans/trivy-image.txt new file mode 100644 index 000000000..b4a5cc8cb --- /dev/null +++ b/submissions/lab9-scans/trivy-image.txt @@ -0,0 +1,86 @@ +2026-07-07T09:55:40+03:00 INFO [vulndb] Need to update DB +2026-07-07T09:55:40+03:00 INFO [vulndb] Downloading vulnerability DB... +2026-07-07T09:55:40+03:00 INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T09:55:48+03:00 INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T09:55:48+03:00 INFO [vuln] Vulnerability scanning is enabled +2026-07-07T09:55:48+03:00 INFO [secret] Secret scanning is enabled +2026-07-07T09:55:48+03:00 INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T09:55:48+03:00 INFO [secret] Please see https://trivy.dev/docs/v0.72/guide/scanner/secret#recommendation for faster secret detection +2026-07-07T09:55:48+03:00 INFO Number of language-specific files num=1 +2026-07-07T09:55:48+03:00 INFO [gobinary] Detecting vulnerabilities... +2026-07-07T09:55:48+03:00 WARN Using severities from other vendors for some vulnerabilities. Read https://trivy.dev/docs/v0.72/guide/scanner/vulnerability#severity-selection for details. + +Report Summary + +┌────────┬──────────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├────────┼──────────┼─────────────────┼─────────┤ +│ bin/qn │ gobinary │ 14 │ - │ +└────────┴──────────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +bin/qn (gobinary) +================= +Total: 14 (HIGH: 13, CRITICAL: 1) + +┌─────────┬────────────────┬──────────┬────────┬───────────────────┬──────────────────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼────────┼───────────────────┼──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ stdlib │ CVE-2025-68121 │ CRITICAL │ fixed │ v1.24.5 │ 1.24.13, 1.25.7, 1.26.0-rc.3 │ crypto/tls: crypto/tls: Incorrect certificate validation │ +│ │ │ │ │ │ │ during TLS session resumption │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2025-68121 │ +│ ├────────────────┼──────────┤ │ ├──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2025-61726 │ HIGH │ │ │ 1.24.12, 1.25.6 │ golang: net/url: Memory exhaustion in query parameter │ +│ │ │ │ │ │ │ parsing in net/url │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2025-61726 │ +│ ├────────────────┤ │ │ ├──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2025-61729 │ │ │ │ 1.24.11, 1.25.5 │ crypto/x509: golang: Denial of Service due to excessive │ +│ │ │ │ │ │ │ resource consumption via crafted... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2025-61729 │ +│ ├────────────────┤ │ │ ├──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-25679 │ │ │ │ 1.25.8, 1.26.1 │ net/url: Incorrect parsing of IPv6 host literals in net/url │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-25679 │ +│ ├────────────────┤ │ │ ├──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-27145 │ │ │ │ 1.25.11, 1.26.4 │ crypto/x509: golang: golang crypto/x509: Denial of Service │ +│ │ │ │ │ │ │ via excessive processing of DNS... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-27145 │ +│ ├────────────────┤ │ │ ├──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32280 │ │ │ │ 1.25.9, 1.26.2 │ crypto/x509: crypto/tls: golang: Go: Denial of Service │ +│ │ │ │ │ │ │ vulnerability in certificate chain building... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32280 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32281 │ │ │ │ │ crypto/x509: golang: Go crypto/x509: Denial of Service via │ +│ │ │ │ │ │ │ inefficient certificate chain validation... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32281 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32283 │ │ │ │ │ crypto/tls: golang: Go crypto/tls: Denial of Service via │ +│ │ │ │ │ │ │ multiple TLS 1.3 key... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32283 │ +│ ├────────────────┤ │ │ ├──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33811 │ │ │ │ 1.25.10, 1.26.3 │ net: golang: Go net package: Denial of Service via long │ +│ │ │ │ │ │ │ CNAME response... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33811 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33814 │ │ │ │ │ net/http/internal/http2: golang: golang.org/x/net: Go │ +│ │ │ │ │ │ │ HTTP/2: Denial of Service via malformed │ +│ │ │ │ │ │ │ SETTINGS_MAX_FRAME_SIZE frame... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33814 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39820 │ │ │ │ │ net/mail: golang: Go net/mail: Denial of Service via crafted │ +│ │ │ │ │ │ │ email inputs │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39820 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39836 │ │ │ │ │ ELSA-2026-22121: golang security update (IMPORTANT) │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39836 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42499 │ │ │ │ │ net/mail: golang: net/mail: Denial of Service via │ +│ │ │ │ │ │ │ pathological email address parsing │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42499 │ +│ ├────────────────┤ │ │ ├──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42504 │ │ │ │ 1.25.11, 1.26.4 │ Decoding a maliciously-crafted MIME header containing many │ +│ │ │ │ │ │ │ invalid enc ... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42504 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴──────────────────────────────┴──────────────────────────────────────────────────────────────┘ diff --git a/submissions/lab9-scans/zap-after/zap-health.html b/submissions/lab9-scans/zap-after/zap-health.html new file mode 100644 index 000000000..f240b5d90 --- /dev/null +++ b/submissions/lab9-scans/zap-after/zap-health.html @@ -0,0 +1,651 @@ + + +
+ +| Risk Level | +Number of Alerts | +
|---|---|
|
+ High
+ |
+
+ 0
+ |
+
|
+ Medium
+ |
+
+ 0
+ |
+
|
+ Low
+ |
+
+ 1
+ |
+
|
+ Informational
+ |
+
+ 1
+ |
+
|
+ False Positives:
+ |
+
+ 0
+ |
+
For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).
+ + + + + + +| Name | +Risk Level | +Number of Instances | +
|---|---|---|
| ZAP is Out of Date | +Low | + + +1 | + +
| Non-Storable Content | +Informational | + + +4 | + +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +The latest version of ZAP is 2.17.0 | +
| Instances | + + +1 | + +
| Solution | +
+ Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+
+ |
+
| Reference | ++ https://www.zaproxy.org/download/ + + | +
| CWE Id | +1104 | +
| WASC Id | +45 | +
| Plugin Id | +10116 | +
|
+ Informational |
+ Non-Storable Content | +
|---|---|
| Description | +
+ The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| URL | +http://localhost:8080/health | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| URL | +http://localhost:8080/robots.txt | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| URL | +http://localhost:8080/sitemap.xml | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| Instances | + + +4 | + +
| Solution | +
+ The content may be marked as storable by ensuring that the following conditions are satisfied:
+ + + The request method must be understood by the cache and defined as being cacheable ("GET", "HEAD", and "POST" are currently defined as cacheable)
+ + + The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
+ + + The "no-store" cache directive must not appear in the request or response header fields
+ + + For caching by "shared" caches such as "proxy" caches, the "private" response directive must not appear in the response
+ + + For caching by "shared" caches such as "proxy" caches, the "Authorization" header field must not appear in the request, unless the response explicitly allows it (using one of the "must-revalidate", "public", or "s-maxage" Cache-Control response directives)
+ + + In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
+ + + It must contain an "Expires" header field
+ + + It must contain a "max-age" response directive
+ + + For "shared" caches such as "proxy" caches, it must contain a "s-maxage" response directive
+ + + It must contain a "Cache Control Extension" that allows it to be cached
+ + + It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
+
+ |
+
| 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 Id | +524 | +
| WASC Id | +13 | +
| Plugin Id | +10049 | +
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://localhost: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-1", + "alert": "Non-Storable Content", + "name": "Non-Storable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
", + "instances":[ + { + "id": "6", + "uri": "http://localhost:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "1", + "uri": "http://localhost:8080/health", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "4", + "uri": "http://localhost:8080/robots.txt", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "3", + "uri": "http://localhost:8080/sitemap.xml", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + } + ], + "count": "4", + "systemic": false, + "solution": "The content may be marked as storable by ensuring that the following conditions are satisfied:
The request method must be understood by the cache and defined as being cacheable (\"GET\", \"HEAD\", and \"POST\" are currently defined as cacheable)
The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
The \"no-store\" cache directive must not appear in the request or response header fields
For caching by \"shared\" caches such as \"proxy\" caches, the \"private\" response directive must not appear in the response
For caching by \"shared\" caches such as \"proxy\" caches, the \"Authorization\" header field must not appear in the request, unless the response explicitly allows it (using one of the \"must-revalidate\", \"public\", or \"s-maxage\" Cache-Control response directives)
In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
It must contain an \"Expires\" header field
It must contain a \"max-age\" response directive
For \"shared\" caches such as \"proxy\" caches, it must contain a \"s-maxage\" response directive
It must contain a \"Cache Control Extension\" that allows it to be cached
It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
", + "otherinfo": "", + "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-scans/zap-after/zap-metrics.html b/submissions/lab9-scans/zap-after/zap-metrics.html new file mode 100644 index 000000000..2611189da --- /dev/null +++ b/submissions/lab9-scans/zap-after/zap-metrics.html @@ -0,0 +1,619 @@ + + + + +| Risk Level | +Number of Alerts | +
|---|---|
|
+ High
+ |
+
+ 0
+ |
+
|
+ Medium
+ |
+
+ 0
+ |
+
|
+ Low
+ |
+
+ 1
+ |
+
|
+ Informational
+ |
+
+ 1
+ |
+
|
+ False Positives:
+ |
+
+ 0
+ |
+
For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).
+ + + + + + +| Name | +Risk Level | +Number of Instances | +
|---|---|---|
| ZAP is Out of Date | +Low | + + +1 | + +
| Non-Storable Content | +Informational | + + +3 | + +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/metrics | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +The latest version of ZAP is 2.17.0 | +
| Instances | + + +1 | + +
| Solution | +
+ Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+
+ |
+
| Reference | ++ https://www.zaproxy.org/download/ + + | +
| CWE Id | +1104 | +
| WASC Id | +45 | +
| Plugin Id | +10116 | +
|
+ Informational |
+ Non-Storable Content | +
|---|---|
| Description | +
+ The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| URL | +http://localhost:8080/metrics | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| URL | +http://localhost:8080/robots.txt | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| Instances | + + +3 | + +
| Solution | +
+ The content may be marked as storable by ensuring that the following conditions are satisfied:
+ + + The request method must be understood by the cache and defined as being cacheable ("GET", "HEAD", and "POST" are currently defined as cacheable)
+ + + The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
+ + + The "no-store" cache directive must not appear in the request or response header fields
+ + + For caching by "shared" caches such as "proxy" caches, the "private" response directive must not appear in the response
+ + + For caching by "shared" caches such as "proxy" caches, the "Authorization" header field must not appear in the request, unless the response explicitly allows it (using one of the "must-revalidate", "public", or "s-maxage" Cache-Control response directives)
+ + + In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
+ + + It must contain an "Expires" header field
+ + + It must contain a "max-age" response directive
+ + + For "shared" caches such as "proxy" caches, it must contain a "s-maxage" response directive
+ + + It must contain a "Cache Control Extension" that allows it to be cached
+ + + It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
+
+ |
+
| 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 Id | +524 | +
| WASC Id | +13 | +
| Plugin Id | +10049 | +
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://localhost:8080/metrics", + "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-1", + "alert": "Non-Storable Content", + "name": "Non-Storable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
", + "instances":[ + { + "id": "3", + "uri": "http://localhost:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "6", + "uri": "http://localhost:8080/metrics", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "1", + "uri": "http://localhost:8080/robots.txt", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + } + ], + "count": "3", + "systemic": false, + "solution": "The content may be marked as storable by ensuring that the following conditions are satisfied:
The request method must be understood by the cache and defined as being cacheable (\"GET\", \"HEAD\", and \"POST\" are currently defined as cacheable)
The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
The \"no-store\" cache directive must not appear in the request or response header fields
For caching by \"shared\" caches such as \"proxy\" caches, the \"private\" response directive must not appear in the response
For caching by \"shared\" caches such as \"proxy\" caches, the \"Authorization\" header field must not appear in the request, unless the response explicitly allows it (using one of the \"must-revalidate\", \"public\", or \"s-maxage\" Cache-Control response directives)
In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
It must contain an \"Expires\" header field
It must contain a \"max-age\" response directive
For \"shared\" caches such as \"proxy\" caches, it must contain a \"s-maxage\" response directive
It must contain a \"Cache Control Extension\" that allows it to be cached
It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
", + "otherinfo": "", + "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-scans/zap-after/zap-notes.html b/submissions/lab9-scans/zap-after/zap-notes.html new file mode 100644 index 000000000..ee2432848 --- /dev/null +++ b/submissions/lab9-scans/zap-after/zap-notes.html @@ -0,0 +1,619 @@ + + + + +| Risk Level | +Number of Alerts | +
|---|---|
|
+ High
+ |
+
+ 0
+ |
+
|
+ Medium
+ |
+
+ 0
+ |
+
|
+ Low
+ |
+
+ 1
+ |
+
|
+ Informational
+ |
+
+ 1
+ |
+
|
+ False Positives:
+ |
+
+ 0
+ |
+
For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).
+ + + + + + +| Name | +Risk Level | +Number of Instances | +
|---|---|---|
| ZAP is Out of Date | +Low | + + +1 | + +
| Non-Storable Content | +Informational | + + +3 | + +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +The latest version of ZAP is 2.17.0 | +
| Instances | + + +1 | + +
| Solution | +
+ Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+
+ |
+
| Reference | ++ https://www.zaproxy.org/download/ + + | +
| CWE Id | +1104 | +
| WASC Id | +45 | +
| Plugin Id | +10116 | +
|
+ Informational |
+ Non-Storable Content | +
|---|---|
| Description | +
+ The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| URL | +http://localhost:8080/notes | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| URL | +http://localhost:8080/sitemap.xml | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | +no-store | +
| Other Info | ++ |
| Instances | + + +3 | + +
| Solution | +
+ The content may be marked as storable by ensuring that the following conditions are satisfied:
+ + + The request method must be understood by the cache and defined as being cacheable ("GET", "HEAD", and "POST" are currently defined as cacheable)
+ + + The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
+ + + The "no-store" cache directive must not appear in the request or response header fields
+ + + For caching by "shared" caches such as "proxy" caches, the "private" response directive must not appear in the response
+ + + For caching by "shared" caches such as "proxy" caches, the "Authorization" header field must not appear in the request, unless the response explicitly allows it (using one of the "must-revalidate", "public", or "s-maxage" Cache-Control response directives)
+ + + In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
+ + + It must contain an "Expires" header field
+ + + It must contain a "max-age" response directive
+ + + For "shared" caches such as "proxy" caches, it must contain a "s-maxage" response directive
+ + + It must contain a "Cache Control Extension" that allows it to be cached
+ + + It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
+
+ |
+
| 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 Id | +524 | +
| WASC Id | +13 | +
| Plugin Id | +10049 | +
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://localhost: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": "8" + }, + { + "pluginid": "10049", + "alertRef": "10049-1", + "alert": "Non-Storable Content", + "name": "Non-Storable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
", + "instances":[ + { + "id": "4", + "uri": "http://localhost:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "5", + "uri": "http://localhost:8080/notes", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "2", + "uri": "http://localhost:8080/sitemap.xml", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + } + ], + "count": "3", + "systemic": false, + "solution": "The content may be marked as storable by ensuring that the following conditions are satisfied:
The request method must be understood by the cache and defined as being cacheable (\"GET\", \"HEAD\", and \"POST\" are currently defined as cacheable)
The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
The \"no-store\" cache directive must not appear in the request or response header fields
For caching by \"shared\" caches such as \"proxy\" caches, the \"private\" response directive must not appear in the response
For caching by \"shared\" caches such as \"proxy\" caches, the \"Authorization\" header field must not appear in the request, unless the response explicitly allows it (using one of the \"must-revalidate\", \"public\", or \"s-maxage\" Cache-Control response directives)
In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
It must contain an \"Expires\" header field
It must contain a \"max-age\" response directive
For \"shared\" caches such as \"proxy\" caches, it must contain a \"s-maxage\" response directive
It must contain a \"Cache Control Extension\" that allows it to be cached
It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
", + "otherinfo": "", + "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": "8" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9-scans/zap-after/zap.yaml b/submissions/lab9-scans/zap-after/zap.yaml new file mode 100644 index 000000000..439adc876 --- /dev/null +++ b/submissions/lab9-scans/zap-after/zap.yaml @@ -0,0 +1,41 @@ +env: + contexts: + - excludePaths: [] + name: baseline + urls: + - http://localhost:8080/notes + - http://localhost:8080/ + parameters: + failOnError: true + progressToStdout: false +jobs: +- parameters: + enableTags: false + maxAlertsPerRule: 10 + type: passiveScan-config +- parameters: + maxDuration: 1 + url: http://localhost: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-notes.html + reportTitle: ZAP Scanning Report + template: traditional-html + type: report +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-notes.json + reportTitle: ZAP Scanning Report + template: traditional-json + type: report diff --git a/submissions/lab9-scans/zap-before/zap-health.html b/submissions/lab9-scans/zap-before/zap-health.html new file mode 100644 index 000000000..6fd3a26fb --- /dev/null +++ b/submissions/lab9-scans/zap-before/zap-health.html @@ -0,0 +1,838 @@ + + + + +| Risk Level | +Number of Alerts | +
|---|---|
|
+ High
+ |
+
+ 0
+ |
+
|
+ Medium
+ |
+
+ 0
+ |
+
|
+ Low
+ |
+
+ 3
+ |
+
|
+ Informational
+ |
+
+ 1
+ |
+
|
+ False Positives:
+ |
+
+ 0
+ |
+
For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).
+ + + + + + +| Name | +Risk Level | +Number of Instances | +
|---|---|---|
| Insufficient Site Isolation Against Spectre Vulnerability | +Low | + + +1 | + +
| X-Content-Type-Options Header Missing | +Low | + + +1 | + +
| ZAP is Out of Date | +Low | + + +1 | + +
| Storable and Cacheable Content | +Informational | + + +4 | + +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/health | +
| Method | +GET | +
| Parameter | +Cross-Origin-Resource-Policy | +
| Attack | ++ |
| Evidence | ++ |
| Other Info | ++ |
| Instances | + + +1 | + +
| 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 Id | +693 | +
| WASC Id | +14 | +
| Plugin Id | +90004 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/health | +
| Method | +GET | +
| Parameter | +x-content-type-options | +
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| Instances | + + +1 | + +
| 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 Id | +693 | +
| WASC Id | +15 | +
| Plugin Id | +10021 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/health | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +The latest version of ZAP is 2.17.0 | +
| Instances | + + +1 | + +
| Solution | +
+ Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+
+ |
+
| Reference | ++ https://www.zaproxy.org/download/ + + | +
| CWE Id | +1104 | +
| WASC Id | +45 | +
| Plugin Id | +10116 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/health | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/robots.txt | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/sitemap.xml | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| Instances | + + +4 | + +
| 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 Id | +524 | +
| WASC Id | +13 | +
| Plugin Id | +10049 | +
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://localhost:8080/health", + "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": "1", + "uri": "http://localhost:8080/health", + "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": "9" + }, + { + "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://localhost:8080/health", + "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": "1" + }, + { + "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": "2", + "uri": "http://localhost: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://localhost:8080/health", + "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": "4", + "uri": "http://localhost:8080/robots.txt", + "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": "3", + "uri": "http://localhost: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": "4", + "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": "10" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9-scans/zap-before/zap-metrics.html b/submissions/lab9-scans/zap-before/zap-metrics.html new file mode 100644 index 000000000..920afb096 --- /dev/null +++ b/submissions/lab9-scans/zap-before/zap-metrics.html @@ -0,0 +1,838 @@ + + + + +| Risk Level | +Number of Alerts | +
|---|---|
|
+ High
+ |
+
+ 0
+ |
+
|
+ Medium
+ |
+
+ 0
+ |
+
|
+ Low
+ |
+
+ 3
+ |
+
|
+ Informational
+ |
+
+ 1
+ |
+
|
+ False Positives:
+ |
+
+ 0
+ |
+
For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).
+ + + + + + +| Name | +Risk Level | +Number of Instances | +
|---|---|---|
| Insufficient Site Isolation Against Spectre Vulnerability | +Low | + + +1 | + +
| X-Content-Type-Options Header Missing | +Low | + + +1 | + +
| ZAP is Out of Date | +Low | + + +1 | + +
| Storable and Cacheable Content | +Informational | + + +4 | + +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/metrics | +
| Method | +GET | +
| Parameter | +Cross-Origin-Resource-Policy | +
| Attack | ++ |
| Evidence | ++ |
| Other Info | ++ |
| Instances | + + +1 | + +
| 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 Id | +693 | +
| WASC Id | +14 | +
| Plugin Id | +90004 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/metrics | +
| Method | +GET | +
| Parameter | +x-content-type-options | +
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| Instances | + + +1 | + +
| 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 Id | +693 | +
| WASC Id | +15 | +
| Plugin Id | +10021 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/sitemap.xml | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +The latest version of ZAP is 2.17.0 | +
| Instances | + + +1 | + +
| Solution | +
+ Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+
+ |
+
| Reference | ++ https://www.zaproxy.org/download/ + + | +
| CWE Id | +1104 | +
| WASC Id | +45 | +
| Plugin Id | +10116 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/metrics | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/robots.txt | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/sitemap.xml | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| Instances | + + +4 | + +
| 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 Id | +524 | +
| WASC Id | +13 | +
| Plugin Id | +10049 | +
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://localhost:8080/metrics", + "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://localhost:8080/metrics", + "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://localhost:8080/sitemap.xml", + "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": "11" + }, + { + "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": "1", + "uri": "http://localhost: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://localhost:8080/metrics", + "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://localhost:8080/robots.txt", + "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": "6", + "uri": "http://localhost: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": "4", + "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": "9" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab9-scans/zap-before/zap-notes.html b/submissions/lab9-scans/zap-before/zap-notes.html new file mode 100644 index 000000000..d16f7854a --- /dev/null +++ b/submissions/lab9-scans/zap-before/zap-notes.html @@ -0,0 +1,838 @@ + + + + +| Risk Level | +Number of Alerts | +
|---|---|
|
+ High
+ |
+
+ 0
+ |
+
|
+ Medium
+ |
+
+ 0
+ |
+
|
+ Low
+ |
+
+ 3
+ |
+
|
+ Informational
+ |
+
+ 1
+ |
+
|
+ False Positives:
+ |
+
+ 0
+ |
+
For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).
+ + + + + + +| Name | +Risk Level | +Number of Instances | +
|---|---|---|
| Insufficient Site Isolation Against Spectre Vulnerability | +Low | + + +1 | + +
| X-Content-Type-Options Header Missing | +Low | + + +1 | + +
| ZAP is Out of Date | +Low | + + +1 | + +
| Storable and Cacheable Content | +Informational | + + +4 | + +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/notes | +
| Method | +GET | +
| Parameter | +Cross-Origin-Resource-Policy | +
| Attack | ++ |
| Evidence | ++ |
| Other Info | ++ |
| Instances | + + +1 | + +
| 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 Id | +693 | +
| WASC Id | +14 | +
| Plugin Id | +90004 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/notes | +
| Method | +GET | +
| Parameter | +x-content-type-options | +
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| Instances | + + +1 | + +
| 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 Id | +693 | +
| WASC Id | +15 | +
| Plugin Id | +10021 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/notes | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +The latest version of ZAP is 2.17.0 | +
| Instances | + + +1 | + +
| Solution | +
+ Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+
+ |
+
| Reference | ++ https://www.zaproxy.org/download/ + + | +
| CWE Id | +1104 | +
| WASC Id | +45 | +
| Plugin Id | +10116 | +
|
+ 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.
+
+ |
+
| + | |
| URL | +http://localhost:8080/ | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/notes | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/robots.txt | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| URL | +http://localhost:8080/sitemap.xml | +
| Method | +GET | +
| Parameter | ++ |
| Attack | ++ |
| Evidence | ++ |
| Other Info | +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. | +
| Instances | + + +4 | + +
| 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 Id | +524 | +
| WASC Id | +13 | +
| Plugin Id | +10049 | +
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://localhost: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": "1", + "uri": "http://localhost: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": "11" + }, + { + "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": "4", + "uri": "http://localhost:8080/notes", + "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": "1" + }, + { + "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": "2", + "uri": "http://localhost: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://localhost: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": "6", + "uri": "http://localhost:8080/robots.txt", + "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": "3", + "uri": "http://localhost: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": "4", + "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-scans/zap-before/zap.yaml b/submissions/lab9-scans/zap-before/zap.yaml new file mode 100644 index 000000000..439adc876 --- /dev/null +++ b/submissions/lab9-scans/zap-before/zap.yaml @@ -0,0 +1,41 @@ +env: + contexts: + - excludePaths: [] + name: baseline + urls: + - http://localhost:8080/notes + - http://localhost:8080/ + parameters: + failOnError: true + progressToStdout: false +jobs: +- parameters: + enableTags: false + maxAlertsPerRule: 10 + type: passiveScan-config +- parameters: + maxDuration: 1 + url: http://localhost: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-notes.html + reportTitle: ZAP Scanning Report + template: traditional-html + type: report +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-notes.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..f3945ec3b --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,234 @@ +# Lab 9 — DevSecOps: Scan QuickNotes with Trivy + ZAP + +Tooling: Trivy `0.72.0` (native CLI, Homebrew — pinned, not `latest`). Full raw outputs are in [`submissions/lab9-scans/`](./lab9-scans/). + +## Task 1 — Trivy: Image + Filesystem + Config + SBOM + +### 1.1 Scans run + +1. `trivy image --severity HIGH,CRITICAL quicknotes:lab6` → [`trivy-image.txt`](./lab9-scans/trivy-image.txt) +2. `trivy fs --severity HIGH,CRITICAL .` → [`trivy-fs.txt`](./lab9-scans/trivy-fs.txt) +3. `trivy config .` → [`trivy-config.txt`](./lab9-scans/trivy-config.txt) +4. `trivy image --format cyclonedx quicknotes:lab6` → [`sbom-cyclonedx.json`](./lab9-scans/sbom-cyclonedx.json) + +**Image scan (before fix)** — summary: + +``` +Report Summary +┌────────┬──────────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├────────┼──────────┼─────────────────┼─────────┤ +│ bin/qn │ gobinary │ 14 │ - │ +└────────┴──────────┴─────────────────┴─────────┘ + +bin/qn (gobinary) +Total: 14 (HIGH: 13, CRITICAL: 1) +``` + +The image is `scratch` + a single static Go binary (Lab 6). Every finding is in `stdlib`, tied to the `golang:1.24.5` build-stage toolchain — there is no OS package layer to scan, which is exactly the distroless/scratch payoff (see 1.3.b). + +**Filesystem scan** — summary: + +``` +Report Summary +┌──────────────────────────────────────────────────┬───────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├──────────────────────────────────────────────────┼───────┼─────────────────┼─────────┤ +│ app/go.mod │ gomod │ 0 │ - │ +├──────────────────────────────────────────────────┼───────┼─────────────────┼─────────┤ +│ .vagrant/machines/default/virtualbox/private_key │ text │ - │ 1 │ +└──────────────────────────────────────────────────┴───────┴─────────────────┴─────────┘ +``` + +`app/go.mod` has zero third-party dependencies (QuickNotes only imports Go stdlib), so `gomod` scanning finds nothing — all risk is in the stdlib version, already covered by the image scan. The one HIGH is a secret scanner hit, not a CVE (see triage table). + +**Config scan** — summary: + +``` +Report Summary +┌────────────┬────────────┬───────────────────┐ +│ Target │ Type │ Misconfigurations │ +├────────────┼────────────┼───────────────────┤ +│ Dockerfile │ dockerfile │ 1 │ +└────────────┴────────────┴───────────────────┘ + +Dockerfile (dockerfile) +Tests: 27 (SUCCESSES: 26, FAILURES: 1) +Failures: 1 (LOW: 1) — DS-0026: Add HEALTHCHECK instruction +``` + +Only `Dockerfile` was scanned — Trivy's `config` misconfig scanner does not have a check pack for Compose files (only Dockerfile / Terraform / Kubernetes / CloudFormation as of 0.72.0), so `docker-compose.yml` shows `num=0` detected config files when scanned directly. No HIGH/CRITICAL misconfigs found. + +**SBOM (first 30 lines):** + +```json +{ + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "serialNumber": "urn:uuid:08cddf6a-9627-4f99-a3d7-2900ea1fbdd3", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T06:59:12+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "0.72.0" + } + ] + }, + "component": { + "bom-ref": "4ef8bb1e-50c8-40e2-8094-ff6b93e59122", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:70a952b1d983fbc9a4af340c4493196b673e1f6e085e4dcbd5b8fb791f3662ab" + }, +``` + +### 1.2 Triage — every HIGH/CRITICAL finding + +Reachability context used throughout: QuickNotes imports only `net/http`, `encoding/json`, `errors`, `sort`, `strconv`, `sync/atomic`, `context`, `log`, `os`, `os/signal`, `syscall`, `time` (checked via `grep import app/*.go`). It serves **plain HTTP/1.1** on `:8080` — no TLS termination, no outbound DNS/mail/multipart parsing in app code. Trivy's `gobinary` scanner flags a CVE if it's present in the *stdlib version string* embedded in the binary, regardless of whether the vulnerable function is ever called — unlike `govulncheck`, which does call-graph reachability analysis and only reports CVEs actually reachable from the code (see Bonus task, if attempted, for that contrast in practice). + +**Fix applied:** bumped the build stage from `golang:1.24.5` → `golang:1.24.13` in [`Dockerfile`](../Dockerfile) (same `go 1.24` line pinned in `go.mod` — a safe patch bump, no toolchain/API risk). Rebuilt and rescanned: CRITICAL and 2 of the 13 HIGH findings disappeared (fixed at ≤1.24.13); 11 HIGH remain, all requiring a Go **1.25/1.26 minor** bump not yet available on the pinned `1.24` line. + +| # | Source | Finding | Severity | Disposition | Reason | +|---|--------|---------|----------|--------------|--------| +| 1 | image | CVE-2025-68121 — `crypto/tls` incorrect cert validation on TLS session resumption | CRITICAL | **FIX** | Fixed by bumping build stage to `golang:1.24.13` (this PR, `Dockerfile`). Moot for reachability too — app never terminates TLS — but free to fix, so fixed. | +| 2 | image | CVE-2025-61726 — `net/url` memory exhaustion in query parsing | HIGH | **FIX** | Same bump to `1.24.13` resolves it; `net/url` is reachable (used internally by `net/http` request parsing), so worth fixing regardless. | +| 3 | image | CVE-2025-61729 — `crypto/x509` DoS via crafted cert chain | HIGH | **FIX** | Same bump to `1.24.13` resolves it. | +| 4 | image | CVE-2026-25679 — `net/url` IPv6 host literal parsing | HIGH | **ACCEPT** (re-eval 2026-10-01) | Fix needs Go 1.25.8+, a minor bump outside the pinned `go 1.24` line — bigger change than a patch bump. `net/url` is reachable via request parsing, so this stays a scheduled re-eval, not WATCH. | +| 5 | image | CVE-2026-27145 — `crypto/x509` DoS via DNS name processing | HIGH | **ACCEPT** (re-eval 2026-10-01) | Needs Go 1.25.11+. Not reachable today (app performs no x509 operations — no TLS, no cert parsing) but re-evaluate if that changes. | +| 6 | image | CVE-2026-32280 — `crypto/x509`/`crypto/tls` DoS via cert chain building | HIGH | **ACCEPT** (re-eval 2026-10-01) | Needs Go 1.25.9+. Same reasoning as #5 — no TLS/x509 code path in QuickNotes. | +| 7 | image | CVE-2026-32281 — `crypto/x509` DoS via inefficient chain validation | HIGH | **ACCEPT** (re-eval 2026-10-01) | Needs Go 1.25.9+ (no separate fixed version listed). Same reasoning as #5. | +| 8 | image | CVE-2026-32283 — `crypto/tls` DoS via multiple TLS 1.3 key shares | HIGH | **ACCEPT** (re-eval 2026-10-01) | Needs Go 1.25.9+. QuickNotes never negotiates TLS 1.3 (plain HTTP). | +| 9 | image | CVE-2026-33811 — `net` DoS via long CNAME response | HIGH | **WATCH** | Needs Go 1.25.10+, not yet on the pinned `1.24` line. App does no outbound DNS resolution of untrusted names; effectively unreachable. Re-check: next `go.mod` bump to Go 1.25, or 2026-10-01, whichever comes first. | +| 10 | image | CVE-2026-33814 — `net/http/internal/http2` DoS via malformed SETTINGS frame | HIGH | **ACCEPT** (re-eval 2026-10-01) | Needs Go 1.25.10+. Go's HTTP/2 only activates over TLS ALPN negotiation; QuickNotes serves HTTP/1.1 only, so unreachable today. Re-eval if TLS/h2 is ever added in front of the app. | +| 11 | image | CVE-2026-39820 — `net/mail` DoS via crafted email input | HIGH | **WATCH** | Needs Go 1.25.11+. `net/mail` is not imported anywhere in `app/*.go` — flagged purely because it's part of the linked stdlib version, not because it's reachable. Re-check: next `go.mod` bump to Go 1.25, or 2026-10-01, whichever comes first. | +| 12 | image | CVE-2026-39836 — ELSA-2026-22121 general Go security bundle advisory | HIGH | **WATCH** | Vendor umbrella advisory bundling several of the above; no distinct fixed version given. Re-check: resolves automatically once #4–#11/#13/#14 are fixed by the Go 1.25 bump; re-verify with `trivy image` at that point. | +| 13 | image | CVE-2026-42499 — `net/mail` DoS via pathological address parsing | HIGH | **WATCH** | Same as #11 — `net/mail` unused by QuickNotes. Re-check: next `go.mod` bump to Go 1.25, or 2026-10-01, whichever comes first. | +| 14 | image | CVE-2026-42504 — MIME header decoding DoS (many invalid encoded words) | HIGH | **WATCH** | Needs Go 1.25.11+. QuickNotes does no multipart/MIME parsing (`grep -n multipart app/*.go` empty). Re-check: next `go.mod` bump to Go 1.25, or 2026-10-01, whichever comes first. | +| 15 | fs | `.vagrant/machines/default/virtualbox/private_key` — AsymmetricPrivateKey (Lab 5 Vagrant keypair) | HIGH | **ACCEPT** (re-eval 2026-10-01) | The scanner is correct — a real private key is on disk — so this isn't a false positive; the risk is accepted because it's the per-VM keypair Vagrant auto-generates locally (Lab 5), confirmed gitignored (`git check-ignore -v` matches `.gitignore:27 .vagrant/`), so it's never committed and never leaves this machine. Re-evaluate if `.vagrant/` is ever exempted from `.gitignore` or the repo starts using a shared/remote Vagrant provider. | + +Config scan produced only one LOW finding (`DS-0026`, missing `HEALTHCHECK`) — no HIGH/CRITICAL to triage there. + +### 1.3 Design questions + +**a) CVE severity is one input, not the answer. What else matters when triaging?** +Severity is CVSS-style "how bad if exploited," but it says nothing about whether the vulnerable code path is ever exercised. In this scan, Trivy's `gobinary` mode flags every stdlib CVE for the embedded Go version string, whether or not QuickNotes calls the affected package — `net/mail` shows up as HIGH even though the app never imports it. **Reachability** (is the vulnerable function on any call path from an entry point we expose?), **exploit availability** (is there a public PoC / is it being exploited in the wild, vs. a theoretical DoS requiring a crafted, hard-to-reach input?), and **deployment context** (does QuickNotes terminate TLS itself, or sit behind a proxy that already terminates TLS and only forwards plain HTTP to it? Is it internet-facing or LAN-only?) all determine whether a HIGH finding is "fix tonight" or "watch." That's the whole reason the triage table above splits FIX/ACCEPT/WATCH instead of blanket-patching every HIGH. + +**b) Distroless images often show zero HIGH/CRITICAL. Why is the minimal base the strongest single security control?** +Every installed package is attack surface a scanner can report on *and* an attacker can abuse post-compromise (a shell, `curl`, a package manager to pull in more tools). QuickNotes' `FROM scratch` final stage has no OS layer at all — the fs/image scans here found **zero** OS package CVEs; the only findings are in the Go stdlib baked into the one static binary, which is unavoidable no matter how minimal the base is. A `debian:slim` or even `alpine` base would add dozens of packages, each a separate CVE surface, on top of that. Minimal base wins by *subtraction* — it removes the target rather than trying to secure it. + +**c) `.trivyignore` — when is suppression the right move vs. security theater?** +Right move: a finding has been triaged with a written reason and a re-evaluation date (i.e., a documented ACCEPT/WATCH/FALSE POSITIVE like the table above), and you want CI to stop failing on a *known, decided* item while still failing on new ones. The ignore entry should reference the CVE and point at where the disposition is recorded. Theater: using `.trivyignore` to make a red pipeline green without writing down *why* — that's just hiding the finding, not deciding on it, and it silently rots (nobody remembers to remove it when a fix ships). The discipline is: no `.trivyignore` entry without a dated, reasoned row in a triage table first. + +**d) The SBOM — what future problem does having it today solve?** +An SBOM is a queryable inventory of every component and version in the image, generated once, cheap to keep current. The value shows up the day a **new** CVE drops for something already inside your image — Log4Shell is the canonical case: organizations that had no SBOM spent days grepping filesystems and asking every team "do you use Log4j, and which version?" across their whole fleet. With a CycloneDX SBOM already generated (like `sbom-cyclonedx.json` here), answering "are we affected by CVE-X" becomes a one-line search over existing JSON instead of an emergency inventory exercise. The SBOM doesn't prevent the vulnerability — it collapses the *time-to-know* from days to minutes. + +--- + +## Task 2 — OWASP ZAP Baseline + Fix at Least One Finding + +### 2.1 Run ZAP baseline + +Tooling: `ghcr.io/zaproxy/zaproxy:2.16.1` (pinned, not `stable`/`latest`). App started via `docker compose up -d quicknotes` (Lab 6/8 image, port 8080). + +QuickNotes is a pure JSON API with no HTML/links (`GET /health`, `GET /metrics`, `GET /notes`, `POST /notes`, `GET|DELETE /notes/{id}`) and no root page — pointing `zap-baseline.py` at `http://localhost:8080` directly makes ZAP's spider fail on `/` (404) and it only discovers 3 boilerplate probe URLs (`/`, `/robots.txt`, `/sitemap.xml`), all 404. To actually get ZAP to passively scan real responses, I ran baseline separately against each real endpoint that returns 200 (`/health`, `/metrics`, `/notes`): + +``` +docker run --rm -v "$(pwd):/zap/wrk:rw" --network host ghcr.io/zaproxy/zaproxy:2.16.1 \ + zap-baseline.py -t http://localhost:8080/health -J zap-health.json -r zap-health.html +# repeated for /metrics and /notes +``` + +Full HTML + JSON reports for all three runs are in [`submissions/lab9-scans/zap-before/`](./lab9-scans/zap-before/). No active scan (`zap-full-scan.py`) was run — baseline only, per the lab's safety instructions. + +### 2.2 Triage — every ZAP finding + +Findings were identical across all three real endpoints (same middleware stack), so listed once with the endpoints they applied to: + +| ID | Name | Risk | Affected URL(s) | Disposition | Reason | +|----|------|------|------------------|--------------|--------| +| 10021 | X-Content-Type-Options Header Missing | Low (Medium confidence) | `/health`, `/metrics`, `/notes` | **FIX** | Fixed in this PR via `SecurityHeaders` middleware (`app/security.go`) — sets `X-Content-Type-Options: nosniff` on every route. See 2.3/2.4 for before/after proof. | +| 90004 | Insufficient Site Isolation Against Spectre Vulnerability | Low (Medium confidence) | `/health`, `/metrics`, `/notes` | **FIX** | Fixed in the same middleware — sets `Cross-Origin-Resource-Policy: same-origin`. See 2.3/2.4. | +| 10049 | Storable and Cacheable Content | Informational | `/`, `/health`, `/metrics`, `/notes`, `/robots.txt`, `/sitemap.xml` | **FIX** (bonus, not required) | The middleware also sets `Cache-Control: no-store`. After the fix ZAP re-labels this alert `Non-Storable Content` (informational, confirms the header worked) rather than removing it outright — informational either way, no action needed beyond what's already fixed. | +| 10116 | ZAP is Out of Date | Low (High confidence) | scanner-internal | **FALSE POSITIVE** | Not a QuickNotes finding — ZAP flagging that its own passive-scan rule pack is older than the latest release. Unrelated to the app; pin the ZAP image version deliberately (2.16.1) rather than chasing "latest". | + +Every alert ZAP raised is accounted for above — nothing left untriaged. Design question (g) covers why blanket-accepting informational/false-positive rows without reading them (rather than the reasoned dispositions above) would be the wrong habit. + +### 2.3 Fix: `SecurityHeaders` middleware + +Implemented in [`app/security.go`](../app/security.go), wired around the whole router (not per-handler) in [`app/main.go`](../app/main.go): + +```go +// app/security.go +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("Cross-Origin-Resource-Policy", "same-origin") + h.Set("Content-Security-Policy", "default-src 'none'") + h.Set("Cache-Control", "no-store") + next.ServeHTTP(w, r) + }) +} +``` + +```go +// app/main.go — wraps the router once, applies to every route +srv := &http.Server{ + Addr: addr, + Handler: SecurityHeaders(server.Routes()), + ReadHeaderTimeout: 5 * time.Second, +} +``` + +Requirements checklist: +1. ✅ Middleware wraps the router (`SecurityHeaders(server.Routes())`), not scattered `Header().Set` calls in handlers +2. ✅ Applies to all routes — `/health`, `/metrics`, `/notes`, `/notes/{id}` all pass through the same wrapped handler +3. ✅ Unit test asserts headers present: [`app/security_test.go`](../app/security_test.go) (`TestSecurityHeaders_SetOnEveryRoute`, `TestSecurityHeaders_WrapsHandler`) +4. ✅ Test genuinely guards the fix — verified by temporarily gutting `SecurityHeaders` to `return next` and re-running `go test`: both tests failed with explicit header-mismatch errors before the middleware was restored. + +### 2.4 Re-scan: before/after proof + +**Before** (`submissions/lab9-scans/zap-before/zap-health.json`, and identical for `-metrics`/`-notes`): +``` +10021-1 X-Content-Type-Options Header Missing | risk=Low (Medium) | count=1 +90004-1 Insufficient Site Isolation Against Spectre Vulnerability | risk=Low (Medium) | count=1 +``` + +**After** (`submissions/lab9-scans/zap-after/zap-health.json`, and identical for `-metrics`/`-notes`), rebuilt image + re-ran the exact same baseline command: +``` +10116 ZAP is Out of Date | risk=Low (High) | count=1 <- unrelated to app, see 2.2 +10049-1 Non-Storable Content | risk=Informational (Medium) <- confirms Cache-Control: no-store took effect +``` + +`10021` and `90004` no longer appear in any of the three re-scanned endpoints — both are fully gone. Raw response headers after the fix (`curl -i http://localhost:8080/health`): +``` +HTTP/1.1 200 OK +Cache-Control: no-store +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Cross-Origin-Resource-Policy: same-origin +X-Content-Type-Options: nosniff +``` + +### 2.5 Design questions + +**e) Why a middleware and not per-handler header sets?** +A middleware wrapping the router is a single point of enforcement: every current and future route gets the headers automatically, and there's exactly one place to audit or change the policy. Per-handler `Header().Set` calls are copy-paste that *will* drift — someone adds `PATCH /notes/{id}` next sprint, forgets the header lines, and now that one route silently regresses. The unit test in 2.3 tests the middleware itself, not each handler, so the guarantee scales to routes that don't exist yet. + +**f) `Content-Security-Policy: default-src 'none'` — what does it break, and why is it fine here but not for a website?** +`default-src 'none'` blocks the browser from loading *anything* by default — no scripts, styles, images, fonts, XHR/fetch targets, unless another directive explicitly allows it. For a real website this breaks inline scripts, external stylesheets, embedded images, analytics beacons, web fonts — basically anything a page normally loads, unless every single resource type is allowlisted directive-by-directive. QuickNotes has zero HTML responses — every route returns JSON or plaintext metrics — so there's no page for a browser to render, no script/style/image to fetch, and thus nothing for the strictest CSP to break. The CSP header only matters here if a browser ever renders a QuickNotes response directly (e.g., an error page), and locking it down costs nothing since there's no legitimate content to allowlist. + +**g) False positives vs. accepted findings — cost of blanket-accepting informational ZAP alerts?** +Blanket-marking every informational alert "accepted" without reading it means the one time an informational alert *is* meaningful — a stray comment leaking an internal hostname, a cache header exposing a session token, a version string revealing an unpatched framework — it slides through unnoticed, identical in the report to twenty harmless noise alerts. It also erodes the audit trail: "accepted" should mean "a human read this and decided," not "the scanner said informational so I didn't look." The table in 2.2 reads and reasons about all four alerts (including two purely informational/tooling ones) rather than rubber-stamping them, which is the whole point of the triage discipline this lab is testing. diff --git a/submissions/prometheus.png b/submissions/prometheus.png new file mode 100644 index 000000000..52927112b Binary files /dev/null and b/submissions/prometheus.png differ diff --git a/submissions/signed_commit_image.png b/submissions/signed_commit_image.png new file mode 100644 index 000000000..cf99a989e Binary files /dev/null and b/submissions/signed_commit_image.png differ