@bookedsolid/rea 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +339 -0
- package/SECURITY.md +104 -0
- package/THREAT_MODEL.md +245 -0
- package/agents/accessibility-engineer.md +101 -0
- package/agents/backend-engineer.md +126 -0
- package/agents/code-reviewer.md +144 -0
- package/agents/codex-adversarial.md +107 -0
- package/agents/frontend-specialist.md +84 -0
- package/agents/qa-engineer.md +138 -0
- package/agents/rea-orchestrator.md +101 -0
- package/agents/security-engineer.md +108 -0
- package/agents/technical-writer.md +140 -0
- package/agents/typescript-specialist.md +111 -0
- package/commands/codex-review.md +104 -0
- package/commands/freeze.md +81 -0
- package/commands/halt-check.md +120 -0
- package/commands/rea.md +52 -0
- package/commands/review.md +79 -0
- package/dist/cli/check.d.ts +1 -0
- package/dist/cli/check.js +66 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +93 -0
- package/dist/cli/freeze.d.ts +8 -0
- package/dist/cli/freeze.js +61 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +65 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +237 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/utils.d.ts +23 -0
- package/dist/cli/utils.js +51 -0
- package/dist/config/tier-map.d.ts +11 -0
- package/dist/config/tier-map.js +108 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +1 -0
- package/dist/gateway/circuit-breaker.d.ts +43 -0
- package/dist/gateway/circuit-breaker.js +86 -0
- package/dist/gateway/middleware/audit-types.d.ts +16 -0
- package/dist/gateway/middleware/audit-types.js +1 -0
- package/dist/gateway/middleware/audit.d.ts +12 -0
- package/dist/gateway/middleware/audit.js +98 -0
- package/dist/gateway/middleware/blocked-paths.d.ts +12 -0
- package/dist/gateway/middleware/blocked-paths.js +117 -0
- package/dist/gateway/middleware/chain.d.ts +28 -0
- package/dist/gateway/middleware/chain.js +40 -0
- package/dist/gateway/middleware/circuit-breaker.d.ts +11 -0
- package/dist/gateway/middleware/circuit-breaker.js +43 -0
- package/dist/gateway/middleware/injection.d.ts +22 -0
- package/dist/gateway/middleware/injection.js +128 -0
- package/dist/gateway/middleware/kill-switch.d.ts +10 -0
- package/dist/gateway/middleware/kill-switch.js +58 -0
- package/dist/gateway/middleware/policy.d.ts +12 -0
- package/dist/gateway/middleware/policy.js +70 -0
- package/dist/gateway/middleware/rate-limit.d.ts +12 -0
- package/dist/gateway/middleware/rate-limit.js +31 -0
- package/dist/gateway/middleware/redact.d.ts +16 -0
- package/dist/gateway/middleware/redact.js +128 -0
- package/dist/gateway/middleware/result-size-cap.d.ts +13 -0
- package/dist/gateway/middleware/result-size-cap.js +48 -0
- package/dist/gateway/middleware/session.d.ts +10 -0
- package/dist/gateway/middleware/session.js +18 -0
- package/dist/gateway/middleware/tier.d.ts +6 -0
- package/dist/gateway/middleware/tier.js +10 -0
- package/dist/gateway/rate-limiter.d.ts +36 -0
- package/dist/gateway/rate-limiter.js +75 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/policy/loader.d.ts +80 -0
- package/dist/policy/loader.js +146 -0
- package/dist/policy/types.d.ts +34 -0
- package/dist/policy/types.js +19 -0
- package/hooks/_lib/common.sh +105 -0
- package/hooks/_lib/halt-check.sh +39 -0
- package/hooks/_lib/policy-read.sh +79 -0
- package/hooks/architecture-review-gate.sh +84 -0
- package/hooks/attribution-advisory.sh +126 -0
- package/hooks/blocked-paths-enforcer.sh +176 -0
- package/hooks/changeset-security-gate.sh +143 -0
- package/hooks/commit-review-gate.sh +166 -0
- package/hooks/dangerous-bash-interceptor.sh +362 -0
- package/hooks/dependency-audit-gate.sh +118 -0
- package/hooks/env-file-protection.sh +110 -0
- package/hooks/pr-issue-link-gate.sh +65 -0
- package/hooks/push-review-gate.sh +120 -0
- package/hooks/secret-scanner.sh +229 -0
- package/hooks/security-disclosure-gate.sh +146 -0
- package/hooks/settings-protection.sh +147 -0
- package/package.json +93 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: push-review-gate.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call that matches "git push".
|
|
4
|
+
# Runs a full diff analysis against the target branch and requests
|
|
5
|
+
# security + code review before allowing the push.
|
|
6
|
+
#
|
|
7
|
+
# Exit codes:
|
|
8
|
+
# 0 = allow (no meaningful diff, or review cached)
|
|
9
|
+
# 2 = block (needs review)
|
|
10
|
+
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
|
|
13
|
+
# ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
17
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
18
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
19
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
20
|
+
exit 2
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
24
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
25
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
26
|
+
if [ -f "$HALT_FILE" ]; then
|
|
27
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
28
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# ── 4. Parse command ──────────────────────────────────────────────────────────
|
|
33
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
34
|
+
|
|
35
|
+
if [[ -z "$CMD" ]]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Only trigger on git push commands
|
|
40
|
+
if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+push'; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ── 5. Check if quality gates are enabled ─────────────────────────────────────
|
|
45
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
46
|
+
if [[ -f "$POLICY_FILE" ]]; then
|
|
47
|
+
if grep -qE 'push_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ── 6. Determine target branch ───────────────────────────────────────────────
|
|
53
|
+
CURRENT_BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
|
|
54
|
+
TARGET_BRANCH="main"
|
|
55
|
+
|
|
56
|
+
# Try to extract target from push command (git push origin <branch>)
|
|
57
|
+
PUSH_TARGET=$(printf '%s' "$CMD" | grep -oE 'git[[:space:]]+push[[:space:]]+[a-zA-Z_-]+[[:space:]]+([a-zA-Z0-9/_-]+)' | awk '{print $NF}' 2>/dev/null || echo "")
|
|
58
|
+
if [[ -n "$PUSH_TARGET" ]]; then
|
|
59
|
+
TARGET_BRANCH="$PUSH_TARGET"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# ── 7. Get diff against target ───────────────────────────────────────────────
|
|
63
|
+
MERGE_BASE=$(cd "$REA_ROOT" && git merge-base "$TARGET_BRANCH" HEAD 2>/dev/null || echo "")
|
|
64
|
+
|
|
65
|
+
if [[ -z "$MERGE_BASE" ]]; then
|
|
66
|
+
# Can't determine merge base — fail-open
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
DIFF_FULL=$(cd "$REA_ROOT" && git diff "$MERGE_BASE"...HEAD 2>/dev/null || echo "")
|
|
71
|
+
|
|
72
|
+
if [[ -z "$DIFF_FULL" ]]; then
|
|
73
|
+
# No diff — nothing to review
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || echo "0")
|
|
78
|
+
|
|
79
|
+
# ── 8. Check review cache ────────────────────────────────────────────────────
|
|
80
|
+
PUSH_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 | cut -d' ' -f1 2>/dev/null || echo "")
|
|
81
|
+
|
|
82
|
+
# Resolve rea CLI (node_modules/.bin first, dist fallback)
|
|
83
|
+
REA_CLI_ARGS=()
|
|
84
|
+
if [[ -f "${REA_ROOT}/node_modules/.bin/rea" ]]; then
|
|
85
|
+
REA_CLI_ARGS=(node "${REA_ROOT}/node_modules/.bin/rea")
|
|
86
|
+
elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
|
|
87
|
+
REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [[ -n "$PUSH_SHA" ]] && [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
|
|
91
|
+
CACHE_RESULT=$("${REA_CLI_ARGS[@]}" cache check "$PUSH_SHA" --branch "$CURRENT_BRANCH" --base "$TARGET_BRANCH" 2>/dev/null || echo '{"hit":false}')
|
|
92
|
+
if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true' >/dev/null 2>&1; then
|
|
93
|
+
# Review was already approved — notify and allow the push through
|
|
94
|
+
DISCORD_LIB="${REA_ROOT}/hooks/_lib/discord.sh"
|
|
95
|
+
if [ -f "$DISCORD_LIB" ]; then
|
|
96
|
+
# shellcheck source=/dev/null
|
|
97
|
+
source "$DISCORD_LIB"
|
|
98
|
+
discord_notify "dev" "Push passed quality gates on \`${CURRENT_BRANCH}\` -- $(cd "$REA_ROOT" && git log -1 --oneline 2>/dev/null)" "green"
|
|
99
|
+
fi
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# ── 9. Block and request review ──────────────────────────────────────────────
|
|
105
|
+
FILE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -c '^\+\+\+ ' 2>/dev/null || echo "0")
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
printf 'PUSH REVIEW GATE: Review required before pushing\n'
|
|
109
|
+
printf '\n'
|
|
110
|
+
printf ' Branch: %s → %s\n' "$CURRENT_BRANCH" "$TARGET_BRANCH"
|
|
111
|
+
printf ' Scope: %s files changed, %s lines\n' "$FILE_COUNT" "$LINE_COUNT"
|
|
112
|
+
printf '\n'
|
|
113
|
+
printf ' Action required:\n'
|
|
114
|
+
printf ' 1. Spawn a code-reviewer agent to review: git diff %s...HEAD\n' "$MERGE_BASE"
|
|
115
|
+
printf ' 2. Spawn a security-engineer agent for security review\n'
|
|
116
|
+
printf ' 3. After both pass, cache the result:\n'
|
|
117
|
+
printf ' rea cache set %s pass --branch %s --base %s\n' "$PUSH_SHA" "$CURRENT_BRANCH" "$TARGET_BRANCH"
|
|
118
|
+
printf '\n'
|
|
119
|
+
} >&2
|
|
120
|
+
exit 2
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: secret-scanner.sh
|
|
3
|
+
# Fires BEFORE every Write or Edit tool call.
|
|
4
|
+
# Scans content about to be written for credential patterns and blocks (exit 2)
|
|
5
|
+
# if real secrets are detected — before they ever touch disk.
|
|
6
|
+
#
|
|
7
|
+
# Content extraction:
|
|
8
|
+
# Write tool → tool_input.content
|
|
9
|
+
# Edit tool → tool_input.new_string
|
|
10
|
+
#
|
|
11
|
+
# NOTE: This hook is a last-resort pre-write guard. The primary secret gate is
|
|
12
|
+
# gitleaks running in the pre-commit hook. This hook stops obvious credentials
|
|
13
|
+
# before they hit disk. It cannot catch all encoding tricks — rely on gitleaks
|
|
14
|
+
# for comprehensive coverage.
|
|
15
|
+
#
|
|
16
|
+
# Exit codes:
|
|
17
|
+
# 0 = no secrets detected — allow the tool to proceed
|
|
18
|
+
# 2 = secrets detected — block the tool call
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
INPUT=$(cat)
|
|
23
|
+
|
|
24
|
+
# ── Dependency check ──────────────────────────────────────────────────────────
|
|
25
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
26
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
27
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
28
|
+
exit 2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# ── HALT check ────────────────────────────────────────────────────────────────
|
|
32
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
33
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
34
|
+
if [ -f "$HALT_FILE" ]; then
|
|
35
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
36
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
41
|
+
CONTENT_WRITE=$(printf '%s' "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
|
|
42
|
+
CONTENT_EDIT=$(printf '%s' "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
|
|
43
|
+
|
|
44
|
+
if [[ -n "$CONTENT_WRITE" ]]; then
|
|
45
|
+
CONTENT="$CONTENT_WRITE"
|
|
46
|
+
elif [[ -n "$CONTENT_EDIT" ]]; then
|
|
47
|
+
CONTENT="$CONTENT_EDIT"
|
|
48
|
+
else
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Smart file-path exclusions (suffix-based only — no directory exclusions)
|
|
53
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
54
|
+
if [[ "$FILE_PATH" == *.env.example || "$FILE_PATH" == *.env.sample ]]; then
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
# Test files are NOT excluded — real secrets in test files must be caught.
|
|
58
|
+
# The is_placeholder() function handles false positives from test fixtures.
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Build line-filtered content
|
|
62
|
+
# Strip: shell comment lines (#) and lines where process.env.VAR is the RHS of an assignment
|
|
63
|
+
# NOT stripped: lines that merely mention process.env somewhere (bypass vector if too broad)
|
|
64
|
+
FILTERED_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-secret-scan-XXXXXX") || {
|
|
65
|
+
printf 'SECRET-SCAN ERROR: Failed to create temp file — blocking write (fail-secure)\n' >&2
|
|
66
|
+
exit 2
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
VIOLATIONS_FILE=""
|
|
70
|
+
|
|
71
|
+
cleanup() {
|
|
72
|
+
rm -f "$FILTERED_FILE"
|
|
73
|
+
[[ -n "$VIOLATIONS_FILE" ]] && rm -f "$VIOLATIONS_FILE"
|
|
74
|
+
}
|
|
75
|
+
trap cleanup EXIT
|
|
76
|
+
|
|
77
|
+
printf '%s' "$CONTENT" | awk '
|
|
78
|
+
{
|
|
79
|
+
line = $0
|
|
80
|
+
trimmed = line
|
|
81
|
+
sub(/^[[:space:]]+/, "", trimmed)
|
|
82
|
+
# Skip shell comment lines only
|
|
83
|
+
if (substr(trimmed, 1, 1) == "#") next
|
|
84
|
+
# Skip lines where process.env.VAR is the RHS of an assignment
|
|
85
|
+
# Pattern: = process.env.SOMETHING (not just any mention of process.env)
|
|
86
|
+
if (trimmed ~ /=[[:space:]]*process\.env\.[A-Z_]+[^a-zA-Z]?$/) next
|
|
87
|
+
if (trimmed ~ /=[[:space:]]*process\.env\.[A-Z_]+[[:space:]]*[;,)]/) next
|
|
88
|
+
if (trimmed ~ /os\.environ\[/) next
|
|
89
|
+
print line
|
|
90
|
+
}
|
|
91
|
+
' > "$FILTERED_FILE" 2>/dev/null
|
|
92
|
+
|
|
93
|
+
if [[ ! -s "$FILTERED_FILE" ]]; then
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
is_placeholder() {
|
|
98
|
+
local MATCH
|
|
99
|
+
MATCH=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
|
|
100
|
+
[[ "$MATCH" =~ \<[a-z_]+\> ]] && return 0
|
|
101
|
+
[[ "$MATCH" =~ your_key_here ]] && return 0
|
|
102
|
+
[[ "$MATCH" =~ your_api_key ]] && return 0
|
|
103
|
+
[[ "$MATCH" =~ your_secret ]] && return 0
|
|
104
|
+
[[ "$MATCH" =~ placeholder ]] && return 0
|
|
105
|
+
[[ "$MATCH" =~ changeme ]] && return 0
|
|
106
|
+
[[ "$MATCH" =~ insert.*here ]] && return 0
|
|
107
|
+
# Prefix checks: require full placeholder compound, not just a prefix
|
|
108
|
+
[[ "$MATCH" =~ ^(test|fake|mock|demo|example)_(key|token|secret|credential|api)$ ]] && return 0
|
|
109
|
+
[[ "$MATCH" =~ ^test_[a-z_]+_key$ ]] && return 0
|
|
110
|
+
# Repeated-character dummies (aaaaaaa, 1111111, etc.)
|
|
111
|
+
if printf '%s' "$MATCH" | grep -qE '^(.)\1{7,}$'; then return 0; fi
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
VIOLATIONS_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-secret-violations-XXXXXX") || {
|
|
116
|
+
printf 'SECRET-SCAN ERROR: Failed to create violations file — blocking write (fail-secure)\n' >&2
|
|
117
|
+
exit 2
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
scan_pattern() {
|
|
121
|
+
local SEVERITY="$1"
|
|
122
|
+
local LABEL="$2"
|
|
123
|
+
local PATTERN="$3"
|
|
124
|
+
local MATCHES GREP_EXIT MATCH SNIPPET
|
|
125
|
+
MATCHES=$(grep -oE -e "$PATTERN" "$FILTERED_FILE" 2>/dev/null)
|
|
126
|
+
GREP_EXIT=$?
|
|
127
|
+
[[ $GREP_EXIT -ne 0 ]] && return 0
|
|
128
|
+
[[ -z "$MATCHES" ]] && return 0
|
|
129
|
+
MATCHES=$(printf '%s\n' "$MATCHES" | head -5)
|
|
130
|
+
while IFS= read -r MATCH; do
|
|
131
|
+
[[ -z "$MATCH" ]] && continue
|
|
132
|
+
if is_placeholder "$MATCH"; then continue; fi
|
|
133
|
+
if [[ ${#MATCH} -gt 60 ]]; then
|
|
134
|
+
SNIPPET="${MATCH:0:60}..."
|
|
135
|
+
else
|
|
136
|
+
SNIPPET="$MATCH"
|
|
137
|
+
fi
|
|
138
|
+
printf '%s|%s|%s\n' "$SEVERITY" "$LABEL" "$SNIPPET" >> "$VIOLATIONS_FILE"
|
|
139
|
+
done <<< "$MATCHES"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ── HIGH severity patterns ─────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
scan_pattern "HIGH" "AWS Access Key ID" \
|
|
145
|
+
'AKIA[0-9A-Z]{16}'
|
|
146
|
+
|
|
147
|
+
scan_pattern "HIGH" "AWS Secret Access Key" \
|
|
148
|
+
'[Aa][Ww][Ss]_SECRET_ACCESS_KEY[[:space:]]*=[[:space:]]*[A-Za-z0-9/+]{40}'
|
|
149
|
+
|
|
150
|
+
scan_pattern "HIGH" "Private key block" \
|
|
151
|
+
'-----BEGIN (RSA|EC|OPENSSH|PGP) PRIVATE KEY-----'
|
|
152
|
+
|
|
153
|
+
scan_pattern "HIGH" "Anthropic API key" \
|
|
154
|
+
'sk-ant-api03-[A-Za-z0-9_-]{93}'
|
|
155
|
+
|
|
156
|
+
scan_pattern "HIGH" "Anthropic OAuth token" \
|
|
157
|
+
'sk-ant-oat01-[A-Za-z0-9_-]{86}'
|
|
158
|
+
|
|
159
|
+
scan_pattern "HIGH" "GitHub classic Personal Access Token" \
|
|
160
|
+
'gh[puors]_[A-Za-z0-9]{36}'
|
|
161
|
+
|
|
162
|
+
scan_pattern "HIGH" "GitHub fine-grained Personal Access Token" \
|
|
163
|
+
'github_pat_[A-Za-z0-9_]{82}'
|
|
164
|
+
|
|
165
|
+
scan_pattern "HIGH" "Stripe live secret/restricted key" \
|
|
166
|
+
'(sk|rk)_live_[A-Za-z0-9]{24,}'
|
|
167
|
+
|
|
168
|
+
scan_pattern "HIGH" "Stripe webhook signing secret" \
|
|
169
|
+
'whsec_[A-Za-z0-9+/]{40,}'
|
|
170
|
+
|
|
171
|
+
scan_pattern "HIGH" "Generic secret assignment (double-quoted)" \
|
|
172
|
+
'(SECRET|PASSWORD|PRIVATE_KEY|API_SECRET)[[:space:]]*=[[:space:]]*"[^"]{20,}"'
|
|
173
|
+
|
|
174
|
+
scan_pattern "HIGH" "Generic secret assignment (single-quoted)" \
|
|
175
|
+
"(SECRET|PASSWORD|PRIVATE_KEY|API_SECRET)[[:space:]]*=[[:space:]]*'[^']{20,}'"
|
|
176
|
+
|
|
177
|
+
scan_pattern "HIGH" "Supabase service role key (JWT)" \
|
|
178
|
+
'SUPABASE_SERVICE_ROLE_KEY[[:space:]]*=[[:space:]]*["\'"'"']eyJ[A-Za-z0-9._-]{50,}'
|
|
179
|
+
|
|
180
|
+
# ── MEDIUM severity patterns ───────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
scan_pattern "MEDIUM" ".env credential assignment" \
|
|
183
|
+
'^(ANTHROPIC_API_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|STRIPE_SECRET)[[:space:]]*=[[:space:]]*[^[:space:]]+'
|
|
184
|
+
|
|
185
|
+
scan_pattern "MEDIUM" "Stripe test API key (real credential, test env)" \
|
|
186
|
+
'(sk|pk|rk)_test_[A-Za-z0-9]{24,}'
|
|
187
|
+
|
|
188
|
+
scan_pattern "MEDIUM" "Stripe live publishable key" \
|
|
189
|
+
'pk_live_[A-Za-z0-9]{24,}'
|
|
190
|
+
|
|
191
|
+
scan_pattern "MEDIUM" "Hardcoded DB connection string with password" \
|
|
192
|
+
'postgresql://[^:]+:[^@]{8,}@'
|
|
193
|
+
|
|
194
|
+
scan_pattern "MEDIUM" "Supabase anon key in non-client context" \
|
|
195
|
+
'SUPABASE_ANON_KEY[[:space:]]*=[[:space:]]*["\'"'"']eyJ[A-Za-z0-9._-]{50,}'
|
|
196
|
+
|
|
197
|
+
if [[ ! -s "$VIOLATIONS_FILE" ]]; then
|
|
198
|
+
exit 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
FILE_BASENAME=$(basename "${FILE_PATH:-unknown}")
|
|
202
|
+
HIGH_COUNT=$(grep -cF 'HIGH|' "$VIOLATIONS_FILE" 2>/dev/null || true)
|
|
203
|
+
: "${HIGH_COUNT:=0}"
|
|
204
|
+
|
|
205
|
+
if [[ "$HIGH_COUNT" -gt 0 ]]; then
|
|
206
|
+
{
|
|
207
|
+
printf 'SECRET DETECTED: Potential credential in %s\n' "$FILE_BASENAME"
|
|
208
|
+
COUNT=0
|
|
209
|
+
while IFS='|' read -r SEVERITY LABEL SNIPPET; do
|
|
210
|
+
[[ -z "$SEVERITY" ]] && continue
|
|
211
|
+
COUNT=$(( COUNT + 1 ))
|
|
212
|
+
if [[ $COUNT -gt 5 ]]; then break; fi
|
|
213
|
+
printf ' %s: %s — '"'"'%s'"'"'\n' "$SEVERITY" "$LABEL" "$SNIPPET"
|
|
214
|
+
done < "$VIOLATIONS_FILE"
|
|
215
|
+
printf 'Block reason: Writing credentials to disk risks exposure via git history.\n'
|
|
216
|
+
printf 'Fix: Load credentials from environment variables — never hardcode secrets.\n'
|
|
217
|
+
} >&2
|
|
218
|
+
exit 2
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
{
|
|
222
|
+
printf 'SECRET-SCAN WARN: Low-confidence credential pattern in %s (advisory — not blocking)\n' "$FILE_BASENAME"
|
|
223
|
+
while IFS='|' read -r SEVERITY LABEL SNIPPET; do
|
|
224
|
+
[[ -z "$SEVERITY" ]] && continue
|
|
225
|
+
printf ' %s: %s — '"'"'%s'"'"'\n' "$SEVERITY" "$LABEL" "$SNIPPET"
|
|
226
|
+
done < "$VIOLATIONS_FILE"
|
|
227
|
+
printf 'Note: Heuristic match — may be a false positive. If real, load from environment.\n'
|
|
228
|
+
} >&2
|
|
229
|
+
exit 0
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# security-disclosure-gate.sh — PreToolUse: Bash
|
|
3
|
+
#
|
|
4
|
+
# Intercepts `gh issue create` commands that contain security-sensitive
|
|
5
|
+
# keywords and blocks them. Routing depends on REA_DISCLOSURE_MODE:
|
|
6
|
+
#
|
|
7
|
+
# advisory (default) — redirect to GitHub Security Advisories (private)
|
|
8
|
+
# Use for public OSS repos
|
|
9
|
+
# issues — redirect to gh issue create with security + internal labels
|
|
10
|
+
# Use for permanently private client repos
|
|
11
|
+
# disabled — pass through (not recommended)
|
|
12
|
+
#
|
|
13
|
+
# Set REA_DISCLOSURE_MODE in .rea/policy.yaml (written to settings.json
|
|
14
|
+
# env by rea init). Defaults to "advisory" when unset.
|
|
15
|
+
#
|
|
16
|
+
# Triggered by: PreToolUse — Bash tool
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
# shellcheck source=_lib/common.sh
|
|
21
|
+
source "$(dirname "$0")/_lib/common.sh"
|
|
22
|
+
|
|
23
|
+
check_halt
|
|
24
|
+
|
|
25
|
+
# Read disclosure mode — default to advisory
|
|
26
|
+
DISCLOSURE_MODE="${REA_DISCLOSURE_MODE:-advisory}"
|
|
27
|
+
|
|
28
|
+
# Disabled mode: pass through entirely
|
|
29
|
+
if [[ "$DISCLOSURE_MODE" == "disabled" ]]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
INPUT="$(cat)"
|
|
34
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
35
|
+
|
|
36
|
+
if [[ "$TOOL_NAME" != "Bash" ]]; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
41
|
+
|
|
42
|
+
# Only intercept gh issue create
|
|
43
|
+
if ! echo "$COMMAND" | grep -qE 'gh\s+issue\s+create'; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
require_jq
|
|
48
|
+
|
|
49
|
+
# Security-sensitive keywords that should not appear in public issues —
|
|
50
|
+
# these terms suggest a vulnerability, exploit path, or bypass technique
|
|
51
|
+
SECURITY_PATTERNS=(
|
|
52
|
+
# Vulnerability classes
|
|
53
|
+
'bypass'
|
|
54
|
+
'exploit'
|
|
55
|
+
'injection'
|
|
56
|
+
'traversal'
|
|
57
|
+
'exfiltrat'
|
|
58
|
+
'escalat'
|
|
59
|
+
'privilege'
|
|
60
|
+
'rce'
|
|
61
|
+
'remote.code.exec'
|
|
62
|
+
'arbitrary.code'
|
|
63
|
+
'code.execution'
|
|
64
|
+
'zero.day'
|
|
65
|
+
'0day'
|
|
66
|
+
'CVE-'
|
|
67
|
+
'CVSS'
|
|
68
|
+
'GHSA-'
|
|
69
|
+
# Reagent-specific sensitive terms
|
|
70
|
+
'hook.bypass'
|
|
71
|
+
'HALT.bypass'
|
|
72
|
+
'redaction.bypass'
|
|
73
|
+
'policy.bypass'
|
|
74
|
+
'middleware.bypass'
|
|
75
|
+
'skip.*gate'
|
|
76
|
+
'evad'
|
|
77
|
+
# Credential/secret exposure
|
|
78
|
+
'secret.*leak'
|
|
79
|
+
'credential.*leak'
|
|
80
|
+
'token.*leak'
|
|
81
|
+
'key.*expos'
|
|
82
|
+
'expos.*secret'
|
|
83
|
+
# Prompt injection
|
|
84
|
+
'prompt.inject'
|
|
85
|
+
'jailbreak'
|
|
86
|
+
'jail.break'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Scan the full command text (title + body + flags) for sensitive patterns
|
|
90
|
+
FULL_TEXT=$(echo "$COMMAND" | tr '[:upper:]' '[:lower:]')
|
|
91
|
+
|
|
92
|
+
MATCHED_PATTERN=""
|
|
93
|
+
for PATTERN in "${SECURITY_PATTERNS[@]}"; do
|
|
94
|
+
if echo "$FULL_TEXT" | grep -qiE "$PATTERN"; then
|
|
95
|
+
MATCHED_PATTERN="$PATTERN"
|
|
96
|
+
break
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
|
|
100
|
+
if [[ -z "$MATCHED_PATTERN" ]]; then
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# ─── Route based on disclosure mode ──────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
if [[ "$DISCLOSURE_MODE" == "issues" ]]; then
|
|
107
|
+
# Private repo mode: redirect to labeled internal issue
|
|
108
|
+
json_output "block" \
|
|
109
|
+
"SECURITY DISCLOSURE GATE: This issue appears to describe a security finding (matched: '${MATCHED_PATTERN}').
|
|
110
|
+
|
|
111
|
+
This project is configured for PRIVATE disclosure (REA_DISCLOSURE_MODE=issues).
|
|
112
|
+
|
|
113
|
+
CORRECT PATH for security findings in this private repo:
|
|
114
|
+
Use: gh issue create --label 'security,internal' --title '...' --body '...'
|
|
115
|
+
|
|
116
|
+
The 'security' and 'internal' labels keep this off public project boards and
|
|
117
|
+
mark it for maintainer-only triage. Do NOT use the public issue queue without
|
|
118
|
+
these labels for security findings.
|
|
119
|
+
|
|
120
|
+
If this is NOT a security finding, rephrase the title/body to avoid triggering
|
|
121
|
+
security patterns, then retry."
|
|
122
|
+
|
|
123
|
+
else
|
|
124
|
+
# Advisory mode (default): redirect to GitHub Security Advisories
|
|
125
|
+
json_output "block" \
|
|
126
|
+
"SECURITY DISCLOSURE GATE: This issue appears to describe a security vulnerability (matched: '${MATCHED_PATTERN}'). Do NOT create a public GitHub issue for security vulnerabilities.
|
|
127
|
+
|
|
128
|
+
CORRECT DISCLOSURE PATH:
|
|
129
|
+
1. Use GitHub Security Advisories (private):
|
|
130
|
+
gh api repos/{owner}/{repo}/security-advisories --method POST --input - <<'JSON'
|
|
131
|
+
{ \"summary\": \"...\", \"description\": \"...\", \"severity\": \"medium|high|critical\",
|
|
132
|
+
\"vulnerabilities\": [{\"package\": {\"name\": \"@pkg\", \"ecosystem\": \"npm\"}}] }
|
|
133
|
+
JSON
|
|
134
|
+
2. Or navigate to: Security tab → Advisories → 'Report a vulnerability'
|
|
135
|
+
3. Or email security@bookedsolid.tech (see SECURITY.md)
|
|
136
|
+
|
|
137
|
+
The finding will be publicly disclosed AFTER a patch is released (coordinated disclosure).
|
|
138
|
+
|
|
139
|
+
WHY: Public issues expose vulnerabilities before users can patch. This is enforced by the
|
|
140
|
+
security-disclosure-gate hook (REA_DISCLOSURE_MODE=${DISCLOSURE_MODE}).
|
|
141
|
+
|
|
142
|
+
If this is NOT a security vulnerability, rephrase the issue to avoid triggering
|
|
143
|
+
security patterns, then retry."
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
exit 2
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: settings-protection.sh
|
|
3
|
+
# Fires BEFORE every Write or Edit tool call.
|
|
4
|
+
# Blocks modifications to critical configuration files that, if tampered with,
|
|
5
|
+
# would disable the entire hook safety layer.
|
|
6
|
+
#
|
|
7
|
+
# Protected paths (security controls and hook infrastructure ONLY):
|
|
8
|
+
# .claude/settings.json — hook configuration
|
|
9
|
+
# .claude/settings.local.json — local hook overrides
|
|
10
|
+
# .claude/hooks/* — hook scripts themselves
|
|
11
|
+
# .husky/* — git hook scripts
|
|
12
|
+
# .rea/policy.yaml — autonomy/blocking policy
|
|
13
|
+
# .rea/HALT — kill switch file
|
|
14
|
+
#
|
|
15
|
+
# NOT protected (operational files agents may legitimately write):
|
|
16
|
+
# .rea/review-cache.json — cache file, writable by CLI and agents
|
|
17
|
+
# .rea/tasks.jsonl — task store, managed by task MCP tools
|
|
18
|
+
#
|
|
19
|
+
# Exit codes:
|
|
20
|
+
# 0 = allow (path not protected)
|
|
21
|
+
# 2 = block (protected path modification attempt)
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
# ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
|
|
28
|
+
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
29
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
30
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
31
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
32
|
+
exit 2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
36
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
37
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
38
|
+
if [ -f "$HALT_FILE" ]; then
|
|
39
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
40
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
41
|
+
exit 2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ── 4. Extract file path from payload ─────────────────────────────────────────
|
|
45
|
+
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
46
|
+
|
|
47
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ── 5. Normalize path for comparison ──────────────────────────────────────────
|
|
52
|
+
# Convert to relative path from project root for consistent matching
|
|
53
|
+
normalize_path() {
|
|
54
|
+
local p="$1"
|
|
55
|
+
local root="$REA_ROOT"
|
|
56
|
+
|
|
57
|
+
# Strip project root prefix if present
|
|
58
|
+
if [[ "$p" == "$root"/* ]]; then
|
|
59
|
+
p="${p#$root/}"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# URL decode common sequences
|
|
63
|
+
p=$(printf '%s' "$p" | sed 's/%2[Ff]/\//g; s/%2[Ee]/./g; s/%20/ /g')
|
|
64
|
+
|
|
65
|
+
# Collapse path traversals
|
|
66
|
+
# Remove ./ components
|
|
67
|
+
p=$(printf '%s' "$p" | sed 's|\./||g')
|
|
68
|
+
|
|
69
|
+
# Remove leading ./
|
|
70
|
+
p="${p#./}"
|
|
71
|
+
|
|
72
|
+
printf '%s' "$p"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
NORMALIZED=$(normalize_path "$FILE_PATH")
|
|
76
|
+
|
|
77
|
+
# ── 6. Protected path patterns ────────────────────────────────────────────────
|
|
78
|
+
PROTECTED_PATTERNS=(
|
|
79
|
+
'.claude/settings.json'
|
|
80
|
+
'.claude/settings.local.json'
|
|
81
|
+
'.claude/hooks/'
|
|
82
|
+
'.husky/'
|
|
83
|
+
'.rea/policy.yaml'
|
|
84
|
+
'.rea/HALT'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
|
88
|
+
# Exact match
|
|
89
|
+
if [[ "$NORMALIZED" == "$pattern" ]]; then
|
|
90
|
+
{
|
|
91
|
+
printf 'SETTINGS PROTECTION: Modification blocked\n'
|
|
92
|
+
printf '\n'
|
|
93
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
94
|
+
printf ' Rule: This file is protected from agent modification.\n'
|
|
95
|
+
printf '\n'
|
|
96
|
+
printf ' Protected files include hook scripts, settings, policy,\n'
|
|
97
|
+
printf ' and kill switch files. These must be modified by humans\n'
|
|
98
|
+
printf ' via rea CLI or direct editing.\n'
|
|
99
|
+
printf '\n'
|
|
100
|
+
printf ' Use: rea init (to update hooks/settings)\n'
|
|
101
|
+
printf ' rea freeze/unfreeze (for HALT file)\n'
|
|
102
|
+
printf ' Edit .rea/policy.yaml manually\n'
|
|
103
|
+
} >&2
|
|
104
|
+
exit 2
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Directory prefix match (patterns ending in /)
|
|
108
|
+
if [[ "$pattern" == */ ]] && [[ "$NORMALIZED" == "$pattern"* ]]; then
|
|
109
|
+
{
|
|
110
|
+
printf 'SETTINGS PROTECTION: Modification blocked\n'
|
|
111
|
+
printf '\n'
|
|
112
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
113
|
+
printf ' Rule: Files under %s are protected from agent modification.\n' "$pattern"
|
|
114
|
+
printf '\n'
|
|
115
|
+
printf ' These files control the hook safety layer and must be\n'
|
|
116
|
+
printf ' modified by humans via rea CLI or direct editing.\n'
|
|
117
|
+
} >&2
|
|
118
|
+
exit 2
|
|
119
|
+
fi
|
|
120
|
+
done
|
|
121
|
+
|
|
122
|
+
# ── 7. Case-insensitive fallback check ────────────────────────────────────────
|
|
123
|
+
# Catch case-manipulation bypass attempts (e.g., .Claude/Settings.json)
|
|
124
|
+
LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
|
|
125
|
+
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
|
126
|
+
LOWER_PATTERN=$(printf '%s' "$pattern" | tr '[:upper:]' '[:lower:]')
|
|
127
|
+
if [[ "$LOWER_NORM" == "$LOWER_PATTERN" ]]; then
|
|
128
|
+
{
|
|
129
|
+
printf 'SETTINGS PROTECTION: Modification blocked (case-insensitive match)\n'
|
|
130
|
+
printf '\n'
|
|
131
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
132
|
+
printf ' Matched: %s\n' "$pattern"
|
|
133
|
+
} >&2
|
|
134
|
+
exit 2
|
|
135
|
+
fi
|
|
136
|
+
if [[ "$LOWER_PATTERN" == */ ]] && [[ "$LOWER_NORM" == "$LOWER_PATTERN"* ]]; then
|
|
137
|
+
{
|
|
138
|
+
printf 'SETTINGS PROTECTION: Modification blocked (case-insensitive match)\n'
|
|
139
|
+
printf '\n'
|
|
140
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
141
|
+
printf ' Matched: %s*\n' "$pattern"
|
|
142
|
+
} >&2
|
|
143
|
+
exit 2
|
|
144
|
+
fi
|
|
145
|
+
done
|
|
146
|
+
|
|
147
|
+
exit 0
|