@bookedsolid/rea 0.37.0 → 0.38.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/hooks/_lib/shim-runtime.sh +405 -0
- package/hooks/architecture-review-gate.sh +11 -103
- package/hooks/attribution-advisory.sh +38 -209
- package/hooks/blocked-paths-bash-gate.sh +32 -146
- package/hooks/blocked-paths-enforcer.sh +32 -137
- package/hooks/changeset-security-gate.sh +26 -119
- package/hooks/dangerous-bash-interceptor.sh +46 -170
- package/hooks/delegation-advisory.sh +26 -144
- package/hooks/delegation-capture.sh +33 -139
- package/hooks/dependency-audit-gate.sh +29 -121
- package/hooks/env-file-protection.sh +30 -141
- package/hooks/local-review-gate.sh +117 -352
- package/hooks/pr-issue-link-gate.sh +16 -118
- package/hooks/protected-paths-bash-gate.sh +53 -152
- package/hooks/secret-scanner.sh +90 -213
- package/hooks/security-disclosure-gate.sh +32 -155
- package/hooks/settings-protection.sh +56 -176
- package/package.json +1 -1
- package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
- package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
- package/templates/attribution-advisory.dogfood-staged.sh +38 -209
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +32 -146
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +32 -137
- package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
- package/templates/delegation-advisory.dogfood-staged.sh +44 -0
- package/templates/delegation-capture.dogfood-staged.sh +52 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
- package/templates/env-file-protection.dogfood-staged.sh +30 -141
- package/templates/local-review-gate.dogfood-staged.sh +117 -352
- package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +53 -152
- package/templates/secret-scanner.dogfood-staged.sh +90 -213
- package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
- package/templates/settings-protection.dogfood-staged.sh +56 -176
package/hooks/secret-scanner.sh
CHANGED
|
@@ -1,240 +1,117 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: secret-scanner.sh
|
|
3
3
|
# 0.34.0+ — Node-binary shim for `rea hook secret-scanner`.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
# Pre-0.34.0 the gate's full body lived here as bash (230 LOC,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# dispatcher's view of the hook — it forwards stdin to the CLI and
|
|
11
|
-
# exits with whatever the CLI returns.
|
|
6
|
+
# Pre-0.34.0 the gate's full body lived here as bash (230 LOC, awk
|
|
7
|
+
# line filter + 17-pattern catalog + placeholder-rejection + MultiEdit
|
|
8
|
+
# fragment join). Migration in `src/hooks/secret-scanner/index.ts`.
|
|
9
|
+
# Behavioral contract preserved byte-for-byte: exit 0 on no-match or
|
|
10
|
+
# MEDIUM-only advisory, exit 2 on HALT / HIGH match / malformed payload.
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
# or MEDIUM-only advisory, exit 2 on HALT / HIGH match / malformed
|
|
15
|
-
# payload.
|
|
12
|
+
# # Shim short-circuits (codex round-1 P2 fix from 0.34.0)
|
|
16
13
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# The 0.34.0 round-0 shim deferred ALL decisions to the CLI, including
|
|
20
|
-
# empty-content and `.env.example` suffix exclusion. That regressed
|
|
21
|
-
# benign workflows on fresh/unbuilt installs: clearing a file or
|
|
22
|
-
# editing an example env file would fail closed when `dist/cli/index.js`
|
|
23
|
-
# wasn't built yet.
|
|
24
|
-
#
|
|
25
|
-
# Round-1 P2 fix: replicate the pre-0.34.0 bash body's three
|
|
26
|
-
# short-circuits in the shim BEFORE CLI resolution:
|
|
14
|
+
# Replicate the pre-0.34.0 bash body's two short-circuits BEFORE CLI
|
|
15
|
+
# resolution:
|
|
27
16
|
# - Empty content (no `content`, `new_string`, `edits[]`, or
|
|
28
|
-
# `new_source` in the payload) → exit 0
|
|
17
|
+
# `new_source` in the payload) → exit 0.
|
|
29
18
|
# - file_path / notebook_path with `.env.example` or `.env.sample`
|
|
30
|
-
# suffix → exit 0
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# # CLI-resolution trust boundary
|
|
19
|
+
# suffix → exit 0.
|
|
20
|
+
# This unblocks workflows on fresh/unbuilt installs (clearing a file
|
|
21
|
+
# or editing an example env file would otherwise fail closed).
|
|
35
22
|
#
|
|
36
|
-
#
|
|
23
|
+
# # CLI-missing relevance scan (round-7 P1)
|
|
37
24
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# pre-0.34.0 bash body refused credential-bearing writes without any
|
|
42
|
-
# compiled CLI. Early-exit branches fail closed AFTER the shim
|
|
43
|
-
# short-circuits.
|
|
25
|
+
# When the CLI is missing AND content contains a credential marker
|
|
26
|
+
# from the catalog, preserve fail-closed. When no marker matches,
|
|
27
|
+
# exit 0 (pre-port body would have allowed).
|
|
44
28
|
|
|
45
29
|
set -uo pipefail
|
|
46
30
|
|
|
47
|
-
# 1. HALT check.
|
|
48
31
|
# shellcheck source=_lib/halt-check.sh
|
|
49
32
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
50
33
|
check_halt
|
|
51
34
|
REA_ROOT=$(rea_root)
|
|
52
35
|
|
|
53
|
-
|
|
36
|
+
SHIM_NAME="secret-scanner"
|
|
37
|
+
SHIM_INTRODUCED_IN="0.34.0"
|
|
38
|
+
SHIM_FAIL_OPEN=0
|
|
39
|
+
SHIM_REFUSAL_NOUN="credential refusal"
|
|
54
40
|
|
|
55
|
-
#
|
|
56
|
-
INPUT
|
|
41
|
+
# Module-level: populated by shim_is_relevant for use by
|
|
42
|
+
# shim_cli_missing_relevant (avoids re-parsing INPUT via jq twice).
|
|
43
|
+
_SS_CONTENT=""
|
|
44
|
+
_SS_FILE_PATH=""
|
|
45
|
+
_SS_JQ_PARSE_CLEAN=0
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
if command -v jq >/dev/null 2>&1; then
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.tool_input.new_source // ""
|
|
91
|
-
) | tostring
|
|
92
|
-
' 2>/dev/null)
|
|
93
|
-
jq_content_status=$?
|
|
94
|
-
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '
|
|
95
|
-
.tool_input.file_path // .tool_input.notebook_path // ""
|
|
96
|
-
' 2>/dev/null)
|
|
97
|
-
jq_path_status=$?
|
|
98
|
-
# Only honor the shim short-circuits when BOTH jq probes parsed
|
|
99
|
-
# cleanly. Otherwise forward to the CLI which fails closed via Zod.
|
|
100
|
-
if [ "$jq_content_status" -eq 0 ] && [ "$jq_path_status" -eq 0 ]; then
|
|
101
|
-
if [ -z "$CONTENT" ]; then
|
|
102
|
-
exit 0
|
|
47
|
+
shim_is_relevant() {
|
|
48
|
+
# Two short-circuits: empty content, and *.env.example / *.env.sample
|
|
49
|
+
# suffix. Only honored when BOTH jq probes parse cleanly; on parse
|
|
50
|
+
# failure fall through to the CLI which fails closed via Zod.
|
|
51
|
+
if command -v jq >/dev/null 2>&1; then
|
|
52
|
+
# 0.34.0 round-2 fix: tostring so non-string `new_string`
|
|
53
|
+
# (object/number/null) doesn't trip jq with "Cannot iterate".
|
|
54
|
+
_SS_CONTENT=$(printf '%s' "$INPUT" | jq -r '
|
|
55
|
+
(.tool_input.content // .tool_input.new_string //
|
|
56
|
+
(
|
|
57
|
+
if (.tool_input.edits | type) == "array"
|
|
58
|
+
then (.tool_input.edits | map((.new_string // "") | tostring) | join("\n"))
|
|
59
|
+
else ""
|
|
60
|
+
end
|
|
61
|
+
) //
|
|
62
|
+
.tool_input.new_source // ""
|
|
63
|
+
) | tostring
|
|
64
|
+
' 2>/dev/null)
|
|
65
|
+
local jq_content_status=$?
|
|
66
|
+
_SS_FILE_PATH=$(printf '%s' "$INPUT" | jq -r '
|
|
67
|
+
.tool_input.file_path // .tool_input.notebook_path // ""
|
|
68
|
+
' 2>/dev/null)
|
|
69
|
+
local jq_path_status=$?
|
|
70
|
+
if [ "$jq_content_status" -eq 0 ] && [ "$jq_path_status" -eq 0 ]; then
|
|
71
|
+
_SS_JQ_PARSE_CLEAN=1
|
|
72
|
+
if [ -z "$_SS_CONTENT" ]; then
|
|
73
|
+
# Empty content — pre-port body exit 0.
|
|
74
|
+
return 1
|
|
75
|
+
fi
|
|
76
|
+
case "$_SS_FILE_PATH" in
|
|
77
|
+
*.env.example|*.env.sample) return 1 ;;
|
|
78
|
+
esac
|
|
103
79
|
fi
|
|
104
|
-
# Suffix-based exclusion. Mirrors the bash hook's:
|
|
105
|
-
# if [[ "$FILE_PATH" == *.env.example || "$FILE_PATH" == *.env.sample ]]; then exit 0; fi
|
|
106
|
-
case "$FILE_PATH" in
|
|
107
|
-
*.env.example|*.env.sample) exit 0 ;;
|
|
108
|
-
esac
|
|
109
80
|
fi
|
|
110
|
-
# jq parse failure
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# When jq is unavailable, fall through — the CLI does the same parse
|
|
114
|
-
# in TypeScript-space and will short-circuit on empty content there.
|
|
115
|
-
|
|
116
|
-
# 4. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
117
|
-
REA_ARGV=()
|
|
118
|
-
RESOLVED_CLI_PATH=""
|
|
119
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
120
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
121
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
122
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
123
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
124
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
125
|
-
fi
|
|
81
|
+
# Either jq missing OR jq parse failure OR non-excluded payload → relevant.
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
126
84
|
|
|
127
|
-
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# credential markers in the catalog. When CLI is missing AND no
|
|
135
|
-
# marker matches, exit 0 (the pre-0.34.0 body would have done
|
|
136
|
-
# the same — no pattern hit). When CLI is missing AND a marker
|
|
137
|
-
# DOES match, preserve fail-closed (refuse rather than silently
|
|
138
|
-
# allow a credential-shaped write).
|
|
139
|
-
#
|
|
140
|
-
# Substrings cover every entry in SECRET_PATTERNS (catalog in
|
|
141
|
-
# `src/hooks/secret-scanner/index.ts`). Coarse — over-trigger is
|
|
142
|
-
# fine, under-trigger is the bypass we MUST avoid. Same posture
|
|
143
|
-
# as the round-7 dangerous-bash relevance pre-gate.
|
|
144
|
-
CONTENT_FOR_SCAN=""
|
|
145
|
-
if [ -n "${CONTENT:-}" ]; then
|
|
146
|
-
CONTENT_FOR_SCAN="$CONTENT"
|
|
85
|
+
shim_cli_missing_relevant() {
|
|
86
|
+
# 0.34.0 round-7 P1: when the CLI is missing AND the content carries
|
|
87
|
+
# a credential marker, preserve fail-closed. When no marker matches,
|
|
88
|
+
# the pre-port bash body would have allowed.
|
|
89
|
+
local content_for_scan
|
|
90
|
+
if [ -n "$_SS_CONTENT" ]; then
|
|
91
|
+
content_for_scan="$_SS_CONTENT"
|
|
147
92
|
else
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
# credential markers embedded in JSON-string form.
|
|
151
|
-
CONTENT_FOR_SCAN="$INPUT"
|
|
93
|
+
# jq missing or parse-failed — substring scan the raw payload.
|
|
94
|
+
content_for_scan="$INPUT"
|
|
152
95
|
fi
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
*"
|
|
156
|
-
*"
|
|
157
|
-
*"
|
|
158
|
-
*"
|
|
159
|
-
*"
|
|
160
|
-
*"
|
|
161
|
-
*"
|
|
162
|
-
*"
|
|
163
|
-
*"
|
|
164
|
-
*"
|
|
165
|
-
*"
|
|
166
|
-
*"
|
|
167
|
-
*"
|
|
168
|
-
*"eyJ"*) CRED_RELEVANT=1 ;; # JWT prefix — catches Supabase keys
|
|
96
|
+
case "$content_for_scan" in
|
|
97
|
+
*"AKIA"*) return 0 ;;
|
|
98
|
+
*"AWS_SECRET_ACCESS_KEY"*|*"aws_secret_access_key"*) return 0 ;;
|
|
99
|
+
*"-----BEGIN"*) return 0 ;;
|
|
100
|
+
*"sk-ant-"*) return 0 ;;
|
|
101
|
+
*"ghp_"*|*"ghs_"*|*"gho_"*|*"ghu_"*|*"ghr_"*) return 0 ;;
|
|
102
|
+
*"github_pat_"*) return 0 ;;
|
|
103
|
+
*"sk_live_"*|*"rk_live_"*|*"pk_live_"*) return 0 ;;
|
|
104
|
+
*"sk_test_"*|*"rk_test_"*|*"pk_test_"*) return 0 ;;
|
|
105
|
+
*"whsec_"*) return 0 ;;
|
|
106
|
+
*"SECRET"*|*"PASSWORD"*|*"PRIVATE_KEY"*|*"API_SECRET"*) return 0 ;;
|
|
107
|
+
*"SUPABASE_SERVICE_ROLE_KEY"*|*"SUPABASE_ANON_KEY"*) return 0 ;;
|
|
108
|
+
*"ANTHROPIC_API_KEY"*|*"STRIPE_SECRET"*|*"DATABASE_URL"*) return 0 ;;
|
|
109
|
+
*"postgresql://"*) return 0 ;;
|
|
110
|
+
*"eyJ"*) return 0 ;;
|
|
169
111
|
esac
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# this write — exit 0 to unblock `npx rea init` and pre-build
|
|
173
|
-
# checkouts.
|
|
174
|
-
exit 0
|
|
175
|
-
fi
|
|
176
|
-
# Credential marker matched. Preserve fail-closed posture.
|
|
177
|
-
printf 'rea: secret-scanner cannot run — the rea CLI is not built.\n' >&2
|
|
178
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
179
|
-
printf 'This shim fails closed because the pre-0.34.0 bash body enforced secret refusal without a CLI.\n' >&2
|
|
180
|
-
exit 2
|
|
181
|
-
fi
|
|
182
|
-
|
|
183
|
-
# 5. Realpath sandbox check.
|
|
184
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
185
|
-
printf 'rea: secret-scanner cannot run — `node` is not on PATH.\n' >&2
|
|
186
|
-
printf 'Install Node 22+ (engines.node) to restore credential refusal.\n' >&2
|
|
187
|
-
exit 2
|
|
188
|
-
fi
|
|
189
|
-
|
|
190
|
-
sandbox_check=$(node -e '
|
|
191
|
-
const fs = require("fs");
|
|
192
|
-
const path = require("path");
|
|
193
|
-
const cli = process.argv[1];
|
|
194
|
-
const projDir = process.argv[2];
|
|
195
|
-
let real, realProj;
|
|
196
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
197
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
200
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
201
|
-
}
|
|
202
|
-
const sep = path.sep;
|
|
203
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
204
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
205
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
208
|
-
let found = false;
|
|
209
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
210
|
-
const pj = path.join(cur, "package.json");
|
|
211
|
-
if (fs.existsSync(pj)) {
|
|
212
|
-
try {
|
|
213
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
214
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
215
|
-
} catch (e) { /* keep walking */ }
|
|
216
|
-
}
|
|
217
|
-
cur = path.dirname(cur);
|
|
218
|
-
}
|
|
219
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
220
|
-
process.stdout.write("ok");
|
|
221
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
222
|
-
|
|
223
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
224
|
-
printf 'rea: secret-scanner FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
225
|
-
exit 2
|
|
226
|
-
fi
|
|
227
|
-
|
|
228
|
-
# 6. Version-probe.
|
|
229
|
-
probe_out=$("${REA_ARGV[@]}" hook secret-scanner --help 2>&1)
|
|
230
|
-
probe_status=$?
|
|
231
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'secret-scanner'; then
|
|
232
|
-
printf 'rea: this shim requires the `rea hook secret-scanner` subcommand (introduced in 0.34.0).\n' >&2
|
|
233
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
234
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
235
|
-
exit 2
|
|
236
|
-
fi
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
237
114
|
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
|
|
115
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
116
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
117
|
+
shim_run
|
|
@@ -1,171 +1,48 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: security-disclosure-gate.sh
|
|
3
3
|
# 0.32.0+ — Node-binary shim for `rea hook security-disclosure-gate`.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# that into `src/hooks/security-disclosure-gate/index.ts`. This shim is
|
|
9
|
-
# the Claude Code dispatcher's view of the hook — it forwards stdin
|
|
10
|
-
# AND the REA_DISCLOSURE_MODE env var to the CLI and exits with
|
|
11
|
-
# whatever the CLI returns.
|
|
6
|
+
# Blocking-tier: refuses `gh issue create` payloads carrying
|
|
7
|
+
# disclosure keywords. Pre-port body was 339 LOC; migration in
|
|
8
|
+
# `src/hooks/security-disclosure-gate/index.ts`.
|
|
12
9
|
#
|
|
13
|
-
#
|
|
14
|
-
# pass-through / no-match, exit 2 on HALT / pattern match / traversal
|
|
15
|
-
# refusal / malformed payload (fail-closed).
|
|
10
|
+
# # Relevance pre-gate
|
|
16
11
|
#
|
|
17
|
-
#
|
|
12
|
+
# Substring scan for `gh issue create`. Plain (NOT JSON-aware) so
|
|
13
|
+
# escaped quotes in quoted env prefixes don't break the match.
|
|
18
14
|
#
|
|
19
|
-
#
|
|
20
|
-
# delegation-advisory.sh §3. The resolved CLI MUST live INSIDE
|
|
21
|
-
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor `package.json`
|
|
22
|
-
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out
|
|
23
|
-
# and tarball-replacement attacks that could otherwise forge the
|
|
24
|
-
# pattern matcher and either suppress real findings or leak a
|
|
25
|
-
# vulnerability through the disclosure gate.
|
|
15
|
+
# # Mode short-circuit (round-6 P2)
|
|
26
16
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# When NO rea CLI is reachable, the hook falls through to allow —
|
|
32
|
-
# same posture as the bash-resident version, which `source`d
|
|
33
|
-
# _lib/common.sh first and exited cleanly if the lib was missing.
|
|
17
|
+
# `REA_DISCLOSURE_MODE=disabled` exits 0 — pre-port body no-op'd only
|
|
18
|
+
# in that mode (advisory + issues modes both enforced). This runs
|
|
19
|
+
# BEFORE sandbox check because it reads an env-var, no policy/CLI.
|
|
34
20
|
|
|
35
21
|
set -uo pipefail
|
|
36
22
|
|
|
37
|
-
# 1. HALT check.
|
|
38
23
|
# shellcheck source=_lib/halt-check.sh
|
|
39
24
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
40
25
|
check_halt
|
|
41
26
|
REA_ROOT=$(rea_root)
|
|
42
27
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
exit 0
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
# 2b. Mode short-circuit (round-6 P2). The pre-0.32.0 bash body
|
|
68
|
-
# no-op'd ONLY when `REA_DISCLOSURE_MODE=disabled` — `advisory`
|
|
69
|
-
# mode and the `issues` mode (default) BOTH enforced. Without
|
|
70
|
-
# this check, an unbuilt/stale install would refuse every relevant
|
|
71
|
-
# `gh issue create` even when the operator has deliberately set
|
|
72
|
-
# mode=disabled.
|
|
73
|
-
MODE="${REA_DISCLOSURE_MODE:-advisory}"
|
|
74
|
-
if [ "$MODE" = "disabled" ]; then
|
|
75
|
-
exit 0
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
# 3. Resolve the rea CLI.
|
|
79
|
-
REA_ARGV=()
|
|
80
|
-
RESOLVED_CLI_PATH=""
|
|
81
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
82
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
83
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
84
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
85
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
86
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
87
|
-
fi
|
|
88
|
-
|
|
89
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
90
|
-
# 0.32.0 round-4 P1: this is a blocking-tier gate — the pre-0.32.0
|
|
91
|
-
# bash body enforced the disclosure policy WITHOUT a compiled CLI.
|
|
92
|
-
# Falling through to exit 0 here would silently disable security-
|
|
93
|
-
# keyword blocking on `gh issue create` until the operator runs
|
|
94
|
-
# `pnpm install` / `pnpm build`. Fail closed: refuse the operation
|
|
95
|
-
# and tell the operator how to restore protection.
|
|
96
|
-
printf 'rea: security-disclosure-gate cannot run — the rea CLI is not built.\n' >&2
|
|
97
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
98
|
-
printf 'This shim fails closed because the pre-0.32.0 bash body enforced disclosure policy without a CLI.\n' >&2
|
|
99
|
-
exit 2
|
|
100
|
-
fi
|
|
101
|
-
|
|
102
|
-
# 3. Realpath sandbox check.
|
|
103
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
104
|
-
printf 'rea: security-disclosure-gate cannot run — `node` is not on PATH.\n' >&2
|
|
105
|
-
printf 'Install Node 22+ (engines.node) to restore disclosure-policy enforcement.\n' >&2
|
|
106
|
-
exit 2
|
|
107
|
-
fi
|
|
108
|
-
|
|
109
|
-
sandbox_check=$(node -e '
|
|
110
|
-
const fs = require("fs");
|
|
111
|
-
const path = require("path");
|
|
112
|
-
const cli = process.argv[1];
|
|
113
|
-
const projDir = process.argv[2];
|
|
114
|
-
let real, realProj;
|
|
115
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
116
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
119
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
const sep = path.sep;
|
|
122
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
123
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
124
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
127
|
-
let found = false;
|
|
128
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
129
|
-
const pj = path.join(cur, "package.json");
|
|
130
|
-
if (fs.existsSync(pj)) {
|
|
131
|
-
try {
|
|
132
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
133
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
134
|
-
} catch (e) { /* keep walking */ }
|
|
135
|
-
}
|
|
136
|
-
cur = path.dirname(cur);
|
|
137
|
-
}
|
|
138
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
139
|
-
process.stdout.write("ok");
|
|
140
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
141
|
-
|
|
142
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
143
|
-
# 0.32.0 round-4 P1: fail closed (blocking-tier — see exit-0 → exit-2
|
|
144
|
-
# rationale at the top). A failed sandbox check means the CLI we
|
|
145
|
-
# would run cannot be authenticated as the rea binary; refusing is
|
|
146
|
-
# both the safest posture AND preserves the pre-0.32.0 bash-body
|
|
147
|
-
# contract that this hook always enforces policy.
|
|
148
|
-
printf 'rea: security-disclosure-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
149
|
-
exit 2
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
# 4. Version-probe: confirm the resolved CLI implements
|
|
153
|
-
# `hook security-disclosure-gate`. Codex round 1 P1.
|
|
154
|
-
probe_out=$("${REA_ARGV[@]}" hook security-disclosure-gate --help 2>&1)
|
|
155
|
-
probe_status=$?
|
|
156
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'security-disclosure-gate'; then
|
|
157
|
-
# 0.32.0 round-4 P1: a stale/older CLI without the new subcommand is
|
|
158
|
-
# NOT a "harmless availability fallback" for this hook — the bash
|
|
159
|
-
# body it replaces always enforced. Fail closed and tell the
|
|
160
|
-
# operator exactly how to fix.
|
|
161
|
-
printf 'rea: this shim requires the `rea hook security-disclosure-gate` subcommand (introduced in 0.32.0).\n' >&2
|
|
162
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
163
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
164
|
-
exit 2
|
|
165
|
-
fi
|
|
166
|
-
|
|
167
|
-
# 5. Forward stdin (already captured up-front for the relevance gate).
|
|
168
|
-
# REA_DISCLOSURE_MODE is in env already; the Node binary reads it
|
|
169
|
-
# directly.
|
|
170
|
-
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook security-disclosure-gate
|
|
171
|
-
exit $?
|
|
28
|
+
SHIM_NAME="security-disclosure-gate"
|
|
29
|
+
SHIM_INTRODUCED_IN="0.32.0"
|
|
30
|
+
SHIM_FAIL_OPEN=0
|
|
31
|
+
SHIM_REFUSAL_NOUN="disclosure-policy enforcement"
|
|
32
|
+
|
|
33
|
+
shim_is_relevant() {
|
|
34
|
+
if ! printf '%s' "$INPUT" | grep -qE 'gh[[:space:]]+issue[[:space:]]+create'; then
|
|
35
|
+
return 1
|
|
36
|
+
fi
|
|
37
|
+
# Mode short-circuit: REA_DISCLOSURE_MODE=disabled bypasses BEFORE
|
|
38
|
+
# any CLI work. Implemented inline (no policy read needed).
|
|
39
|
+
local mode="${REA_DISCLOSURE_MODE:-advisory}"
|
|
40
|
+
if [ "$mode" = "disabled" ]; then
|
|
41
|
+
return 1
|
|
42
|
+
fi
|
|
43
|
+
return 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
47
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
48
|
+
shim_run
|