mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 11:30:32 +00:00
187 lines
8.0 KiB
Bash
Executable File
187 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ============================================================================
|
|
# NORA Pre-Release QA Panel
|
|
# Usage: ./qa-panel.sh [port] [storage_path]
|
|
# port — test server port (default: 4091)
|
|
# storage_path — temp storage dir (default: /tmp/nora-qa-$$)
|
|
#
|
|
# Requires: nora binary already built (./target/release/nora)
|
|
# Run from repo root: /srv/projects/nora/
|
|
#
|
|
# Exit codes: 0 = all passed, 1 = failures found
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
PORT="${1:-4091}"
|
|
STORAGE="${2:-/tmp/nora-qa-$$}"
|
|
BINARY="./target/release/nora"
|
|
PASS=0
|
|
FAIL=0
|
|
TOTAL_START=$(date +%s)
|
|
|
|
# Colors
|
|
GREEN='\033[0;32m'
|
|
RED='\033[0;31m'
|
|
YELLOW='\033[0;33m'
|
|
NC='\033[0m'
|
|
|
|
check() {
|
|
if [ "$1" = "$2" ]; then
|
|
echo -e " ${GREEN}[PASS]${NC} $3"
|
|
PASS=$((PASS+1))
|
|
else
|
|
echo -e " ${RED}[FAIL]${NC} $3 (got '$1', expected '$2')"
|
|
FAIL=$((FAIL+1))
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
pkill -f "nora.*${PORT}" 2>/dev/null || true
|
|
rm -rf "$STORAGE" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# ============================================================================
|
|
echo "======================================================================"
|
|
echo " NORA Pre-Release QA Panel"
|
|
echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
|
echo " Port: $PORT | Storage: $STORAGE"
|
|
echo "======================================================================"
|
|
echo
|
|
|
|
# --- Phase 1: Static checks ---
|
|
echo "=== Phase 1: Static Analysis ==="
|
|
|
|
if [ -f "$BINARY" ]; then
|
|
check "exists" "exists" "Binary exists"
|
|
SIZE=$(du -sh "$BINARY" | cut -f1)
|
|
echo " [INFO] Binary size: $SIZE"
|
|
else
|
|
check "missing" "exists" "Binary exists"
|
|
echo "FATAL: Binary not found. Run: cargo build --release"
|
|
exit 1
|
|
fi
|
|
|
|
echo
|
|
|
|
# --- Phase 2: Start server ---
|
|
echo "=== Phase 2: Server Startup ==="
|
|
rm -rf "$STORAGE"
|
|
NORA_STORAGE_PATH="$STORAGE" NORA_PORT="$PORT" NORA_HOST=127.0.0.1 \
|
|
NORA_RATE_LIMIT_ENABLED=false "$BINARY" serve > "${STORAGE}.log" 2>&1 &
|
|
SERVER_PID=$!
|
|
|
|
for i in $(seq 1 15); do
|
|
curl -sf "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1 && break || sleep 1
|
|
done
|
|
|
|
if curl -sf "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
check "up" "up" "Server started (PID $SERVER_PID)"
|
|
VERSION=$(curl -sf "http://127.0.0.1:${PORT}/health" | python3 -c "import sys,json; print(json.load(sys.stdin).get('version','?'))" 2>/dev/null || echo "?")
|
|
echo " [INFO] Version: $VERSION"
|
|
else
|
|
check "down" "up" "Server started"
|
|
echo "FATAL: Server failed to start. Log:"
|
|
tail -20 "${STORAGE}.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo
|
|
|
|
# --- Phase 3: Endpoints ---
|
|
echo "=== Phase 3: Endpoint Health ==="
|
|
for EP in /health /ready /metrics /ui/ /api/ui/dashboard /api/ui/stats /v2/ /v2/_catalog /simple/ /api-docs; do
|
|
CODE=$(curl -sf -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT}${EP}" 2>/dev/null || echo "000")
|
|
if [ "$CODE" = "200" ] || [ "$CODE" = "303" ]; then
|
|
check "$CODE" "$CODE" "GET $EP"
|
|
else
|
|
check "$CODE" "200" "GET $EP"
|
|
fi
|
|
done
|
|
|
|
echo
|
|
|
|
# --- Phase 4: Security Hardening ---
|
|
echo "=== Phase 4: Security Hardening ==="
|
|
|
|
# SEC-001: .meta tag filter
|
|
mkdir -p "$STORAGE/docker/sec001/manifests"
|
|
echo '{}' > "$STORAGE/docker/sec001/manifests/v1.json"
|
|
echo '{}' > "$STORAGE/docker/sec001/manifests/v1.meta.json"
|
|
echo '{}' > "$STORAGE/docker/sec001/manifests/v2.json"
|
|
echo '{}' > "$STORAGE/docker/sec001/manifests/v2.meta.meta.json"
|
|
TAGS=$(curl -sf "http://127.0.0.1:${PORT}/v2/sec001/tags/list" 2>/dev/null || echo "{}")
|
|
echo "$TAGS" | grep -q "meta" && check "leaked" "filtered" "SEC-001: .meta tag filter" || check "filtered" "filtered" "SEC-001: .meta tag filter"
|
|
|
|
# SEC-002: Digest verification
|
|
DATA="qa-test-$(date +%s)"
|
|
DIGEST=$(echo -n "$DATA" | sha256sum | cut -d' ' -f1)
|
|
LOC=$(curl -sf -X POST "http://127.0.0.1:${PORT}/v2/sec002/blobs/uploads/" -D- -o /dev/null 2>&1 | grep -i location | tr -d '\r' | awk '{print $2}')
|
|
check "$(curl -sf -X PUT "http://127.0.0.1:${PORT}${LOC}?digest=sha256:${DIGEST}" -d "$DATA" -o /dev/null -w '%{http_code}')" "201" "SEC-002: valid SHA256 accepted"
|
|
|
|
LOC2=$(curl -sf -X POST "http://127.0.0.1:${PORT}/v2/sec002/blobs/uploads/" -D- -o /dev/null 2>&1 | grep -i location | tr -d '\r' | awk '{print $2}')
|
|
check "$(curl -sf -X PUT "http://127.0.0.1:${PORT}${LOC2}?digest=sha256:0000000000000000000000000000000000000000000000000000000000000000" -d "tampered" -o /dev/null -w '%{http_code}')" "400" "SEC-002: wrong digest rejected"
|
|
|
|
LOC3=$(curl -sf -X POST "http://127.0.0.1:${PORT}/v2/sec002/blobs/uploads/" -D- -o /dev/null 2>&1 | grep -i location | tr -d '\r' | awk '{print $2}')
|
|
check "$(curl -sf -X PUT "http://127.0.0.1:${PORT}${LOC3}?digest=sha512:$(python3 -c "print('a'*128)")" -d "x" -o /dev/null -w '%{http_code}')" "400" "SEC-002: sha512 rejected"
|
|
|
|
# SEC-004: Session limits
|
|
LOC4=$(curl -sf -X POST "http://127.0.0.1:${PORT}/v2/repo-a/blobs/uploads/" -D- -o /dev/null 2>&1 | grep -i location | tr -d '\r' | awk '{print $2}')
|
|
UUID4=$(echo "$LOC4" | grep -oP '[^/]+$')
|
|
check "$(curl -sf -X PATCH "http://127.0.0.1:${PORT}/v2/repo-b/blobs/uploads/${UUID4}" -d "x" -o /dev/null -w '%{http_code}')" "400" "SEC-004: session fixation blocked"
|
|
check "$(curl -sf -X PATCH "http://127.0.0.1:${PORT}/v2/t/blobs/uploads/nonexistent" -d "x" -o /dev/null -w '%{http_code}')" "404" "SEC-004: ghost session rejected"
|
|
|
|
# SEC-005: Security headers
|
|
HDRS=$(curl -sf -D- "http://127.0.0.1:${PORT}/health" -o /dev/null 2>&1)
|
|
echo "$HDRS" | grep -q "x-content-type-options: nosniff" && check y y "SEC-005: X-Content-Type-Options" || check n y "SEC-005: X-Content-Type-Options"
|
|
echo "$HDRS" | grep -q "x-frame-options: DENY" && check y y "SEC-005: X-Frame-Options" || check n y "SEC-005: X-Frame-Options"
|
|
echo "$HDRS" | grep -q "referrer-policy:" && check y y "SEC-005: Referrer-Policy" || check n y "SEC-005: Referrer-Policy"
|
|
echo "$HDRS" | grep -q "content-security-policy:" && check y y "SEC-005: CSP present" || check n y "SEC-005: CSP present"
|
|
echo "$HDRS" | grep -q "'self'" && check y y "SEC-005: CSP quotes correct" || check n y "SEC-005: CSP quotes correct"
|
|
|
|
# SEC-006: Namespaced catalog
|
|
mkdir -p "$STORAGE/docker/library/alpine/manifests"
|
|
echo '{}' > "$STORAGE/docker/library/alpine/manifests/latest.json"
|
|
CATALOG=$(curl -sf "http://127.0.0.1:${PORT}/v2/_catalog" 2>/dev/null || echo "{}")
|
|
echo "$CATALOG" | grep -q "library/alpine" && check y y "SEC-006: namespaced catalog" || check n y "SEC-006: namespaced catalog"
|
|
|
|
echo
|
|
|
|
# --- Phase 5: Protocol Round-Trips ---
|
|
echo "=== Phase 5: Protocol Round-Trips ==="
|
|
|
|
# Docker chunked upload flow
|
|
LOC5=$(curl -sf -X POST "http://127.0.0.1:${PORT}/v2/roundtrip/blobs/uploads/" -D- -o /dev/null 2>&1 | grep -i location | tr -d '\r' | awk '{print $2}')
|
|
BLOB="docker-roundtrip-test"
|
|
BDIGEST=$(echo -n "$BLOB" | sha256sum | cut -d' ' -f1)
|
|
curl -sf -X PATCH "http://127.0.0.1:${PORT}${LOC5}" -d "$BLOB" -o /dev/null
|
|
check "$(curl -sf -X PUT "http://127.0.0.1:${PORT}${LOC5}?digest=sha256:${BDIGEST}" -o /dev/null -w '%{http_code}')" "201" "Docker: chunked upload (POST→PATCH→PUT)"
|
|
|
|
# Maven
|
|
check "$(curl -sf -X PUT -d "maven-artifact" "http://127.0.0.1:${PORT}/maven2/com/test/a/1/a.jar" -o /dev/null -w '%{http_code}')" "201" "Maven: upload"
|
|
check "$(curl -sf -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/maven2/com/test/a/1/a.jar")" "200" "Maven: download"
|
|
|
|
# Raw
|
|
check "$(curl -sf -X PUT -d "raw-file" "http://127.0.0.1:${PORT}/raw/test/doc.txt" -o /dev/null -w '%{http_code}')" "201" "Raw: upload"
|
|
check "$(curl -sf -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/raw/test/doc.txt")" "200" "Raw: download"
|
|
|
|
echo
|
|
|
|
# --- Summary ---
|
|
TOTAL_END=$(date +%s)
|
|
DURATION=$((TOTAL_END - TOTAL_START))
|
|
|
|
echo "======================================================================"
|
|
if [ "$FAIL" -eq 0 ]; then
|
|
echo -e " ${GREEN}RESULT: $PASS passed, $FAIL failed${NC} (${DURATION}s)"
|
|
echo " STATUS: READY FOR RELEASE"
|
|
else
|
|
echo -e " ${RED}RESULT: $PASS passed, $FAIL failed${NC} (${DURATION}s)"
|
|
echo " STATUS: FIXES REQUIRED"
|
|
fi
|
|
echo " Log: ${STORAGE}.log"
|
|
echo "======================================================================"
|
|
|
|
exit $FAIL
|