#!/usr/bin/env bash # Security re-scan (Приложение 4 «Отчет об устранении уязвимостей»): # 1. govulncheck, source mode — vulnerabilities reachable from the code # 2. govulncheck -mode=binary — the binary shipped in the image # 3. gosec — static analysis (HIGH severity counted) # 4. trivy fs — dependencies, secrets, IaC misconfig # 5. trivy image — the application image (built here) # 6. golangci-lint (if installed) — code quality, informational only # # test/acceptance/scan.sh # # Environment: SCAN_IMAGE (default gis-scan:local, built from # build/package/Dockerfile), SKIP_IMAGE=1 (no docker: skip 2 via image and 5; # the binary for 2 is then built locally with cgo + GDAL). # govulncheck has no severity scale: every vulnerability reachable from the # code (source) or present in the binary symbol table is counted as High. # Prints High/Critical per tool; exit code 1 if any High/Critical is found. # Raw reports: test/acceptance/results/scan-/. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$HERE/../.." && pwd)" STAMP="$(date +%Y%m%d-%H%M%S)" OUT="$HERE/results/scan-$STAMP"; mkdir -p "$OUT" SCAN_IMAGE="${SCAN_IMAGE:-gis-scan:local}" SKIP_IMAGE="${SKIP_IMAGE:-0}" GOBIN_DIR="$(go env GOPATH 2>/dev/null)/bin" PATH="$PATH:$GOBIN_DIR" cd "$ROOT" ROWS=""; FAIL=0 row() { # tool target high critical note ROWS="${ROWS}$(printf '%-26s | %-40s | %5s | %8s | %s' "$1" "$2" "$3" "$4" "$5")"$'\n' case "$3$4" in *[!0-9]*) ;; *) [ "$3" -eq 0 ] && [ "$4" -eq 0 ] || FAIL=1;; esac } have() { command -v "$1" >/dev/null 2>&1; } log() { echo "[$(date +%H:%M:%S)] $*"; } log "commit $(git rev-parse --short HEAD 2>/dev/null || echo '?'), $(go version)" # --- govulncheck (source) ------------------------------------------------- gv_count() { # json stream -> number of distinct OSV ids with a call trace (reachable) jq -rs '[.[] | select(.finding != null) | .finding | select((.trace // []) | length > 0) | select(.trace[0].function != null) | .osv] | unique | length' "$1" } gv_ids() { jq -rs '[.[] | select(.finding != null) | .finding | select(.trace[0].function != null) | .osv] | unique | join(", ")' "$1"; } if have govulncheck; then log "govulncheck ./... (source)" govulncheck -format json ./... > "$OUT/govulncheck-source.json" 2> "$OUT/govulncheck-source.err" || true n="$(gv_count "$OUT/govulncheck-source.json")" mods="$(jq -rs '[.[] | select(.finding != null) | .finding.osv] | unique | length' "$OUT/govulncheck-source.json")" row "govulncheck (source)" "./..." "$n" 0 "reachable: ${n}$( [ "$n" -gt 0 ] && echo " ($(gv_ids "$OUT/govulncheck-source.json"))"); in dependencies but not called: $((mods - n))" else row "govulncheck (source)" "./..." "n/a" "n/a" "not installed (go install golang.org/x/vuln/cmd/govulncheck@latest)"; fi # --- application image + binary ------------------------------------------- BIN="" if [ "$SKIP_IMAGE" != 1 ] && have docker; then log "docker build -t $SCAN_IMAGE -f build/package/Dockerfile ." docker build -q -t "$SCAN_IMAGE" -f build/package/Dockerfile . > "$OUT/docker-build.txt" cid="$(docker create "$SCAN_IMAGE")" docker cp "$cid:/usr/local/bin/gis" "$OUT/gis" >/dev/null; docker rm "$cid" >/dev/null BIN="$OUT/gis"; BIN_SRC="image $SCAN_IMAGE:/usr/local/bin/gis" else log "go build (cgo + GDAL) for binary-mode scan" CGO_ENABLED=1 go build -trimpath -o "$OUT/gis" ./cmd/gis; BIN="$OUT/gis"; BIN_SRC="local build" fi # --- govulncheck (binary) ------------------------------------------------- if have govulncheck; then log "govulncheck -mode=binary ($BIN_SRC)" govulncheck -mode=binary -format json "$BIN" > "$OUT/govulncheck-binary.json" 2> "$OUT/govulncheck-binary.err" || true n="$(gv_count "$OUT/govulncheck-binary.json")" gov="$(go version "$BIN" 2>/dev/null | awk '{print $2}')" row "govulncheck (binary)" "$BIN_SRC" "$n" 0 "built with $gov; vulnerable symbols present: $n$( [ "$n" -gt 0 ] && echo " ($(gv_ids "$OUT/govulncheck-binary.json"))")" fi # --- gosec ----------------------------------------------------------------- if have gosec; then log "gosec ./..." gosec -quiet -fmt json -out "$OUT/gosec.json" ./... 2> "$OUT/gosec.err" || true hi="$(jq '[.Issues[]? | select(.severity=="HIGH")] | length' "$OUT/gosec.json")" me="$(jq '[.Issues[]? | select(.severity=="MEDIUM")] | length' "$OUT/gosec.json")" lo="$(jq '[.Issues[]? | select(.severity=="LOW")] | length' "$OUT/gosec.json")" ns="$(jq '.Stats.nosec // 0' "$OUT/gosec.json")" row "gosec" "./... ($(jq '.Stats.files' "$OUT/gosec.json") files)" "$hi" 0 "MEDIUM $me, LOW $lo; #nosec-annotated $ns" else row "gosec" "./..." "n/a" "n/a" "not installed"; fi # --- trivy ------------------------------------------------------------------ trivy_counts() { # json -> "high critical" jq -r '[.Results[]? | (.Vulnerabilities[]?, .Secrets[]?, .Misconfigurations[]?) | .Severity] as $s | "\([$s[] | select(.=="HIGH")] | length) \([$s[] | select(.=="CRITICAL")] | length)"' "$1" } if have trivy; then log "trivy fs (vuln, secret, misconfig)" trivy fs --quiet --scanners vuln,secret,misconfig --format json --output "$OUT/trivy-fs.json" \ --skip-dirs test/acceptance/results --skip-dirs graft . 2> "$OUT/trivy-fs.err" || true read -r h c <<< "$(trivy_counts "$OUT/trivy-fs.json")" all="$(jq '[.Results[]? | (.Vulnerabilities[]?, .Secrets[]?, .Misconfigurations[]?)] | length' "$OUT/trivy-fs.json")" row "trivy fs" "repository (go.mod, Dockerfile, compose)" "$h" "$c" "all severities: $all" if [ -n "$BIN" ] && [ "$SKIP_IMAGE" != 1 ] && have docker; then log "trivy image $SCAN_IMAGE" trivy image --quiet --format json --output "$OUT/trivy-image.json" "$SCAN_IMAGE" 2> "$OUT/trivy-image.err" || true read -r h c <<< "$(trivy_counts "$OUT/trivy-image.json")" all="$(jq '[.Results[]? | .Vulnerabilities[]?] | length' "$OUT/trivy-image.json")" os="$(jq -r '.Metadata.OS | "\(.Family) \(.Name)"' "$OUT/trivy-image.json")" row "trivy image" "$SCAN_IMAGE ($os)" "$h" "$c" "all severities: $all" fi else row "trivy" "-" "n/a" "n/a" "not installed"; fi # --- golangci-lint (informational) ------------------------------------------ if have golangci-lint; then log "golangci-lint run ./..." note="" golangci-lint run --output.json.path "$OUT/golangci-lint.json" ./... > "$OUT/golangci-lint.txt" 2>&1 || true if grep -q "unsupported version of the configuration" "$OUT/golangci-lint.txt"; then # .golangci.yml is in the v1 format; a v2 binary refuses it — use the default linters note=" (.golangci.yml is v1 format, $(golangci-lint version 2>/dev/null | awk '{print $4}') ran with --no-config)" golangci-lint run --no-config --output.json.path "$OUT/golangci-lint.json" ./... > "$OUT/golangci-lint.txt" 2>&1 || true fi n="$(jq '[.Issues[]?] | length' "$OUT/golangci-lint.json" 2>/dev/null || echo '?')" row "golangci-lint" "./..." "-" "-" "issues: $n$note; no severity scale, informational" fi echo printf '%-26s | %-40s | %5s | %8s | %s\n' "Tool" "Target" "High" "Critical" "Notes" printf '%s\n' "---------------------------+------------------------------------------+-------+----------+------------------------------" printf '%s' "$ROWS" echo if [ "$FAIL" = 0 ]; then echo "RESULT: 0 High / 0 Critical -> PASS"; else echo "RESULT: High/Critical findings present -> FAIL"; fi echo "raw reports: $OUT" exit "$FAIL"