Integrates the 5-layer Gitea webhook security system from sol/clawgravity-hook-security (v2.0) into the setup wizard. ## What's added ### New files (from clawgravity-hook-security v2.0) - scripts/webhook-security/gitea-hmac-verify.js -- njs HMAC-SHA256 module - scripts/webhook-security/gitea-approve-repo -- allowlist helper - scripts/webhook-security/rotate-webhook-secret.sh -- monthly secret rotation (templated) - scripts/webhook-security/webhook-audit-alert.sh -- daily audit summaries (templated) - scripts/webhook-security/ntfy-blocked-pickup.sh -- blocked webhook alerts (templated) - templates/webhook-security/nginx-site.conf.example - templates/webhook-security/nginx.conf.example - templates/webhook-security/gitea-repo-allowlist.json.example - docs/WEBHOOK-SECURITY.md -- full documentation - docs/SECURITY-AUDIT.md -- 35-case test matrix - tests/test-webhook-security.sh -- 48 offline tests ### Modified files - setup.sh: Step 11 (webhook security wizard with 6 sub-sections) - scripts/uninstall.sh: webhook security cleanup section - README.md: Webhook Security section after Quick Start - Makefile: test target now runs test-webhook-security.sh - .secret-scan-allowlist: allowlist docs/SECURITY-AUDIT.md (test fixture) ## Security layers 1. IP allowlisting (nginx) 2. Rate limiting 10 req/s burst 20 (nginx) 3. Payload size 1MB (nginx) 4. HMAC-SHA256 signature verification (njs) 5. Per-repository allowlist (njs) ## make check - prettier: PASS - secret-scan: PASS - tests: 48/48 PASS Closes #2
254 lines
7.7 KiB
Bash
Executable File
254 lines
7.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# test-webhook-security.sh - Tests for webhook security integration
|
|
#
|
|
# Validates that all webhook security files are present and correctly structured
|
|
# in the repository. Tests run offline (no system services required).
|
|
|
|
set -uo pipefail
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
NC='\033[0m'
|
|
|
|
pass() { echo -e "${GREEN}[PASS]${NC} $*"; }
|
|
fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); }
|
|
|
|
FAILURES=0
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
echo ""
|
|
echo "Testing Webhook Security Integration"
|
|
echo "======================================"
|
|
echo ""
|
|
|
|
# --- 1. File existence checks ---
|
|
echo "1. File existence"
|
|
|
|
EXPECTED_FILES=(
|
|
"scripts/webhook-security/gitea-hmac-verify.js"
|
|
"scripts/webhook-security/gitea-approve-repo"
|
|
"scripts/webhook-security/rotate-webhook-secret.sh"
|
|
"scripts/webhook-security/webhook-audit-alert.sh"
|
|
"scripts/webhook-security/ntfy-blocked-pickup.sh"
|
|
"templates/webhook-security/nginx-site.conf.example"
|
|
"templates/webhook-security/nginx.conf.example"
|
|
"templates/webhook-security/gitea-repo-allowlist.json.example"
|
|
"docs/WEBHOOK-SECURITY.md"
|
|
"docs/SECURITY-AUDIT.md"
|
|
)
|
|
|
|
for f in "${EXPECTED_FILES[@]}"; do
|
|
if [ -f "$REPO_ROOT/$f" ]; then
|
|
pass "$f exists"
|
|
else
|
|
fail "$f MISSING"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
|
|
# --- 2. Template placeholder checks ---
|
|
echo "2. Template placeholders in scripts"
|
|
|
|
TEMPLATED_SCRIPTS=(
|
|
"scripts/webhook-security/rotate-webhook-secret.sh"
|
|
"scripts/webhook-security/webhook-audit-alert.sh"
|
|
"scripts/webhook-security/ntfy-blocked-pickup.sh"
|
|
)
|
|
|
|
for script in "${TEMPLATED_SCRIPTS[@]}"; do
|
|
if grep -qE '@@[A-Z_]+@@' "$REPO_ROOT/$script"; then
|
|
pass "$script has placeholder tokens (@@...@@)"
|
|
else
|
|
fail "$script has no placeholder tokens — check that hardcoded values were templated"
|
|
fi
|
|
done
|
|
|
|
# Verify no hardcoded URLs in templated scripts
|
|
for script in "${TEMPLATED_SCRIPTS[@]}"; do
|
|
if grep -qE 'https://[a-z][a-z0-9.-]+\.(com|org|net|sh)' "$REPO_ROOT/$script" 2>/dev/null; then
|
|
# Check if it's a placeholder URL or a real one
|
|
REAL_URLS=$(grep -E 'https://[a-z][a-z0-9.-]+\.(com|org|net|sh)' "$REPO_ROOT/$script" | grep -v '@@\|example\|YOUR_\|placeholder\|ntfy.sh/@@' || true)
|
|
if [ -n "$REAL_URLS" ]; then
|
|
fail "$script may contain hardcoded URLs: $REAL_URLS"
|
|
else
|
|
pass "$script URL patterns are placeholders"
|
|
fi
|
|
else
|
|
pass "$script has no hardcoded domain URLs"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
|
|
# --- 3. Allowlist JSON validity ---
|
|
echo "3. Template JSON validity"
|
|
|
|
ALLOWLIST_EXAMPLE="$REPO_ROOT/templates/webhook-security/gitea-repo-allowlist.json.example"
|
|
if [ -f "$ALLOWLIST_EXAMPLE" ]; then
|
|
if python3 -c "
|
|
import json, sys
|
|
with open('$ALLOWLIST_EXAMPLE') as f:
|
|
data = json.load(f)
|
|
has_repos = 'repos' in data
|
|
has_owners = 'trusted_owners' in data
|
|
if not has_repos or not has_owners:
|
|
sys.exit(1)
|
|
" 2>/dev/null; then
|
|
pass "gitea-repo-allowlist.json.example is valid JSON with repos + trusted_owners"
|
|
else
|
|
fail "gitea-repo-allowlist.json.example is invalid JSON or missing required keys"
|
|
fi
|
|
else
|
|
fail "gitea-repo-allowlist.json.example not found"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# --- 4. Secret generation format ---
|
|
echo "4. Secret generation (openssl)"
|
|
|
|
if command -v openssl &>/dev/null; then
|
|
GENERATED_SECRET=$(openssl rand -hex 32)
|
|
SECRET_LEN=${#GENERATED_SECRET}
|
|
if [ "$SECRET_LEN" -eq 64 ]; then
|
|
pass "openssl rand -hex 32 produces 64-char hex string (got: ${GENERATED_SECRET:0:8}...)"
|
|
else
|
|
fail "openssl rand -hex 32 produced ${SECRET_LEN}-char string (expected 64)"
|
|
fi
|
|
|
|
# Verify it's valid hex
|
|
if echo "$GENERATED_SECRET" | grep -qE '^[0-9a-f]{64}$'; then
|
|
pass "Generated secret is valid lowercase hex"
|
|
else
|
|
fail "Generated secret contains non-hex characters"
|
|
fi
|
|
else
|
|
pass "openssl not available — skipping secret generation test (ok in CI)"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# --- 5. njs module structure check ---
|
|
echo "5. njs module (gitea-hmac-verify.js)"
|
|
|
|
NJS_FILE="$REPO_ROOT/scripts/webhook-security/gitea-hmac-verify.js"
|
|
if [ -f "$NJS_FILE" ]; then
|
|
# Check it's an ES module with the expected export
|
|
if grep -q "export default" "$NJS_FILE"; then
|
|
pass "gitea-hmac-verify.js has ES module export"
|
|
else
|
|
fail "gitea-hmac-verify.js missing 'export default'"
|
|
fi
|
|
|
|
# Check key functions are present
|
|
for fn in verifyAndProxy isRepoAllowed constantTimeEqual getSecret loadAllowlist; do
|
|
if grep -q "$fn" "$NJS_FILE"; then
|
|
pass "gitea-hmac-verify.js has function: $fn"
|
|
else
|
|
fail "gitea-hmac-verify.js missing function: $fn"
|
|
fi
|
|
done
|
|
|
|
# Check HMAC-SHA256 usage
|
|
if grep -q "createHmac.*sha256" "$NJS_FILE"; then
|
|
pass "gitea-hmac-verify.js uses HMAC-SHA256"
|
|
else
|
|
fail "gitea-hmac-verify.js missing HMAC-SHA256 usage"
|
|
fi
|
|
else
|
|
fail "gitea-hmac-verify.js not found"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# --- 6. setup.sh integration check ---
|
|
echo "6. setup.sh integration"
|
|
|
|
SETUP_FILE="$REPO_ROOT/setup.sh"
|
|
if [ -f "$SETUP_FILE" ]; then
|
|
# Check Step 11 is present
|
|
if grep -q "Step 11.*Gitea Webhook Security" "$SETUP_FILE"; then
|
|
pass "setup.sh contains Step 11 (Gitea Webhook Security)"
|
|
else
|
|
fail "setup.sh missing Step 11"
|
|
fi
|
|
|
|
# Check key deployment steps
|
|
for marker in "gitea-hmac-verify.js" "gitea-webhook-secret" "gitea-repo-allowlist.json" "gitea-approve-repo" "WEBHOOK_SECURITY_INSTALLED"; do
|
|
if grep -q "$marker" "$SETUP_FILE"; then
|
|
pass "setup.sh references: $marker"
|
|
else
|
|
fail "setup.sh missing reference to: $marker"
|
|
fi
|
|
done
|
|
|
|
# Check that step 11 comes before SUMMARY
|
|
STEP11_LINE=$(grep -n "Step 11.*Gitea Webhook Security" "$SETUP_FILE" | head -1 | cut -d: -f1)
|
|
SUMMARY_LINE=$(grep -n "^# SUMMARY" "$SETUP_FILE" | head -1 | cut -d: -f1)
|
|
if [ -n "$STEP11_LINE" ] && [ -n "$SUMMARY_LINE" ] && [ "$STEP11_LINE" -lt "$SUMMARY_LINE" ]; then
|
|
pass "Step 11 appears before SUMMARY (lines $STEP11_LINE vs $SUMMARY_LINE)"
|
|
else
|
|
fail "Step 11 ordering issue (step11=$STEP11_LINE, summary=$SUMMARY_LINE)"
|
|
fi
|
|
else
|
|
fail "setup.sh not found"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# --- 7. uninstall.sh integration check ---
|
|
echo "7. uninstall.sh integration"
|
|
|
|
UNINSTALL_FILE="$REPO_ROOT/scripts/uninstall.sh"
|
|
if [ -f "$UNINSTALL_FILE" ]; then
|
|
for marker in "gitea-hmac-verify.js" "gitea-webhook-secret" "gitea-repo-allowlist.json" "opt/webhook-security" "gitea-approve-repo"; do
|
|
if grep -q "$marker" "$UNINSTALL_FILE"; then
|
|
pass "uninstall.sh references: $marker"
|
|
else
|
|
fail "uninstall.sh missing reference to: $marker"
|
|
fi
|
|
done
|
|
|
|
# Check cron cleanup
|
|
if grep -q "crontab\|cron" "$UNINSTALL_FILE"; then
|
|
pass "uninstall.sh has cron cleanup"
|
|
else
|
|
fail "uninstall.sh missing cron cleanup"
|
|
fi
|
|
else
|
|
fail "uninstall.sh not found"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# --- 8. Bash syntax checks ---
|
|
echo "8. Bash syntax"
|
|
|
|
for script in \
|
|
"$REPO_ROOT/setup.sh" \
|
|
"$REPO_ROOT/scripts/uninstall.sh" \
|
|
"$REPO_ROOT/scripts/webhook-security/rotate-webhook-secret.sh" \
|
|
"$REPO_ROOT/scripts/webhook-security/webhook-audit-alert.sh" \
|
|
"$REPO_ROOT/scripts/webhook-security/ntfy-blocked-pickup.sh" \
|
|
"$REPO_ROOT/scripts/webhook-security/gitea-approve-repo"; do
|
|
FNAME=$(basename "$script")
|
|
if bash -n "$script" 2>/dev/null; then
|
|
pass "bash syntax OK: $FNAME"
|
|
else
|
|
fail "bash syntax ERROR: $FNAME"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
|
|
# --- Summary ---
|
|
echo "======================================"
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
echo -e "${GREEN}All tests passed${NC}"
|
|
exit 0
|
|
else
|
|
echo -e "${RED}$FAILURES test(s) failed${NC}"
|
|
exit 1
|
|
fi
|