Files
vdleakfinder/scripts/e2e.sh
T
Pit Friedrich d9a9647742 Initial commit
2026-10-11 17:45:06 +02:00

103 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# End-to-end check: runs the demo app (production fat jar) with the agent, drives two user sessions
# through the leaky views with plain HTTP requests and asserts the agent's JSON report.
#
# scripts/e2e.sh build everything, then test
# SKIP_BUILD=1 scripts/e2e.sh reuse existing jars
# MVN=/path/to/mvn PORT=18765 override maven / port
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
PORT=${PORT:-18765}
BASE="http://localhost:$PORT"
MVN=${MVN:-mvn}
WORK=$(mktemp -d)
REPORT="$WORK/leaks.json"
LOG="$WORK/app.log"
AGENT="$ROOT/leakfinder-agent/target/leakfinder-agent-0.1.0-SNAPSHOT.jar"
APP="$ROOT/demo-app/target/demo-app-0.1.0-SNAPSHOT.jar"
if [[ "${SKIP_BUILD:-0}" != 1 ]]; then
(cd "$ROOT" && "$MVN" -q -pl leakfinder-agent install -DskipTests && "$MVN" -q -pl demo-app -Pproduction package -DskipTests)
fi
APP_PID=
cleanup() {
[[ -n "$APP_PID" ]] && kill "$APP_PID" 2>/dev/null || true
echo "app log: $LOG"
}
trap cleanup EXIT
PORT=$PORT java "-javaagent:$AGENT=scanInterval=2s,gcWait=1s,report=$REPORT" -jar "$APP" >"$LOG" 2>&1 &
APP_PID=$!
for _ in $(seq 1 120); do
grep -q 'Started Application' "$LOG" && break
if ! kill -0 "$APP_PID" 2>/dev/null; then echo "app died"; tail -50 "$LOG"; exit 1; fi
sleep 1
done
grep -q 'Started Application' "$LOG" || { echo "app did not start"; exit 1; }
# Opens a route in a new UI of the session identified by cookie jar $1, like a browser tab would:
# bootstrap request (v-r=init) followed by the client side "ui-navigate" event.
open_view() {
local jar="$WORK/$1" route="$2" init key ui
init=$(curl -sf --max-time 20 -c "$jar" -b "$jar" "$BASE/?v-r=init&location=$route")
key=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["appConfig"]["uidl"]["Vaadin-Security-Key"])' <<<"$init")
ui=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["appConfig"]["v-uiId"])' <<<"$init")
curl -sf --max-time 20 -o /dev/null -c "$jar" -b "$jar" -H 'Content-Type: application/json' \
-X POST "$BASE/?v-r=uidl&v-uiId=$ui" \
-d "{\"csrfToken\":\"$key\",\"rpc\":[{\"type\":\"event\",\"node\":1,\"event\":\"ui-navigate\",\"data\":{\"route\":\"$route\",\"query\":\"\",\"appShellTitle\":\"\",\"historyState\":null,\"trigger\":\"\"}}],\"syncId\":0,\"clientId\":0}"
echo " $1 -> /$route"
}
echo "driving sessions"
open_view alice static-leak
open_view alice singleton-leak
open_view alice broadcaster-leak
open_view alice shared-component
open_view bob shared-component # steals alice's banner
open_view bob singleton-leak
open_view bob broadcaster-leak
open_view bob snoop # keeps a reference to alice's view
open_view bob "" # clean view
curl -sf --max-time 20 "$BASE/api/broadcast" >/dev/null
curl -sf --max-time 20 -c "$WORK/alice" -b "$WORK/alice" "$BASE/api/logout" >/dev/null
echo " alice logged out"
sleep 8
python3 - "$REPORT" <<'EOF'
import json, sys
report = json.load(open(sys.argv[1]))
findings = report["findings"]
print(report["lastScanSummary"])
for f in findings:
print(f" {f['severity']:5} {f['kind']:34} sessions={f['sessions']} {f['title']}")
def text(f):
return f["title"] + "\n" + "\n".join(f["details"])
def expect(kind, *needles):
if not any(f["kind"] == kind and all(n in text(f) for n in needles) for f in findings):
failures.append(f"missing {kind} mentioning {needles}")
failures = []
expect("SHARED_ROOT_HOLDS_SESSION_STATE", "static com.example.leakdemo.StaticLeakView.RECENT_VIEWS")
expect("SHARED_ROOT_HOLDS_SESSION_STATE", "static com.example.leakdemo.Broadcaster.LISTENERS")
expect("SHARED_ROOT_HOLDS_SESSION_STATE", "spring singleton 'gridCache'", "Grid")
expect("SHARED_ROOT_HOLDS_SESSION_STATE", "static com.example.leakdemo.SharedComponentView.BANNER")
expect("CROSS_SESSION_ATTACH", "com.vaadin.flow.component.html.Span", "SharedComponentView")
expect("CROSS_SESSION_MUTATION", "SharedComponentView")
expect("SESSION_REFERENCES_FOREIGN_STATE", "SnoopView", ".peeked", "StaticLeakView")
expect("DESTROYED_SESSION_RETAINED", "StaticLeakView.RECENT_VIEWS")
if any("CleanView" in text(f) for f in findings):
failures.append("false positive: CleanView mentioned")
if failures:
print("\nE2E FAILED:\n " + "\n ".join(failures))
sys.exit(1)
print("\nE2E PASSED")
EOF