Chapter 3. Triage

Plan (short & actionable)

  • Scope parsed: artifacts/semgrep.json, trufflehog.json, checkov.json, trivy-image.json, sbom.json Code & k8s: app/main.go, app/Dockerfile, infra/k8s/deployment.yaml, service.yaml, kustomization.yaml

  • Top issues to fix now (P0/P1):

    1. SCA (Trivy):HIGH CVE in Go stdlib (Go 1.22.12) with fixes available.
    2. IaC (Checkov): missing readiness/liveness probes, service account token hardening, seccomp, caps drop, default namespace, image digest, imagePullPolicy, network policy.
    3. Secrets (TruffleHog): AWS key pattern in app/main.go, unverified canary (training string) — clean up or allowlist.
    4. SAST (Semgrep): “HTTP server without TLS” warning (local dev) + we know a reflected XSS in /echo (not flagged by the current ruleset) — we’ll remediate.
  • Retest targets: make sca iac sast secrets cd zap then capture deltas under ./artifacts.


Triage (actual findings)

ToolFinding (short)EvidencePriorityRationale
Trivy (SCA)CVE-2025-47907 in Go stdlib v1.22.12; fixed in 1.23.12 / 1.24.6artifacts/trivy-image.jsonHIGH (1)P0Known high with fix; affects runtime (Go stdlib).
Checkov (IaC)Default namespace usedCKV_K8S_21 in infra/k8s/deployment.yamlP1We apply into ship at runtime, but static file lacks metadata.namespace → fail.
Checkov (IaC)No readiness/liveness probesCKV_K8S_8, CKV_K8S_9P1Basic resilience & security posture.
Checkov (IaC)SA token hardeningCKV_K8S_38P1Add automountServiceAccountToken: false + SA binding as needed.
Checkov (IaC)Seccomp not setCKV_K8S_31P1Add seccompProfile: RuntimeDefault.
Checkov (IaC)Capabilities not explicitly dropped / NET_RAWCKV_K8S_37, CKV_K8S_28P1Explicitly drop ALL; add only what’s needed (likely none).
Checkov (IaC)Image pull policy & digestCKV_K8S_15, CKV_K8S_43P2In dev this is flexible; still recommend a digest pin pattern for prod chapter.
Checkov (IaC)No NetworkPolicyCKV2_K8S_6P1Add minimal default deny + allow app ingress/DNS egress.
TruffleHog (Secrets)AWS canary token string in code, Verified=falseartifacts/trufflehog.json → file /repo/app/main.go:28P1Training string; remove or allowlist to prevent noise.
Semgrep (SAST)HTTP server without TLS (WARNING)artifacts/semgrep.jsonP2Dev cluster uses Ingress/Tunnel; note TLS termination guidance.
Known (not flagged)Reflected XSS at /echoCode review app/main.goP1Intentionally insecure; we’ll fix now and show DAST improvement.

Validate (quick proofs)

  • CVE-2025-47907 present: Trivy HIGH lists stdlib v1.22.12 with fixed versions 1.23.12 / 1.24.6. Our go.mod is go 1.22; build base image is golang:1.22-alpine.
  • K8s misconfigs: Checkov flags map to manifest conditions (namespace missing, no probes, no seccomp, etc.).
  • Secrets: Raw & RawV2 include AWS-looking key; Verified=false and is_canary=true in ExtraData → safe training token, still noisy.
  • XSS: /echo?input=<script>alert(1)</script> will reflect unescaped input (see fmt.Fprintf(w, "You said: %s", input)).

Remediation (concrete diffs/snippets)

Keep changes minimal, safe, and easy to teach. I’ve split by domain.

1) SCA: upgrade Go toolchain & runtime image

app/Dockerfile (build base) Change base to a fixed Go stream (e.g., 1.24.6):

# build stage
-FROM golang:1.22-alpine AS build
+FROM golang:1.24.6-alpine AS build
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./main.go

(Optionally keep alpine runtime, or use gcr.io/distroless/static:nonroot in a later “golden images” chapter.)

Retest: make build sca → confirm HIGH count drops to 0.


2) App: fix reflected XSS & add basic headers

app/main.go — escape user input and set common headers:

import (
    "fmt"
    "html/template"
    "html"        // add
    "log"
    "net/http"
    "os"
)

mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
    input := r.URL.Query().Get("input")
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    // Security headers (baseline)
    w.Header().Set("X-Content-Type-Options", "nosniff")
    w.Header().Set("X-Frame-Options", "DENY")
    w.Header().Set("Referrer-Policy", "no-referrer")

    // Optional minimal CSP for demo
    w.Header().Set("Content-Security-Policy", "default-src 'self'")

    // Escape untrusted input to prevent reflected XSS
    fmt.Fprintf(w, "You said: %s", html.EscapeString(input))
})

Retest: make cd && make zap → XSS should no longer be reported (and manual <script> should render as text).


3) Secrets: remove canary key from code (or allowlist)

If it’s in the source (line ~28), delete it or replace with a clearly fake, non-pattern string. If you need it for teaching, add a comment and allowlist it in your TruffleHog config later so CI doesn’t nag students.

Retest: make secretsverified_secrets: 0, no AWS key finding.


4) K8s Hardening (pass key Checkov rules)

infra/k8s/deployment.yaml

Add namespace, SA token control, seccomp, drop caps, probes, and (optionally) imagePullPolicy:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: ship             # add to satisfy CKV_K8S_21
  labels: { app: app }
spec:
  replicas: 1
  selector: { matchLabels: { app: app } }
  template:
    metadata: { labels: { app: app } }
    spec:
      automountServiceAccountToken: false   # CKV_K8S_38
      securityContext:
        seccompProfile:
          type: RuntimeDefault             # CKV_K8S_31
      containers:
        - name: app
          image: ship-securely/app:dev
+         imagePullPolicy: Always           # CKV_K8S_15 (dev-friendly but satisfies rule)
          ports: [{ containerPort: 3000 }]
          securityContext:
            runAsUser: 10001
            runAsGroup: 10001
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
+           capabilities:
+             drop: ["ALL"]                 # CKV_K8S_37 / CKV_K8S_28
          resources:
            requests: { cpu: "50m", memory: "64Mi" }
            limits:   { cpu: "250m", memory: "256Mi" }
+         readinessProbe:
+           httpGet: { path: /healthz, port: 3000 }
+           initialDelaySeconds: 2
+           periodSeconds: 5
+         livenessProbe:
+           httpGet: { path: /healthz, port: 3000 }
+           initialDelaySeconds: 5
+           periodSeconds: 10

NetworkPolicy (satisfy CKV2_K8S_6): add default deny + allow app ingress & DNS egress:

# infra/k8s/netpol/00-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: ship
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
# infra/k8s/netpol/20-allow-app-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-ingress
  namespace: ship
spec:
  podSelector:
    matchLabels: { app: app }
  ingress:
    - from:
        - podSelector: {}   # relax as needed for demo
      ports:
        - protocol: TCP
          port: 3000
# infra/k8s/netpol/10-allow-dns-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: ship
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53

(You already have netpol-apply in your full Makefile; for the starter, include these three files and a simple make target later in the book.)

Retest: make iac → Checkov failed set should drop (probes, seccomp, SA token, caps, default ns, netpol).

Note on image digest (CKV_K8S_43): For the local-only starter, we can document digest pinning and enforce it in the “golden images” chapter (students pin a digest when they have a registry). We’ll mark this P2 for now.


Retest (commands to run)

# Rebuild and rescan image after Go upgrade
make build sca

# Re-scan code and secrets after app/main.go changes
make sast secrets

# Re-apply K8s and re-scan IaC
make cd
make iac

# Optional: run ZAP baseline to confirm no reflected XSS
make zap

Capture summaries (before/after) into ARTIFACTS.md, and drop a dated bundle via your evidence target (if present).


Close & Document

  • Closed (fixed):

    • CVE-2025-47907 (Go stdlib) — fixed by upgrading to golang:1.24.6-alpine; Trivy HIGH=0.
    • Reflected XSS in /echofixed by html.EscapeString and security headers; ZAP/Manual confirm.
    • Checkov: namespace, probes, seccomp, caps drop, SA token, netpol — fixed with manifest edits; make iac clean for these checks.
    • TruffleHog AWS token — removed or allowlisted with clear comment (no verified secrets).
  • Accepted/Deferred (documented with reason & revisit date):

    • Image digest pinning (CKV_K8S_43) in local starter — defer to “Golden Images” chapter; add note in text and set a reminder to enforce later.
  • Prevention:

    • Keep Go base image current; consider pinning digests in golden-image workflow.
    • Keep Semgrep rules curated; add .semgrepignore if needed for tests/gen code.
    • Maintain .trivyignore for vetted, low-risk CVEs if/when needed.
    • Preserve K8s hardening defaults in base manifests; teach overrides deliberately.

Quick “diff of impact” (what should change in artifacts)

  • artifacts/trivy-image.json: HIGH count from 1 → 0.
  • artifacts/semgrep.json: still a TLS warning (expected in local dev), but no XSS path (it wasn’t flagged before; now provably mitigated).
  • artifacts/trufflehog.json: no AWS detector hit / verified_secrets=0.
  • artifacts/checkov.json: failures for CKV_K8S_8/9/31/37/28/38/21/CKV2_K8S_6 drop.
  • artifacts/zap/: XSS alert (if any) gone; headers findings likely improved.