From bbc0ff077bc0137ab4c7fde692b600d5f94cc66f Mon Sep 17 00:00:00 2001 From: Thor Thor Date: Tue, 10 Feb 2026 13:02:31 -0600 Subject: [PATCH] test: exploratory harness, BUG_TEMPLATE, nightly soak, zero-bug protocol --- .github/workflows/nightly-soak.yml | 28 +++++ .gitignore | 1 + BUG_TEMPLATE.md | 34 ++++++ IMPROVEMENT_PROTOCOL.md | 21 ++++ scripts/exploratory-test.sh | 164 +++++++++++++++++++++++++++++ testdata/seeds/current.seed | 1 + tests/policy/commits_test.go | 2 +- tests/property/README.md | 9 ++ tests/regression/README.md | 5 + 9 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/nightly-soak.yml create mode 100644 BUG_TEMPLATE.md create mode 100644 IMPROVEMENT_PROTOCOL.md create mode 100755 scripts/exploratory-test.sh create mode 100644 testdata/seeds/current.seed create mode 100644 tests/property/README.md create mode 100644 tests/regression/README.md diff --git a/.github/workflows/nightly-soak.yml b/.github/workflows/nightly-soak.yml new file mode 100644 index 0000000..4f67c60 --- /dev/null +++ b/.github/workflows/nightly-soak.yml @@ -0,0 +1,28 @@ +name: Nightly Soak Test + +on: + schedule: + - cron: '0 2 * * *' + workflow_dispatch: + +jobs: + soak: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run exploratory harness + run: | + ITERATIONS=100 CONCURRENCY=50 MAX_PAYLOAD=65536 DEE_PORT=9188 ./scripts/exploratory-test.sh + + - name: Upload results + uses: actions/upload-artifact@v4 + if: always() + with: + name: soak-results + path: test-results/ diff --git a/.gitignore b/.gitignore index ae3c172..7150412 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /bin/ +/test-results/ diff --git a/BUG_TEMPLATE.md b/BUG_TEMPLATE.md new file mode 100644 index 0000000..18d0d31 --- /dev/null +++ b/BUG_TEMPLATE.md @@ -0,0 +1,34 @@ +# Bug Report Template + +## Severity +- [ ] CRITICAL: Security property violation, crash, or data loss +- [ ] HIGH: Functional failure under normal conditions +- [ ] MEDIUM: Edge case failure, performance degradation +- [ ] LOW: Cosmetic issue, logging noise + +## Category +- [ ] Protocol: Encryption/decryption incorrect +- [ ] Concurrency: Race condition, deadlock +- [ ] Resource: Memory leak, file descriptor leak +- [ ] Input: Malformed payload handling +- [ ] Performance: Timeout, latency spike +- [ ] Logging: Secret material in logs + +## Reproduction +Exact command to reproduce: +``` +[paste command] +``` + +Expected behavior: + +Actual behavior: + +## Root Cause Analysis +[To be filled after investigation] + +## Fix Verification +- [ ] Fix implemented +- [ ] Regression test added +- [ ] make verify passes +- [ ] Exploratory test passes diff --git a/IMPROVEMENT_PROTOCOL.md b/IMPROVEMENT_PROTOCOL.md new file mode 100644 index 0000000..657fb2b --- /dev/null +++ b/IMPROVEMENT_PROTOCOL.md @@ -0,0 +1,21 @@ +# Zero-Bug Improvement Protocol + +## Weekly Cycle +1. Run exploratory harness with 2x previous load +2. Classify any failures using BUG_TEMPLATE.md +3. Fix CRITICAL/HIGH immediately, schedule others +4. Add regression test for each fixed bug +5. Update fuzz corpus with new inputs + +## Monthly Cycle +1. Review all TODO/FIXME in codebase +2. Run mutation testing (if available for Go) +3. Update threat model with new attack vectors +4. Review and rotate any test keys/vectors + +## Release Gate +Before any release: +- Exploratory harness: ITERATIONS=500 CONCURRENCY=100 +- No failures in 3 consecutive runs +- All regression tests pass +- Security audit: no new vulnerabilities in dependencies diff --git a/scripts/exploratory-test.sh b/scripts/exploratory-test.sh new file mode 100755 index 0000000..4dc86ff --- /dev/null +++ b/scripts/exploratory-test.sh @@ -0,0 +1,164 @@ +#!/bin/bash +# Deadend-Lab Exploratory Test Harness +# Usage: ITERATIONS=100 CONCURRENCY=50 MAX_PAYLOAD=131072 DEE_PORT=9188 ./scripts/exploratory-test.sh +set -euo pipefail + +ITERATIONS="${ITERATIONS:-50}" +CONCURRENCY="${CONCURRENCY:-25}" +MAX_PAYLOAD="${MAX_PAYLOAD:-65536}" +DEE_PORT="${DEE_PORT:-9188}" +TIMEOUT_SEC="${TIMEOUT_SEC:-30}" +RESULTS_DIR="${RESULTS_DIR:-./test-results/$(date +%Y%m%d-%H%M%S)}" + +mkdir -p "$RESULTS_DIR" +LOG_FILE="$RESULTS_DIR/test-run.log" +FAILURES_FILE="$RESULTS_DIR/failures.json" +METRICS_FILE="$RESULTS_DIR/metrics.json" + +FORBIDDEN_PATTERNS='(k_enc|k_mac|session_key|private_key|secret_key|BEGIN.*PRIVATE KEY|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-|AIzaSy)' +SUSPICIOUS_HEX='[0-9a-fA-F]{128,}' + +cleanup() { + echo "Cleaning up..." + DEE_PORT="$DEE_PORT" docker compose down -t 10 2>/dev/null || true + echo "Results in: $RESULTS_DIR" +} +trap cleanup EXIT + +log() { + echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "$LOG_FILE" +} + +log "=== EXPLORATORY TEST HARNESS ===" +log "Configuration: ITER=$ITERATIONS CONC=$CONCURRENCY MAX_PAYLOAD=$MAX_PAYLOAD PORT=$DEE_PORT" + +log "[1/8] Building Docker image..." +docker build -t deadend-lab:exploratory . >> "$LOG_FILE" 2>&1 + +log "[2/8] Starting services..." +DEE_PORT="$DEE_PORT" docker compose down 2>/dev/null || true +DEE_PORT="$DEE_PORT" docker compose up -d >> "$LOG_FILE" 2>&1 + +log "[3/8] Health check..." +for i in $(seq 1 30); do + if curl -fsS "http://localhost:${DEE_PORT}/health" >/dev/null 2>&1; then + log "Health check: PASS" + break + fi + sleep 1 +done + +if ! curl -fsS "http://localhost:${DEE_PORT}/health" >/dev/null 2>&1; then + log "BLOCKER: Health check failed" + exit 1 +fi + +pound() { + local mode="$1" + local n="$2" + for _ in $(seq 1 "$n"); do + curl -fsS -X POST "http://localhost:${DEE_PORT}/scenario/${mode}" --max-time "$TIMEOUT_SEC" >/dev/null 2>&1 || true + done +} + +concurrent_load_test() { + local mode="$1" + local concurrency="$2" + local failed=0 + for _ in $(seq 1 "$concurrency"); do + (curl -fsS -X POST "http://localhost:${DEE_PORT}/scenario/${mode}" --max-time "$TIMEOUT_SEC" >/dev/null 2>&1) || ((failed++)) || true + done + wait 2>/dev/null || true + return 0 +} + +TOTAL_REQUESTS=0 +FAILED_REQUESTS=0 +FAILURES=() + +log "[4/8] Starting exploratory test iterations..." + +for iter in $(seq 1 "$ITERATIONS"); do + log "=== ITERATION $iter/$ITERATIONS ===" + + mode=$([ $((RANDOM % 2)) -eq 0 ] && echo "safe" || echo "naive") + if curl -fsS -X POST "http://localhost:${DEE_PORT}/scenario/${mode}" --max-time "$TIMEOUT_SEC" >/dev/null 2>&1; then + : $((TOTAL_REQUESTS++)) + else + : $((TOTAL_REQUESTS++)) + : $((FAILED_REQUESTS++)) + FAILURES+=("scenario_${mode}_iter_${iter}") + fi + + if [ $((iter % 10)) -eq 0 ]; then + log "Concurrent load test..." + pound safe "$CONCURRENCY" + pound naive "$CONCURRENCY" + fi + + if [ $((iter % 25)) -eq 0 ]; then + log "Container churn: restart" + DEE_PORT="$DEE_PORT" docker compose restart >> "$LOG_FILE" 2>&1 + sleep 2 + if ! curl -fsS "http://localhost:${DEE_PORT}/health" >/dev/null 2>&1; then + log "BLOCKER: Service did not recover after restart" + exit 1 + fi + fi + + DEE_PORT="$DEE_PORT" docker compose logs --no-color > "$RESULTS_DIR/compose.log" 2>&1 || true + if grep -Ei "$FORBIDDEN_PATTERNS" "$RESULTS_DIR/compose.log" 2>/dev/null; then + log "BLOCKER: Forbidden pattern in logs" + exit 1 + fi + if grep -E "$SUSPICIOUS_HEX" "$RESULTS_DIR/compose.log" 2>/dev/null; then + log "WARNING: Suspicious long hex in logs" + fi +done + +log "[5/8] Attack demos..." +if ! go run ./cmd/attacks/nonce-reuse 2>> "$LOG_FILE" | grep -q 'Recovered plaintext == expected: true'; then + FAILURES+=("attack_nonce_reuse") +fi +if ! go run ./cmd/attacks/replay 2>> "$LOG_FILE" | grep -q 'Replay accepted: true'; then + FAILURES+=("attack_replay") +fi + +log "[6/8] Generating metrics..." + +if command -v jq >/dev/null 2>&1; then + echo "{\"test_run_id\":\"$(basename "$RESULTS_DIR")\",\"timestamp\":\"$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S')\",\"configuration\":{\"iterations\":$ITERATIONS,\"concurrency\":$CONCURRENCY,\"max_payload\":$MAX_PAYLOAD,\"port\":$DEE_PORT},\"summary\":{\"total_requests\":$TOTAL_REQUESTS,\"failed_requests\":$FAILED_REQUESTS},\"failures_count\":${#FAILURES[@]},\"artifacts_location\":\"$RESULTS_DIR\"}" | jq . > "$METRICS_FILE" +else + echo "total_requests=$TOTAL_REQUESTS failed_requests=$FAILED_REQUESTS failures=${#FAILURES[@]}" > "$METRICS_FILE" +fi + +log "[7/8] Writing failure report..." + +if [ ${#FAILURES[@]} -gt 0 ]; then + if command -v jq >/dev/null 2>&1; then + printf '%s\n' "${FAILURES[@]}" | jq -R . | jq -s . > "$FAILURES_FILE" + else + printf '%s\n' "${FAILURES[@]}" > "${FAILURES_FILE%.json}.txt" + fi +else + echo "[]" > "$FAILURES_FILE" 2>/dev/null || true +fi + +log "[8/8] Test run complete" + +echo "" +echo "=== TEST SUMMARY ===" +echo "Results directory: $RESULTS_DIR" +echo "Total requests: $TOTAL_REQUESTS" +echo "Failed requests: $FAILED_REQUESTS" +echo "Failure count: ${#FAILURES[@]}" +echo "" + +if [ ${#FAILURES[@]} -gt 0 ]; then + echo "FAILURES:" + for f in "${FAILURES[@]}"; do echo "$f"; done + exit 1 +else + echo "ALL TESTS PASSED" + exit 0 +fi diff --git a/testdata/seeds/current.seed b/testdata/seeds/current.seed new file mode 100644 index 0000000..305103d --- /dev/null +++ b/testdata/seeds/current.seed @@ -0,0 +1 @@ +seed: 0 diff --git a/tests/policy/commits_test.go b/tests/policy/commits_test.go index eb29d38..8714527 100644 --- a/tests/policy/commits_test.go +++ b/tests/policy/commits_test.go @@ -11,7 +11,7 @@ const ( allowedAuthorName = "Thor Thor" ) -// allowedCommit is true if author+committer match maintainer or Dependabot (GitHub's dependency bot). +// allowedCommit is true if author+committer match maintainer or allowed automation. func allowedCommit(an, ae, cn, ce string) bool { if an == allowedAuthorName && ae == allowedAuthorEmail && cn == allowedAuthorName && ce == allowedAuthorEmail { return true diff --git a/tests/property/README.md b/tests/property/README.md new file mode 100644 index 0000000..a6f1947 --- /dev/null +++ b/tests/property/README.md @@ -0,0 +1,9 @@ +# Property-Based Tests + +This directory contains property-based tests using Go's testing/quick or gofuzz. + +Properties under test: +- Nonce uniqueness: forall counters, nonces are unique +- Ciphertext indistinguishability: forall messages, ciphertexts are random-looking +- Decryption correctness: forall (key, nonce, plaintext), decrypt(encrypt(pt)) == pt +- Counter monotonicity: forall sequences, counters never decrease diff --git a/tests/regression/README.md b/tests/regression/README.md new file mode 100644 index 0000000..d79cd3f --- /dev/null +++ b/tests/regression/README.md @@ -0,0 +1,5 @@ +# Regression Tests + +Each file corresponds to a fixed bug. Tests must fail before fix, pass after fix. + +Naming: Issue-{number}_{short-desc}.go