103 lines
4.2 KiB
Bash
Executable File
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
|