@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.
- package/README.md +15 -0
- package/THREAT_MODEL.md +582 -0
- package/dist/audit/append.js +1 -1
- package/dist/cli/doctor.js +11 -12
- package/dist/cli/hook.d.ts +37 -3
- package/dist/cli/hook.js +167 -5
- package/dist/cli/init.js +14 -26
- package/dist/cli/install/canonical.js +18 -3
- package/dist/cli/install/commit-msg.js +1 -2
- package/dist/cli/install/copy.js +4 -13
- package/dist/cli/install/fs-safe.js +5 -16
- package/dist/cli/install/gitignore.js +1 -5
- package/dist/cli/install/pre-push.js +3 -8
- package/dist/cli/install/settings-merge.js +79 -16
- package/dist/cli/upgrade.js +14 -10
- package/dist/gateway/downstream.js +1 -2
- package/dist/gateway/live-state.js +3 -1
- package/dist/gateway/log.js +1 -3
- package/dist/gateway/middleware/audit.js +1 -1
- package/dist/gateway/middleware/injection.js +3 -9
- package/dist/gateway/middleware/policy.js +3 -1
- package/dist/gateway/middleware/redact.js +1 -1
- package/dist/gateway/observability/codex-telemetry.js +1 -2
- package/dist/gateway/reviewers/claude-self.js +10 -6
- package/dist/hooks/bash-scanner/blocked-scan.d.ts +26 -0
- package/dist/hooks/bash-scanner/blocked-scan.js +467 -0
- package/dist/hooks/bash-scanner/index.d.ts +41 -0
- package/dist/hooks/bash-scanner/index.js +62 -0
- package/dist/hooks/bash-scanner/parse-fail-closed.d.ts +31 -0
- package/dist/hooks/bash-scanner/parse-fail-closed.js +27 -0
- package/dist/hooks/bash-scanner/parser.d.ts +42 -0
- package/dist/hooks/bash-scanner/parser.js +92 -0
- package/dist/hooks/bash-scanner/protected-scan.d.ts +76 -0
- package/dist/hooks/bash-scanner/protected-scan.js +815 -0
- package/dist/hooks/bash-scanner/verdict.d.ts +80 -0
- package/dist/hooks/bash-scanner/verdict.js +49 -0
- package/dist/hooks/bash-scanner/walker.d.ts +165 -0
- package/dist/hooks/bash-scanner/walker.js +7954 -0
- package/dist/hooks/push-gate/base.js +2 -6
- package/dist/hooks/push-gate/codex-runner.js +3 -1
- package/dist/hooks/push-gate/index.js +9 -10
- package/dist/policy/loader.js +4 -1
- package/dist/registry/tofu-gate.js +2 -2
- package/hooks/_lib/cmd-segments.sh +32 -7
- package/hooks/_lib/interpreter-scanner.sh +71 -0
- package/hooks/_lib/path-normalize.sh +30 -3
- package/hooks/blocked-paths-bash-gate.sh +141 -277
- package/hooks/protected-paths-bash-gate.sh +230 -326
- package/package.json +3 -2
- package/profiles/bst-internal-no-codex.yaml +1 -1
- package/profiles/bst-internal.yaml +1 -1
- package/profiles/client-engagement.yaml +1 -1
- package/profiles/lit-wc.yaml +1 -1
- package/profiles/minimal.yaml +1 -1
- package/profiles/open-source-no-codex.yaml +1 -1
- package/profiles/open-source.yaml +1 -1
- package/scripts/postinstall.mjs +1 -2
- 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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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
|
-
#
|
|
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 =
|
|
29
|
-
# 2 =
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
52
|
-
printf '
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 [
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
exit 0
|
|
145
|
+
;;
|
|
146
|
+
2)
|
|
147
|
+
if [ "$verdict_check_status" -ne 0 ]; then
|
|
148
|
+
exit 2
|
|
108
149
|
fi
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
exit
|
|
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
|