@bookedsolid/rea 0.21.1 → 0.23.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.
Files changed (58) hide show
  1. package/README.md +15 -0
  2. package/THREAT_MODEL.md +582 -0
  3. package/dist/audit/append.js +1 -1
  4. package/dist/cli/doctor.js +11 -12
  5. package/dist/cli/hook.d.ts +37 -3
  6. package/dist/cli/hook.js +167 -5
  7. package/dist/cli/init.js +14 -26
  8. package/dist/cli/install/canonical.js +18 -3
  9. package/dist/cli/install/commit-msg.js +1 -2
  10. package/dist/cli/install/copy.js +4 -13
  11. package/dist/cli/install/fs-safe.js +5 -16
  12. package/dist/cli/install/gitignore.js +1 -5
  13. package/dist/cli/install/pre-push.js +3 -8
  14. package/dist/cli/install/settings-merge.js +79 -16
  15. package/dist/cli/upgrade.js +14 -10
  16. package/dist/gateway/downstream.js +1 -2
  17. package/dist/gateway/live-state.js +3 -1
  18. package/dist/gateway/log.js +1 -3
  19. package/dist/gateway/middleware/audit.js +1 -1
  20. package/dist/gateway/middleware/injection.js +3 -9
  21. package/dist/gateway/middleware/policy.js +3 -1
  22. package/dist/gateway/middleware/redact.js +1 -1
  23. package/dist/gateway/observability/codex-telemetry.js +1 -2
  24. package/dist/gateway/reviewers/claude-self.js +10 -6
  25. package/dist/hooks/bash-scanner/blocked-scan.d.ts +26 -0
  26. package/dist/hooks/bash-scanner/blocked-scan.js +467 -0
  27. package/dist/hooks/bash-scanner/index.d.ts +41 -0
  28. package/dist/hooks/bash-scanner/index.js +62 -0
  29. package/dist/hooks/bash-scanner/parse-fail-closed.d.ts +31 -0
  30. package/dist/hooks/bash-scanner/parse-fail-closed.js +27 -0
  31. package/dist/hooks/bash-scanner/parser.d.ts +42 -0
  32. package/dist/hooks/bash-scanner/parser.js +92 -0
  33. package/dist/hooks/bash-scanner/protected-scan.d.ts +76 -0
  34. package/dist/hooks/bash-scanner/protected-scan.js +815 -0
  35. package/dist/hooks/bash-scanner/verdict.d.ts +80 -0
  36. package/dist/hooks/bash-scanner/verdict.js +49 -0
  37. package/dist/hooks/bash-scanner/walker.d.ts +165 -0
  38. package/dist/hooks/bash-scanner/walker.js +7954 -0
  39. package/dist/hooks/push-gate/base.js +2 -6
  40. package/dist/hooks/push-gate/codex-runner.js +3 -1
  41. package/dist/hooks/push-gate/index.js +9 -10
  42. package/dist/policy/loader.js +4 -1
  43. package/dist/registry/tofu-gate.js +2 -2
  44. package/hooks/_lib/cmd-segments.sh +32 -7
  45. package/hooks/_lib/interpreter-scanner.sh +71 -0
  46. package/hooks/_lib/path-normalize.sh +30 -3
  47. package/hooks/blocked-paths-bash-gate.sh +141 -277
  48. package/hooks/protected-paths-bash-gate.sh +230 -326
  49. package/package.json +3 -2
  50. package/profiles/bst-internal-no-codex.yaml +1 -1
  51. package/profiles/bst-internal.yaml +1 -1
  52. package/profiles/client-engagement.yaml +1 -1
  53. package/profiles/lit-wc.yaml +1 -1
  54. package/profiles/minimal.yaml +1 -1
  55. package/profiles/open-source-no-codex.yaml +1 -1
  56. package/profiles/open-source.yaml +1 -1
  57. package/scripts/postinstall.mjs +1 -2
  58. package/scripts/run-vitest.mjs +117 -0
@@ -1,299 +1,163 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
2
  # PreToolUse hook: blocked-paths-bash-gate.sh
3
- # Fires BEFORE every Bash tool call.
4
- # Refuses Bash commands that write to entries in policy.yaml's
5
- # `blocked_paths` list via shell redirection or write-flag utilities.
6
3
  #
7
- # Background (0.16.3, discord-ops Round 9 #1): the existing
8
- # blocked-paths-enforcer.sh only fires on Write/Edit/MultiEdit/
9
- # NotebookEdit. Bash-tier writes to blocked_paths entries bypass it
10
- # entirely:
4
+ # 0.23.0+ thin shim. Forwards stdin to `rea hook scan-bash --mode blocked`.
5
+ # See protected-paths-bash-gate.sh for the architectural rationale + CLI
6
+ # resolution strategy + verdict-verification model; this shim differs
7
+ # only in the --mode flag.
11
8
  #
12
- # echo x > .env
13
- # cp src.txt .env
14
- # sed -i '' '1d' .env.production
15
- # node -e "fs.writeFileSync('.env','x')"
9
+ # Codex round 4 Finding 2: 2-tier sandboxed resolver (drops PATH lookup
10
+ # and node_modules/.bin/rea symlink). See protected-paths-bash-gate.sh
11
+ # for rationale.
16
12
  #
17
- # `protected-paths-bash-gate.sh` covers the HARD list (HALT, policy.yaml,
18
- # settings.json, .husky/*) — but the soft, runtime-configurable
19
- # `blocked_paths` list never had a Bash-tier counterpart. discord-ops
20
- # independently caught this gap during their cycle 9 audit.
21
- #
22
- # This hook closes the gap by reading the same `blocked_paths` list that
23
- # blocked-paths-enforcer.sh reads, applying the same redirect / write-
24
- # utility detection pipeline as protected-paths-bash-gate.sh, and
25
- # blocking when the resolved target matches any entry.
13
+ # Codex round 2 R2-3: REA_NODE_CLI env-var honoring REMOVED.
26
14
  #
27
15
  # Exit codes:
28
- # 0 = no blocked-path write detected — allow
29
- # 2 = blocked-path write via Bash detected block
30
- #
31
- # Detection: `node -e "fs.writeFileSync('.env','x')"` — Node's
32
- # fs.writeFileSync called against a blocked path is also detected by
33
- # argument scan. Other interpreter constructions (perl, python, etc.)
34
- # remain a known coverage gap for the same reason the env-file-protection
35
- # hook lists hard caps in its header comment: defense-in-depth, not an
36
- # adversarial firewall.
16
+ # 0 = allow
17
+ # 2 = block (verdict, missing CLI, malformed payload, verdict mismatch)
37
18
 
38
19
  set -uo pipefail
39
20
 
40
- # shellcheck source=_lib/cmd-segments.sh
41
- source "$(dirname "$0")/_lib/cmd-segments.sh"
42
- # shellcheck source=_lib/path-normalize.sh
43
- source "$(dirname "$0")/_lib/path-normalize.sh"
44
- # shellcheck source=_lib/policy-read.sh
45
- source "$(dirname "$0")/_lib/policy-read.sh"
46
- # shellcheck source=_lib/halt-check.sh
47
- source "$(dirname "$0")/_lib/halt-check.sh"
48
-
49
- INPUT=$(cat)
21
+ proj="${CLAUDE_PROJECT_DIR:-$(pwd)}"
22
+
23
+ # 2-tier sandboxed CLI resolver. NO PATH lookup, NO env-var override.
24
+ REA_ARGV=()
25
+ RESOLVED_CLI_PATH=""
26
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
27
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
28
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
29
+ elif [ -f "$proj/dist/cli/index.js" ]; then
30
+ REA_ARGV=(node "$proj/dist/cli/index.js")
31
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
32
+ fi
50
33
 
51
- if ! command -v jq >/dev/null 2>&1; then
52
- printf 'REA ERROR: jq is required but not installed.\n' >&2
34
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
35
+ printf 'rea: CLI not found at sandboxed tiers (node_modules/@bookedsolid/rea/dist or dist/).\n' >&2
36
+ printf 'Install @bookedsolid/rea via npm/pnpm and run `rea doctor`.\n' >&2
37
+ printf 'Refusing the Bash command on uncertainty.\n' >&2
53
38
  exit 2
54
39
  fi
55
40
 
56
- check_halt
57
- REA_ROOT=$(rea_root)
58
-
59
- CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
60
- if [[ -z "$CMD" ]]; then
61
- exit 0
41
+ # Codex round 4 Finding 2 + round 5 F2 tier defense: realpath the
42
+ # resolved CLI; PRIMARY check is project-root containment, SECONDARY
43
+ # is ancestor `package.json` with the protected name. See
44
+ # protected-paths-bash-gate.sh for the full rationale.
45
+ if ! command -v node >/dev/null 2>&1; then
46
+ printf 'rea: node not on PATH (required to realpath verify scan-bash CLI). Refusing.\n' >&2
47
+ exit 2
48
+ fi
49
+ sandbox_check=$(node -e '
50
+ const fs = require("fs");
51
+ const path = require("path");
52
+ const cli = process.argv[1];
53
+ const projDir = process.argv[2];
54
+ let real;
55
+ try { real = fs.realpathSync(cli); } catch (e) {
56
+ process.stdout.write("bad:realpath:" + (e && e.message ? e.message : String(e)));
57
+ process.exit(1);
58
+ }
59
+ // PRIMARY (round 5 F2): realCli must live INSIDE realProj. Catches
60
+ // node_modules/@bookedsolid/rea -> /tmp/sym-attacker symlink-out.
61
+ let realProj;
62
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
63
+ process.stdout.write("bad:realpath-proj:" + (e && e.message ? e.message : String(e)));
64
+ process.exit(1);
65
+ }
66
+ const projWithSep = realProj.endsWith(path.sep) ? realProj : realProj + path.sep;
67
+ if (!(real === realProj || real.startsWith(projWithSep))) {
68
+ process.stdout.write("bad:cli-escapes-project:" + real + ":proj=" + realProj);
69
+ process.exit(1);
70
+ }
71
+ // SECONDARY (round 4 #2): shape + ancestor `package.json` with
72
+ // `@bookedsolid/rea`. Guards against intra-project hijack.
73
+ const expectedEnd = path.join("dist", "cli", "index.js");
74
+ if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
75
+ process.stdout.write("bad:cli-shape:" + real);
76
+ process.exit(1);
77
+ }
78
+ let cur = path.dirname(path.dirname(path.dirname(real)));
79
+ let found = false;
80
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
81
+ const pj = path.join(cur, "package.json");
82
+ if (fs.existsSync(pj)) {
83
+ try {
84
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
85
+ if (data && data.name === "@bookedsolid/rea") {
86
+ found = true;
87
+ break;
88
+ }
89
+ } catch (e) {
90
+ // Continue.
91
+ }
92
+ }
93
+ cur = path.dirname(cur);
94
+ }
95
+ if (!found) {
96
+ process.stdout.write("bad:no-rea-pkg:" + real);
97
+ process.exit(1);
98
+ }
99
+ process.stdout.write("ok");
100
+ process.exit(0);
101
+ ' "$RESOLVED_CLI_PATH" "$proj" 2>&1)
102
+ sandbox_status=$?
103
+ if [ "$sandbox_status" -ne 0 ] || [ "$sandbox_check" != "ok" ]; then
104
+ printf 'rea: scan-bash CLI realpath escapes sandbox (%s). Refusing.\n' "$sandbox_check" >&2
105
+ exit 2
62
106
  fi
63
107
 
64
- # Load blocked_paths list. If the policy is missing or the list is empty,
65
- # this hook is a no-op (matches blocked-paths-enforcer.sh semantics).
66
- BLOCKED_PATHS=()
67
- while IFS= read -r entry; do
68
- [[ -z "$entry" ]] && continue
69
- BLOCKED_PATHS+=("$entry")
70
- done < <(policy_list "blocked_paths")
71
-
72
- if [[ ${#BLOCKED_PATHS[@]} -eq 0 ]]; then
108
+ payload=$(cat)
109
+ if [ -z "$payload" ]; then
73
110
  exit 0
74
111
  fi
75
112
 
76
- # Match a normalized project-relative path against the loaded
77
- # blocked_paths list using the same matching rules as
78
- # blocked-paths-enforcer.sh:
79
- # - directory match (entry ends with `/`) → prefix match
80
- # - glob entry (contains `*`) → ERE conversion + anchored match
81
- # - otherwise exact (case-insensitive) match
82
- # Returns 0 + sets MATCHED on hit, 1 on no hit.
83
- MATCHED=""
84
- _match_blocked() {
85
- local target_lc="$1"
86
- MATCHED=""
87
- local entry entry_lc regex
88
- for entry in "${BLOCKED_PATHS[@]}"; do
89
- entry_lc=$(printf '%s' "$entry" | tr '[:upper:]' '[:lower:]')
90
- if [[ "$entry_lc" == */ ]]; then
91
- if [[ "$target_lc" == "$entry_lc"* ]] || [[ "$target_lc" == "${entry_lc%/}" ]]; then
92
- MATCHED="$entry"
93
- return 0
94
- fi
95
- continue
113
+ verdict=$(printf '%s' "$payload" | "${REA_ARGV[@]}" hook scan-bash --mode blocked)
114
+ status=$?
115
+
116
+ verifier='try {
117
+ const raw = require("fs").readFileSync(0, "utf8");
118
+ if (raw.trim().length === 0) { process.stdout.write("bad:empty"); process.exit(1); }
119
+ const v = JSON.parse(raw);
120
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
121
+ process.stdout.write("bad:non-object"); process.exit(1);
122
+ }
123
+ if (v.verdict !== "allow" && v.verdict !== "block") {
124
+ process.stdout.write("bad:verdict-shape:" + String(v.verdict)); process.exit(1);
125
+ }
126
+ process.stdout.write("ok:" + v.verdict); process.exit(0);
127
+ } catch (e) {
128
+ process.stdout.write("bad:" + (e && e.message ? e.message : String(e))); process.exit(1);
129
+ }'
130
+
131
+ verdict_check=$(printf '%s' "$verdict" | node -e "$verifier" 2>&1)
132
+ verdict_check_status=$?
133
+
134
+ case "$status" in
135
+ 0)
136
+ if [ "$verdict_check_status" -ne 0 ]; then
137
+ printf 'rea: scan-bash exited 0 but verdict JSON is malformed (%s). Refusing on uncertainty.\n' "$verdict_check" >&2
138
+ exit 2
96
139
  fi
97
- if [[ "$entry" == *'*'* ]]; then
98
- regex=$(printf '%s' "$entry_lc" | sed 's/\./\\./g; s/\*/.*/g')
99
- if printf '%s' "$target_lc" | grep -qE "^${regex}$"; then
100
- MATCHED="$entry"
101
- return 0
102
- fi
103
- continue
140
+ if [ "$verdict_check" != "ok:allow" ]; then
141
+ printf 'rea: scan-bash exit 0 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
142
+ exit 2
104
143
  fi
105
- if [[ "$target_lc" == "$entry_lc" ]]; then
106
- MATCHED="$entry"
107
- return 0
144
+ exit 0
145
+ ;;
146
+ 2)
147
+ if [ "$verdict_check_status" -ne 0 ]; then
148
+ exit 2
108
149
  fi
109
- done
110
- return 1
111
- }
112
-
113
- # Normalize a path token and apply the same `..` walk + outside-REA_ROOT
114
- # sentinel trick as protected-paths-bash-gate.sh::_normalize_target.
115
- # Returns the normalized lowercased project-relative path on stdout, or
116
- # `__rea_outside_root__:<resolved>` when the path resolves outside the
117
- # project root.
118
- _normalize_target() {
119
- local t="$1"
120
- if [[ "$t" =~ ^\"(.*)\"$ ]]; then t="${BASH_REMATCH[1]}"; fi
121
- if [[ "$t" =~ ^\'(.*)\'$ ]]; then t="${BASH_REMATCH[1]}"; fi
122
- case "/$t/" in
123
- */../*)
124
- local abs="$t"
125
- [[ "$abs" != /* ]] && abs="$REA_ROOT/$abs"
126
- local -a raw_parts parts=()
127
- IFS='/' read -ra raw_parts <<<"$abs"
128
- for part in "${raw_parts[@]}"; do
129
- case "$part" in
130
- ''|.) continue ;;
131
- ..) [[ "${#parts[@]}" -gt 0 ]] && unset 'parts[${#parts[@]}-1]' ;;
132
- *) parts+=("$part") ;;
133
- esac
134
- done
135
- t="/$(IFS=/; printf '%s' "${parts[*]}")"
136
- if [[ "$t" != "$REA_ROOT" && "$t" != "$REA_ROOT"/* ]]; then
137
- printf '__rea_outside_root__:%s' "$t"
138
- return 0
139
- fi
140
- ;;
141
- esac
142
- t=$(normalize_path "$t")
143
- printf '%s' "$t" | tr '[:upper:]' '[:lower:]'
144
- }
145
-
146
- _refuse() {
147
- local pattern="$1" target="$2" segment="$3"
148
- {
149
- printf 'BLOCKED PATH (bash): write denied by policy\n'
150
- printf '\n'
151
- printf ' Blocked by: %s\n' "$pattern"
152
- printf ' Resolved target: %s\n' "$target"
153
- printf ' Segment: %s\n' "$segment"
154
- printf '\n'
155
- printf ' Source: .rea/policy.yaml → blocked_paths\n'
156
- printf ' Rule: blocked_paths entries are unreachable via Bash redirects\n'
157
- printf ' too — not just Write/Edit/MultiEdit. To modify, a human\n'
158
- printf ' must edit directly or update blocked_paths in policy.yaml.\n'
159
- } >&2
160
- exit 2
161
- }
162
-
163
- # Check a single resolved-target token. Refuses on hit.
164
- #
165
- # 0.20.1 helix-021 #2: in addition to the logical post-_normalize_target
166
- # form, also check the symlink-resolved form. Pre-fix `ln -s . linkroot;
167
- # printf x > linkroot/.env` had a logical form of `linkroot/.env`
168
- # (no match against blocked_paths) but a resolved form of `.env`
169
- # (which DOES match). Refuse on either match. Write-tier
170
- # `blocked-paths-enforcer.sh` already has this resolution since 0.10.x.
171
- _check_token() {
172
- local token="$1" segment="$2"
173
- [[ -z "$token" ]] && return 0
174
- local resolved
175
- resolved=$(_normalize_target "$token")
176
- if [[ "$resolved" == __rea_outside_root__:* ]]; then
177
- # Outside REA_ROOT → can't be in blocked_paths (blocked_paths is
178
- # project-relative). Allow; the protected-paths gate handles
179
- # outside-root rejection on the protected list itself.
180
- return 0
181
- fi
182
- # Symlink-resolved form via shared helper. Returns empty when the
183
- # parent doesn't exist (legitimate "creating the parent" case);
184
- # outside-REA_ROOT sentinel when the symlink walks out of the
185
- # project (silently allow — same as the logical-path branch above).
186
- local resolved_symlink
187
- resolved_symlink=$(rea_resolved_relative_form "$token")
188
- if [[ "$resolved_symlink" == __rea_outside_root__:* ]]; then
189
- resolved_symlink=""
190
- fi
191
- if _match_blocked "$resolved"; then
192
- _refuse "$MATCHED" "$resolved" "$segment"
193
- fi
194
- if [[ -n "$resolved_symlink" ]] && _match_blocked "$resolved_symlink"; then
195
- _refuse "$MATCHED" "$resolved_symlink" "$segment"
196
- fi
197
- return 0
198
- }
199
-
200
- # Scan one segment for redirect / write-utility / node-fs targets and
201
- # refuse on any hit. Mirrors protected-paths-bash-gate.sh::_check_segment
202
- # layout, with a few additions to catch discord-ops Round 9 #1's exact
203
- # Node-interpreter and sed-script-on-target shapes.
204
- _check_segment() {
205
- local _raw="$1" segment="$2"
206
- [[ -z "$segment" ]] && return 0
207
-
208
- # Same regex set as protected-paths-bash-gate.sh — fd-prefix-aware
209
- # redirects, cp/mv tail target, sed -i target, dd of=, plus a
210
- # token-walk for tee/truncate/install/ln. Keeps behavior consistent
211
- # across the two bash gates.
212
- local re_redirect='(^|[[:space:]])(&>>|&>|[0-9]+>>|[0-9]+>\||[0-9]+>|>>|>\||>)[[:space:]]*([^[:space:]&|;<>]+)'
213
- local re_cpmv='(^|[[:space:]])(cp|mv)[[:space:]]+[^&|;<>]+[[:space:]]([^[:space:]&|;<>]+)[[:space:]]*$'
214
- local re_sed='(^|[[:space:]])sed[[:space:]]+(-[a-zA-Z]*i[a-zA-Z]*[^[:space:]]*)[[:space:]]+[^&|;<>]+[[:space:]]([^[:space:]&|;<>]+)[[:space:]]*$'
215
- local re_dd='(^|[[:space:]])dd[[:space:]]+[^&|;<>]*of=([^[:space:]&|;<>]+)'
216
-
217
- if [[ "$segment" =~ $re_redirect ]]; then
218
- _check_token "${BASH_REMATCH[3]}" "$segment"
219
- fi
220
- if [[ "$segment" =~ $re_cpmv ]]; then
221
- _check_token "${BASH_REMATCH[3]}" "$segment"
222
- fi
223
- if [[ "$segment" =~ $re_sed ]]; then
224
- _check_token "${BASH_REMATCH[3]}" "$segment"
225
- fi
226
- if [[ "$segment" =~ $re_dd ]]; then
227
- _check_token "${BASH_REMATCH[2]}" "$segment"
228
- fi
229
-
230
- # tee / truncate / install / ln — token-walk identical to
231
- # protected-paths-bash-gate.sh.
232
- local _seg_for_walk="$segment"
233
- _seg_for_walk="${_seg_for_walk#"${_seg_for_walk%%[![:space:]]*}"}"
234
- local first_tok
235
- first_tok=$(printf '%s' "$_seg_for_walk" | awk '{print $1}')
236
- case "$first_tok" in
237
- tee|truncate|install|ln)
238
- local found_cmd=""
239
- # shellcheck disable=SC2086
240
- set -- $_seg_for_walk
241
- while [ "$#" -gt 0 ]; do
242
- local tok="$1"
243
- shift
244
- if [[ -z "$found_cmd" ]]; then
245
- case "$tok" in
246
- tee|truncate|install|ln) found_cmd="$tok" ;;
247
- esac
248
- continue
249
- fi
250
- case "$tok" in
251
- --) continue ;;
252
- --*=*) continue ;;
253
- --*)
254
- case "$tok" in
255
- --append|--ignore-interrupts|--no-clobber|--force|--no-target-directory|--symbolic|--no-dereference|--reference=*) continue ;;
256
- *) shift 2>/dev/null || true; continue ;;
257
- esac
258
- ;;
259
- -*)
260
- case "$tok" in
261
- -s*|-m*|-o*|-g*|-t*) shift 2>/dev/null || true ;;
262
- esac
263
- continue
264
- ;;
265
- *)
266
- _check_token "$tok" "$segment"
267
- ;;
268
- esac
269
- done
270
- ;;
271
- esac
272
-
273
- # Node-interpreter fs.writeFileSync / fs.appendFileSync / fs.createWriteStream
274
- # detection (discord-ops Round 9 #1 explicit shape). Anchored on
275
- # `node -e ...` or `node --eval ...`. Conservative regex: pulls the
276
- # first quoted argument out of the call.
277
- local re_node_write='(^|[[:space:]])node[[:space:]]+(-e|--eval|-p|--print)[[:space:]]+'
278
- if [[ "$segment" =~ $re_node_write ]]; then
279
- # Find any quoted-string argument that contains fs.write* /
280
- # fs.append* / createWriteStream + a path-looking arg. This is a
281
- # best-effort scan; the goal is the obvious vector, not full JS.
282
- local node_targets
283
- node_targets=$(printf '%s' "$segment" \
284
- | grep -oE "fs\.(writeFileSync|writeFile|appendFileSync|appendFile|createWriteStream)\([[:space:]]*[\"'][^\"']+[\"']" \
285
- | sed -E "s/.*\([[:space:]]*[\"']([^\"']+)[\"'].*/\\1/" || true)
286
- if [[ -n "$node_targets" ]]; then
287
- while IFS= read -r tgt; do
288
- [[ -z "$tgt" ]] && continue
289
- _check_token "$tgt" "$segment"
290
- done <<<"$node_targets"
150
+ if [ "$verdict_check" != "ok:block" ]; then
151
+ printf 'rea: scan-bash exit 2 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
152
+ exit 2
291
153
  fi
292
- fi
293
-
294
- return 0
295
- }
296
-
297
- for_each_segment "$CMD" _check_segment
298
-
299
- exit 0
154
+ exit 2
155
+ ;;
156
+ *)
157
+ printf 'rea: scan-bash exited %d (expected 0/2). Refusing on uncertainty.\n' "$status" >&2
158
+ if [ -n "$verdict" ]; then
159
+ printf 'rea: scan-bash stdout was: %s\n' "$verdict" >&2
160
+ fi
161
+ exit 2
162
+ ;;
163
+ esac