@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,26 +1,58 @@
|
|
|
1
|
-
#!/
|
|
1
|
+
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: blocked-paths-bash-gate.sh
|
|
3
|
+
# 0.35.0+ — Node-binary shim for `rea hook blocked-paths-bash-gate`.
|
|
3
4
|
#
|
|
4
|
-
# 0.
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Pre-0.35.0 this was a thin bash shim over `rea hook scan-bash --mode
|
|
6
|
+
# blocked` (the parser-backed AST walker that closes 9 bypass classes
|
|
7
|
+
# from helix-023 + discord-ops Round 13 — see `src/hooks/bash-scanner/`).
|
|
8
|
+
# The full bash body is preserved at
|
|
9
|
+
# `__tests__/hooks/parity/baselines/blocked-paths-bash-gate.sh.pre-0.35.0`.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
# and
|
|
11
|
-
#
|
|
11
|
+
# This shim now resolves the CLI through the same 2-tier sandboxed
|
|
12
|
+
# resolver as the 0.32.0+ pilots and calls `rea hook blocked-paths-
|
|
13
|
+
# bash-gate` directly — eliminating the shim → CLI → scanner-module
|
|
14
|
+
# subprocess hop entirely.
|
|
12
15
|
#
|
|
13
|
-
#
|
|
16
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on allow,
|
|
17
|
+
# exit 2 on HALT / verdict block / malformed payload / sandbox fail.
|
|
14
18
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
19
|
+
# # CLI-resolution trust boundary
|
|
20
|
+
#
|
|
21
|
+
# Mirrors the 0.32.0 final shim shape. The resolved CLI MUST live
|
|
22
|
+
# INSIDE realpath(CLAUDE_PROJECT_DIR) AND have an ancestor
|
|
23
|
+
# `package.json` whose `name` is `@bookedsolid/rea`. Defends against
|
|
24
|
+
# symlink-out and tarball-replacement attacks on the resolved CLI.
|
|
25
|
+
#
|
|
26
|
+
# # Fail-closed posture
|
|
27
|
+
#
|
|
28
|
+
# blocked-paths-bash-gate is a Tier-1 security gate (PreToolUse Bash).
|
|
29
|
+
# The pre-0.35.0 bash body refused on uncertainty for every failure
|
|
30
|
+
# class. Early-exit branches (CLI missing, node missing, sandbox failed,
|
|
31
|
+
# version skew) fail closed AFTER the relevance pre-gate passes.
|
|
32
|
+
# Irrelevant Bash calls exit 0 regardless of CLI state.
|
|
33
|
+
#
|
|
34
|
+
# # Relevance pre-gate
|
|
35
|
+
#
|
|
36
|
+
# Same posture as 0.34.0 dangerous-bash + secret-scanner. When the CLI
|
|
37
|
+
# is missing, refuse only when the extracted command MENTIONS a path
|
|
38
|
+
# from `policy.blocked_paths`. Empty policy → no enforcement, exit 0.
|
|
39
|
+
# This unblocks the install path itself: `npx rea init`, pre-`pnpm build`
|
|
40
|
+
# checkouts can still run benign Bash like `ls`/`mkdir`/`pnpm install`.
|
|
18
41
|
|
|
19
42
|
set -uo pipefail
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
# 1. HALT check.
|
|
45
|
+
# shellcheck source=_lib/halt-check.sh
|
|
46
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
47
|
+
check_halt
|
|
48
|
+
REA_ROOT=$(rea_root)
|
|
49
|
+
|
|
50
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
22
51
|
|
|
23
|
-
# 2
|
|
52
|
+
# 2. Capture stdin once.
|
|
53
|
+
INPUT=$(cat)
|
|
54
|
+
|
|
55
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
24
56
|
REA_ARGV=()
|
|
25
57
|
RESOLVED_CLI_PATH=""
|
|
26
58
|
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
@@ -31,49 +63,83 @@ elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
|
31
63
|
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
32
64
|
fi
|
|
33
65
|
|
|
66
|
+
# 3b. Relevance pre-gate. Only used when the CLI is missing.
|
|
34
67
|
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
CLI_MISSING_CMD=""
|
|
69
|
+
if command -v jq >/dev/null 2>&1; then
|
|
70
|
+
CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
|
|
71
|
+
(.tool_input.command // "") | tostring
|
|
72
|
+
' 2>/dev/null || true)
|
|
73
|
+
else
|
|
74
|
+
CLI_MISSING_CMD="$INPUT"
|
|
75
|
+
fi
|
|
76
|
+
if [ -z "$CLI_MISSING_CMD" ]; then
|
|
77
|
+
# Empty/non-Bash payload → pre-0.35.0 body would have exited 0.
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
# Empty policy.blocked_paths → no enforcement, exit 0.
|
|
81
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
82
|
+
if [ ! -f "$POLICY_FILE" ]; then
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
# Substring scan: does the command mention any blocked_paths entry?
|
|
86
|
+
# Coarse — over-trigger is fine, under-trigger is the bypass we MUST
|
|
87
|
+
# avoid. Strip YAML quotes/comments via a minimal awk filter.
|
|
88
|
+
CLI_MISSING_RELEVANT=0
|
|
89
|
+
while IFS= read -r entry; do
|
|
90
|
+
[ -z "$entry" ] && continue
|
|
91
|
+
case "$CLI_MISSING_CMD" in
|
|
92
|
+
*"$entry"*) CLI_MISSING_RELEVANT=1; break ;;
|
|
93
|
+
esac
|
|
94
|
+
done < <(awk '
|
|
95
|
+
/^blocked_paths:/ { in_block=1; next }
|
|
96
|
+
in_block && /^[[:space:]]*-/ {
|
|
97
|
+
sub(/^[[:space:]]*-[[:space:]]*/, "")
|
|
98
|
+
gsub(/^["'\'']/, "")
|
|
99
|
+
gsub(/["'\'']$/, "")
|
|
100
|
+
print
|
|
101
|
+
next
|
|
102
|
+
}
|
|
103
|
+
in_block && /^[^[:space:]-]/ { in_block=0 }
|
|
104
|
+
' "$POLICY_FILE" 2>/dev/null)
|
|
105
|
+
if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
|
|
106
|
+
exit 0
|
|
107
|
+
fi
|
|
108
|
+
printf 'rea: blocked-paths-bash-gate cannot run — the rea CLI is not built.\n' >&2
|
|
109
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
110
|
+
printf 'This shim fails closed because the pre-0.35.0 bash body enforced blocked_paths refusal without a CLI.\n' >&2
|
|
38
111
|
exit 2
|
|
39
112
|
fi
|
|
40
113
|
|
|
41
|
-
#
|
|
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.
|
|
114
|
+
# 4. Realpath sandbox check.
|
|
45
115
|
if ! command -v node >/dev/null 2>&1; then
|
|
46
|
-
printf 'rea:
|
|
116
|
+
printf 'rea: blocked-paths-bash-gate cannot run — `node` is not on PATH.\n' >&2
|
|
117
|
+
printf 'Install Node 22+ (engines.node) to restore blocked_paths refusal.\n' >&2
|
|
47
118
|
exit 2
|
|
48
119
|
fi
|
|
120
|
+
|
|
49
121
|
sandbox_check=$(node -e '
|
|
50
122
|
const fs = require("fs");
|
|
51
123
|
const path = require("path");
|
|
52
124
|
const cli = process.argv[1];
|
|
53
125
|
const projDir = process.argv[2];
|
|
54
|
-
let real;
|
|
126
|
+
let real, realProj;
|
|
55
127
|
try { real = fs.realpathSync(cli); } catch (e) {
|
|
56
|
-
process.stdout.write("bad:realpath
|
|
57
|
-
process.exit(1);
|
|
128
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
58
129
|
}
|
|
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
130
|
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
63
|
-
process.stdout.write("bad:realpath-proj
|
|
64
|
-
process.exit(1);
|
|
131
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
65
132
|
}
|
|
66
|
-
const
|
|
133
|
+
const sep = path.sep;
|
|
134
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
67
135
|
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
68
|
-
process.stdout.write("bad:cli-escapes-project
|
|
69
|
-
process.exit(1);
|
|
136
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
70
137
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
138
|
+
// Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
|
|
139
|
+
// settings-protection.sh).
|
|
73
140
|
const expectedEnd = path.join("dist", "cli", "index.js");
|
|
74
141
|
if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
|
|
75
|
-
process.stdout.write("bad:cli-shape
|
|
76
|
-
process.exit(1);
|
|
142
|
+
process.stdout.write("bad:cli-shape"); process.exit(1);
|
|
77
143
|
}
|
|
78
144
|
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
79
145
|
let found = false;
|
|
@@ -82,94 +148,30 @@ sandbox_check=$(node -e '
|
|
|
82
148
|
if (fs.existsSync(pj)) {
|
|
83
149
|
try {
|
|
84
150
|
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
85
|
-
if (data && data.name === "@bookedsolid/rea") {
|
|
86
|
-
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
} catch (e) {
|
|
90
|
-
// Continue.
|
|
91
|
-
}
|
|
151
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
152
|
+
} catch (e) { /* keep walking */ }
|
|
92
153
|
}
|
|
93
154
|
cur = path.dirname(cur);
|
|
94
155
|
}
|
|
95
|
-
if (!found) {
|
|
96
|
-
process.stdout.write("bad:no-rea-pkg:" + real);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
156
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
99
157
|
process.stdout.write("ok");
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
printf 'rea: scan-bash CLI realpath escapes sandbox (%s). Refusing.\n' "$sandbox_check" >&2
|
|
158
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
159
|
+
|
|
160
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
161
|
+
printf 'rea: blocked-paths-bash-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
105
162
|
exit 2
|
|
106
163
|
fi
|
|
107
164
|
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
# shim mirrors the behavior to detect a stale CLI before payload reach.
|
|
111
|
-
probe_out=$("${REA_ARGV[@]}" hook scan-bash --help 2>&1)
|
|
165
|
+
# 5. Version-probe.
|
|
166
|
+
probe_out=$("${REA_ARGV[@]}" hook blocked-paths-bash-gate --help 2>&1)
|
|
112
167
|
probe_status=$?
|
|
113
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e '
|
|
114
|
-
printf 'rea: this shim requires the `rea hook
|
|
168
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'blocked-paths-bash-gate'; then
|
|
169
|
+
printf 'rea: this shim requires the `rea hook blocked-paths-bash-gate` subcommand (introduced in 0.35.0).\n' >&2
|
|
115
170
|
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
116
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI
|
|
171
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
117
172
|
exit 2
|
|
118
173
|
fi
|
|
119
174
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
fi
|
|
124
|
-
|
|
125
|
-
verdict=$(printf '%s' "$payload" | "${REA_ARGV[@]}" hook scan-bash --mode blocked)
|
|
126
|
-
status=$?
|
|
127
|
-
|
|
128
|
-
verifier='try {
|
|
129
|
-
const raw = require("fs").readFileSync(0, "utf8");
|
|
130
|
-
if (raw.trim().length === 0) { process.stdout.write("bad:empty"); process.exit(1); }
|
|
131
|
-
const v = JSON.parse(raw);
|
|
132
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
133
|
-
process.stdout.write("bad:non-object"); process.exit(1);
|
|
134
|
-
}
|
|
135
|
-
if (v.verdict !== "allow" && v.verdict !== "block") {
|
|
136
|
-
process.stdout.write("bad:verdict-shape:" + String(v.verdict)); process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
process.stdout.write("ok:" + v.verdict); process.exit(0);
|
|
139
|
-
} catch (e) {
|
|
140
|
-
process.stdout.write("bad:" + (e && e.message ? e.message : String(e))); process.exit(1);
|
|
141
|
-
}'
|
|
142
|
-
|
|
143
|
-
verdict_check=$(printf '%s' "$verdict" | node -e "$verifier" 2>&1)
|
|
144
|
-
verdict_check_status=$?
|
|
145
|
-
|
|
146
|
-
case "$status" in
|
|
147
|
-
0)
|
|
148
|
-
if [ "$verdict_check_status" -ne 0 ]; then
|
|
149
|
-
printf 'rea: scan-bash exited 0 but verdict JSON is malformed (%s). Refusing on uncertainty.\n' "$verdict_check" >&2
|
|
150
|
-
exit 2
|
|
151
|
-
fi
|
|
152
|
-
if [ "$verdict_check" != "ok:allow" ]; then
|
|
153
|
-
printf 'rea: scan-bash exit 0 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
|
|
154
|
-
exit 2
|
|
155
|
-
fi
|
|
156
|
-
exit 0
|
|
157
|
-
;;
|
|
158
|
-
2)
|
|
159
|
-
if [ "$verdict_check_status" -ne 0 ]; then
|
|
160
|
-
exit 2
|
|
161
|
-
fi
|
|
162
|
-
if [ "$verdict_check" != "ok:block" ]; then
|
|
163
|
-
printf 'rea: scan-bash exit 2 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
|
|
164
|
-
exit 2
|
|
165
|
-
fi
|
|
166
|
-
exit 2
|
|
167
|
-
;;
|
|
168
|
-
*)
|
|
169
|
-
printf 'rea: scan-bash exited %d (expected 0/2). Refusing on uncertainty.\n' "$status" >&2
|
|
170
|
-
if [ -n "$verdict" ]; then
|
|
171
|
-
printf 'rea: scan-bash stdout was: %s\n' "$verdict" >&2
|
|
172
|
-
fi
|
|
173
|
-
exit 2
|
|
174
|
-
;;
|
|
175
|
-
esac
|
|
175
|
+
# 6. Forward stdin (already captured up-front).
|
|
176
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook blocked-paths-bash-gate
|
|
177
|
+
exit $?
|