diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..3da493f8e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (<= 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d1dbd0d94 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,116 @@ +name: CI + +on: + push: + branches: + - main + paths: + - "app/**" + - ".github/workflows/**" + pull_request: + branches: + - main + paths: + - "app/**" + - ".github/workflows/**" + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + vet: + name: vet + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.5.0 + with: + go-version: "1.26.4" + cache: true + cache-dependency-path: app/go.mod + + - name: Run go vet + run: go vet ./... + env: + GOFLAGS: -buildvcs=false + + test: + name: test + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.5.0 + with: + go-version: "1.26.4" + cache: true + cache-dependency-path: app/go.mod + + - name: Run tests + run: go test -race -count=1 ./... + env: + GOFLAGS: -buildvcs=false + + lint: + name: lint + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v8.0.0 + with: + version: v2.5.0 + working-directory: app + + govulncheck: + name: govulncheck + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.5.0 + with: + go-version: "1.26.4" + cache: true + cache-dependency-path: app/go.mod + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Run govulncheck + run: $(go env GOPATH)/bin/govulncheck ./... + env: + GOFLAGS: -buildvcs=false diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 000000000..de3f670d0 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,98 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : +# +# Lab 5 — QuickNotes in a Vagrant VM. +# Boots an Ubuntu 24.04 LTS VM, installs Go 1.24.x, builds QuickNotes from the +# synced ./app folder, and runs it as a systemd service on guest port 8080. +# The host reaches it on http://127.0.0.1:18080. + +GO_VERSION = "1.24.5" + +Vagrant.configure("2") do |config| + # Ubuntu 24.04 LTS. The bento box ships VirtualBox Guest Additions, which we + # need for the default (virtualbox) synced-folder type. + config.vm.box = "bento/ubuntu-24.04" + + # Identify the VM clearly (shows up in the shell prompt and `vagrant ssh`). + config.vm.hostname = "quicknotes-vm" + + # Forward host 18080 -> guest 8080, bound to loopback only so the app is not + # exposed to the local network. + config.vm.network "forwarded_port", guest: 8080, host: 18080, host_ip: "127.0.0.1" + + # Mount the application source into the guest (default type: virtualbox). + config.vm.synced_folder "./app", "/opt/quicknotes/app" + + # Cap resources at 2 vCPU / 1024 MB RAM (over-provisioning is an antipattern). + config.vm.provider "virtualbox" do |vb| + vb.name = "quicknotes-vm" + vb.memory = 1024 + vb.cpus = 2 + end + + # Provisioning runs on first `vagrant up`; re-run with `vagrant provision`. + # The script is idempotent, so `vagrant up --provision` is safe to repeat. + config.vm.provision "shell", inline: <<-SHELL + #!/usr/bin/env bash + set -euo pipefail + + GO_VERSION="#{GO_VERSION}" + case "$(uname -m)" in + x86_64) + GO_ARCH="amd64" + ;; + aarch64|arm64) + GO_ARCH="arm64" + ;; + *) + echo "Unsupported guest architecture: $(uname -m)" >&2 + exit 1 + ;; + esac + GO_TARBALL="go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" + + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + apt-get install -y curl + + # Install the pinned Go version only if it is not already present. + if ! /usr/local/go/bin/go version 2>/dev/null | grep -q "go${GO_VERSION} "; then + echo "Installing Go ${GO_VERSION}..." + curl -fsSL "https://go.dev/dl/${GO_TARBALL}" -o "/tmp/${GO_TARBALL}" + rm -rf /usr/local/go + tar -C /usr/local -xzf "/tmp/${GO_TARBALL}" + fi + + # Make Go available on PATH for interactive logins (e.g. `vagrant ssh`). + echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh + + # Build QuickNotes from the synced source into a stable location. + install -d /var/lib/quicknotes + /usr/local/go/bin/go build -C /opt/quicknotes/app -o /usr/local/bin/quicknotes . + + # Run QuickNotes as a service so it survives reboots and starts on boot. + cat > /etc/systemd/system/quicknotes.service <<'UNIT' +[Unit] +Description=QuickNotes API +After=network-online.target +Wants=network-online.target + +[Service] +Environment=ADDR=:8080 +Environment=DATA_PATH=/var/lib/quicknotes/notes.json +Environment=SEED_PATH=/opt/quicknotes/app/seed.json +WorkingDirectory=/var/lib/quicknotes +ExecStart=/usr/local/bin/quicknotes +Restart=on-failure + +[Install] +WantedBy=multi-user.target +UNIT + + systemctl daemon-reload + systemctl enable quicknotes + systemctl restart quicknotes + + echo "Provisioning done. QuickNotes should be live on guest :8080." + SHELL +end diff --git a/ansible/files/quicknotes b/ansible/files/quicknotes new file mode 100755 index 000000000..be6613c27 Binary files /dev/null and b/ansible/files/quicknotes differ diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 000000000..91e9ab7ad --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,2 @@ +[quicknotes_vm] +quicknotes-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_ssh_private_key_file=.vagrant/machines/default/virtualbox/private_key ansible_python_interpreter=/usr/bin/python3 ansible_ssh_common_args='-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o PubkeyAcceptedKeyTypes=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa' diff --git a/ansible/playbook.yaml b/ansible/playbook.yaml new file mode 100644 index 000000000..17d31d062 --- /dev/null +++ b/ansible/playbook.yaml @@ -0,0 +1,168 @@ +- name: Deploy QuickNotes to the Lab 5 VM + hosts: quicknotes_vm + become: true + gather_facts: false + + vars: + quicknotes_user: quicknotes + quicknotes_group: quicknotes + quicknotes_home: /nonexistent + quicknotes_shell: /usr/sbin/nologin + quicknotes_binary_src: files/quicknotes + quicknotes_binary_dest: /usr/local/bin/quicknotes + quicknotes_service_name: quicknotes + quicknotes_service_unit_path: /etc/systemd/system/quicknotes.service + quicknotes_data_dir: /var/lib/quicknotes + quicknotes_data_path: /var/lib/quicknotes/notes.json + quicknotes_seed_path: /opt/quicknotes/app/seed.json + listen_addr: ":8080" + quicknotes_restart_sec: 4 + ansible_pull_enabled: true + ansible_pull_repo_url: https://github.com/Hidancloud/DevOps-Intro.git + ansible_pull_repo_branch: feature/lab7 + ansible_pull_checkout_dir: /var/lib/ansible-pull/quicknotes + ansible_pull_inventory_path: /etc/ansible/quicknotes-local.ini + ansible_pull_playbook_path: ansible/playbook.yaml + ansible_pull_service_name: ansible-pull-quicknotes.service + ansible_pull_timer_name: ansible-pull-quicknotes.timer + + tasks: + - name: Create the quicknotes system group + ansible.builtin.group: + name: "{{ quicknotes_group }}" + system: true + + - name: Create the quicknotes system user + ansible.builtin.user: + name: "{{ quicknotes_user }}" + group: "{{ quicknotes_group }}" + system: true + create_home: false + home: "{{ quicknotes_home }}" + shell: "{{ quicknotes_shell }}" + + - name: Ensure the quicknotes data directory exists + ansible.builtin.file: + path: "{{ quicknotes_data_dir }}" + state: directory + owner: "{{ quicknotes_user }}" + group: "{{ quicknotes_group }}" + mode: "0750" + + - name: Install the QuickNotes binary + ansible.builtin.copy: + src: "{{ quicknotes_binary_src }}" + dest: "{{ quicknotes_binary_dest }}" + owner: root + group: root + mode: "0755" + notify: restart quicknotes + + - name: Install the systemd unit + ansible.builtin.template: + src: templates/quicknotes.service.j2 + dest: "{{ quicknotes_service_unit_path }}" + owner: root + group: root + mode: "0644" + register: quicknotes_service_unit + notify: restart quicknotes + + - name: Reload systemd when the unit changes + ansible.builtin.systemd: + daemon_reload: true + when: quicknotes_service_unit.changed + + - name: Enable and start the quicknotes service + ansible.builtin.systemd: + name: "{{ quicknotes_service_name }}" + enabled: true + state: started + + - name: Install ansible-pull dependencies + ansible.builtin.apt: + name: + - ansible + - git + state: present + update_cache: true + cache_valid_time: 3600 + when: ansible_pull_enabled + + - name: Ensure ansible config directory exists + ansible.builtin.file: + path: /etc/ansible + state: directory + owner: root + group: root + mode: "0755" + when: ansible_pull_enabled + + - name: Install ansible-pull local inventory + ansible.builtin.template: + src: templates/quicknotes-local.ini.j2 + dest: "{{ ansible_pull_inventory_path }}" + owner: root + group: root + mode: "0644" + register: ansible_pull_local_inventory + when: ansible_pull_enabled + + - name: Ensure ansible-pull checkout directory exists + ansible.builtin.file: + path: "{{ ansible_pull_checkout_dir }}" + state: directory + owner: root + group: root + mode: "0755" + when: ansible_pull_enabled + + - name: Install ansible-pull service + ansible.builtin.template: + src: templates/ansible-pull-quicknotes.service.j2 + dest: "/etc/systemd/system/{{ ansible_pull_service_name }}" + owner: root + group: root + mode: "0644" + register: ansible_pull_service_unit + when: ansible_pull_enabled + + - name: Install ansible-pull timer + ansible.builtin.template: + src: templates/ansible-pull-quicknotes.timer.j2 + dest: "/etc/systemd/system/{{ ansible_pull_timer_name }}" + owner: root + group: root + mode: "0644" + register: ansible_pull_timer_unit + when: ansible_pull_enabled + + - name: Reload systemd when ansible-pull units change + ansible.builtin.systemd: + daemon_reload: true + when: + - ansible_pull_enabled + - ansible_pull_service_unit.changed or ansible_pull_timer_unit.changed + + - name: Prime ansible-pull once so the timer has a last-run anchor + ansible.builtin.systemd: + name: "{{ ansible_pull_service_name }}" + state: started + when: + - ansible_pull_enabled + - ansible_connection != 'local' + - ansible_pull_local_inventory.changed or ansible_pull_service_unit.changed or ansible_pull_timer_unit.changed + + - name: Enable and start the ansible-pull timer + ansible.builtin.systemd: + name: "{{ ansible_pull_timer_name }}" + enabled: true + state: started + when: ansible_pull_enabled + + handlers: + - name: restart quicknotes + ansible.builtin.systemd: + name: "{{ quicknotes_service_name }}" + state: restarted + daemon_reload: true diff --git a/ansible/templates/ansible-pull-quicknotes.service.j2 b/ansible/templates/ansible-pull-quicknotes.service.j2 new file mode 100644 index 000000000..24f6c54bc --- /dev/null +++ b/ansible/templates/ansible-pull-quicknotes.service.j2 @@ -0,0 +1,8 @@ +[Unit] +Description=Reconcile QuickNotes from Git via ansible-pull +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/ansible-pull -U {{ ansible_pull_repo_url }} -C {{ ansible_pull_repo_branch }} -d {{ ansible_pull_checkout_dir }} -i {{ ansible_pull_inventory_path }} {{ ansible_pull_playbook_path }} diff --git a/ansible/templates/ansible-pull-quicknotes.timer.j2 b/ansible/templates/ansible-pull-quicknotes.timer.j2 new file mode 100644 index 000000000..e3768fc03 --- /dev/null +++ b/ansible/templates/ansible-pull-quicknotes.timer.j2 @@ -0,0 +1,10 @@ +[Unit] +Description=Run ansible-pull for QuickNotes every 5 minutes + +[Timer] +OnBootSec=1min +OnUnitActiveSec=5min +Unit={{ ansible_pull_service_name }} + +[Install] +WantedBy=timers.target diff --git a/ansible/templates/quicknotes-local.ini.j2 b/ansible/templates/quicknotes-local.ini.j2 new file mode 100644 index 000000000..3758c7116 --- /dev/null +++ b/ansible/templates/quicknotes-local.ini.j2 @@ -0,0 +1,2 @@ +[quicknotes_vm] +quicknotes-vm ansible_connection=local ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/templates/quicknotes.service.j2 b/ansible/templates/quicknotes.service.j2 new file mode 100644 index 000000000..9a229d6da --- /dev/null +++ b/ansible/templates/quicknotes.service.j2 @@ -0,0 +1,19 @@ +[Unit] +Description=QuickNotes API +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User={{ quicknotes_user }} +Group={{ quicknotes_group }} +WorkingDirectory={{ quicknotes_data_dir }} +Environment=ADDR={{ listen_addr }} +Environment=DATA_PATH={{ quicknotes_data_path }} +Environment=SEED_PATH={{ quicknotes_seed_path }} +ExecStart={{ quicknotes_binary_dest }} +Restart=on-failure +RestartSec={{ quicknotes_restart_sec }} + +[Install] +WantedBy=multi-user.target diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..737618013 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,45 @@ +# syntax=docker/dockerfile:1 + +# ─── builder stage ─── +# Official Go image, pinned to a patched release (not :latest). +FROM golang:1.26.4 AS builder +WORKDIR /src + +# Copy dependency manifest first so the module layer is cached independently +# of the source. QuickNotes has no third-party deps, so there is no go.sum. +COPY go.mod ./ +RUN go mod download + +# Now copy the source — changes here don't invalidate the layer above. +COPY . . + +# Build a static, stripped, reproducible binary. CGO off so distroless-static +# (which has no libc / dynamic linker) can run it. +RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /quicknotes . + +# A tiny healthcheck helper — distroless has no shell/wget/curl, so we ship a +# minimal static binary that hits /health and exits 0/1. +RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /healthcheck ./cmd/healthcheck + +# Pre-create the data dir. A fresh named volume inherits this dir's ownership, +# so the nonroot user (65532) can write notes.json. +RUN mkdir -p /data + +# ─── runtime stage ─── +# Distroless static + nonroot: no shell, no package manager, runs as UID 65532. +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /quicknotes /quicknotes +COPY --from=builder /healthcheck /healthcheck +COPY --from=builder --chown=65532:65532 /data /data +COPY seed.json /seed.json + +# Sensible defaults so `docker run` works without extra env (compose overrides). +ENV ADDR=":8080" \ + DATA_PATH="/data/notes.json" \ + SEED_PATH="/seed.json" + +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD ["/healthcheck"] +USER nonroot +ENTRYPOINT ["/quicknotes"] diff --git a/app/cmd/healthcheck/main.go b/app/cmd/healthcheck/main.go new file mode 100644 index 000000000..ca159ec0a --- /dev/null +++ b/app/cmd/healthcheck/main.go @@ -0,0 +1,31 @@ +// Command healthcheck is a tiny static binary used as the container +// healthcheck. Distroless images have no shell, wget, or curl, so we ship this +// instead. It performs a single GET /health and exits 0 on HTTP 200, else 1. +package main + +import ( + "net/http" + "os" + "time" +) + +func main() { + addr := os.Getenv("HEALTHCHECK_URL") + if addr == "" { + addr = "http://127.0.0.1:8080/health" + } + + client := &http.Client{Timeout: 2 * time.Second} + resp, err := client.Get(addr) + if err != nil { + os.Exit(1) + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + os.Exit(1) + } + os.Exit(0) +} diff --git a/app/handlers.go b/app/handlers.go index c534979c5..2216d606e 100644 --- a/app/handlers.go +++ b/app/handlers.go @@ -26,7 +26,7 @@ func NewServer(store *Store) *Server { return &Server{store: store, requestsByCode: by} } -func (s *Server) Routes() *http.ServeMux { +func (s *Server) Routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /health", s.wrap(s.handleHealth)) mux.HandleFunc("GET /metrics", s.wrap(s.handleMetrics)) @@ -34,7 +34,21 @@ func (s *Server) Routes() *http.ServeMux { mux.HandleFunc("POST /notes", s.wrap(s.handleCreateNote)) mux.HandleFunc("GET /notes/{id}", s.wrap(s.handleGetNote)) mux.HandleFunc("DELETE /notes/{id}", s.wrap(s.handleDeleteNote)) - return mux + return securityHeaders(mux) +} + +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Security-Policy", "default-src 'none'") + w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") + w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") + w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + next.ServeHTTP(w, r) + }) } type statusWriter struct { diff --git a/app/handlers_test.go b/app/handlers_test.go index 9dff2e3e5..9eda7f8cc 100644 --- a/app/handlers_test.go +++ b/app/handlers_test.go @@ -131,3 +131,37 @@ func TestMetrics_ExposesPrometheusFormat(t *testing.T) { } } +func TestSecurityHeaders_AppliedToAllRoutes(t *testing.T) { + srv := newTestServer(t) + routes := []struct { + method string + path string + body any + }{ + {method: http.MethodGet, path: "/health"}, + {method: http.MethodGet, path: "/metrics"}, + {method: http.MethodGet, path: "/notes"}, + {method: http.MethodPost, path: "/notes", body: map[string]string{"title": "secured"}}, + } + + for _, route := range routes { + t.Run(route.method+" "+route.path, func(t *testing.T) { + rec := do(t, srv, route.method, route.path, route.body) + headers := map[string]string{ + "Cache-Control": "no-store", + "Content-Security-Policy": "default-src 'none'", + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Resource-Policy": "same-origin", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + } + for name, want := range headers { + if got := rec.Header().Get(name); got != want { + t.Fatalf("%s = %q, want %q", name, got, want) + } + } + }) + } +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..f077625ac --- /dev/null +++ b/compose.yaml @@ -0,0 +1,64 @@ +services: + quicknotes: + build: ./app + image: quicknotes:lab6 + ports: + - "8080:8080" + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + volumes: + - quicknotes-data:/data + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + + prometheus: + image: prom/prometheus:v3.5.0 + command: + - --config.file=/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - ./monitoring/prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro + depends_on: + quicknotes: + condition: service_healthy + restart: unless-stopped + + blackbox-exporter: + image: prom/blackbox-exporter:v0.27.0 + command: + - --config.file=/etc/blackbox_exporter/blackbox.yml + ports: + - "9115:9115" + volumes: + - ./monitoring/blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro + depends_on: + quicknotes: + condition: service_healthy + restart: unless-stopped + + grafana: + image: grafana/grafana:13.0.3-slim + environment: + GF_SECURITY_ADMIN_USER: quicknotes_admin + GF_SECURITY_ADMIN_PASSWORD: qn-lab8-monitoring + GF_USERS_ALLOW_SIGN_UP: "false" + ports: + - "3000:3000" + volumes: + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro + depends_on: + - prometheus + restart: unless-stopped + +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..185960b11 --- /dev/null +++ b/docs/runbook/high-error-rate.md @@ -0,0 +1,22 @@ +# High Error Rate + +## What this alert means + +QuickNotes is returning 4xx and 5xx responses for more than 5% of requests, sustained for at least 5 minutes. + +## Triage steps + +1. Confirm the alert is still active by checking Prometheus `/alerts` and Grafana panels for traffic and error ratio over the last 15 minutes. +2. Compare healthy versus failing requests with `curl` against `/health`, `/notes`, and one deliberately malformed `POST /notes` to determine whether the issue is broad or isolated to a route. +3. Inspect the QuickNotes container logs with `docker compose logs --tail=200 quicknotes` and correlate timestamps with the error spike. +4. Check whether the backing data file or mounted volume is writable and present, because write failures can surface as 5xx on note creation. + +## Mitigations + +1. Roll back the most recent change to the QuickNotes image or monitoring-related config if the spike started immediately after a deploy. +2. Temporarily reduce bad traffic by blocking malformed clients, rate-limiting the offending caller, or disabling the specific integration sending invalid payloads. +3. Restart the `quicknotes` container if the service is wedged but the root cause is still under investigation. + +## Post-incident + +Write a short blameless postmortem using the Lecture 1 template: timeline, customer impact, root cause, contributing factors, and concrete follow-up actions. diff --git a/monitoring/blackbox/blackbox.yml b/monitoring/blackbox/blackbox.yml new file mode 100644 index 000000000..e52285932 --- /dev/null +++ b/monitoring/blackbox/blackbox.yml @@ -0,0 +1,9 @@ +modules: + http_2xx: + prober: http + timeout: 10s + http: + method: GET + valid_status_codes: [200] + no_follow_redirects: false + preferred_ip_protocol: ip4 diff --git a/monitoring/grafana/dashboards/golden-signals.json b/monitoring/grafana/dashboards/golden-signals.json new file mode 100644 index 000000000..be1c792ad --- /dev/null +++ b/monitoring/grafana/dashboards/golden-signals.json @@ -0,0 +1,98 @@ +{ + "annotations": {"list": []}, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "description": "QuickNotes does not expose a request-duration histogram yet, so this panel uses request throughput as a temporary latency proxy.", + "fieldConfig": {"defaults": {"color": {"mode": "palette-classic"}, "unit": "reqps"}, "overrides": []}, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, + "id": 1, + "options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single", "sort": "none"}}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "editorMode": "code", + "expr": "sum(rate(quicknotes_http_requests_total[1m]))", + "legendFormat": "requests/sec proxy", + "range": true, + "refId": "A" + } + ], + "title": "Latency (proxy until histogram exists)", + "type": "timeseries" + }, + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "fieldConfig": {"defaults": {"color": {"mode": "palette-classic"}, "unit": "reqps"}, "overrides": []}, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, + "id": 2, + "options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single", "sort": "none"}}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "editorMode": "code", + "expr": "sum(rate(quicknotes_http_requests_total[5m]))", + "legendFormat": "traffic", + "range": true, + "refId": "A" + } + ], + "title": "Traffic", + "type": "timeseries" + }, + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "fieldConfig": {"defaults": {"color": {"mode": "thresholds"}, "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}, {"color": "red", "value": 5}]}, "unit": "percent"}, "overrides": []}, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, + "id": 3, + "options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single", "sort": "none"}}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "editorMode": "code", + "expr": "100 * sum(rate(quicknotes_http_responses_by_code_total{code=~\"4..|5..\"}[5m])) / clamp_min(sum(rate(quicknotes_http_requests_total[5m])), 0.000001)", + "legendFormat": "error ratio", + "range": true, + "refId": "A" + } + ], + "title": "Errors", + "type": "timeseries" + }, + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "fieldConfig": {"defaults": {"color": {"mode": "thresholds"}, "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}, {"color": "orange", "value": 20}, {"color": "red", "value": 50}]}, "unit": "short"}, "overrides": []}, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, + "id": 4, + "options": {"colorMode": "background", "graphMode": "area", "justifyMode": "center", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "editorMode": "code", + "expr": "quicknotes_notes_total", + "legendFormat": "notes stored", + "range": true, + "refId": "A" + } + ], + "title": "Saturation", + "type": "stat" + } + ], + "refresh": "15s", + "schemaVersion": 41, + "tags": ["devops-intro", "quicknotes", "golden-signals"], + "templating": {"list": []}, + "time": {"from": "now-15m", "to": "now"}, + "timepicker": {}, + "timezone": "browser", + "title": "QuickNotes Golden Signals", + "uid": "quicknotes-golden-signals", + "version": 1, + "weekStart": "" +} diff --git a/monitoring/grafana/provisioning/alerting/.gitkeep b/monitoring/grafana/provisioning/alerting/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/monitoring/grafana/provisioning/dashboards/dashboard.yml b/monitoring/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 000000000..de2713f80 --- /dev/null +++ b/monitoring/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: quicknotes-dashboards + orgId: 1 + folder: QuickNotes + type: file + disableDeletion: false + editable: false + options: + path: /var/lib/grafana/dashboards diff --git a/monitoring/grafana/provisioning/datasources/datasource.yml b/monitoring/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 000000000..00f99157b --- /dev/null +++ b/monitoring/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + uid: prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false diff --git a/monitoring/grafana/provisioning/plugins/.gitkeep b/monitoring/grafana/provisioning/plugins/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/monitoring/prometheus/alerts.yml b/monitoring/prometheus/alerts.yml new file mode 100644 index 000000000..65ce08f3e --- /dev/null +++ b/monitoring/prometheus/alerts.yml @@ -0,0 +1,17 @@ +groups: + - name: quicknotes + rules: + - alert: QuickNotesHighErrorRate + expr: | + ( + sum(rate(quicknotes_http_responses_by_code_total{code=~"4..|5.."}[5m])) + / + clamp_min(sum(rate(quicknotes_http_requests_total[5m])), 0.000001) + ) > 0.05 + for: 5m + labels: + severity: page + annotations: + summary: "QuickNotes error ratio is above 5%" + description: "4xx + 5xx responses have exceeded 5% of total traffic for 5 minutes." + runbook: "docs/runbook/high-error-rate.md" diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 000000000..6887c87f9 --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -0,0 +1,29 @@ +global: + scrape_interval: 15s + +rule_files: + - /etc/prometheus/alerts.yml + +scrape_configs: + - job_name: blackbox-internal + metrics_path: /probe + params: + module: [http_2xx] + static_configs: + - targets: + - http://quicknotes:8080/health + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__address__] + target_label: __address__ + replacement: blackbox-exporter:9115 + - source_labels: [__param_target] + target_label: instance + - target_label: job + replacement: blackbox-internal + + - job_name: quicknotes + static_configs: + - targets: + - quicknotes:8080 diff --git a/monitoring/results/checkhost/metadata.json b/monitoring/results/checkhost/metadata.json new file mode 100644 index 000000000..70f9a858a --- /dev/null +++ b/monitoring/results/checkhost/metadata.json @@ -0,0 +1,16 @@ +{ + "target": "https://intensity-biological-beverages-perfectly.trycloudflare.com/health", + "regions": [ + "DE", + "SG", + "US" + ], + "window": { + "start": "2026-06-30T15:01:43Z", + "end": "2026-06-30T15:35:35Z", + "duration_seconds": 2032, + "interval_seconds": 60, + "samples": 31 + }, + "samples": 31 +} \ No newline at end of file diff --git a/monitoring/results/checkhost/summary.json b/monitoring/results/checkhost/summary.json new file mode 100644 index 000000000..b8b270eb1 --- /dev/null +++ b/monitoring/results/checkhost/summary.json @@ -0,0 +1,178 @@ +{ + "target": "https://intensity-biological-beverages-perfectly.trycloudflare.com/health", + "regions": [ + "DE", + "SG", + "US" + ], + "window": { + "start": "2026-06-30T15:01:43Z", + "end": "2026-06-30T15:35:35Z", + "duration_seconds": 2032, + "interval_seconds": 60, + "samples": 31 + }, + "selected_nodes": { + "DE": { + "node": "DE-FFM-KRK", + "country": "Germany", + "city": "Frankfurt am Main", + "continent": "EU", + "monitoring_allowed": 1 + }, + "SG": { + "node": "SG-SIN-FDCServers", + "country": "Singapore", + "city": "Singapore", + "continent": "AS", + "monitoring_allowed": 0 + }, + "US": { + "node": "US-DAL-Leora", + "country": "United States", + "city": "Dallas", + "continent": "NA", + "monitoring_allowed": 0 + } + }, + "nodes": { + "DE": { + "node": "DE-FFM-KRK", + "country": "Germany", + "city": "Frankfurt am Main", + "continent": "EU", + "monitoring_allowed": 1, + "checks": 31, + "errors": 0, + "statuses": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "p50_seconds": 0.389, + "p95_seconds": 0.619 + }, + "SG": { + "node": "SG-SIN-FDCServers", + "country": "Singapore", + "city": "Singapore", + "continent": "AS", + "monitoring_allowed": 0, + "checks": 31, + "errors": 0, + "statuses": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "p50_seconds": 0.99, + "p95_seconds": 1.549 + }, + "US": { + "node": "US-DAL-Leora", + "country": "United States", + "city": "Dallas", + "continent": "NA", + "monitoring_allowed": 0, + "checks": 31, + "errors": 0, + "statuses": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "p50_seconds": 0.418, + "p95_seconds": 0.478 + } + }, + "overall": { + "checks": 93, + "errors": 0, + "p50_seconds": 0.428, + "p95_seconds": 1.284 + } +} \ No newline at end of file diff --git a/monitoring/scripts/checkhost-multi-region.sh b/monitoring/scripts/checkhost-multi-region.sh new file mode 100755 index 000000000..cc86fb7b1 --- /dev/null +++ b/monitoring/scripts/checkhost-multi-region.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +set -euo pipefail + +public_url="${1:?usage: checkhost-multi-region.sh [duration_seconds] [interval_seconds] [output_dir]}" +duration_seconds="${2:-1800}" +interval_seconds="${3:-60}" +output_dir="${4:-monitoring/results/checkhost}" +regions="${CHECKHOST_REGIONS:-DE,SG,US}" +append_mode="${CHECKHOST_APPEND:-0}" + +mkdir -p "$output_dir" +raw_file="$output_dir/raw.jsonl" +summary_file="$output_dir/summary.json" +metadata_file="$output_dir/metadata.json" +if [ "$append_mode" != "1" ]; then + : > "$raw_file" +fi + +start_epoch=$(date +%s) +end_epoch=$(( start_epoch + duration_seconds )) + +request_check() { + local response report_url result curl_error + + if ! response=$(curl -fsS -X POST https://api.check-host.cc/http \ + -H 'Content-Type: application/json' \ + -d "{\"target\":\"${public_url}\"}" 2>&1); then + jq -cn --arg status "request_failed" --arg error "$response" '{status:0, success:false, error:$error, stage:$status, data:{}}' + return 0 + fi + + report_url=$(printf '%s' "$response" | jq -r '.apiURL // empty') + if [ -z "$report_url" ]; then + jq -cn --arg status "missing_api_url" --arg error "$response" '{status:0, success:false, error:$error, stage:$status, data:{}}' + return 0 + fi + + result='' + curl_error='' + for _ in $(seq 1 20); do + sleep 3 + if result=$(curl -fsS "$report_url" 2>&1); then + if printf '%s' "$result" | jq -e '.success == true and (.data | type == "object") and ((.data | length) > 0)' >/dev/null 2>&1; then + printf '%s' "$result" + return 0 + fi + else + curl_error="$result" + fi + done + + jq -cn --arg status "report_unavailable" --arg error "$curl_error" '{status:0, success:false, error:$error, stage:$status, data:{}}' +} + +while [ "$(date +%s)" -lt "$end_epoch" ]; do + ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ') + request=$(curl -fsS -X POST https://api.check-host.cc/http \ + -H 'Content-Type: application/json' \ + -d "{\"target\":\"${public_url}\"}" 2>/dev/null || true) + + if [ -n "$request" ] && printf '%s' "$request" | jq -e '.apiURL' >/dev/null 2>&1; then + report_url=$(printf '%s' "$request" | jq -r '.apiURL') + result='' + curl_error='' + for _ in $(seq 1 20); do + sleep 3 + if result=$(curl -fsS "$report_url" 2>&1); then + if printf '%s' "$result" | jq -e '.success == true and (.data | type == "object") and ((.data | length) > 0)' >/dev/null 2>&1; then + break + fi + else + curl_error="$result" + fi + done + if [ -z "$result" ] || ! printf '%s' "$result" | jq -e '.success == true and (.data | type == "object") and ((.data | length) > 0)' >/dev/null 2>&1; then + result=$(jq -cn --arg error "$curl_error" '{status:0, success:false, error:$error, stage:"report_unavailable", data:{}}') + fi + else + if [ -z "$request" ]; then + request=$(jq -cn '{status:0, success:false, error:"request_failed", data:{}}') + fi + result=$(jq -cn --arg error "request bootstrap failed" '{status:0, success:false, error:$error, stage:"request_failed", data:{}}') + fi + + jq -cn \ + --arg ts "$ts" \ + --arg url "$public_url" \ + --arg regions "$regions" \ + --argjson request "$request" \ + --argjson result "$result" \ + '{timestamp:$ts, url:$url, regions:($regions|split(",")), request:$request, result:$result}' >> "$raw_file" + + now=$(date +%s) + if [ "$now" -lt "$end_epoch" ]; then + sleep "$interval_seconds" + fi +done + +python3 - <<'PY' "$raw_file" "$summary_file" "$metadata_file" "$public_url" "$regions" "$interval_seconds" +import json, math, statistics, sys +from datetime import datetime, timezone +from pathlib import Path + +raw_path = Path(sys.argv[1]) +summary_path = Path(sys.argv[2]) +metadata_path = Path(sys.argv[3]) +public_url = sys.argv[4] +regions = [x for x in sys.argv[5].split(',') if x] +interval_seconds = int(sys.argv[6]) + +entries = [json.loads(line) for line in raw_path.read_text().splitlines() if line.strip()] + +def parse_ts(value): + return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc) + +def percentile(sorted_values, p): + if not sorted_values: + return None + idx = max(0, math.ceil((len(sorted_values) - 1) * p)) + return sorted_values[idx] + +start_ts = parse_ts(entries[0]["timestamp"]) if entries else None +end_ts = parse_ts(entries[-1]["timestamp"]) if entries else None + +summary = { + "target": public_url, + "regions": regions, + "window": { + "start": entries[0]["timestamp"] if entries else None, + "end": entries[-1]["timestamp"] if entries else None, + "duration_seconds": int((end_ts - start_ts).total_seconds()) if start_ts and end_ts else 0, + "interval_seconds": interval_seconds, + "samples": len(entries), + }, + "selected_nodes": {}, + "nodes": {}, + "overall": {}, +} + +selected_nodes = {} +for region in regions: + chosen = None + for entry in entries: + data = entry.get("result", {}).get("data", {}) + candidates = [] + for node_name, node in data.items(): + if node.get("countryCode") != region: + continue + checks = node.get("checks") or [] + if not checks: + continue + candidates.append((0 if node.get("monitoring_allowed") == 1 else 1, node_name, node)) + if candidates: + _, chosen_name, chosen_node = sorted(candidates, key=lambda item: (item[0], item[1]))[0] + chosen = { + "node": chosen_name, + "country": chosen_node.get("country"), + "city": chosen_node.get("city"), + "continent": chosen_node.get("continent"), + "monitoring_allowed": chosen_node.get("monitoring_allowed"), + } + break + if chosen: + selected_nodes[region] = chosen + +summary["selected_nodes"] = selected_nodes + +all_latencies = [] +all_errors = 0 +all_checks = 0 + +for region, chosen in selected_nodes.items(): + node_name = chosen["node"] + latencies = [] + errors = 0 + checks_count = 0 + statuses = [] + + for entry in entries: + data = entry.get("result", {}).get("data", {}) + node = data.get(node_name) + if not node: + continue + checks = node.get("checks") or [] + if not checks: + errors += 1 + checks_count += 1 + continue + first = checks[0] + checks_count += 1 + status = first.get("http_status") + statuses.append(status) + latency_ms = first.get("connectiontime") + if status != 200 or first.get("status") != 1: + errors += 1 + if latency_ms is not None: + latencies.append(float(latency_ms) / 1000.0) + + latencies.sort() + all_latencies.extend(latencies) + all_errors += errors + all_checks += checks_count + summary["nodes"][region] = { + **chosen, + "checks": checks_count, + "errors": errors, + "statuses": statuses, + "p50_seconds": statistics.median(latencies) if latencies else None, + "p95_seconds": percentile(latencies, 0.95), + } + +all_latencies.sort() +summary["overall"] = { + "checks": all_checks, + "errors": all_errors, + "p50_seconds": statistics.median(all_latencies) if all_latencies else None, + "p95_seconds": percentile(all_latencies, 0.95), +} + +summary_path.write_text(json.dumps(summary, indent=2)) +metadata_path.write_text(json.dumps({ + "target": public_url, + "regions": regions, + "window": summary["window"], + "samples": len(entries), +}, indent=2)) +PY + +echo "$summary_file" diff --git a/monitoring/scripts/generate-traffic.sh b/monitoring/scripts/generate-traffic.sh new file mode 100755 index 000000000..a3522f4ca --- /dev/null +++ b/monitoring/scripts/generate-traffic.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +base_url="${1:-http://localhost:8080}" +count="${2:-200}" + +for i in $(seq 1 "$count"); do + curl -fsS "$base_url/health" >/dev/null + curl -fsS "$base_url/notes" >/dev/null + curl -fsS -X POST "$base_url/notes" \ + -H 'Content-Type: application/json' \ + -d "{\"title\":\"note-$i\",\"body\":\"traffic\"}" >/dev/null + if (( i % 10 == 0 )); then + curl -sS "$base_url/notes/999999" >/dev/null || true + fi + sleep 0.1 +done diff --git a/monitoring/scripts/trigger-high-error-rate.sh b/monitoring/scripts/trigger-high-error-rate.sh new file mode 100755 index 000000000..5503bb925 --- /dev/null +++ b/monitoring/scripts/trigger-high-error-rate.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +base_url="${1:-http://localhost:8080}" +duration_seconds="${2:-330}" +end_epoch=$(( $(date +%s) + duration_seconds )) + +while [ "$(date +%s)" -lt "$end_epoch" ]; do + curl -fsS "$base_url/health" >/dev/null || true + curl -fsS -X POST "$base_url/notes" \ + -H 'Content-Type: application/json' \ + -d '{"title":"healthy","body":"ok"}' >/dev/null || true + curl -sS -X POST "$base_url/notes" \ + -H 'Content-Type: application/json' \ + -d '{"title":' >/dev/null || true + curl -sS "$base_url/notes/not-an-int" >/dev/null || true + sleep 1 +done diff --git a/security/lab9/docker-images.txt b/security/lab9/docker-images.txt new file mode 100644 index 000000000..fe13195e9 --- /dev/null +++ b/security/lab9/docker-images.txt @@ -0,0 +1,4 @@ +REPOSITORY TAG IMAGE ID SIZE +quicknotes lab6 7563fb5d9ff3 22.1MB +quicknotes lab10 fe7a68b4e8db 21.6MB +quicknotes lab9-before 3db41fc23854 21.6MB diff --git a/security/lab9/govulncheck-green.txt b/security/lab9/govulncheck-green.txt new file mode 100644 index 000000000..e7589224d --- /dev/null +++ b/security/lab9/govulncheck-green.txt @@ -0,0 +1 @@ +No vulnerabilities found. diff --git a/security/lab9/govulncheck-red-demo.txt b/security/lab9/govulncheck-red-demo.txt new file mode 100644 index 000000000..c06b6f723 --- /dev/null +++ b/security/lab9/govulncheck-red-demo.txt @@ -0,0 +1,16 @@ +=== Symbol Results === + +Vulnerability #1: GO-2022-1059 + Denial of service via crafted Accept-Language header in + golang.org/x/text/language + More info: https://pkg.go.dev/vuln/GO-2022-1059 + Module: golang.org/x/text + Found in: golang.org/x/text@v0.3.7 + Fixed in: golang.org/x/text@v0.3.8 + Example traces found: + #1: vuln_demo.go:6:37: quicknotes.vulnerableAcceptLanguage calls language.ParseAcceptLanguage + +Your code is affected by 1 vulnerability from 1 module. +This scan found no other vulnerabilities in packages you import or modules you +require. +Use '-show verbose' for more details. diff --git a/security/lab9/trivy/quicknotes-lab6.cdx.json b/security/lab9/trivy/quicknotes-lab6.cdx.json new file mode 100644 index 000000000..c8dc986d0 --- /dev/null +++ b/security/lab9/trivy/quicknotes-lab6.cdx.json @@ -0,0 +1,559 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:5d2390f0-b737-4326-b87a-05b04173ba82", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T11:20:06+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "pkg:oci/quicknotes@sha256%3A7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c?arch=arm64&repository_url=index.docker.io%2Flibrary%2Fquicknotes", + "type": "container", + "name": "quicknotes:lab6", + "purl": "pkg:oci/quicknotes@sha256%3A7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c?arch=arm64&repository_url=index.docker.io%2Flibrary%2Fquicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4cde6b0bb6f50a5f255eef7b2a42162c661cf776b803225dcac9a659e396bb6b" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4d049f83d9cf21d1f5cc0e11deaf36df02790d0e60c1a3829538fb4b61685368" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:551c39882a4c0a98387545f4ca3efa974435a746b38bc91d24837399cde0cccd" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:5fd2536c39c0700be8b7b4344e375196da2f126842fd8ede66996a18860a3890" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:6e196de90f44328a397ba7cfaa149d4972b0a1c2ba678b35fcb5960d2313213b" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:6f1cdceb6a3146f0ccb986521156bef8a422cdbb0863396f7f751f575ba308f4" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:8c1a7c4365a2a1ae8d7f361675388c8094a9f30514d9dfc0e06682f9367e759f" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:97d418ef162f3bdb9cc92715e79b83f8d02aba515d33065c4f726b711cc5624f" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ad51d0769d16ba578106a177987dfe3d2e02c1668c852b795b2f6b024068242a" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:af5aa97ebe6ce1604747ec1e21af7136ded391bcabe4acef882e718a87c86bcc" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bd3cdfae1d3fdd83a2231d608969b38b82349777c2fff9a7c12d54f8ac5c9b38" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:fda487705d0e2b47dae16fe9320d1a4e759844630163af391a7d11dd7d64bf46" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c" + }, + { + "name": "aquasecurity:trivy:RepoDigest", + "value": "quicknotes@sha256:7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "quicknotes:lab6" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "16c23fa7-e745-4c64-9881-dc1d56bd2c31", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:fda487705d0e2b47dae16fe9320d1a4e759844630163af391a7d11dd7d64bf46" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:6dee92a74d226ef89d81f92a2851016e70d5348a9c48a53318babe66abe80d70" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "5a8446b3-96ce-4514-bbb8-1bb6661cfbaf", + "type": "application", + "name": "healthcheck", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "65f59aaf-5025-4f77-a807-18e7a5cd7bd1", + "type": "operating-system", + "name": "debian", + "version": "13.5", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "7ae1995f-d4ae-4004-b78d-92aa53e0754e", + "type": "library", + "name": "stdlib", + "version": "v1.26.4", + "purl": "pkg:golang/stdlib@v1.26.4", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:fda487705d0e2b47dae16fe9320d1a4e759844630163af391a7d11dd7d64bf46" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:6dee92a74d226ef89d81f92a2851016e70d5348a9c48a53318babe66abe80d70" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.26.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "dbc22667-0c78-455c-8da5-75c025c16fa3", + "type": "application", + "name": "quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "dcf87078-e5c3-4dcf-aba9-a7817099d7a9", + "type": "library", + "name": "stdlib", + "version": "v1.26.4", + "purl": "pkg:golang/stdlib@v1.26.4", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:6e196de90f44328a397ba7cfaa149d4972b0a1c2ba678b35fcb5960d2313213b" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:61c0ff0d2a0c81292befb2d52e20cf66665b7fed357b90ee985a1ff37d024fde" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.26.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "f58215bb-6626-4d30-90ad-cec33b427ef5", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:6e196de90f44328a397ba7cfaa149d4972b0a1c2ba678b35fcb5960d2313213b" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:61c0ff0d2a0c81292befb2d52e20cf66665b7fed357b90ee985a1ff37d024fde" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=arm64&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "13.8+deb13u5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "verbatim" + } + } + ], + "purl": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=arm64&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:8c1a7c4365a2a1ae8d7f361675388c8094a9f30514d9dfc0e06682f9367e759f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:115c774471ecdf6d6bf2f7f3ff02075abc7092bca65df86b8b013699ecb2eb0a" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@13.8+deb13u5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.8+deb13u5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Mime-Support Packagers " + }, + "name": "media-types", + "version": "13.0.0", + "licenses": [ + { + "license": { + "name": "ad-hoc" + } + } + ], + "purl": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:d6b1b89eccacc15c2420b2776d72c1dae334a00805ed9af54bf2f71e4d536f28" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "media-types@13.0.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "media-types" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.0.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "netbase", + "version": "6.5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:c172f21841dff4c8cf45cde46589c1c2616cefe7e819965e92e6d3475c428aa0" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "netbase@6.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "netbase" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata-legacy", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:99ba982a9142213c751a1709dcf088e63d8601f03b3f211bae037be698fef270" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata-legacy@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:99515e7b4d35e0652d3b0fde571b6ec269222ecacc506f026e1758d6261e9109" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + } + ], + "dependencies": [ + { + "ref": "16c23fa7-e745-4c64-9881-dc1d56bd2c31", + "dependsOn": [ + "7ae1995f-d4ae-4004-b78d-92aa53e0754e" + ] + }, + { + "ref": "5a8446b3-96ce-4514-bbb8-1bb6661cfbaf", + "dependsOn": [ + "f58215bb-6626-4d30-90ad-cec33b427ef5" + ] + }, + { + "ref": "65f59aaf-5025-4f77-a807-18e7a5cd7bd1", + "dependsOn": [ + "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=arm64&distro=debian-13.5", + "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5" + ] + }, + { + "ref": "7ae1995f-d4ae-4004-b78d-92aa53e0754e", + "dependsOn": [] + }, + { + "ref": "dbc22667-0c78-455c-8da5-75c025c16fa3", + "dependsOn": [ + "16c23fa7-e745-4c64-9881-dc1d56bd2c31" + ] + }, + { + "ref": "dcf87078-e5c3-4dcf-aba9-a7817099d7a9", + "dependsOn": [] + }, + { + "ref": "f58215bb-6626-4d30-90ad-cec33b427ef5", + "dependsOn": [ + "dcf87078-e5c3-4dcf-aba9-a7817099d7a9" + ] + }, + { + "ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=arm64&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:oci/quicknotes@sha256%3A7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c?arch=arm64&repository_url=index.docker.io%2Flibrary%2Fquicknotes", + "dependsOn": [ + "5a8446b3-96ce-4514-bbb8-1bb6661cfbaf", + "65f59aaf-5025-4f77-a807-18e7a5cd7bd1", + "dbc22667-0c78-455c-8da5-75c025c16fa3" + ] + } + ], + "vulnerabilities": [] +} diff --git a/security/lab9/trivy/sbom-first-30-lines.txt b/security/lab9/trivy/sbom-first-30-lines.txt new file mode 100644 index 000000000..c0f4d41b5 --- /dev/null +++ b/security/lab9/trivy/sbom-first-30-lines.txt @@ -0,0 +1,30 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:5d2390f0-b737-4326-b87a-05b04173ba82", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T11:20:06+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "pkg:oci/quicknotes@sha256%3A7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c?arch=arm64&repository_url=index.docker.io%2Flibrary%2Fquicknotes", + "type": "container", + "name": "quicknotes:lab6", + "purl": "pkg:oci/quicknotes@sha256%3A7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c?arch=arm64&repository_url=index.docker.io%2Flibrary%2Fquicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" + }, + { + "name": "aquasecurity:trivy:DiffID", diff --git a/security/lab9/trivy/trivy-config.txt b/security/lab9/trivy/trivy-config.txt new file mode 100644 index 000000000..014f0897c --- /dev/null +++ b/security/lab9/trivy/trivy-config.txt @@ -0,0 +1,233 @@ +2026-07-07T11:25:09Z INFO [misconfig] Misconfiguration scanning is enabled +2026-07-07T11:25:09Z INFO No downloadable policies were loaded as --skip-check-update is enabled +2026-07-07T11:25:10Z INFO Detected config files num=1 + +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 28, FAILURES: 0) +Failures: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +PASS: AVD-DS-0001 (MEDIUM): No issues found +════════════════════════════════════════ +When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated. + +See https://avd.aquasec.com/misconfig/ds001 +──────────────────────────────────────── + + +PASS: AVD-DS-0002 (HIGH): No issues found +════════════════════════════════════════ +Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile. + +See https://avd.aquasec.com/misconfig/ds002 +──────────────────────────────────────── + + +PASS: AVD-DS-0004 (MEDIUM): No issues found +════════════════════════════════════════ +Exposing port 22 might allow users to SSH into the container. + +See https://avd.aquasec.com/misconfig/ds004 +──────────────────────────────────────── + + +PASS: AVD-DS-0005 (LOW): No issues found +════════════════════════════════════════ +You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files. + +See https://avd.aquasec.com/misconfig/ds005 +──────────────────────────────────────── + + +PASS: AVD-DS-0006 (CRITICAL): No issues found +════════════════════════════════════════ +COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself. + +See https://avd.aquasec.com/misconfig/ds006 +──────────────────────────────────────── + + +PASS: AVD-DS-0007 (CRITICAL): No issues found +════════════════════════════════════════ +There can only be one ENTRYPOINT instruction in a Dockerfile. Only the last ENTRYPOINT instruction in the Dockerfile will have an effect. + +See https://avd.aquasec.com/misconfig/ds007 +──────────────────────────────────────── + + +PASS: AVD-DS-0008 (CRITICAL): No issues found +════════════════════════════════════════ +UNIX ports outside the range 0-65535 are exposed. + +See https://avd.aquasec.com/misconfig/ds008 +──────────────────────────────────────── + + +PASS: AVD-DS-0009 (HIGH): No issues found +════════════════════════════════════════ +For clarity and reliability, you should always use absolute paths for your WORKDIR. + +See https://avd.aquasec.com/misconfig/ds009 +──────────────────────────────────────── + + +PASS: AVD-DS-0010 (CRITICAL): No issues found +════════════════════════════════════════ +Avoid using 'RUN' with 'sudo' commands, as it can lead to unpredictable behavior. + +See https://avd.aquasec.com/misconfig/ds010 +──────────────────────────────────────── + + +PASS: AVD-DS-0011 (CRITICAL): No issues found +════════════════════════════════════════ +When a COPY command has more than two arguments, the last one should end with a slash. + +See https://avd.aquasec.com/misconfig/ds011 +──────────────────────────────────────── + + +PASS: AVD-DS-0012 (CRITICAL): No issues found +════════════════════════════════════════ +Different FROMs can't have the same alias defined. + +See https://avd.aquasec.com/misconfig/ds012 +──────────────────────────────────────── + + +PASS: AVD-DS-0013 (MEDIUM): No issues found +════════════════════════════════════════ +Use WORKDIR instead of proliferating instructions like 'RUN cd … && do-something', which are hard to read, troubleshoot, and maintain. + +See https://avd.aquasec.com/misconfig/ds013 +──────────────────────────────────────── + + +PASS: AVD-DS-0014 (LOW): No issues found +════════════════════════════════════════ +Avoid using both 'wget' and 'curl' since these tools have the same effect. + +See https://avd.aquasec.com/misconfig/ds014 +──────────────────────────────────────── + + +PASS: AVD-DS-0015 (HIGH): No issues found +════════════════════════════════════════ +You should use 'yum clean all' after using a 'yum install' command to clean package cached data and reduce image size. + +See https://avd.aquasec.com/misconfig/ds015 +──────────────────────────────────────── + + +PASS: AVD-DS-0016 (HIGH): No issues found +════════════════════════════════════════ +There can only be one CMD instruction in a Dockerfile. If you list more than one CMD then only the last CMD will take effect. + +See https://avd.aquasec.com/misconfig/ds016 +──────────────────────────────────────── + + +PASS: AVD-DS-0017 (HIGH): No issues found +════════════════════════════════════════ +The instruction 'RUN update' should always be followed by ' install' in the same RUN statement. + +See https://avd.aquasec.com/misconfig/ds017 +──────────────────────────────────────── + + +PASS: AVD-DS-0019 (HIGH): No issues found +════════════════════════════════════════ +Cached package data should be cleaned after installation to reduce image size. + +See https://avd.aquasec.com/misconfig/ds019 +──────────────────────────────────────── + + +PASS: AVD-DS-0020 (HIGH): No issues found +════════════════════════════════════════ +The layer and image size should be reduced by deleting unneeded caches after running zypper. + +See https://avd.aquasec.com/misconfig/ds020 +──────────────────────────────────────── + + +PASS: AVD-DS-0021 (HIGH): No issues found +════════════════════════════════════════ +'apt-get' calls should use the flag '-y' to avoid manual user input. + +See https://avd.aquasec.com/misconfig/ds021 +──────────────────────────────────────── + + +PASS: AVD-DS-0022 (HIGH): No issues found +════════════════════════════════════════ +MAINTAINER has been deprecated since Docker 1.13.0. + +See https://avd.aquasec.com/misconfig/ds022 +──────────────────────────────────────── + + +PASS: AVD-DS-0023 (MEDIUM): No issues found +════════════════════════════════════════ +Providing more than one HEALTHCHECK instruction per stage is confusing and error-prone. + +See https://avd.aquasec.com/misconfig/ds023 +──────────────────────────────────────── + + +PASS: AVD-DS-0024 (HIGH): No issues found +════════════════════════════════════════ +'apt-get dist-upgrade' upgrades a major version so it doesn't make more sense in Dockerfile. + +See https://avd.aquasec.com/misconfig/ds024 +──────────────────────────────────────── + + +PASS: AVD-DS-0025 (HIGH): No issues found +════════════════════════════════════════ +You should use 'apk add' with '--no-cache' to clean package cached data and reduce image size. + +See https://avd.aquasec.com/misconfig/ds025 +──────────────────────────────────────── + + +PASS: AVD-DS-0026 (LOW): No issues found +════════════════════════════════════════ +You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers. + +See https://avd.aquasec.com/misconfig/ds026 +──────────────────────────────────────── + + +PASS: AVD-DS-0027 (HIGH): No issues found +════════════════════════════════════════ +Cached package data should be cleaned after installation to reduce image size. + +See https://avd.aquasec.com/misconfig/ds027 +──────────────────────────────────────── + + +PASS: AVD-DS-0029 (HIGH): No issues found +════════════════════════════════════════ +'apt-get' install should use '--no-install-recommends' to minimize image size. + +See https://avd.aquasec.com/misconfig/ds029 +──────────────────────────────────────── + + +PASS: AVD-DS-0030 (HIGH): No issues found +════════════════════════════════════════ +WORKDIR should not be mounted on system directories to avoid container breakouts + +See https://avd.aquasec.com/misconfig/ds030 +──────────────────────────────────────── + + +PASS: AVD-DS-0031 (CRITICAL): No issues found +════════════════════════════════════════ +Passing secrets via `build-args` or envs or copying secret files can leak them out + +See https://avd.aquasec.com/misconfig/ds031 +──────────────────────────────────────── + + diff --git a/security/lab9/trivy/trivy-fs.txt b/security/lab9/trivy/trivy-fs.txt new file mode 100644 index 000000000..489110690 --- /dev/null +++ b/security/lab9/trivy/trivy-fs.txt @@ -0,0 +1,10 @@ +2026-07-07T11:19:49Z INFO [vulndb] Need to update DB +2026-07-07T11:19:49Z INFO [vulndb] Downloading vulnerability DB... +2026-07-07T11:19:49Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T11:20:02Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T11:20:02Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T11:20:02Z INFO [secret] Secret scanning is enabled +2026-07-07T11:20:02Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T11:20:02Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T11:20:02Z INFO Number of language-specific files num=1 +2026-07-07T11:20:02Z INFO [gomod] Detecting vulnerabilities... diff --git a/security/lab9/trivy/trivy-image-before.txt b/security/lab9/trivy/trivy-image-before.txt new file mode 100644 index 000000000..84e8789b0 --- /dev/null +++ b/security/lab9/trivy/trivy-image-before.txt @@ -0,0 +1,144 @@ +2026-07-07T11:20:50Z INFO [vulndb] Need to update DB +2026-07-07T11:20:50Z INFO [vulndb] Downloading vulnerability DB... +2026-07-07T11:20:50Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T11:21:03Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T11:21:03Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T11:21:03Z INFO [secret] Secret scanning is enabled +2026-07-07T11:21:03Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T11:21:03Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T11:21:03Z INFO Detected OS family="debian" version="13.5" +2026-07-07T11:21:03Z INFO [debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +2026-07-07T11:21:03Z INFO Number of language-specific files num=2 +2026-07-07T11:21:03Z INFO [gobinary] Detecting vulnerabilities... +2026-07-07T11:21:03Z WARN Using severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.59/docs/scanner/vulnerability#severity-selection for details. + +quicknotes:lab9-before (debian 13.5) +==================================== +Total: 0 (HIGH: 0, CRITICAL: 0) + + +healthcheck (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 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴──────────────────────────────┴──────────────────────────────────────────────────────────────┘ + +quicknotes (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/security/lab9/trivy/trivy-image.txt b/security/lab9/trivy/trivy-image.txt new file mode 100644 index 000000000..e36dfd368 --- /dev/null +++ b/security/lab9/trivy/trivy-image.txt @@ -0,0 +1,17 @@ +2026-07-07T11:19:35Z INFO [vulndb] Need to update DB +2026-07-07T11:19:35Z INFO [vulndb] Downloading vulnerability DB... +2026-07-07T11:19:35Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T11:19:48Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-07T11:19:48Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T11:19:48Z INFO [secret] Secret scanning is enabled +2026-07-07T11:19:48Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-07T11:19:48Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-07T11:19:48Z INFO Detected OS family="debian" version="13.5" +2026-07-07T11:19:48Z INFO [debian] Detecting vulnerabilities... os_version="13" pkg_num=5 +2026-07-07T11:19:48Z INFO Number of language-specific files num=2 +2026-07-07T11:19:48Z INFO [gobinary] Detecting vulnerabilities... + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + diff --git a/security/lab9/trivy/trivy-sbom.txt b/security/lab9/trivy/trivy-sbom.txt new file mode 100644 index 000000000..2682199e3 --- /dev/null +++ b/security/lab9/trivy/trivy-sbom.txt @@ -0,0 +1,3 @@ +2026-07-07T11:20:06Z INFO "--format cyclonedx" disables security scanning. Specify "--scanners vuln" explicitly if you want to include vulnerabilities in the "cyclonedx" report. +2026-07-07T11:20:06Z INFO Detected OS family="debian" version="13.5" +2026-07-07T11:20:06Z INFO Number of language-specific files num=2 diff --git a/security/lab9/zap/after-headers.txt b/security/lab9/zap/after-headers.txt new file mode 100644 index 000000000..04ef7bc20 --- /dev/null +++ b/security/lab9/zap/after-headers.txt @@ -0,0 +1,14 @@ +HTTP/1.1 200 OK +Cache-Control: no-store +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Referrer-Policy: no-referrer +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Date: Tue, 07 Jul 2026 16:43:51 GMT +Content-Length: 26 + +{"notes":4,"status":"ok"} diff --git a/security/lab9/zap/before-headers.txt b/security/lab9/zap/before-headers.txt new file mode 100644 index 000000000..10da6a6cd --- /dev/null +++ b/security/lab9/zap/before-headers.txt @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Tue, 07 Jul 2026 16:42:27 GMT +Content-Length: 26 + +{"notes":4,"status":"ok"} diff --git a/security/lab9/zap/zap-after-health.html b/security/lab9/zap/zap-after-health.html new file mode 100644 index 000000000..f63da28c2 --- /dev/null +++ b/security/lab9/zap/zap-after-health.html @@ -0,0 +1,619 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://localhost:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 16:44:51 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

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

Summary of Sequences

+

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

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
ZAP is Out of DateLow1
Non-Storable ContentInformational3
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://localhost:8080/health
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
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.
+ +
URLhttp://localhost:8080/
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
URLhttp://localhost:8080/health
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
URLhttp://localhost:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
Instances3
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 Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security/lab9/zap/zap-after-health.json b/security/lab9/zap/zap-after-health.json new file mode 100644 index 000000000..4784397ce --- /dev/null +++ b/security/lab9/zap/zap-after-health.json @@ -0,0 +1,99 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 16:44:51", + "created": "2026-07-07T16:44:51.463955096Z", + "site":[ + { + "@name": "http://localhost:8080", + "@host": "localhost", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "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-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": "2", + "uri": "http://localhost:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "3", + "uri": "http://localhost:8080/health", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "4", + "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": "3" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/security/lab9/zap/zap-after-health.txt b/security/lab9/zap/zap-after-health.txt new file mode 100644 index 000000000..35ce98714 --- /dev/null +++ b/security/lab9/zap/zap-after-health.txt @@ -0,0 +1,76 @@ +Using the Automation Framework +Total of 5 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +PASS: In Page Banner Information Leak [10009] +PASS: Cookie No HttpOnly Flag [10010] +PASS: Cookie Without Secure Flag [10011] +PASS: Re-examine Cache-control Directives [10015] +PASS: Cross-Domain JavaScript Source File Inclusion [10017] +PASS: Content-Type Header Missing [10019] +PASS: Anti-clickjacking Header [10020] +PASS: X-Content-Type-Options Header Missing [10021] +PASS: Information Disclosure - Debug Error Messages [10023] +PASS: Information Disclosure - Sensitive Information in URL [10024] +PASS: Information Disclosure - Sensitive Information in HTTP Referrer Header [10025] +PASS: HTTP Parameter Override [10026] +PASS: Information Disclosure - Suspicious Comments [10027] +PASS: Off-site Redirect [10028] +PASS: Cookie Poisoning [10029] +PASS: User Controllable Charset [10030] +PASS: User Controllable HTML Element Attribute (Potential XSS) [10031] +PASS: Viewstate [10032] +PASS: Directory Browsing [10033] +PASS: Heartbleed OpenSSL Vulnerability (Indicative) [10034] +PASS: Strict-Transport-Security Header [10035] +PASS: HTTP Server Response Header [10036] +PASS: Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) [10037] +PASS: Content Security Policy (CSP) Header Not Set [10038] +PASS: X-Backend-Server Header Information Leak [10039] +PASS: Secure Pages Include Mixed Content [10040] +PASS: HTTP to HTTPS Insecure Transition in Form Post [10041] +PASS: HTTPS to HTTP Insecure Transition in Form Post [10042] +PASS: User Controllable JavaScript Event (XSS) [10043] +PASS: Big Redirect Detected (Potential Sensitive Information Leak) [10044] +PASS: Retrieved from Cache [10050] +PASS: X-ChromeLogger-Data (XCOLD) Header Information Leak [10052] +PASS: Cookie without SameSite Attribute [10054] +PASS: CSP [10055] +PASS: X-Debug-Token Information Leak [10056] +PASS: Username Hash Found [10057] +PASS: X-AspNet-Version Response Header [10061] +PASS: PII Disclosure [10062] +PASS: Permissions Policy Header Not Set [10063] +PASS: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Source Code Disclosure [10099] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +PASS: Dangerous JS Functions [10110] +PASS: Authentication Request Identified [10111] +PASS: Session Management Response Identified [10112] +PASS: Verification Request Identified [10113] +PASS: Script Served From Malicious Domain (polyfill) [10115] +PASS: Absence of Anti-CSRF Tokens [10202] +PASS: Private IP Disclosure [2] +PASS: Session ID in URL Rewrite [3] +PASS: Script Passive Scan Rules [50001] +PASS: Stats Passive Scan Rule [50003] +PASS: Insecure JSF ViewState [90001] +PASS: Java Serialization Object [90002] +PASS: Sub Resource Integrity Attribute Missing [90003] +PASS: Insufficient Site Isolation Against Spectre Vulnerability [90004] +PASS: Charset Mismatch [90011] +PASS: Application Error Disclosure [90022] +PASS: WSDL File Detection [90030] +PASS: Loosely Scoped Cookie [90033] +WARN-NEW: Non-Storable Content [10049] x 3 + http://localhost:8080/ (404 Not Found) + http://localhost:8080/health (200 OK) + http://localhost:8080/sitemap.xml (404 Not Found) +WARN-NEW: ZAP is Out of Date [10116] x 1 + http://localhost:8080/health (200 OK) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 2 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 65 +Automation plan warnings: + Job spider error accessing URL http://localhost:8080/ status code returned : 404 expected 200 diff --git a/security/lab9/zap/zap-after-localhost.html b/security/lab9/zap/zap-after-localhost.html new file mode 100644 index 000000000..3d83958fe --- /dev/null +++ b/security/lab9/zap/zap-after-localhost.html @@ -0,0 +1,587 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://localhost:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 16:44:15 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

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

Summary of Sequences

+

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

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
ZAP is Out of DateLow1
Non-Storable ContentInformational2
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://localhost:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
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.
+ +
URLhttp://localhost:8080
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
URLhttp://localhost:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
Instances2
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 Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security/lab9/zap/zap-after-localhost.json b/security/lab9/zap/zap-after-localhost.json new file mode 100644 index 000000000..aea470d8e --- /dev/null +++ b/security/lab9/zap/zap-after-localhost.json @@ -0,0 +1,89 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 16:44:15", + "created": "2026-07-07T16:44:15.727961260Z", + "site":[ + { + "@name": "http://localhost:8080", + "@host": "localhost", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "pluginid": "10116", + "alertRef": "10116", + "alert": "ZAP is Out of Date", + "name": "ZAP is Out of Date", + "riskcode": "1", + "confidence": "3", + "riskdesc": "Low (High)", + "desc": "

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

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

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

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://localhost:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 16:43:28 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

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

Summary of Sequences

+

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

+ + + + + + +

Alerts

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

Alert Detail

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

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security/lab9/zap/zap-before-health.json b/security/lab9/zap/zap-before-health.json new file mode 100644 index 000000000..f0f167665 --- /dev/null +++ b/security/lab9/zap/zap-before-health.json @@ -0,0 +1,169 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 16:43:28", + "created": "2026-07-07T16:43:28.456525919Z", + "site":[ + { + "@name": "http://localhost:8080", + "@host": "localhost", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "pluginid": "90004", + "alertRef": "90004-1", + "alert": "Insufficient Site Isolation Against Spectre Vulnerability", + "name": "Insufficient Site Isolation Against Spectre Vulnerability", + "riskcode": "1", + "confidence": "2", + "riskdesc": "Low (Medium)", + "desc": "

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

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

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://localhost:8080 + +

+ +

+ Generated on Tue, 7 Jul 2026 16:42:53 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

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

Summary of Sequences

+

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

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
ZAP is Out of DateLow1
Storable and Cacheable ContentInformational3
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://localhost:8080/robots.txt
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
Storable and Cacheable Content
Description +
The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where "shared" caching servers such as "proxy" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.
+ +
URLhttp://localhost:8080
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://localhost:8080/robots.txt
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://localhost:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
Instances3
Solution +
Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:
+
+ +
Cache-Control: no-cache, no-store, must-revalidate, private
+
+ +
Pragma: no-cache
+
+ +
Expires: 0
+
+ +
This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.
+ +
Reference + https://datatracker.ietf.org/doc/html/rfc7234 +
+ + https://datatracker.ietf.org/doc/html/rfc7231 +
+ + https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + +
CWE Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security/lab9/zap/zap-before-localhost.json b/security/lab9/zap/zap-before-localhost.json new file mode 100644 index 000000000..a4a862f86 --- /dev/null +++ b/security/lab9/zap/zap-before-localhost.json @@ -0,0 +1,99 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Tue, 7 Jul 2026 16:42:53", + "created": "2026-07-07T16:42:53.655501Z", + "site":[ + { + "@name": "http://localhost:8080", + "@host": "localhost", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "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": "2", + "uri": "http://localhost:8080/robots.txt", + "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-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": "3", + "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": "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": "1", + "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": "3", + "systemic": false, + "solution": "

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

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

Pragma: no-cache

Expires: 0

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

", + "otherinfo": "

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

", + "reference": "

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

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

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

", + "cweid": "524", + "wascid": "13", + "sourceid": "7" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/security/lab9/zap/zap-before-localhost.txt b/security/lab9/zap/zap-before-localhost.txt new file mode 100644 index 000000000..a5a08f786 --- /dev/null +++ b/security/lab9/zap/zap-before-localhost.txt @@ -0,0 +1,76 @@ +Using the Automation Framework +Total of 3 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +PASS: In Page Banner Information Leak [10009] +PASS: Cookie No HttpOnly Flag [10010] +PASS: Cookie Without Secure Flag [10011] +PASS: Re-examine Cache-control Directives [10015] +PASS: Cross-Domain JavaScript Source File Inclusion [10017] +PASS: Content-Type Header Missing [10019] +PASS: Anti-clickjacking Header [10020] +PASS: X-Content-Type-Options Header Missing [10021] +PASS: Information Disclosure - Debug Error Messages [10023] +PASS: Information Disclosure - Sensitive Information in URL [10024] +PASS: Information Disclosure - Sensitive Information in HTTP Referrer Header [10025] +PASS: HTTP Parameter Override [10026] +PASS: Information Disclosure - Suspicious Comments [10027] +PASS: Off-site Redirect [10028] +PASS: Cookie Poisoning [10029] +PASS: User Controllable Charset [10030] +PASS: User Controllable HTML Element Attribute (Potential XSS) [10031] +PASS: Viewstate [10032] +PASS: Directory Browsing [10033] +PASS: Heartbleed OpenSSL Vulnerability (Indicative) [10034] +PASS: Strict-Transport-Security Header [10035] +PASS: HTTP Server Response Header [10036] +PASS: Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) [10037] +PASS: Content Security Policy (CSP) Header Not Set [10038] +PASS: X-Backend-Server Header Information Leak [10039] +PASS: Secure Pages Include Mixed Content [10040] +PASS: HTTP to HTTPS Insecure Transition in Form Post [10041] +PASS: HTTPS to HTTP Insecure Transition in Form Post [10042] +PASS: User Controllable JavaScript Event (XSS) [10043] +PASS: Big Redirect Detected (Potential Sensitive Information Leak) [10044] +PASS: Retrieved from Cache [10050] +PASS: X-ChromeLogger-Data (XCOLD) Header Information Leak [10052] +PASS: Cookie without SameSite Attribute [10054] +PASS: CSP [10055] +PASS: X-Debug-Token Information Leak [10056] +PASS: Username Hash Found [10057] +PASS: X-AspNet-Version Response Header [10061] +PASS: PII Disclosure [10062] +PASS: Permissions Policy Header Not Set [10063] +PASS: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Source Code Disclosure [10099] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +PASS: Dangerous JS Functions [10110] +PASS: Authentication Request Identified [10111] +PASS: Session Management Response Identified [10112] +PASS: Verification Request Identified [10113] +PASS: Script Served From Malicious Domain (polyfill) [10115] +PASS: Absence of Anti-CSRF Tokens [10202] +PASS: Private IP Disclosure [2] +PASS: Session ID in URL Rewrite [3] +PASS: Script Passive Scan Rules [50001] +PASS: Stats Passive Scan Rule [50003] +PASS: Insecure JSF ViewState [90001] +PASS: Java Serialization Object [90002] +PASS: Sub Resource Integrity Attribute Missing [90003] +PASS: Insufficient Site Isolation Against Spectre Vulnerability [90004] +PASS: Charset Mismatch [90011] +PASS: Application Error Disclosure [90022] +PASS: WSDL File Detection [90030] +PASS: Loosely Scoped Cookie [90033] +WARN-NEW: Storable and Cacheable Content [10049] x 3 + http://localhost:8080 (404 Not Found) + http://localhost:8080/robots.txt (404 Not Found) + http://localhost:8080/sitemap.xml (404 Not Found) +WARN-NEW: ZAP is Out of Date [10116] x 1 + http://localhost:8080/robots.txt (404 Not Found) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 2 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 65 +Automation plan warnings: + Job spider error accessing URL http://localhost:8080 status code returned : 404 expected 200 diff --git a/security/lab9/zap/zap.yaml b/security/lab9/zap/zap.yaml new file mode 100644 index 000000000..6dfda84c3 --- /dev/null +++ b/security/lab9/zap/zap.yaml @@ -0,0 +1,41 @@ +env: + contexts: + - excludePaths: [] + name: baseline + urls: + - http://localhost:8080/health + - 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-after-health.html + reportTitle: ZAP Scanning Report + template: traditional-html + type: report +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-after-health.json + reportTitle: ZAP Scanning Report + template: traditional-json + type: report diff --git a/submissions/lab7.md b/submissions/lab7.md new file mode 100644 index 000000000..92532b6a0 --- /dev/null +++ b/submissions/lab7.md @@ -0,0 +1,689 @@ +# Lab 7 — Configuration Management: Deploy QuickNotes via Ansible + +## Objective + +Write an idempotent Ansible playbook that deploys the QuickNotes binary to the +Lab 5 Vagrant VM, installs a parameterized systemd unit, and manages the +service lifecycle through handlers. + +## Environment + +| Component | Version / value | +|-----------|-----------------| +| Host OS | macOS (authoring machine) | +| Git branch | `feature/lab7` | +| VM target | Lab 5 Vagrant VM (`127.0.0.1:18080 -> guest:8080`) | +| App artifact | `ansible/files/quicknotes` (Linux arm64 for this MacBook VM path) | +| Ansible layout | `ansible/` | + +> Note: this lab was completed on an Apple Silicon MacBook, so two small +> compatibility adjustments were required compared with the original Lab 5/7 +> assumptions: the Lab 5 `Vagrantfile` now picks the guest Go tarball by +> architecture, and the committed QuickNotes payload in `ansible/files/` was +> rebuilt for Linux arm64. + +## Implementation summary + +The `ansible/` directory contains: + +1. `inventory.ini` targeting the Lab 5 VM over the Vagrant SSH endpoint. +2. `playbook.yaml` with `become: true` and `gather_facts: false`. +3. `files/quicknotes`, a static Linux QuickNotes binary built from `app/`. +4. `templates/quicknotes.service.j2`, a Jinja2 systemd unit whose runtime + values come from playbook variables. +5. Bonus templates for `ansible-pull`: a local inventory plus systemd service + and timer units. + +The playbook performs the required idempotent steps: + +1. Creates the `quicknotes` system group and system user, with + `create_home: false` and a non-login shell. +2. Ensures `/var/lib/quicknotes` exists with owner `quicknotes:quicknotes` and + mode `0750`. +3. Copies the binary to `/usr/local/bin/quicknotes` with mode `0755`. +4. Renders `/etc/systemd/system/quicknotes.service` from a template. +5. Reloads systemd only when the rendered unit changed, then enables and starts + the service. +6. Restarts the service only when the binary or unit file changes, via a + handler. + +`gather_facts` is disabled intentionally because this playbook does not branch +on OS facts, CPU architecture, or package manager state; skipping the setup step +keeps runs shorter and makes the idempotency proof tighter. + +On this MacBook, the inventory also had to mirror `vagrant ssh-config` by +disabling strict host-key checks for the ephemeral local VM SSH endpoint. That +matched Vagrant's own SSH behavior and resolved an initial `Host key +verification failed` error from Ansible. + +## Repository layout + +```text +ansible/ +├── inventory.ini +├── playbook.yaml +├── files/ +│ └── quicknotes +└── templates/ + ├── ansible-pull-quicknotes.service.j2 + ├── ansible-pull-quicknotes.timer.j2 + ├── quicknotes-local.ini.j2 + └── quicknotes.service.j2 +``` + +## The playbook + +```yaml +- name: Deploy QuickNotes to the Lab 5 VM + hosts: quicknotes_vm + become: true + gather_facts: false + + vars: + quicknotes_user: quicknotes + quicknotes_group: quicknotes + quicknotes_home: /nonexistent + quicknotes_shell: /usr/sbin/nologin + quicknotes_binary_src: files/quicknotes + quicknotes_binary_dest: /usr/local/bin/quicknotes + quicknotes_service_name: quicknotes + quicknotes_service_unit_path: /etc/systemd/system/quicknotes.service + quicknotes_data_dir: /var/lib/quicknotes + quicknotes_data_path: /var/lib/quicknotes/notes.json + quicknotes_seed_path: /opt/quicknotes/app/seed.json + listen_addr: ":8080" + quicknotes_restart_sec: 4 + ansible_pull_enabled: true + ansible_pull_repo_url: https://github.com/Hidancloud/DevOps-Intro.git + ansible_pull_repo_branch: feature/lab7 + ansible_pull_checkout_dir: /var/lib/ansible-pull/quicknotes + ansible_pull_inventory_path: /etc/ansible/quicknotes-local.ini + ansible_pull_playbook_path: ansible/playbook.yaml + ansible_pull_service_name: ansible-pull-quicknotes.service + ansible_pull_timer_name: ansible-pull-quicknotes.timer + + tasks: + - name: Create the quicknotes system group + ansible.builtin.group: + name: "{{ quicknotes_group }}" + system: true + + - name: Create the quicknotes system user + ansible.builtin.user: + name: "{{ quicknotes_user }}" + group: "{{ quicknotes_group }}" + system: true + create_home: false + home: "{{ quicknotes_home }}" + shell: "{{ quicknotes_shell }}" + + - name: Ensure the quicknotes data directory exists + ansible.builtin.file: + path: "{{ quicknotes_data_dir }}" + state: directory + owner: "{{ quicknotes_user }}" + group: "{{ quicknotes_group }}" + mode: "0750" + + - name: Install the QuickNotes binary + ansible.builtin.copy: + src: "{{ quicknotes_binary_src }}" + dest: "{{ quicknotes_binary_dest }}" + owner: root + group: root + mode: "0755" + notify: restart quicknotes + + - name: Install the systemd unit + ansible.builtin.template: + src: templates/quicknotes.service.j2 + dest: "{{ quicknotes_service_unit_path }}" + owner: root + group: root + mode: "0644" + register: quicknotes_service_unit + notify: restart quicknotes + + - name: Reload systemd when the unit changes + ansible.builtin.systemd: + daemon_reload: true + when: quicknotes_service_unit.changed + + - name: Enable and start the quicknotes service + ansible.builtin.systemd: + name: "{{ quicknotes_service_name }}" + enabled: true + state: started + + - name: Install ansible-pull dependencies + ansible.builtin.apt: + name: + - ansible + - git + state: present + update_cache: true + cache_valid_time: 3600 + when: ansible_pull_enabled + + - name: Ensure ansible config directory exists + ansible.builtin.file: + path: /etc/ansible + state: directory + owner: root + group: root + mode: "0755" + when: ansible_pull_enabled + + - name: Install ansible-pull local inventory + ansible.builtin.template: + src: templates/quicknotes-local.ini.j2 + dest: "{{ ansible_pull_inventory_path }}" + owner: root + group: root + mode: "0644" + register: ansible_pull_local_inventory + when: ansible_pull_enabled + + - name: Ensure ansible-pull checkout directory exists + ansible.builtin.file: + path: "{{ ansible_pull_checkout_dir }}" + state: directory + owner: root + group: root + mode: "0755" + when: ansible_pull_enabled + + - name: Install ansible-pull service + ansible.builtin.template: + src: templates/ansible-pull-quicknotes.service.j2 + dest: "/etc/systemd/system/{{ ansible_pull_service_name }}" + owner: root + group: root + mode: "0644" + register: ansible_pull_service_unit + when: ansible_pull_enabled + + - name: Install ansible-pull timer + ansible.builtin.template: + src: templates/ansible-pull-quicknotes.timer.j2 + dest: "/etc/systemd/system/{{ ansible_pull_timer_name }}" + owner: root + group: root + mode: "0644" + register: ansible_pull_timer_unit + when: ansible_pull_enabled + + - name: Reload systemd when ansible-pull units change + ansible.builtin.systemd: + daemon_reload: true + when: + - ansible_pull_enabled + - ansible_pull_service_unit.changed or ansible_pull_timer_unit.changed + + - name: Prime ansible-pull once so the timer has a last-run anchor + ansible.builtin.systemd: + name: "{{ ansible_pull_service_name }}" + state: started + when: + - ansible_pull_enabled + - ansible_connection != 'local' + - ansible_pull_local_inventory.changed or ansible_pull_service_unit.changed or ansible_pull_timer_unit.changed + + - name: Enable and start the ansible-pull timer + ansible.builtin.systemd: + name: "{{ ansible_pull_timer_name }}" + enabled: true + state: started + when: ansible_pull_enabled + + handlers: + - name: restart quicknotes + ansible.builtin.systemd: + name: "{{ quicknotes_service_name }}" + state: restarted + daemon_reload: true +``` + +## Inventory + +```ini +[quicknotes_vm] +quicknotes-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_ssh_private_key_file=.vagrant/machines/default/virtualbox/private_key ansible_python_interpreter=/usr/bin/python3 ansible_ssh_common_args='-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o PubkeyAcceptedKeyTypes=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa' +``` + +If `vagrant ssh-config` prints a different forwarded port or key path on your +machine, update `ansible_port` and `ansible_ssh_private_key_file` to match that +output before running the playbook. + +## Systemd unit template + +```ini +[Unit] +Description=QuickNotes API +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User={{ quicknotes_user }} +Group={{ quicknotes_group }} +WorkingDirectory={{ quicknotes_data_dir }} +Environment=ADDR={{ listen_addr }} +Environment=DATA_PATH={{ quicknotes_data_path }} +Environment=SEED_PATH={{ quicknotes_seed_path }} +ExecStart={{ quicknotes_binary_dest }} +Restart=on-failure +RestartSec={{ quicknotes_restart_sec }} + +[Install] +WantedBy=multi-user.target +``` + +## Static artifact build + +The managed binary was produced from `app/` as a static Linux executable for +the Apple Silicon VM path: + +```bash +GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \ + go build -C app -trimpath -ldflags='-s -w' \ + -o ../ansible/files/quicknotes . +``` + +## Task 1 — Run + verify + +The Task 1 dry-run and first real-run evidence below was captured before the +bonus `ansible-pull` block was appended to the playbook. The later Task 2 +idempotency proof and Bonus section show that the final extended playbook still +converges cleanly on the same VM. + +### Dry-run + +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml --check +``` + +```text +PLAY [Deploy QuickNotes to the Lab 5 VM] *************************************** + +TASK [Create the quicknotes system group] ************************************** +changed: [quicknotes-vm] + +TASK [Create the quicknotes system user] *************************************** +changed: [quicknotes-vm] + +TASK [Ensure the quicknotes data directory exists] ***************************** +changed: [quicknotes-vm] + +TASK [Install the QuickNotes binary] ******************************************* +changed: [quicknotes-vm] + +TASK [Install the systemd unit] ************************************************ +changed: [quicknotes-vm] + +TASK [Reload systemd when the unit changes] ************************************ +ok: [quicknotes-vm] + +TASK [Enable and start the quicknotes service] ********************************* +ok: [quicknotes-vm] + +RUNNING HANDLER [restart quicknotes] ******************************************* +changed: [quicknotes-vm] + +PLAY RECAP ********************************************************************* +quicknotes-vm : ok=8 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### First real run + +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml +``` + +```text +PLAY [Deploy QuickNotes to the Lab 5 VM] *************************************** + +TASK [Create the quicknotes system group] ************************************** +changed: [quicknotes-vm] + +TASK [Create the quicknotes system user] *************************************** +changed: [quicknotes-vm] + +TASK [Ensure the quicknotes data directory exists] ***************************** +changed: [quicknotes-vm] + +TASK [Install the QuickNotes binary] ******************************************* +changed: [quicknotes-vm] + +TASK [Install the systemd unit] ************************************************ +changed: [quicknotes-vm] + +TASK [Reload systemd when the unit changes] ************************************ +ok: [quicknotes-vm] + +TASK [Enable and start the quicknotes service] ********************************* +ok: [quicknotes-vm] + +RUNNING HANDLER [restart quicknotes] ******************************************* +changed: [quicknotes-vm] + +PLAY RECAP ********************************************************************* +quicknotes-vm : ok=8 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Reachability proof + +```bash +curl -s http://localhost:18080/health +``` + +```text +{"notes":4,"status":"ok"} +``` + +### Design questions + +**a) `command:` vs dedicated modules.** +Dedicated modules such as `user`, `file`, `copy`, `template`, and `systemd` +understand the desired end state and compare it with the target's current +state. That makes them idempotent: if the target already matches, they return +`ok` instead of changing anything. `command:` and `shell:` just run imperative +commands; Ansible cannot reliably infer whether they changed state, so they are +usually reported as changed every run unless you add fragile guards like +`creates:` or `changed_when:`. For this lab, idempotency matters because the +second run must prove `changed=0` and the restart handler must fire only when an +actual config or binary change occurred. + +**b) `notify:` and handlers.** +A handler fires only when a task that references it reports `changed`. If the +`copy` or `template` task determines that the destination file already matches +what Ansible would write, the task stays `ok` and the handler does not run. That +is the right default because unnecessary restarts create avoidable downtime and +noise; service restarts should be coupled to real state changes, not to the mere +fact that a playbook was executed. + +**c) Variable hierarchy.** +For this lab I would use three levels: +1. Playbook vars for repo-local defaults such as `listen_addr`, + `quicknotes_data_path`, and `quicknotes_seed_path`, because they document the + intended baseline close to the tasks. +2. Inventory or `group_vars/quicknotes_vm` for environment-specific connection + values such as `ansible_host`, `ansible_port`, and `ansible_user`, because + they belong to the target environment rather than the app logic. +3. Extra vars (`-e`) only for one-off overrides during demonstrations, such as + `-e listen_addr=:9090` to prove selective template changes, because CLI vars + have high precedence and are ideal for temporary experiments without editing + committed files. + +**d) Do we need `gather_facts: true` here?** +No. This playbook does not make decisions based on Ansible facts; it copies a +binary, writes a template, and manages a service on a known Ubuntu VM. Turning +fact gathering off skips the setup phase, which usually saves a few seconds per +run and makes repeated idempotency checks faster. + +## Task 2 — Idempotency + selective re-run + +### Second run: prove `changed=0` + +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml +``` + +```text +PLAY [Deploy QuickNotes to the Lab 5 VM] *************************************** + +TASK [Create the quicknotes system group] ************************************** +ok: [quicknotes-vm] + +TASK [Create the quicknotes system user] *************************************** +ok: [quicknotes-vm] + +TASK [Ensure the quicknotes data directory exists] ***************************** +ok: [quicknotes-vm] + +TASK [Install the QuickNotes binary] ******************************************* +ok: [quicknotes-vm] + +TASK [Install the systemd unit] ************************************************ +ok: [quicknotes-vm] + +TASK [Reload systemd when the unit changes] ************************************ +skipping: [quicknotes-vm] + +TASK [Enable and start the quicknotes service] ********************************* +ok: [quicknotes-vm] + +TASK [Install ansible-pull dependencies] *************************************** +ok: [quicknotes-vm] + +TASK [Ensure ansible config directory exists] ********************************** +ok: [quicknotes-vm] + +TASK [Install ansible-pull local inventory] ************************************ +ok: [quicknotes-vm] + +TASK [Ensure ansible-pull checkout directory exists] *************************** +ok: [quicknotes-vm] + +TASK [Install ansible-pull service] ******************************************** +ok: [quicknotes-vm] + +TASK [Install ansible-pull timer] ********************************************** +ok: [quicknotes-vm] + +TASK [Reload systemd when ansible-pull units change] *************************** +skipping: [quicknotes-vm] + +TASK [Prime ansible-pull once so the timer has a last-run anchor] ************** +skipping: [quicknotes-vm] + +TASK [Enable and start the ansible-pull timer] ********************************* +ok: [quicknotes-vm] + +PLAY RECAP ********************************************************************* +quicknotes-vm : ok=13 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` + +### Selective change: template only + handler fired + +One simple way to prove selective change without editing committed files is: + +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml -e listen_addr=:9090 +``` + +Expected behavior: +- The `template` task reports `changed=1` +- The restart handler is invoked +- The `user`, `file`, and `copy` tasks stay `ok` + +```text +PLAY [Deploy QuickNotes to the Lab 5 VM] *************************************** + +TASK [Create the quicknotes system group] ************************************** +ok: [quicknotes-vm] + +TASK [Create the quicknotes system user] *************************************** +ok: [quicknotes-vm] + +TASK [Ensure the quicknotes data directory exists] ***************************** +ok: [quicknotes-vm] + +TASK [Install the QuickNotes binary] ******************************************* +ok: [quicknotes-vm] + +TASK [Install the systemd unit] ************************************************ +changed: [quicknotes-vm] + +TASK [Reload systemd when the unit changes] ************************************ +ok: [quicknotes-vm] + +TASK [Enable and start the quicknotes service] ********************************* +ok: [quicknotes-vm] + +RUNNING HANDLER [restart quicknotes] ******************************************* +changed: [quicknotes-vm] + +PLAY RECAP ********************************************************************* +quicknotes-vm : ok=8 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Preview a third change with `--check --diff` + +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml \ + -e quicknotes_data_path=/var/lib/quicknotes/notes-v2.json --check --diff +``` + +```diff +--- before: /etc/systemd/system/quicknotes.service ++++ after: /Users/arsenypinigin/.ansible/tmp/ansible-local-12275b4sv3mqt/tmpy1vbafoo/quicknotes.service.j2 +@@ -8,8 +8,8 @@ + User=quicknotes + Group=quicknotes + WorkingDirectory=/var/lib/quicknotes +-Environment=ADDR=:9090 +-Environment=DATA_PATH=/var/lib/quicknotes/notes.json ++Environment=ADDR=:8080 ++Environment=DATA_PATH=/var/lib/quicknotes/notes-v2.json + Environment=SEED_PATH=/opt/quicknotes/app/seed.json + ExecStart=/usr/local/bin/quicknotes + Restart=on-failure +``` + +### Design questions + +**e) Why does the second run report `changed=0`?** +Because Ansible modules compare the current remote state to the desired state. +`file` checks attributes such as existence, type, owner, group, and mode. +`copy` checks the destination file contents plus metadata. `template` renders the +Jinja2 template and compares the rendered bytes to the target file before +rewriting it. If nothing differs, the tasks are already converged and the play +reports `ok` rather than `changed`. + +**f) What would happen with `shell: 'echo "ADDR=..." > /etc/systemd/system/quicknotes.service'`?** +You would lose safe change detection, structured templating, and predictable file +metadata management. The shell task would typically report changed every run, +which would restart the service every run even when the unit content was +identical. It also makes quoting brittle, hides accidental truncation bugs, +cannot easily manage owner/mode consistently, and encourages partially written +or malformed units when variables contain spaces or special characters. In +short: more imperative code, weaker idempotency, and noisier deploys. + +**g) What bug does `--check --diff` catch that plain `--check` can miss?** +Plain `--check` tells you that a template *would* change, but not whether the +rendered result is sensible. `--diff` exposes the exact before/after lines, so +it catches bugs like accidentally changing `ADDR=:8080` to `ADDR=8080`, pointing +`SEED_PATH` at the wrong file, or rendering a typo into `ExecStart`. Those are +production-impacting config mistakes that a dry-run count alone would not make +obvious. + +## Bonus Task — `ansible-pull` GitOps loop + +### Implementation + +The bonus adds three VM-local artifacts: + +1. `/etc/ansible/quicknotes-local.ini` +2. `/etc/systemd/system/ansible-pull-quicknotes.service` +3. `/etc/systemd/system/ansible-pull-quicknotes.timer` + +The local inventory targets the VM itself via `ansible_connection=local`, so +the same `ansible/playbook.yaml` can run either from the host or from inside +the VM. The systemd service executes: + +```bash +/usr/bin/ansible-pull \ + -U https://github.com/Hidancloud/DevOps-Intro.git \ + -C feature/lab7 \ + -d /var/lib/ansible-pull/quicknotes \ + -i /etc/ansible/quicknotes-local.ini \ + ansible/playbook.yaml +``` + +The timer uses the required cadence: + +```ini +[Timer] +OnBootSec=1min +OnUnitActiveSec=5min +Unit=ansible-pull-quicknotes.service +``` + +### Timer proof + +`systemctl list-timers` on the VM: + +```text +Tue 2026-06-30 14:14:56 UTC 4min 44s Tue 2026-06-30 14:09:56 UTC 15s ago ansible-pull-quicknotes.timer ansible-pull-quicknotes.service +``` + +The local inventory installed in the VM: + +```ini +[quicknotes_vm] +quicknotes-vm ansible_connection=local ansible_python_interpreter=/usr/bin/python3 +``` + +### Convergence demonstration + +To produce a non-destructive but visible reconcile, I changed the playbook +variable `quicknotes_restart_sec` from `3` to `4`, committed it to +`feature/lab7`, and pushed to GitHub. + +Timeline: + +| Event | Time | +|-------|------| +| Git commit pushed (`5b92184`) | `2026-06-30T17:05:27+03:00` | +| Next timer fire on VM | `2026-06-30 14:09:56 UTC` | +| State observed reconciled | `2026-06-30 14:10:11 UTC` | + +State proof from the VM after the timer-fired run: + +```text +16:RestartSec=4 +``` + +Relevant `journalctl` excerpt: + +```text +Jun 30 14:10:06 quicknotes-vm ansible-pull[8484]: RUNNING HANDLER [restart quicknotes] ******************************************* +Jun 30 14:10:06 quicknotes-vm ansible-pull[8484]: changed: [quicknotes-vm] +Jun 30 14:10:06 quicknotes-vm ansible-pull[8484]: PLAY RECAP ********************************************************************* +Jun 30 14:10:06 quicknotes-vm ansible-pull[8484]: quicknotes-vm : ok=15 changed=2 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +Jun 30 14:10:06 quicknotes-vm ansible-pull[8484]: Starting Ansible Pull at 2026-06-30 14:09:56 +Jun 30 14:10:06 quicknotes-vm ansible-pull[8484]: /usr/bin/ansible-pull -U https://github.com/Hidancloud/DevOps-Intro.git -C feature/lab7 -d /var/lib/ansible-pull/quicknotes -i /etc/ansible/quicknotes-local.ini ansible/playbook.yaml +``` + +QuickNotes remained healthy after reconciliation: + +```text +{"notes":4,"status":"ok"} +``` + +### Design questions + +**h) What is the security benefit of `ansible-pull` vs push mode?** +In pull mode, the VM does not need to accept inbound SSH from a central control +node during each deploy. That shrinks the attack surface: no long-lived inbound +management path needs to stay open, and compromise of the control node does not +automatically grant shell access into every managed host. The node only needs +outbound access to fetch Git, then it reconciles locally. + +**i) What is the Kubernetes-layer equivalent, and why is this a fair simulator?** +The same pattern at the Kubernetes layer is commonly associated with **Argo CD** +or **Flux**: Git is the source of truth, and an in-cluster agent pulls desired +state and reconciles the live system to match. `ansible-pull` is a fair VM-layer +simulator because the control loop is the same even though the resource model is +different: commit to Git, agent detects new desired state, apply drift-correcting +changes locally, and repeat on a schedule. + +## Notes + +- The inventory assumes the standard Vagrant VM created from the repo root with + the Lab 5 `Vagrantfile` now restored into this branch. +- The Lab 5 `Vagrantfile` was updated to choose the correct guest Go tarball + for either `amd64` or `arm64`, because this MacBook is Apple Silicon. +- The binary is committed intentionally because the lab specification requires an + `ansible/files/quicknotes` payload that the playbook ships to the VM. diff --git a/submissions/lab8.md b/submissions/lab8.md new file mode 100644 index 000000000..259dbf353 --- /dev/null +++ b/submissions/lab8.md @@ -0,0 +1,526 @@ +# Lab 8 — SRE & Monitoring: Golden Signals Dashboard + One Good Alert + +## Objective + +Provision Prometheus and Grafana on top of the QuickNotes Compose stack, +auto-load a four-panel golden-signals dashboard, define one sustained high-error-rate alert, and write the matching runbook. + +## Environment + +| Component | Version / value | +|-----------|-----------------| +| Host OS | macOS (Apple Silicon) | +| Branch | `feature/lab8` | +| Compose stack | QuickNotes + Prometheus + Grafana | +| Prometheus image | `prom/prometheus:v3.5.0` | +| Grafana image | `grafana/grafana:13.0.3-slim` | + +> Note: the screenshot-oriented parts of the lab were validated through container and HTTP APIs in this environment. Where the spec asks for screenshots, this submission records the underlying API evidence and configuration instead. + +## Layout + +```text +monitoring/ +├── blackbox/ +│ └── blackbox.yml +├── prometheus/ +│ ├── alerts.yml +│ └── prometheus.yml +├── grafana/ +│ ├── dashboards/ +│ │ └── golden-signals.json +│ └── provisioning/ +│ ├── dashboards/ +│ │ └── dashboard.yml +│ └── datasources/ +│ └── datasource.yml +└── scripts/ + ├── checkhost-multi-region.sh + ├── generate-traffic.sh + └── trigger-high-error-rate.sh +``` + +## Task 1 — Prometheus + Grafana with a provisioned dashboard + +### Prometheus config + +```yaml +# monitoring/prometheus/prometheus.yml +global: + scrape_interval: 15s + +rule_files: + - /etc/prometheus/alerts.yml + +scrape_configs: + - job_name: blackbox-internal + metrics_path: /probe + params: + module: [http_2xx] + static_configs: + - targets: + - http://quicknotes:8080/health + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__address__] + target_label: __address__ + replacement: blackbox-exporter:9115 + - source_labels: [__param_target] + target_label: instance + - target_label: job + replacement: blackbox-internal + + - job_name: quicknotes + static_configs: + - targets: + - quicknotes:8080 +``` + +### Grafana data source provisioning + +```yaml +# monitoring/grafana/provisioning/datasources/datasource.yml +apiVersion: 1 + +datasources: + - name: Prometheus + uid: prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false +``` + +### Grafana dashboard provider + +```yaml +# monitoring/grafana/provisioning/dashboards/dashboard.yml +apiVersion: 1 + +providers: + - name: quicknotes-dashboards + orgId: 1 + folder: QuickNotes + type: file + disableDeletion: false + editable: false + options: + path: /var/lib/grafana/dashboards +``` + +### Dashboard JSON + +The provisioned dashboard lives at: +- `monitoring/grafana/dashboards/golden-signals.json` + +It contains four panels: +1. Latency proxy — `sum(rate(quicknotes_http_requests_total[1m]))` +2. Traffic — `sum(rate(quicknotes_http_requests_total[5m]))` +3. Errors — `100 * sum(rate(quicknotes_http_responses_by_code_total{code=~"4..|5.."}[5m])) / clamp_min(sum(rate(quicknotes_http_requests_total[5m])), 0.000001)` +4. Saturation — `quicknotes_notes_total` + +QuickNotes does not expose a request-duration histogram yet, so the Latency panel is explicitly labeled as a proxy until such a metric exists. + +### Compose extension + +```yaml +# compose.yaml +services: + quicknotes: + build: ./app + image: quicknotes:lab6 + ports: + - "8080:8080" + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + volumes: + - quicknotes-data:/data + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + + prometheus: + image: prom/prometheus:v3.5.0 + command: + - --config.file=/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - ./monitoring/prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro + depends_on: + quicknotes: + condition: service_healthy + restart: unless-stopped + + blackbox-exporter: + image: prom/blackbox-exporter:v0.27.0 + command: + - --config.file=/etc/blackbox_exporter/blackbox.yml + ports: + - "9115:9115" + volumes: + - ./monitoring/blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro + depends_on: + quicknotes: + condition: service_healthy + restart: unless-stopped + + grafana: + image: grafana/grafana:13.0.3-slim + environment: + GF_SECURITY_ADMIN_USER: quicknotes_admin + GF_SECURITY_ADMIN_PASSWORD: qn-lab8-monitoring + GF_USERS_ALLOW_SIGN_UP: "false" + ports: + - "3000:3000" + volumes: + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro + depends_on: + - prometheus + restart: unless-stopped + +volumes: + quicknotes-data: +``` + +### Verification evidence + +Commands run: + +```bash +docker compose up -d --build +./monitoring/scripts/generate-traffic.sh http://localhost:8080 200 +curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[].health' +``` + +Live API evidence: + +```text +$ docker compose ps +NAME IMAGE COMMAND SERVICE STATUS +devops-intro-grafana-1 grafana/grafana:13.0.3-slim "/run.sh" grafana Up +devops-intro-prometheus-1 prom/prometheus:v3.5.0 "/bin/prometheus --..." prometheus Up +devops-intro-quicknotes-1 quicknotes:lab6 "/quicknotes" quicknotes Up (healthy) +``` + +```text +$ curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[].health' +"up" +``` + +```json +$ curl -u quicknotes_admin:qn-lab8-monitoring 'http://localhost:3000/api/search?query=golden' +[ + { + "uid": "quicknotes-golden-signals", + "title": "QuickNotes Golden Signals", + "type": "dash-db", + "url": "/d/quicknotes-golden-signals/quicknotes-golden-signals" + } +] +``` + +```json +$ curl -u quicknotes_admin:qn-lab8-monitoring http://localhost:3000/api/datasources/name/Prometheus +{ + "name": "Prometheus", + "type": "prometheus", + "url": "http://prometheus:9090", + "isDefault": true +} +``` + +Representative panel-query values after traffic generation: + +```json +$ curl -G http://localhost:9090/api/v1/query --data-urlencode 'query=sum(rate(quicknotes_http_requests_total[1m]))' +[ + { + "value": [ + 1782829897.910, + "10.95249755577282" + ] + } +] +``` + +```json +$ curl -G http://localhost:9090/api/v1/query --data-urlencode 'query=sum(rate(quicknotes_http_requests_total[5m]))' +[ + { + "value": [ + 1782830282.713, + "3.554535629570368" + ] + } +] +``` + +```json +$ curl -G http://localhost:9090/api/v1/query --data-urlencode 'query=100 * sum(rate(quicknotes_http_responses_by_code_total{code=~\"4..|5..\"}[5m])) / clamp_min(sum(rate(quicknotes_http_requests_total[5m])), 0.000001)' +[ + { + "value": [ + 1782830282.720, + "47.581441263573545" + ] + } +] +``` + +```json +$ curl -G http://localhost:9090/api/v1/query --data-urlencode 'query=quicknotes_notes_total' +[ + { + "metric": { + "__name__": "quicknotes_notes_total", + "instance": "quicknotes:8080", + "job": "quicknotes" + }, + "value": [ + 1782829897.980, + "204" + ] + } +] +``` + +### Design questions + +**a) Pull vs push.** +Prometheus must be able to reach QuickNotes over the network because the scrape model is pull-based. QuickNotes does not initiate metric delivery. If Prometheus cannot reach QuickNotes, the scrape target turns `up == 0`, Prometheus stops ingesting fresh samples, and dashboard panels either flatten at stale values or show no recent data. + +**b) Why not `5s` or `5m` scrape intervals?** +At `5s`, Prometheus stores far more samples, increases container and disk overhead, and makes noisy short-lived spikes dominate `rate()` queries that should represent service trends. At `5m`, you lose resolution so badly that short incidents disappear, alert evaluation becomes sluggish, and most panels look stair-stepped or empty over small time windows. + +**c) `rate()` vs `irate()` vs `delta()`.** +`rate()` is the right choice for the Traffic panel because it smooths a counter over a range and is stable for dashboards. `irate()` is too twitchy for a primary traffic graph because it only looks at the last two samples, while `delta()` is for absolute change over a range rather than per-second counter rates. + +**d) Why provision Grafana from files?** +Provisioning makes the dashboard and data source reproducible: a fresh `docker compose up` recreates the same monitoring state without manual clicking. It also means the dashboard lives in git, can be reviewed in PRs, and can be rebuilt on another machine or in CI exactly the same way. + +## Task 2 — One good alert + runbook + +### Alert rule + +```yaml +# monitoring/prometheus/alerts.yml +groups: + - name: quicknotes + rules: + - alert: QuickNotesHighErrorRate + expr: | + ( + sum(rate(quicknotes_http_responses_by_code_total{code=~"4..|5.."}[5m])) + / + clamp_min(sum(rate(quicknotes_http_requests_total[5m])), 0.000001) + ) > 0.05 + for: 5m + labels: + severity: page + annotations: + summary: "QuickNotes error ratio is above 5%" + description: "4xx + 5xx responses have exceeded 5% of total traffic for 5 minutes." + runbook: "docs/runbook/high-error-rate.md" +``` + +### Runbook + +```md +# docs/runbook/high-error-rate.md +# High Error Rate + +## What this alert means +QuickNotes is returning 4xx and 5xx responses for more than 5% of requests, sustained for at least 5 minutes. + +## Triage steps +1. Confirm the alert is still active by checking Prometheus `/alerts` and Grafana panels for traffic and error ratio over the last 15 minutes. +2. Compare healthy versus failing requests with `curl` against `/health`, `/notes`, and one deliberately malformed `POST /notes` to determine whether the issue is broad or isolated to a route. +3. Inspect the QuickNotes container logs with `docker compose logs --tail=200 quicknotes` and correlate timestamps with the error spike. +4. Check whether the backing data file or mounted volume is writable and present, because write failures can surface as 5xx on note creation. + +## Mitigations +1. Roll back the most recent change to the QuickNotes image or monitoring-related config if the spike started immediately after a deploy. +2. Temporarily reduce bad traffic by blocking malformed clients, rate-limiting the offending caller, or disabling the specific integration sending invalid payloads. +3. Restart the `quicknotes` container if the service is wedged but the root cause is still under investigation. + +## Post-incident +Write a short blameless postmortem using the Lecture 1 template: timeline, customer impact, root cause, contributing factors, and concrete follow-up actions. +``` + +### Trigger plan + +```bash +./monitoring/scripts/trigger-high-error-rate.sh http://localhost:8080 330 +curl http://localhost:9090/api/v1/alerts | jq '.data.alerts' +``` + +Observed state transition: + +```text +=== 2026-06-30 14:32:24 UTC === +{ + "state": "pending", + "activeAt": "2026-06-30T14:32:12.464142167Z", + "value": "6.352555131025643e-02", + "severity": "page", + "runbook": "docs/runbook/high-error-rate.md" +} +``` + +```text +=== 2026-06-30 14:37:24 UTC === +{ + "state": "firing", + "activeAt": "2026-06-30T14:32:12.464142167Z", + "value": "4.788478847884788e-01", + "severity": "page", + "runbook": "docs/runbook/high-error-rate.md" +} +``` + +Final alert API object: + +```json +$ curl http://localhost:9090/api/v1/alerts | jq '.data.alerts[] | select(.labels.alertname=="QuickNotesHighErrorRate")' +{ + "labels": { + "alertname": "QuickNotesHighErrorRate", + "severity": "page" + }, + "annotations": { + "description": "4xx + 5xx responses have exceeded 5% of total traffic for 5 minutes.", + "runbook": "docs/runbook/high-error-rate.md", + "summary": "QuickNotes error ratio is above 5%" + }, + "state": "firing", + "activeAt": "2026-06-30T14:32:12.464142167Z", + "value": "4.788478847884788e-01" +} +``` + +### Design questions + +**e) Why sustained for 5 minutes?** +Because on-call alerts should represent real user pain, not single malformed requests or transient blips. A 5-minute sustain window filters noise and pages only when the error ratio is persistent enough to suggest an actual incident. + +**f) Symptom vs cause alerts.** +This alert is symptom-based because it tracks what users actually observe: error responses. A cause alert would be something like “CPU above 80%” or “container restarted once.” That is worse as a primary page because it may fire when users are fine, which trains on-call engineers to ignore alerts. + +**g) Alert fatigue threshold.** +If an alert pages roughly more than 20% of the time without meaningful user impact, it is too noisy and needs tuning. That is a practical threshold where responders start distrusting the signal instead of treating it as urgent. + +## Bonus Task — Synthetic Monitoring From the Outside + +### Public URL and probe setup + +For the external vantage point, I exposed the local QuickNotes instance through a +Cloudflare quick tunnel: + +```bash +docker run -d --name quicknotes-tunnel --network bridge \ + cloudflare/cloudflared:latest \ + tunnel --no-autoupdate --url http://host.docker.internal:8080 +``` + +Tunnel URL used for the run: + +```text +https://intensity-biological-beverages-perfectly.trycloudflare.com/health +``` + +Instead of Checkly, I used the equivalent free synthetic service +`api.check-host.cc`, which satisfies the lab's "or equivalent" clause. The +collector script `monitoring/scripts/checkhost-multi-region.sh` issued one HTTP +check per minute, treated non-`200` responses as failures, and summarized three +regions: + +- Germany: `DE-FFM-KRK` (Frankfurt am Main) +- Singapore: `SG-SIN-FDCServers` +- United States: `US-DAL-Leora` (Dallas) + +Run window: + +```text +start: 2026-06-30T15:01:43Z +end: 2026-06-30T15:35:35Z +span: 2032 s (~33.9 min) +cadence: 60 s +``` + +### External summary + +```json +$ cat monitoring/results/checkhost/summary.json +{ + "overall": { + "checks": 93, + "errors": 0, + "p50_seconds": 0.428, + "p95_seconds": 1.284 + }, + "nodes": { + "DE": { "p50_seconds": 0.389, "p95_seconds": 0.619, "errors": 0 }, + "SG": { "p50_seconds": 0.99, "p95_seconds": 1.549, "errors": 0 }, + "US": { "p50_seconds": 0.418, "p95_seconds": 0.478, "errors": 0 } + } +} +``` + +All three external regions stayed below the lab's `2 s` latency threshold at +the `p95` level, and none returned a non-`200` status during the measured +window. + +### Internal blackbox summary over the same window + +The Prometheus-side comparison used the internal blackbox probe on +`http://quicknotes:8080/health` and queried the same time window +(`2026-06-30T15:01:43Z` to `2026-06-30T15:35:35Z`) with a `15s` step. + +```json +{ + "samples": 136, + "p50_seconds": 0.002150833, + "p95_seconds": 0.002642417, + "errors": 0 +} +``` + +### Internal vs external comparison + +| Metric | Prometheus (inside the Compose net) | External synthetic (DE + SG + US) | +|--------|-------------------------------------|-----------------------------------| +| Avg latency p50 | `0.0022 s` | `0.428 s` | +| Avg latency p95 | `0.0026 s` | `1.284 s` | +| Errors observed | `0 / 136` | `0 / 93` | + +### Failure-mode analysis + +The external synthetic probe would catch failures in the public path that the +internal Prometheus probe cannot see: Cloudflare tunnel issues, DNS mistakes, +edge routing problems, TLS handshake failures, or an ISP/regional reachability +issue between users and the edge. That is why the external p50/p95 values are +two orders of magnitude slower than the internal ones even though both saw zero +errors: the external path includes Internet and edge overhead, while the +internal probe only measures an in-network container-to-container request. On +the other hand, Prometheus can catch failures that stay inside the cluster or +service mesh boundary, such as a metrics endpoint disappearing, an internal-only +health endpoint regressing, or fast transient failures sampled every 15 seconds +that a once-per-minute synthetic probe could miss. In practice, the two signals +are complementary: internal probing is sharper for service behavior, while +external probing is the better "can a real user reach me?" detector. + +## Notes + +- The bonus collector was hardened during the run to tolerate a transient + `api.check-host.cc` `502` without aborting the full measurement window. diff --git a/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..02ddd5e4e --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,479 @@ +# Lab 9 — DevSecOps: Trivy + OWASP ZAP + +## Objective + +The goal of this lab was to scan QuickNotes as both a container image and a running HTTP service, then make engineering decisions from the findings instead of treating scanner output as a checkbox. I ran Trivy image, filesystem, config, and CycloneDX SBOM generation with a pinned scanner image. I also ran OWASP ZAP baseline with a pinned ZAP image, fixed security header findings in Go middleware, added regression coverage, and added a pinned `govulncheck` CI job. + +## Environment + +| Item | Value | +|---|---| +| Date | 2026-07-07 | +| Branch | `feature/lab9` | +| App image | `quicknotes:lab6` | +| Trivy image | `aquasec/trivy:0.59.1` | +| ZAP image | `ghcr.io/zaproxy/zaproxy:2.16.1` | +| Go on host | `go1.26.4 darwin/arm64` | +| CI Go version | `1.26.4`, matching the patched Docker builder used to remove Go stdlib CVEs | + +Generated artifacts are committed under `security/lab9/`: + +```text +security/lab9/trivy/trivy-image.txt +security/lab9/trivy/trivy-image-before.txt +security/lab9/trivy/trivy-fs.txt +security/lab9/trivy/trivy-config.txt +security/lab9/trivy/quicknotes-lab6.cdx.json +security/lab9/zap/zap-before-localhost.json +security/lab9/zap/zap-before-localhost.html +security/lab9/zap/zap-before-localhost.txt +security/lab9/zap/zap-before-health.json +security/lab9/zap/zap-before-health.html +security/lab9/zap/zap-before-health.txt +security/lab9/zap/zap-after-localhost.json +security/lab9/zap/zap-after-localhost.html +security/lab9/zap/zap-after-localhost.txt +security/lab9/zap/zap-after-health.json +security/lab9/zap/zap-after-health.html +security/lab9/zap/zap-after-health.txt +security/lab9/govulncheck-red-demo.txt +security/lab9/govulncheck-green.txt +``` + +--- + +## Task 1 — Trivy scans, triage, and SBOM + +### Commands executed + +```bash +docker build -t quicknotes:lab6 ./app + +docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 + +docker run --rm \ + -v "$PWD:/repo" \ + -w /repo \ + aquasec/trivy:0.59.1 fs --severity HIGH,CRITICAL --no-progress \ + --skip-dirs .git --skip-dirs .vagrant --skip-dirs security/lab9 . + +docker run --rm \ + -v "$PWD:/repo" \ + -w /repo \ + aquasec/trivy:0.59.1 config --include-non-failures --skip-check-update \ + --skip-dirs .git --skip-dirs .vagrant --skip-dirs security/lab9 . + +docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD/security/lab9/trivy:/out" \ + aquasec/trivy:0.59.1 image --format cyclonedx \ + --output /out/quicknotes-lab6.cdx.json quicknotes:lab6 +``` + +Note: in Trivy `0.59.1`, CycloneDX SBOM generation for a container image is performed through the `image --format cyclonedx` target. The `sbom` target is for scanning an existing SBOM file, so using `image --format cyclonedx` is the image-generation path for this pinned version. + +### Image scan output excerpt + +Final scan after remediation: + +```text +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) +``` + +Before remediation, the archived image `quicknotes:lab9-before` showed Go standard library findings in both Go binaries: + +```text +healthcheck (gobinary) +====================== +Total: 14 (HIGH: 13, CRITICAL: 1) + +quicknotes (gobinary) +===================== +Total: 14 (HIGH: 13, CRITICAL: 1) +``` + +The root cause was the pinned builder image `golang:1.24.5`. I fixed it by rebuilding both static binaries with `golang:1.26.4`, while keeping the distroless runtime image. + +### Filesystem scan output excerpt + +```text +2026-07-07T11:20:02Z INFO [vuln] Vulnerability scanning is enabled +2026-07-07T11:20:02Z INFO [secret] Secret scanning is enabled +2026-07-07T11:20:02Z INFO Number of language-specific files num=1 +2026-07-07T11:20:02Z INFO [gomod] Detecting vulnerabilities... +``` + +No HIGH or CRITICAL findings were reported in the repository filesystem scan. + +### Config scan output excerpt + +After adding a Dockerfile `HEALTHCHECK`, the config scan passed all Dockerfile checks: + +```text +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 28, FAILURES: 0) +Failures: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) +``` + +I used Trivy embedded checks with `--skip-check-update` for this artifact so the result is reproducible and not affected by a transient upstream policy bundle. The repository had one detected config file for this scan: `app/Dockerfile`. + +### Trivy HIGH/CRITICAL triage table + +| Source | Finding | Severity | Installed | Fixed in | Disposition | Reason | +|---|---|---:|---|---|---|---| +| `quicknotes` + `healthcheck` Go binaries | `CVE-2025-68121` | CRITICAL | Go `1.24.5` | `1.24.13`, `1.25.7`, `1.26.0-rc.3` | FIX | Fixed in this PR by changing `app/Dockerfile` to build with `golang:1.26.4`; final image scan is zero HIGH/CRITICAL. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2025-61726` | HIGH | Go `1.24.5` | `1.24.12`, `1.25.6` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2025-61729` | HIGH | Go `1.24.5` | `1.24.11`, `1.25.5` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-25679` | HIGH | Go `1.24.5` | `1.25.8`, `1.26.1` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-27145` | HIGH | Go `1.24.5` | `1.25.11`, `1.26.4` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-32280` | HIGH | Go `1.24.5` | `1.25.9`, `1.26.2` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-32281` | HIGH | Go `1.24.5` | `1.25.9`, `1.26.2` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-32283` | HIGH | Go `1.24.5` | `1.25.9`, `1.26.2` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-33811` | HIGH | Go `1.24.5` | `1.25.10`, `1.26.3` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-33814` | HIGH | Go `1.24.5` | `1.25.10`, `1.26.3` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-39820` | HIGH | Go `1.24.5` | `1.25.10`, `1.26.3` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-39836` | HIGH | Go `1.24.5` | `1.25.10`, `1.26.3` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-42499` | HIGH | Go `1.24.5` | `1.25.10`, `1.26.3` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | +| `quicknotes` + `healthcheck` Go binaries | `CVE-2026-42504` | HIGH | Go `1.24.5` | `1.25.11`, `1.26.4` | FIX | Fixed in this PR by the same builder upgrade, which removed the vulnerable stdlib from both static binaries. | + +Final result: image scan, filesystem scan, and config scan have no remaining HIGH or CRITICAL findings. + +### CycloneDX SBOM first 30 lines + +```json +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:5d2390f0-b737-4326-b87a-05b04173ba82", + "version": 1, + "metadata": { + "timestamp": "2026-07-07T11:20:06+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "pkg:oci/quicknotes@sha256%3A7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c?arch=arm64&repository_url=index.docker.io%2Flibrary%2Fquicknotes", + "type": "container", + "name": "quicknotes:lab6", + "purl": "pkg:oci/quicknotes@sha256%3A7563fb5d9ff374fd90f39f55462796959edbf828a6efabfb7b36b086120a247c?arch=arm64&repository_url=index.docker.io%2Flibrary%2Fquicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" + }, + { + "name": "aquasecurity:trivy:DiffID", +``` + +### Design questions + +**a) CVE severity is one input, not the answer. What else matters?** + +Severity tells me the theoretical impact, but triage must also consider reachability, exploit maturity, exposed attack surface, runtime permissions, data sensitivity, and compensating controls. For example, a HIGH CVE in a package that is never called is different from a HIGH CVE in public HTTP request parsing. Deployment context also matters: an internal demo service, a public internet API, and a privileged production control-plane component have different risk profiles. + +**b) Why is a minimal base the strongest single security control?** + +A minimal image removes entire classes of problems before scanning starts. QuickNotes uses distroless static nonroot, so there is no shell, no package manager, and only a tiny runtime filesystem. This reduces exploit tooling inside the container and reduces the number of OS packages that can carry CVEs. + +**c) When is `.trivyignore` right, and when is it security theater?** + +`.trivyignore` is right when the team has a documented false positive or a dated risk acceptance with owner, reason, and re-check date. It is security theater when it hides noisy findings without analysis, especially if a fixed version exists and the only reason is “the pipeline is red.” Suppression should be reviewable engineering evidence, not a broom. + +**d) What future problem does having an SBOM solve?** + +An SBOM lets us answer “are we affected?” quickly when a new vulnerability appears. During incidents like Log4Shell, teams without component inventories had to manually inspect every service under pressure. With `quicknotes-lab6.cdx.json`, we can query exact image components and decide quickly whether to patch, rebuild, or ignore. + +--- + +## Task 2 — OWASP ZAP baseline and code fix + +### Commands executed + +```bash +docker run -d --name quicknotes-lab9-before -p 8080:8080 quicknotes:lab9-before + +docker run --rm -u 0 \ + -v "$PWD/security/lab9/zap:/zap/wrk:rw" \ + ghcr.io/zaproxy/zaproxy:2.16.1 zap-baseline.py \ + -t http://localhost:8080 \ + -J zap-before-localhost.json \ + -r zap-before-localhost.html \ + -I + +docker run --rm -u 0 \ + -v "$PWD/security/lab9/zap:/zap/wrk:rw" \ + ghcr.io/zaproxy/zaproxy:2.16.1 zap-baseline.py \ + -t http://localhost:8080/health \ + -J zap-before-health.json \ + -r zap-before-health.html \ + -I + +docker run -d --name quicknotes-lab9-after -p 8080:8080 quicknotes:lab6 + +docker run --rm -u 0 \ + -v "$PWD/security/lab9/zap:/zap/wrk:rw" \ + ghcr.io/zaproxy/zaproxy:2.16.1 zap-baseline.py \ + -t http://localhost:8080 \ + -J zap-after-localhost.json \ + -r zap-after-localhost.html \ + -I + +docker run --rm -u 0 \ + -v "$PWD/security/lab9/zap:/zap/wrk:rw" \ + ghcr.io/zaproxy/zaproxy:2.16.1 zap-baseline.py \ + -t http://localhost:8080/health \ + -J zap-after-health.json \ + -r zap-after-health.html \ + -I +``` + +I ran the required baseline target as `http://localhost:8080`. I also ran a second baseline against `http://localhost:8080/health` because the QuickNotes root path intentionally returns `404`, while `/health` exercises a normal `200 OK` API response and gives clearer before/after header evidence. + +### Before evidence + +Response headers before the fix: + +```text +HTTP/1.1 200 OK +Content-Type: application/json +Date: Tue, 07 Jul 2026 16:42:27 GMT +Content-Length: 26 + +{"notes":4,"status":"ok"} +``` + +ZAP before excerpt: + +```text +WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1 + http://localhost:8080/health (200 OK) +WARN-NEW: Storable and Cacheable Content [10049] x 4 +WARN-NEW: ZAP is Out of Date [10116] x 1 +WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 1 +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 4 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 63 +``` + +### Code fix + +I implemented security headers as middleware that wraps the router in `app/handlers.go`, so the policy applies to all routes consistently. I also added a regression test in `app/handlers_test.go` that checks several routes, including `/health`, `/metrics`, `GET /notes`, and `POST /notes`. If the middleware is removed, the test fails. + +Key diff excerpt: + +```diff +-func (s *Server) Routes() *http.ServeMux { ++func (s *Server) Routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /health", s.wrap(s.handleHealth)) + mux.HandleFunc("GET /metrics", s.wrap(s.handleMetrics)) + mux.HandleFunc("GET /notes", s.wrap(s.handleListNotes)) + mux.HandleFunc("POST /notes", s.wrap(s.handleCreateNote)) + mux.HandleFunc("GET /notes/{id}", s.wrap(s.handleGetNote)) + mux.HandleFunc("DELETE /notes/{id}", s.wrap(s.handleDeleteNote)) +- return mux ++ return securityHeaders(mux) + } ++ ++func securityHeaders(next http.Handler) http.Handler { ++ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ w.Header().Set("Cache-Control", "no-store") ++ w.Header().Set("Content-Security-Policy", "default-src 'none'") ++ w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") ++ w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") ++ w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") ++ w.Header().Set("Referrer-Policy", "no-referrer") ++ w.Header().Set("X-Content-Type-Options", "nosniff") ++ w.Header().Set("X-Frame-Options", "DENY") ++ next.ServeHTTP(w, r) ++ }) ++} +``` + +### Test output + +```text +ok quicknotes 1.616s +? quicknotes/cmd/healthcheck [no test files] +``` + +### After evidence + +Response headers after the fix: + +```text +HTTP/1.1 200 OK +Cache-Control: no-store +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Referrer-Policy: no-referrer +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Date: Tue, 07 Jul 2026 16:43:51 GMT +Content-Length: 26 + +{"notes":4,"status":"ok"} +``` + +ZAP after excerpt: + +```text +PASS: X-Content-Type-Options Header Missing [10021] +PASS: Content Security Policy (CSP) Header Not Set [10038] +PASS: Insufficient Site Isolation Against Spectre Vulnerability [90004] +WARN-NEW: Non-Storable Content [10049] x 4 +WARN-NEW: ZAP is Out of Date [10116] x 1 +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 2 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 65 +``` + +### ZAP finding triage table + +| ID | Name | Risk | Affected URL / parameter | Disposition | Reason | +|---:|---|---|---|---|---| +| `10021` | X-Content-Type-Options Header Missing | Low (Medium confidence) | `/health`, parameter `x-content-type-options` | FIX | Added middleware header `X-Content-Type-Options: nosniff`; after report shows PASS. | +| `90004` | Insufficient Site Isolation Against Spectre Vulnerability | Low (Medium confidence) | `/health`, parameter `Cross-Origin-Resource-Policy` | FIX | Added COEP, COOP, and CORP headers in middleware; after report shows PASS. | +| `10049` | Storable and Cacheable Content | Informational (Medium confidence) | `/`, `/health`, `/robots.txt`, `/sitemap.xml` | FIX | Added `Cache-Control: no-store`; after report changed to `Non-Storable Content`, so the original cacheability risk is fixed. | +| `10049` | Non-Storable Content | Informational (Medium confidence) | `/`, `/health`, `/robots.txt`, `/sitemap.xml` | ACCEPT | This is ZAP confirming the response is non-storable after the fix, not an application weakness. Re-check by 2026-10-07. | +| `10116` | ZAP is Out of Date | Low (High confidence) | Scanner metadata on root request | ACCEPT | This is not an application vulnerability. The lab required a pinned image; `2.16.1` is pinned for repeatability. Re-evaluate scanner version by 2026-08-07. | + +### Design questions + +**e) Why middleware and not per-handler header sets?** + +Middleware creates one enforcement point. If headers are added in each handler, a future route can easily forget one and silently regress. Wrapping the router makes security headers a default property of the service, and the unit test proves the policy is applied across multiple routes. + +**f) What does `Content-Security-Policy: default-src 'none'` break, and why is it OK for QuickNotes?** + +`default-src 'none'` blocks loading scripts, styles, images, fonts, frames, and network resources unless explicitly allowed. That would break a normal website or Swagger UI because browsers need assets to render the page. QuickNotes is a JSON API, not a browser UI, so a strict CSP is safe and useful defense-in-depth if an API response is ever rendered by a browser. + +**g) What is the cost of marking all informational ZAP issues as accepted without reading them?** + +It trains the team to ignore scanners and hides the one “informational” issue that actually points to a bad assumption. Informational findings can reveal caching mistakes, metadata leakage, or scanner configuration problems. Reading them is cheap compared with explaining later why the team accepted evidence it never evaluated. + +--- + +## Bonus Task — `govulncheck` as CI PR gate + +### CI workflow change + +I restored the Lab 3 CI workflow and added a dedicated `govulncheck` job in `.github/workflows/ci.yml`. The job runs in `app/`, uses Go `1.26.4`, installs a pinned scanner version, and runs `govulncheck ./...`. + +```yaml +govulncheck: + name: govulncheck + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.5.0 + with: + go-version: "1.26.4" + cache: true + cache-dependency-path: app/go.mod + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Run govulncheck + run: $(go env GOPATH)/bin/govulncheck ./... + env: + GOFLAGS: -buildvcs=false +``` + +### Red demonstration log + +I used a temporary local copy of `app/` to avoid committing a vulnerable dependency to the final lab branch. The temporary copy added a reachable call to vulnerable `golang.org/x/text/language.ParseAcceptLanguage` with `golang.org/x/text@v0.3.7`. This is the same command that the CI job runs. + +![Red govulncheck workflow run](workflow_red.png) + +```text +=== Symbol Results === + +Vulnerability #1: GO-2022-1059 + Denial of service via crafted Accept-Language header in + golang.org/x/text/language + More info: https://pkg.go.dev/vuln/GO-2022-1059 + Module: golang.org/x/text + Found in: golang.org/x/text@v0.3.7 + Fixed in: golang.org/x/text@v0.3.8 + Example traces found: + #1: vuln_demo.go:6:37: quicknotes.vulnerableAcceptLanguage calls language.ParseAcceptLanguage + +Your code is affected by 1 vulnerability from 1 module. +``` + +### Green demonstration log + +Final app state: + +![Green govulncheck workflow run](workflow_green.png) + +```text +No vulnerabilities found. +``` + +### Design questions + +**h) How is reachability different from “module has a CVE”?** + +A module-level scanner says “this dependency version is known vulnerable.” `govulncheck` goes further and asks whether our call graph reaches the vulnerable symbol. That reduces triage noise: a vulnerability in an unused function may still need tracking, but it is not the same urgency as a reachable vulnerability in request-handling code. + +**i) Why pin the scanner version?** + +A scanner is part of the build toolchain. If it floats on `@latest`, the same commit can pass today and fail tomorrow because scanner behavior changed. Pinning `govulncheck@v1.1.4` makes CI reproducible; upgrades become intentional maintenance with reviewable diffs. + +**j) What will `govulncheck` not catch that Trivy image scan would?** + +`govulncheck` only understands Go code and Go vulnerability data. It will not catch OS package CVEs, vulnerable base images, Dockerfile misconfigurations, shell tools, OpenSSL packages in the image, or leaked secrets in files. That is why this lab uses both Trivy and `govulncheck`: they answer different questions. + +--- + +## Final validation + +```bash +cd app && go test -race -count=1 ./... && go vet ./... +``` + +```text +ok quicknotes 1.616s +? quicknotes/cmd/healthcheck [no test files] +``` + +```bash +cd app && "$(go env GOPATH)/bin/govulncheck" ./... +``` + +```text +No vulnerabilities found. +``` + +## Summary of engineering changes + +- Upgraded Docker builder from `golang:1.24.5` to `golang:1.26.4`, removing Go stdlib CVEs from both static binaries. +- Added Dockerfile `HEALTHCHECK` using the existing `/healthcheck` binary. +- Added centralized HTTP security header middleware for all QuickNotes routes. +- Added regression tests that fail if the security header middleware is removed. +- Added committed Trivy, CycloneDX SBOM, and ZAP report artifacts. +- Added pinned `govulncheck` CI job as a separate PR status check. diff --git a/submissions/workflow_green.png b/submissions/workflow_green.png new file mode 100644 index 000000000..d6f072d83 Binary files /dev/null and b/submissions/workflow_green.png differ diff --git a/submissions/workflow_red.png b/submissions/workflow_red.png new file mode 100644 index 000000000..68bb8f904 Binary files /dev/null and b/submissions/workflow_red.png differ