ci(quick-261009-p0m): ZAP-Grundpruefung gegen alpha vor Freigaben, erster Lauf
- zap-baseline.sh: passive Aussenpruefung mit festgelegtem ZAP-Abbild, Ziel per ZAP_TARGET (Standard: oeffentliche Adresse von alpha), Anmeldedatei-Weg nur fuer den Ausnahmefall - zap-hooks.py: Spinne fuellt keine Formulare aus und sendet keine (POST=0 lokal bewiesen) - Sicherheitsprotokoll: Abschnitt Pruefung von aussen, erster Lauf direkt gegen die Anwendung (0 hoch, 2 mittel, 6 niedrig, 3 Info), Einordnung und Verlauf - Betriebsanleitung Kapitel 9: Schritt Pruefung von aussen; Entwicklungsanleitung: Abschnitt ZAP; CHANGELOG Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/bin/sh
|
||||
# zap-baseline.sh -- Grundpruefung von aussen vor einer Freigabe (quick-261009-p0m).
|
||||
#
|
||||
# Prueft den Testserver (alpha) wie ein Besucher ohne Konto, mit dem OWASP-ZAP-Abbild:
|
||||
# Startseite, Anmeldeseite, Kopfzeilen, Cookies und ausgelieferte Dateien werden
|
||||
# abgerufen und passiv ausgewertet. Es ist KEIN Angriffswerkzeug im Einsatz:
|
||||
# - nur die klassische Spinne (Links folgen), kein AJAX-Spider (der wuerde das
|
||||
# Anmeldeformular ausfuellen und absenden),
|
||||
# - Formulare werden weder ausgefuellt noch abgesendet (Haken zap-hooks.py),
|
||||
# - nur passive Regeln, kein aktiver Scan, also keine Anfragen mit Schadmustern.
|
||||
# Laeuft von Hand auf dem Entwicklungsrechner, nicht in der Pipeline.
|
||||
#
|
||||
# Aufrufe (aus dem Repository-Wurzelverzeichnis):
|
||||
# sh .gitea/scripts/zap-baseline.sh # Lauf gegen das Standardziel
|
||||
# ZAP_TARGET=http://HOST:3000 sh .gitea/scripts/zap-baseline.sh
|
||||
# sh .gitea/scripts/zap-baseline.sh --print-plan # nur anzeigen, ohne Netz
|
||||
#
|
||||
# Umgebungsvariablen:
|
||||
# ZAP_TARGET Ziel-Adresse (Standard: https://alpha.tessera.ctl.de). Wer den
|
||||
# vorgeschalteten Proxy umgehen will, gibt die direkte Adresse
|
||||
# der Anwendung an (http://HOST:3000).
|
||||
# ZAP_SPIDER_MINUTES Hoechstdauer der Spinne in Minuten (Standard: 3)
|
||||
# ZAP_REPORT_DIR Berichtsordner (Standard: security-reports/zap-{UTC-Zeit});
|
||||
# security-reports/ steht in .gitignore und .dockerignore
|
||||
# ZAP_DOCKER_NETWORK optional: Docker-Netz fuer den Container (--network)
|
||||
# ZAP_BASIC_AUTH_FILE optional: Datei AUSSERHALB des Repositorys mit einer Zeile
|
||||
# benutzer:passwort. Nur noetig, falls das Ziel einmal vom
|
||||
# Entwicklungsrechner aus eine Anmeldung (401) verlangt. Der
|
||||
# Kopf geht ueber eine temporaere Umgebungsdatei (Modus 0600,
|
||||
# wird beim Beenden entfernt) in den Container, nie auf eine
|
||||
# Befehlszeile und in keine Ausgabe.
|
||||
#
|
||||
# Hinweis: Wird die Anmeldedatei benutzt, enthaelt der Bericht die gesendeten Kopfzeilen
|
||||
# und damit die Zugangsdaten. Dann den Berichtsordner nicht weitergeben.
|
||||
#
|
||||
# Ergebnis: zap.html, zap.md, zap.json im Berichtsordner, dazu die Zeile
|
||||
# ZAP-SUMMARY ziel=HOST hoch=N mittel=N niedrig=N info=N
|
||||
# und je Meldungsart eine Zeile. Exit 0, wenn zap.json existiert, sonst 3 (damit der
|
||||
# Freigabeschritt einen fehlenden Bericht bemerkt). Das Skript gibt kein Secret aus.
|
||||
set -u
|
||||
|
||||
IMAGE='ghcr.io/zaproxy/zaproxy@sha256:7aaa659b0d43078febd82e29bad112285c370727e86ab8340444220e17d9f0d2'
|
||||
TARGET="${ZAP_TARGET:-https://alpha.tessera.ctl.de}"
|
||||
TARGET="${TARGET%/}"
|
||||
MINUTES="${ZAP_SPIDER_MINUTES:-3}"
|
||||
STAMP=$(date -u +%Y%m%d-%H%M%S)
|
||||
REPORT_DIR="${ZAP_REPORT_DIR:-security-reports/zap-$STAMP}"
|
||||
HOOK_IN_CONTAINER=/zap/hooks/zap-hooks.py
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
HOOK_FILE="$HERE/zap-hooks.py"
|
||||
|
||||
# Host-Teil des Ziels fuer die Ausgabe (ohne Schema und Pfad)
|
||||
HOST=${TARGET#*://}
|
||||
HOST=${HOST%%/*}
|
||||
|
||||
ARGS="-t $TARGET -m $MINUTES -I -T 15 --autooff --hook=$HOOK_IN_CONTAINER -r zap.html -w zap.md -J zap.json"
|
||||
|
||||
if [ "${1:-}" = "--print-plan" ]; then
|
||||
echo "ziel $TARGET"
|
||||
echo "abbild $IMAGE"
|
||||
echo "berichte $REPORT_DIR"
|
||||
echo "zap-baseline.py $ARGS"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMP_FILES=""
|
||||
cleanup() {
|
||||
for f in $TMP_FILES; do rm -f "$f"; done
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM PIPE
|
||||
|
||||
echo "ZAP-Grundpruefung gegen $TARGET (nur passiv, keine Formulare, keine Angriffe)"
|
||||
|
||||
# --- Vorabpruefung: Antwortet das Ziel, und verlangt es eine Anmeldung? ---------
|
||||
CODE=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 "$TARGET/login" 2>/dev/null) || CODE=000
|
||||
ENV_FILE=""
|
||||
case "$CODE" in
|
||||
200)
|
||||
;;
|
||||
401)
|
||||
if [ -z "${ZAP_BASIC_AUTH_FILE:-}" ] || [ ! -r "${ZAP_BASIC_AUTH_FILE:-}" ]; then
|
||||
echo "Das Ziel verlangt eine Anmeldung (Status 401)." >&2
|
||||
echo "Legen Sie dafuer eine Datei ausserhalb des Repositorys an, mit einer Zeile" >&2
|
||||
echo "benutzer:passwort, und rufen Sie das Skript mit ZAP_BASIC_AUTH_FILE=Pfad auf." >&2
|
||||
exit 4
|
||||
fi
|
||||
CRED=$(head -n 1 "$ZAP_BASIC_AUTH_FILE" | tr -d '\r\n')
|
||||
CURL_CFG=$(mktemp) || exit 4
|
||||
ENV_FILE=$(mktemp) || exit 4
|
||||
TMP_FILES="$CURL_CFG $ENV_FILE"
|
||||
chmod 600 "$CURL_CFG" "$ENV_FILE"
|
||||
printf 'user = "%s"\n' "$CRED" > "$CURL_CFG"
|
||||
CODE2=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 -K "$CURL_CFG" "$TARGET/login" 2>/dev/null) || CODE2=000
|
||||
if [ "$CODE2" != "200" ]; then
|
||||
echo "Mit den hinterlegten Zugangsdaten antwortet das Ziel mit Status $CODE2, Abbruch." >&2
|
||||
exit 4
|
||||
fi
|
||||
printf 'ZAP_BASIC_AUTH=Basic %s\n' "$(printf '%s' "$CRED" | base64 | tr -d '\n')" > "$ENV_FILE"
|
||||
CRED=""
|
||||
;;
|
||||
*)
|
||||
echo "Das Ziel antwortet auf /login nicht mit Status 200 (Status $CODE), Abbruch." >&2
|
||||
echo "Pruefen Sie die Erreichbarkeit vom Entwicklungsrechner aus." >&2
|
||||
exit 4
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Berichtsordner: der Container laeuft als Benutzer zap (Kennung 1000) ---------
|
||||
mkdir -p "$REPORT_DIR" || exit 3
|
||||
REPORT_DIR=$(cd "$REPORT_DIR" && pwd)
|
||||
chmod 777 "$REPORT_DIR"
|
||||
|
||||
set -- docker run --rm
|
||||
[ -n "${ZAP_DOCKER_NETWORK:-}" ] && set -- "$@" --network "$ZAP_DOCKER_NETWORK"
|
||||
[ -n "$ENV_FILE" ] && set -- "$@" --env-file "$ENV_FILE"
|
||||
set -- "$@" -v "$REPORT_DIR:/zap/wrk:rw" -v "$HOOK_FILE:$HOOK_IN_CONTAINER:ro" "$IMAGE" \
|
||||
zap-baseline.py -t "$TARGET" -m "$MINUTES" -I -T 15 --autooff "--hook=$HOOK_IN_CONTAINER" \
|
||||
-r zap.html -w zap.md -J zap.json
|
||||
|
||||
# Exit-Codes von ZAP (0 ok, 1 Fehler, 2 Warnung, 3 Fehlstart) sind hier nicht
|
||||
# entscheidend: es zaehlt, ob der Bericht entstanden ist.
|
||||
"$@" > "$REPORT_DIR/zap-lauf.log" 2>&1
|
||||
# Die Umgebungsdatei mit dem Kopf wird nur fuer den Lauf gebraucht: sofort entfernen.
|
||||
cleanup
|
||||
TMP_FILES=""
|
||||
echo "ZAP-Lauf beendet (Protokoll: $REPORT_DIR/zap-lauf.log)"
|
||||
|
||||
if [ ! -s "$REPORT_DIR/zap.json" ]; then
|
||||
echo "Es ist kein Bericht entstanden (zap.json fehlt)." >&2
|
||||
tail -n 15 "$REPORT_DIR/zap-lauf.log" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
python3 -I - "$REPORT_DIR/zap.json" "$HOST" <<'PY'
|
||||
import json, sys
|
||||
|
||||
path, host = sys.argv[1], sys.argv[2]
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
|
||||
seen = {}
|
||||
for site in data.get("site", []):
|
||||
for alert in site.get("alerts", []):
|
||||
key = (alert.get("pluginid"), alert.get("name"))
|
||||
seen[key] = int(alert.get("riskcode", 0))
|
||||
|
||||
count = {3: 0, 2: 0, 1: 0, 0: 0}
|
||||
for risk in seen.values():
|
||||
count[risk] = count.get(risk, 0) + 1
|
||||
|
||||
print("ZAP-SUMMARY ziel=%s hoch=%d mittel=%d niedrig=%d info=%d"
|
||||
% (host, count[3], count[2], count[1], count[0]))
|
||||
word = {3: "hoch", 2: "mittel", 1: "niedrig", 0: "info"}
|
||||
for (_pid, name), risk in sorted(seen.items(), key=lambda kv: (-kv[1], str(kv[0][1]))):
|
||||
print("ZAP-MELDUNG %s %s" % (word.get(risk, str(risk)), name))
|
||||
PY
|
||||
echo "Berichte: $REPORT_DIR (zap.html, zap.md, zap.json)"
|
||||
exit 0
|
||||
@@ -0,0 +1,32 @@
|
||||
"""zap-hooks.py -- Haken fuer die ZAP-Grundpruefung (quick-261009-p0m).
|
||||
|
||||
Wird von .gitea/scripts/zap-baseline.sh schreibgeschuetzt in den ZAP-Container
|
||||
eingebunden und mit `--hook=` geladen. Nur Standardbibliothek.
|
||||
|
||||
Aufgaben:
|
||||
1. Die klassische Spinne soll Formulare weder ausfuellen noch absenden. So bleibt
|
||||
die Pruefung rein passiv: es werden nur Seiten abgerufen (GET), nie Daten
|
||||
an die Anwendung geschickt (kein POST).
|
||||
2. Nur wenn die Umgebungsvariable ZAP_BASIC_AUTH gesetzt ist (Anmeldedatei-Weg des
|
||||
Skripts), wird ein Ersetzungsregel fuer den Kopf Authorization eingetragen.
|
||||
Der Wert steht nie in einem Aufruf oder einer Ausgabe.
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
def zap_started(zap, target):
|
||||
# Keine Formulare verarbeiten, keine Formulare absenden.
|
||||
zap.spider.set_option_process_form(False)
|
||||
zap.spider.set_option_post_form(False)
|
||||
|
||||
auth = os.environ.get("ZAP_BASIC_AUTH")
|
||||
if auth:
|
||||
zap.replacer.add_rule(
|
||||
description="basicauth",
|
||||
enabled=True,
|
||||
matchtype="REQ_HEADER",
|
||||
matchstring="Authorization",
|
||||
matchregex=False,
|
||||
replacement=auth,
|
||||
initiators="",
|
||||
)
|
||||
Reference in New Issue
Block a user