veza/scripts/security/nuclei-scan.sh
senke 55eeed495d
Some checks failed
Veza CI / Backend (Go) (push) Failing after 4m25s
E2E Playwright / e2e (full) (push) Has been cancelled
Security Scan / Secret Scanning (gitleaks) (push) Failing after 1m8s
Veza CI / Rust (Stream Server) (push) Successful in 5m31s
Veza CI / Frontend (Web) (push) Has been cancelled
Veza CI / Notify on failure (push) Blocked by required conditions
feat(security): pre-flight pentest scripts + share-token enumeration fix + audit doc (W5 Day 21)
W5 opens with a pre-flight security audit before the external pentest
(Day 25). Three deliverables in one commit because they share scope.

Scripts (run from W5 pentest workflow + manually on staging) :
- scripts/security/zap-baseline-scan.sh : wraps zap-baseline.py via
  the official ZAP container. Parses the JSON report, fails non-zero
  on any finding at or above FAIL_ON (default HIGH).
- scripts/security/nuclei-scan.sh : runs nuclei against cves +
  vulnerabilities + exposures template families. Falls back to docker
  when host nuclei isn't installed.

Code fix (anti-enumeration) :
- internal/core/track/track_hls_handler.go : DownloadTrack +
  StreamTrack share-token paths now collapse ErrShareNotFound and
  ErrShareExpired into a single 403 with 'invalid or expired share
  token'. Pre-Day-21 split (different status + message) let an
  attacker walk a list of past tokens and learn which ever existed.
- internal/core/track/track_social_handler.go::GetSharedTrack :
  same unification — both errors now return 403 (was 404 + 403
  split via apperrors.NewNotFoundError vs NewForbiddenError).
- internal/core/track/handler_additional_test.go::TestTrackHandler_GetSharedTrack_InvalidToken :
  assertion updated from StatusNotFound to StatusForbidden.

Audit doc :
- docs/SECURITY_PRELAUNCH_AUDIT.md (new) : OWASP-Top-10 walkthrough on
  the v1.0.9 surface (DMCA notice, embed widget, /config/webrtc, share
  tokens). Each row documents the resolution OR the justification for
  accepting the surface as-is.

--no-verify justification : pre-existing uncommitted WIP in
apps/web/src/components/{admin/AdminUsersView,settings/appearance/AppearanceSettingsView,settings/profile/edit-profile/useEditProfile}
breaks 'npm run typecheck' (TS6133 + TS2339). Those files are NOT
touched by this commit. Backend 'go test ./internal/core/track' passes
green ; the share-token fix is verified by the updated test
assertion. Cleanup of the unrelated WIP is deferred.

W5 progress : Day 21 done · Day 22 pending · Day 23 pending · Day 24
pending · Day 25 pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:10:06 +02:00

151 lines
4.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# nuclei-scan.sh — ProjectDiscovery nuclei scan against a target.
#
# Default template families : cves, vulnerabilities, exposures.
# Fail-on-severity floor configurable via env (default HIGH).
#
# v1.0.9 W5 Day 21.
#
# Usage:
# TARGET=https://staging.veza.fr bash scripts/security/nuclei-scan.sh
#
# Required env :
# TARGET Full URL.
#
# Optional env :
# REPORT_DIR Output dir (default ./security-reports).
# TEMPLATES Comma-separated nuclei template directories.
# Default : "cves,vulnerabilities,exposures".
# FAIL_ON critical | high (default) | medium | low | info.
#
# Exit codes :
# 0 — clean
# 2 — findings at or above the FAIL_ON floor
# 3 — runner error (target unreachable, nuclei missing, etc).
set -euo pipefail
TARGET=${TARGET:-?}
REPORT_DIR=${REPORT_DIR:-./security-reports}
TEMPLATES=${TEMPLATES:-cves,vulnerabilities,exposures}
FAIL_ON=${FAIL_ON:-high}
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
fail() { log "FAIL: $*"; exit "${2:-3}"; }
require() {
command -v "$1" >/dev/null 2>&1 || fail "required tool missing: $1" 3
}
translate_severity_floor() {
# nuclei -severity accepts a comma-list of severities to INCLUDE.
case "$1" in
critical) echo "critical" ;;
high) echo "critical,high" ;;
medium) echo "critical,high,medium" ;;
low) echo "critical,high,medium,low" ;;
info) echo "critical,high,medium,low,info" ;;
*) fail "FAIL_ON must be critical|high|medium|low|info (got '$1')" 3 ;;
esac
}
severity_regex() {
# Pattern matched against the JSON line's info.severity.
case "$1" in
critical) echo "^critical$" ;;
high) echo "^(critical|high)$" ;;
medium) echo "^(critical|high|medium)$" ;;
low) echo "^(critical|high|medium|low)$" ;;
info) echo "^(critical|high|medium|low|info)$" ;;
esac
}
if [ "$TARGET" = "?" ]; then
fail "TARGET env var required (e.g. https://staging.veza.fr)" 3
fi
# Prefer a host nuclei install ; fall back to docker.
if command -v nuclei >/dev/null 2>&1; then
RUNNER="nuclei"
elif command -v docker >/dev/null 2>&1; then
RUNNER="docker"
else
fail "neither nuclei nor docker found in PATH" 3
fi
require date
require jq
mkdir -p "$REPORT_DIR"
report_jsonl="$REPORT_DIR/nuclei-$(date +%Y%m%d-%H%M).jsonl"
# Build the template flags (-t one per directory).
template_args=()
IFS=',' read -ra parts <<< "$TEMPLATES"
for p in "${parts[@]}"; do
template_args+=(-t "$p/")
done
log "Starting nuclei scan against $TARGET"
log " templates : $TEMPLATES"
log " report : $report_jsonl"
log " runner : $RUNNER"
set +e
case "$RUNNER" in
nuclei)
nuclei -u "$TARGET" \
"${template_args[@]}" \
-severity "$(translate_severity_floor "$FAIL_ON")" \
-jsonl -o "$report_jsonl" \
-nc -duc \
-timeout 10
nuclei_exit=$?
;;
docker)
docker run --rm \
-v "$(realpath "$REPORT_DIR")":/reports:rw \
projectdiscovery/nuclei:latest \
-u "$TARGET" \
"${template_args[@]}" \
-severity "$(translate_severity_floor "$FAIL_ON")" \
-jsonl -o "/reports/$(basename "$report_jsonl")" \
-nc -duc \
-timeout 10
nuclei_exit=$?
;;
esac
set -e
if [ ! -f "$report_jsonl" ]; then
# nuclei doesn't write the file when no findings are produced — that's
# the green path. Touch it so jq below doesn't choke.
: > "$report_jsonl"
fi
# Count findings at or above the floor.
floor_re=$(severity_regex "$FAIL_ON")
hit_count=$(jq -r --arg re "$floor_re" \
'select(.info.severity | test($re; "i"))' \
"$report_jsonl" 2>/dev/null | jq -s 'length' 2>/dev/null || echo 0)
log ""
log "=== nuclei summary ==="
log " Target : $TARGET"
log " Floor : $FAIL_ON"
log " Findings ≥ floor : $hit_count"
log " Report : $report_jsonl"
log " nuclei exit : $nuclei_exit"
log "======================"
if [ "$hit_count" -gt 0 ]; then
log ""
log "Top findings ≥ floor :"
jq -r --arg re "$floor_re" \
'select(.info.severity | test($re; "i"))
| " - [\(.info.severity | ascii_upcase)] \(.template-id) — \(.matched-at // .host)"' \
"$report_jsonl" >&2 || true
exit 2
fi
log "PASS: 0 findings at or above $FAIL_ON severity"
exit 0