Complete implementation ready for Coolify: - Node.js 22 + Fastify + socket.io backend - PostgreSQL 16 + Redis 7 services - Docker Compose configuration - Deployment scripts and documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
174 lines
4.6 KiB
Bash
Executable file
174 lines
4.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Basic penetration testing for AgentHub
|
|
# Run before each release
|
|
|
|
set -e
|
|
|
|
BASE_URL="${TEST_URL:-http://localhost:3000}"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
echo "=== AgentHub Pen-Test Suite ==="
|
|
echo "Target: $BASE_URL"
|
|
echo ""
|
|
|
|
# Helper functions
|
|
pass() {
|
|
echo "✅ PASS: $1"
|
|
PASS=$((PASS + 1))
|
|
}
|
|
|
|
fail() {
|
|
echo "❌ FAIL: $1"
|
|
FAIL=$((FAIL + 1))
|
|
}
|
|
|
|
test_endpoint() {
|
|
local method=$1
|
|
local path=$2
|
|
local data=$3
|
|
local expected_status=$4
|
|
local test_name=$5
|
|
|
|
local actual_status
|
|
if [ -n "$data" ]; then
|
|
actual_status=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-X "$method" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$data" \
|
|
"$BASE_URL$path")
|
|
else
|
|
actual_status=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-X "$method" \
|
|
"$BASE_URL$path")
|
|
fi
|
|
|
|
if [ "$actual_status" -eq "$expected_status" ]; then
|
|
pass "$test_name (HTTP $actual_status)"
|
|
else
|
|
fail "$test_name (expected $expected_status, got $actual_status)"
|
|
fi
|
|
}
|
|
|
|
echo "=== 1. SQL Injection Tests ==="
|
|
|
|
# Test agent creation with SQL injection
|
|
test_endpoint POST "/api/v1/agents" \
|
|
'{"name":"test'"'"' OR '"'"'1'"'"'='"'"'1","displayName":"SQLi Test","role":"agent"}' \
|
|
400 \
|
|
"SQL injection in agent name rejected"
|
|
|
|
# Test session creation with malicious token
|
|
test_endpoint POST "/api/v1/sessions" \
|
|
'{"apiToken":"ah_live_XXXX'"'"'; DROP TABLE agents--"}' \
|
|
401 \
|
|
"SQL injection in token rejected"
|
|
|
|
echo ""
|
|
echo "=== 2. Header Injection Tests ==="
|
|
|
|
# Test XSS in headers
|
|
test_endpoint GET "/rooms" "" 401 "Missing auth header returns 401"
|
|
|
|
actual=$(curl -s -H "x-agent-id: <script>alert(1)</script>" "$BASE_URL/rooms" | grep -o "error" || echo "")
|
|
if [ -n "$actual" ]; then
|
|
pass "XSS in x-agent-id header rejected"
|
|
else
|
|
fail "XSS in x-agent-id header not properly rejected"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== 3. Rate Limit Tests ==="
|
|
|
|
# Burst 10 requests to healthz (should pass, allowlisted)
|
|
success_count=0
|
|
for i in {1..10}; do
|
|
status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/healthz")
|
|
if [ "$status" -eq 200 ]; then
|
|
success_count=$((success_count + 1))
|
|
fi
|
|
done
|
|
|
|
if [ $success_count -eq 10 ]; then
|
|
pass "Healthz endpoint not rate-limited"
|
|
else
|
|
fail "Healthz endpoint incorrectly rate-limited ($success_count/10 succeeded)"
|
|
fi
|
|
|
|
# Burst 120 unauthenticated requests to /rooms (should hit rate limit)
|
|
# Rate limit is 100 req/min, so 120 should fail on some
|
|
rate_limited=0
|
|
for i in {1..120}; do
|
|
status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/rooms")
|
|
if [ "$status" -eq 429 ]; then
|
|
rate_limited=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ $rate_limited -eq 1 ]; then
|
|
pass "Unauthenticated rate limit enforced (100 req/min)"
|
|
else
|
|
fail "Unauthenticated rate limit not enforced (expected 429 after 100 requests)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== 4. CORS Tests ==="
|
|
|
|
# Test unauthorized origin
|
|
cors_header=$(curl -s -H "Origin: http://evil.com" -I "$BASE_URL/healthz" | grep -i "access-control-allow-origin" || echo "")
|
|
if [ -z "$cors_header" ]; then
|
|
pass "Unauthorized origin rejected by CORS"
|
|
else
|
|
fail "Unauthorized origin accepted by CORS: $cors_header"
|
|
fi
|
|
|
|
# Test allowed origin (localhost)
|
|
cors_header=$(curl -s -H "Origin: http://localhost:3000" -I "$BASE_URL/healthz" | grep -i "access-control-allow-origin" || echo "")
|
|
if [ -n "$cors_header" ]; then
|
|
pass "Allowed origin accepted by CORS"
|
|
else
|
|
fail "Allowed origin rejected by CORS"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== 5. Security Headers Tests ==="
|
|
|
|
# Check for security headers
|
|
headers=$(curl -s -I "$BASE_URL/healthz")
|
|
|
|
echo "$headers" | grep -q "x-frame-options: DENY" && pass "X-Frame-Options header present" || fail "X-Frame-Options header missing"
|
|
echo "$headers" | grep -q "referrer-policy: strict-origin" && pass "Referrer-Policy header present" || fail "Referrer-Policy header missing"
|
|
echo "$headers" | grep -q "content-security-policy:" && pass "CSP header present" || fail "CSP header missing"
|
|
|
|
# HSTS should be absent in Phase 1 (HTTP LAN)
|
|
echo "$headers" | grep -q "strict-transport-security:" && fail "HSTS enabled in Phase 1 (should be disabled)" || pass "HSTS disabled in Phase 1"
|
|
|
|
echo ""
|
|
echo "=== 6. Input Validation Tests ==="
|
|
|
|
# Test invalid UUID in room creation
|
|
test_endpoint POST "/rooms" \
|
|
'{"slug":"test-room","name":"Test","members":["not-a-uuid"]}' \
|
|
400 \
|
|
"Invalid UUID in members rejected"
|
|
|
|
# Test oversized message body (16KB limit)
|
|
oversized_body=$(printf 'A%.0s' {1..20000})
|
|
test_endpoint POST "/api/v1/sessions" \
|
|
"{\"apiToken\":\"$oversized_body\"}" \
|
|
401 \
|
|
"Oversized payload handled gracefully"
|
|
|
|
echo ""
|
|
echo "=== Summary ==="
|
|
echo "Passed: $PASS"
|
|
echo "Failed: $FAIL"
|
|
|
|
if [ $FAIL -eq 0 ]; then
|
|
echo "✅ All pen-tests passed"
|
|
exit 0
|
|
else
|
|
echo "❌ Some pen-tests failed"
|
|
exit 1
|
|
fi
|