@bookedsolid/rea 0.34.0 → 0.36.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/dist/cli/doctor.js +45 -36
- package/dist/cli/hook.js +28 -0
- package/dist/hooks/_lib/path-normalize.d.ts +81 -0
- package/dist/hooks/_lib/path-normalize.js +171 -0
- package/dist/hooks/_lib/payload.js +1 -1
- package/dist/hooks/_lib/protected-paths.d.ts +0 -0
- package/dist/hooks/_lib/protected-paths.js +232 -0
- package/dist/hooks/_lib/segments.js +67 -7
- package/dist/hooks/blocked-paths-bash-gate/index.d.ts +55 -0
- package/dist/hooks/blocked-paths-bash-gate/index.js +175 -0
- package/dist/hooks/blocked-paths-enforcer/index.d.ts +51 -0
- package/dist/hooks/blocked-paths-enforcer/index.js +287 -0
- package/dist/hooks/protected-paths-bash-gate/index.d.ts +47 -0
- package/dist/hooks/protected-paths-bash-gate/index.js +168 -0
- package/dist/hooks/secret-scanner/index.js +64 -2
- package/dist/hooks/settings-protection/index.d.ts +74 -0
- package/dist/hooks/settings-protection/index.js +485 -0
- package/hooks/blocked-paths-bash-gate.sh +118 -116
- package/hooks/blocked-paths-enforcer.sh +152 -256
- package/hooks/protected-paths-bash-gate.sh +123 -210
- package/hooks/settings-protection.sh +171 -549
- package/package.json +3 -2
- package/scripts/lint-awk-shim-quotes.mjs +386 -0
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +177 -0
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +180 -0
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +186 -0
- package/templates/settings-protection.dogfood-staged.sh +204 -0
|
@@ -1,273 +1,186 @@
|
|
|
1
|
-
#!/
|
|
1
|
+
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: protected-paths-bash-gate.sh
|
|
3
|
+
# 0.35.0+ — Node-binary shim for `rea hook protected-paths-bash-gate`.
|
|
3
4
|
#
|
|
4
|
-
# 0.
|
|
5
|
-
#
|
|
6
|
-
# 0
|
|
5
|
+
# Pre-0.35.0 this was a thin bash shim over `rea hook scan-bash --mode
|
|
6
|
+
# protected` (the parser-backed AST walker that replaces the 536-line
|
|
7
|
+
# pre-0.23.0 regex pipeline). The full bash body is preserved at
|
|
8
|
+
# `__tests__/hooks/parity/baselines/protected-paths-bash-gate.sh.pre-0.35.0`.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# closes them definitionally — there is no segmenter to bypass.
|
|
10
|
+
# This shim now resolves the CLI through the same 2-tier sandboxed
|
|
11
|
+
# resolver as the 0.32.0+ pilots and calls `rea hook protected-paths-
|
|
12
|
+
# bash-gate` directly — eliminating the shim → CLI → scanner-module
|
|
13
|
+
# subprocess hop entirely.
|
|
13
14
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# rea repo's own `dist/cli/index.js`), we REFUSE the command. NEVER
|
|
17
|
-
# ALLOW on uncertainty. Operators need `@bookedsolid/rea` installed for
|
|
18
|
-
# the gate to work; `rea doctor` flags missing CLI as a P0.
|
|
15
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on allow,
|
|
16
|
+
# exit 2 on HALT / verdict block / malformed payload / sandbox fail.
|
|
19
17
|
#
|
|
20
|
-
#
|
|
21
|
-
# After capturing stdout we re-parse the verdict with `node -e` to
|
|
22
|
-
# confirm:
|
|
23
|
-
# 1. it is valid JSON,
|
|
24
|
-
# 2. the top-level shape has `.verdict == "allow"|"block"`,
|
|
25
|
-
# 3. the verdict matches the exit code (allow→0, block→2).
|
|
26
|
-
# Any disagreement → exit 2.
|
|
18
|
+
# # CLI-resolution trust boundary
|
|
27
19
|
#
|
|
28
|
-
#
|
|
29
|
-
# tier 2 (`node_modules/.bin/rea` symlink). Both are workspace-attacker
|
|
30
|
-
# controllable: an attacker who can write a file at
|
|
31
|
-
# `node_modules/.bin/rea` (or set PATH to a directory they own) can
|
|
32
|
-
# stage a fake `rea` binary that exits 0 with `{"verdict":"allow"}` and
|
|
33
|
-
# subvert the gate.
|
|
20
|
+
# Mirrors the 0.32.0 final shim shape.
|
|
34
21
|
#
|
|
35
|
-
#
|
|
36
|
-
# defense is the realpath sandbox (round 4 #2 + round 5 F2). It defeats:
|
|
37
|
-
# - PATH-attacker hijack via fake `rea` binary
|
|
38
|
-
# - node_modules/.bin/rea symlink-bin hijack
|
|
39
|
-
# - node_modules/@bookedsolid/rea -> /tmp/sym-attacker symlink-out
|
|
40
|
-
# - intra-project hijack without a matching package.json
|
|
41
|
-
# It does NOT defeat an attacker who writes a forged dist/cli/index.js
|
|
42
|
-
# AND a matching package.json directly into node_modules/. At that level
|
|
43
|
-
# the attacker has already compromised the package install pipeline (npm
|
|
44
|
-
# registry, lockfile, dependency confusion) and any dependency the agent
|
|
45
|
-
# imports is also forgeable — hook-tier defense is past. The trust
|
|
46
|
-
# boundary is package-tier integrity (npm provenance + manifest
|
|
47
|
-
# verification), not the bash gate. See THREAT_MODEL §8.3 + docs/
|
|
48
|
-
# architecture/bash-scanner.md for the full rationale.
|
|
22
|
+
# # Fail-closed posture
|
|
49
23
|
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# where the attacker writes `node_modules/@bookedsolid/rea` as a
|
|
55
|
-
# symlink to a tree under `/tmp/sym-attacker` containing a forged
|
|
56
|
-
# `package.json` with name `@bookedsolid/rea` and a forged
|
|
57
|
-
# `dist/cli/index.js` that exits 0 with `{"verdict":"allow"}`. Pre-fix
|
|
58
|
-
# the secondary check (package.json walk-up) was the ONLY guard, and
|
|
59
|
-
# the attacker satisfies it by placing a forged package.json in their
|
|
60
|
-
# own tree.
|
|
61
|
-
# SECONDARY: walk up from the resolved CLI looking for an ancestor
|
|
62
|
-
# `package.json` whose `name` is `@bookedsolid/rea`. This guards
|
|
63
|
-
# against intra-project symlinks where the realpath stays inside
|
|
64
|
-
# the project (e.g. accidentally pointing dist/ at node_modules/).
|
|
24
|
+
# protected-paths-bash-gate is a Tier-1 security gate. The pre-0.35.0
|
|
25
|
+
# bash body refused on uncertainty. Early-exit branches fail closed
|
|
26
|
+
# AFTER the relevance pre-gate passes. Irrelevant Bash calls exit 0
|
|
27
|
+
# regardless of CLI state.
|
|
65
28
|
#
|
|
66
|
-
#
|
|
67
|
-
# Test harnesses must set CLAUDE_PROJECT_DIR to a directory whose
|
|
68
|
-
# `dist/cli/index.js` (or `node_modules/@bookedsolid/rea/...`) holds
|
|
69
|
-
# the trusted CLI build. The shim NEVER reads REA_NODE_CLI.
|
|
29
|
+
# # Relevance pre-gate
|
|
70
30
|
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
31
|
+
# Substring scan over the extracted command for any of the protected-
|
|
32
|
+
# path markers: .claude/, .husky/, .rea/policy.yaml, .rea/HALT, the
|
|
33
|
+
# verdict cache paths. When the CLI is missing AND none of these
|
|
34
|
+
# substrings appear, exit 0 (the pre-0.35.0 bash body would have
|
|
35
|
+
# allowed). When the CLI is missing AND a marker DOES match, preserve
|
|
36
|
+
# fail-closed.
|
|
74
37
|
|
|
75
38
|
set -uo pipefail
|
|
76
39
|
|
|
77
|
-
|
|
40
|
+
# 1. HALT check.
|
|
41
|
+
# shellcheck source=_lib/halt-check.sh
|
|
42
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
43
|
+
check_halt
|
|
44
|
+
REA_ROOT=$(rea_root)
|
|
78
45
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
#
|
|
85
|
-
# We build an `argv` array rather than a string so paths containing
|
|
86
|
-
# whitespace round-trip safely.
|
|
46
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
47
|
+
|
|
48
|
+
# 2. Capture stdin once.
|
|
49
|
+
INPUT=$(cat)
|
|
50
|
+
|
|
51
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
87
52
|
REA_ARGV=()
|
|
88
53
|
RESOLVED_CLI_PATH=""
|
|
89
54
|
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
90
55
|
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
91
56
|
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
92
57
|
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
93
|
-
# rea repo dogfood: the project IS @bookedsolid/rea.
|
|
94
58
|
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
95
59
|
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
96
60
|
fi
|
|
97
61
|
|
|
62
|
+
# 3b. Relevance pre-gate. Only used when the CLI is missing.
|
|
98
63
|
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
64
|
+
CLI_MISSING_CMD=""
|
|
65
|
+
if command -v jq >/dev/null 2>&1; then
|
|
66
|
+
CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
|
|
67
|
+
(.tool_input.command // "") | tostring
|
|
68
|
+
' 2>/dev/null || true)
|
|
69
|
+
else
|
|
70
|
+
CLI_MISSING_CMD="$INPUT"
|
|
71
|
+
fi
|
|
72
|
+
if [ -z "$CLI_MISSING_CMD" ]; then
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
CLI_MISSING_RELEVANT=0
|
|
76
|
+
case "$CLI_MISSING_CMD" in
|
|
77
|
+
*".claude/"*) CLI_MISSING_RELEVANT=1 ;;
|
|
78
|
+
*".husky/"*) CLI_MISSING_RELEVANT=1 ;;
|
|
79
|
+
*".rea/policy.yaml"*) CLI_MISSING_RELEVANT=1 ;;
|
|
80
|
+
*".rea/HALT"*) CLI_MISSING_RELEVANT=1 ;;
|
|
81
|
+
*".rea/last-review"*) CLI_MISSING_RELEVANT=1 ;;
|
|
82
|
+
*".claude\\"*|*".husky\\"*|*".rea\\"*) CLI_MISSING_RELEVANT=1 ;;
|
|
83
|
+
esac
|
|
84
|
+
# Codex round-1 P2 fix: scan policy.protected_writes entries too so a
|
|
85
|
+
# consumer-defined protected path isn't silently allowed when the CLI
|
|
86
|
+
# is missing. Read the policy via the same awk parser the consumer-
|
|
87
|
+
# facing relevance pre-gates use for blocked_paths.
|
|
88
|
+
if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
|
|
89
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
90
|
+
if [ -f "$POLICY_FILE" ]; then
|
|
91
|
+
while IFS= read -r entry; do
|
|
92
|
+
[ -z "$entry" ] && continue
|
|
93
|
+
base="$entry"
|
|
94
|
+
case "$base" in
|
|
95
|
+
*/) base="${base%/}" ;;
|
|
96
|
+
esac
|
|
97
|
+
[ -z "$base" ] && continue
|
|
98
|
+
case "$CLI_MISSING_CMD" in
|
|
99
|
+
*"$base"*) CLI_MISSING_RELEVANT=1; break ;;
|
|
100
|
+
esac
|
|
101
|
+
done < <(awk '
|
|
102
|
+
/^protected_writes:/ { in_block=1; next }
|
|
103
|
+
in_block && /^[[:space:]]*-/ {
|
|
104
|
+
sub(/^[[:space:]]*-[[:space:]]*/, "")
|
|
105
|
+
gsub(/^["'\'']/, "")
|
|
106
|
+
gsub(/["'\'']$/, "")
|
|
107
|
+
print
|
|
108
|
+
next
|
|
109
|
+
}
|
|
110
|
+
in_block && /^[^[:space:]-]/ { in_block=0 }
|
|
111
|
+
' "$POLICY_FILE" 2>/dev/null)
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
printf 'rea: protected-paths-bash-gate cannot run — the rea CLI is not built.\n' >&2
|
|
118
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
119
|
+
printf 'This shim fails closed because the pre-0.35.0 bash body enforced protected-path refusal without a CLI.\n' >&2
|
|
102
120
|
exit 2
|
|
103
121
|
fi
|
|
104
122
|
|
|
105
|
-
#
|
|
106
|
-
# verify it lives inside the sandboxed dirs. Catches symlink games.
|
|
107
|
-
# We require Node for the verifier anyway (below) — use it here too.
|
|
123
|
+
# 4. Realpath sandbox check.
|
|
108
124
|
if ! command -v node >/dev/null 2>&1; then
|
|
109
|
-
printf 'rea:
|
|
125
|
+
printf 'rea: protected-paths-bash-gate cannot run — `node` is not on PATH.\n' >&2
|
|
126
|
+
printf 'Install Node 22+ (engines.node) to restore protected-path refusal.\n' >&2
|
|
110
127
|
exit 2
|
|
111
128
|
fi
|
|
129
|
+
|
|
112
130
|
sandbox_check=$(node -e '
|
|
113
131
|
const fs = require("fs");
|
|
114
132
|
const path = require("path");
|
|
115
133
|
const cli = process.argv[1];
|
|
116
134
|
const projDir = process.argv[2];
|
|
117
|
-
let real;
|
|
135
|
+
let real, realProj;
|
|
118
136
|
try { real = fs.realpathSync(cli); } catch (e) {
|
|
119
|
-
process.stdout.write("bad:realpath
|
|
120
|
-
process.exit(1);
|
|
137
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
121
138
|
}
|
|
122
|
-
// Codex round 5 F2 (P0) — PRIMARY check: realpath(cli) must live
|
|
123
|
-
// INSIDE realpath(CLAUDE_PROJECT_DIR). Pre-fix an attacker who
|
|
124
|
-
// writes a symlink at node_modules/@bookedsolid/rea pointing to
|
|
125
|
-
// /tmp/sym-attacker (containing a forged package.json + dist/cli/index.js
|
|
126
|
-
// that exits 0 with verdict:"allow") completely defeated the
|
|
127
|
-
// sandbox — the package.json walk-up was satisfied by the forged
|
|
128
|
-
// file in the attacker tree. Containing realCli to realProj closes
|
|
129
|
-
// every symlink-out attack class because the attacker no longer
|
|
130
|
-
// controls a path inside the project tree.
|
|
131
|
-
let realProj;
|
|
132
139
|
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
133
|
-
process.stdout.write("bad:realpath-proj
|
|
134
|
-
process.exit(1);
|
|
140
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
135
141
|
}
|
|
136
|
-
const
|
|
142
|
+
const sep = path.sep;
|
|
143
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
137
144
|
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
138
|
-
process.stdout.write("bad:cli-escapes-project
|
|
139
|
-
process.exit(1);
|
|
145
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
140
146
|
}
|
|
141
|
-
// Codex round
|
|
142
|
-
//
|
|
143
|
-
// Acceptance: the resolved CLI must end in `.../dist/cli/index.js`
|
|
144
|
-
// and have an ancestor `package.json` whose `name` is `@bookedsolid/rea`.
|
|
145
|
-
// This guards against intra-project hijack where an attacker writes
|
|
146
|
-
// a symlink at node_modules/@bookedsolid/rea pointing to a sibling
|
|
147
|
-
// tree INSIDE the project (e.g. ./scratch/) — the PRIMARY check
|
|
148
|
-
// accepts it (still inside project root) but the package.json walk-up
|
|
149
|
-
// refuses unless that tree contains the canonical package metadata.
|
|
147
|
+
// Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
|
|
148
|
+
// settings-protection.sh).
|
|
150
149
|
const expectedEnd = path.join("dist", "cli", "index.js");
|
|
151
150
|
if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
|
|
152
|
-
process.stdout.write("bad:cli-shape
|
|
153
|
-
process.exit(1);
|
|
151
|
+
process.stdout.write("bad:cli-shape"); process.exit(1);
|
|
154
152
|
}
|
|
155
|
-
|
|
156
|
-
let cur = path.dirname(path.dirname(path.dirname(real))); // pkg root
|
|
153
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
157
154
|
let found = false;
|
|
158
155
|
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
159
156
|
const pj = path.join(cur, "package.json");
|
|
160
157
|
if (fs.existsSync(pj)) {
|
|
161
158
|
try {
|
|
162
159
|
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
163
|
-
if (data && data.name === "@bookedsolid/rea") {
|
|
164
|
-
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
} catch (e) {
|
|
168
|
-
// Continue walking up.
|
|
169
|
-
}
|
|
160
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
161
|
+
} catch (e) { /* keep walking */ }
|
|
170
162
|
}
|
|
171
163
|
cur = path.dirname(cur);
|
|
172
164
|
}
|
|
173
|
-
if (!found) {
|
|
174
|
-
process.stdout.write("bad:no-rea-pkg:" + real);
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
165
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
177
166
|
process.stdout.write("ok");
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
printf 'rea: scan-bash CLI realpath escapes sandbox (%s). Refusing.\n' "$sandbox_check" >&2
|
|
167
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
168
|
+
|
|
169
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
170
|
+
printf 'rea: protected-paths-bash-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
183
171
|
exit 2
|
|
184
172
|
fi
|
|
185
173
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
# CLI is older than 0.23.0 it will refuse with "unknown command" and the
|
|
189
|
-
# shim's exit-code dispatch lands on the catch-all "exit 2" branch
|
|
190
|
-
# WITHOUT explaining why. That was the symptom that locked Jake's
|
|
191
|
-
# helix workspace out of every Bash tool until he ran `pnpm install`.
|
|
192
|
-
#
|
|
193
|
-
# The probe runs `rea hook scan-bash --help` once per shim invocation
|
|
194
|
-
# (~30 LOC) and refuses with an actionable message if the subcommand
|
|
195
|
-
# does not exist. Probe failure is fail-closed (exit 2) — same posture
|
|
196
|
-
# the rest of the shim takes — but the message tells the operator
|
|
197
|
-
# exactly what to do (`pnpm install`).
|
|
198
|
-
probe_out=$("${REA_ARGV[@]}" hook scan-bash --help 2>&1)
|
|
174
|
+
# 5. Version-probe.
|
|
175
|
+
probe_out=$("${REA_ARGV[@]}" hook protected-paths-bash-gate --help 2>&1)
|
|
199
176
|
probe_status=$?
|
|
200
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e '
|
|
201
|
-
printf 'rea: this shim requires the `rea hook
|
|
177
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'protected-paths-bash-gate'; then
|
|
178
|
+
printf 'rea: this shim requires the `rea hook protected-paths-bash-gate` subcommand (introduced in 0.35.0).\n' >&2
|
|
202
179
|
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
203
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI
|
|
180
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
204
181
|
exit 2
|
|
205
182
|
fi
|
|
206
183
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
exit 0
|
|
211
|
-
fi
|
|
212
|
-
|
|
213
|
-
# Run the scanner.
|
|
214
|
-
verdict=$(printf '%s' "$payload" | "${REA_ARGV[@]}" hook scan-bash --mode protected)
|
|
215
|
-
status=$?
|
|
216
|
-
|
|
217
|
-
# Defense in depth — verify the verdict JSON matches the exit code.
|
|
218
|
-
verifier='try {
|
|
219
|
-
const raw = require("fs").readFileSync(0, "utf8");
|
|
220
|
-
if (raw.trim().length === 0) { process.stdout.write("bad:empty"); process.exit(1); }
|
|
221
|
-
const v = JSON.parse(raw);
|
|
222
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
223
|
-
process.stdout.write("bad:non-object"); process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
if (v.verdict !== "allow" && v.verdict !== "block") {
|
|
226
|
-
process.stdout.write("bad:verdict-shape:" + String(v.verdict)); process.exit(1);
|
|
227
|
-
}
|
|
228
|
-
process.stdout.write("ok:" + v.verdict); process.exit(0);
|
|
229
|
-
} catch (e) {
|
|
230
|
-
process.stdout.write("bad:" + (e && e.message ? e.message : String(e))); process.exit(1);
|
|
231
|
-
}'
|
|
232
|
-
|
|
233
|
-
verdict_check=$(printf '%s' "$verdict" | node -e "$verifier" 2>&1)
|
|
234
|
-
verdict_check_status=$?
|
|
235
|
-
|
|
236
|
-
case "$status" in
|
|
237
|
-
0)
|
|
238
|
-
if [ "$verdict_check_status" -ne 0 ]; then
|
|
239
|
-
printf 'rea: scan-bash exited 0 but verdict JSON is malformed (%s). Refusing on uncertainty.\n' "$verdict_check" >&2
|
|
240
|
-
exit 2
|
|
241
|
-
fi
|
|
242
|
-
if [ "$verdict_check" != "ok:allow" ]; then
|
|
243
|
-
printf 'rea: scan-bash exit 0 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
|
|
244
|
-
exit 2
|
|
245
|
-
fi
|
|
246
|
-
exit 0
|
|
247
|
-
;;
|
|
248
|
-
2)
|
|
249
|
-
# Block path — the CLI has already emitted the operator-facing
|
|
250
|
-
# reason on stderr. We additionally verify the JSON shape so a
|
|
251
|
-
# forged `/bin/true` (which would never reach here, but be defensive)
|
|
252
|
-
# cannot bypass.
|
|
253
|
-
if [ "$verdict_check_status" -ne 0 ]; then
|
|
254
|
-
# Malformed stdout under exit 2 is unusual but harmless — the
|
|
255
|
-
# block path is still honored.
|
|
256
|
-
exit 2
|
|
257
|
-
fi
|
|
258
|
-
if [ "$verdict_check" != "ok:block" ]; then
|
|
259
|
-
printf 'rea: scan-bash exit 2 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
|
|
260
|
-
exit 2
|
|
261
|
-
fi
|
|
262
|
-
exit 2
|
|
263
|
-
;;
|
|
264
|
-
*)
|
|
265
|
-
# Unexpected exit code — treat as block on uncertainty. The CLI
|
|
266
|
-
# writes its own diagnostic; we add an explicit refusal.
|
|
267
|
-
printf 'rea: scan-bash exited %d (expected 0/2). Refusing on uncertainty.\n' "$status" >&2
|
|
268
|
-
if [ -n "$verdict" ]; then
|
|
269
|
-
printf 'rea: scan-bash stdout was: %s\n' "$verdict" >&2
|
|
270
|
-
fi
|
|
271
|
-
exit 2
|
|
272
|
-
;;
|
|
273
|
-
esac
|
|
184
|
+
# 6. Forward stdin (already captured up-front).
|
|
185
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook protected-paths-bash-gate
|
|
186
|
+
exit $?
|