@bookedsolid/rea 0.22.0 → 0.23.1
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 +753 -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 +868 -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 +9087 -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/blocked-paths-bash-gate.sh +142 -272
- package/hooks/protected-paths-bash-gate.sh +227 -511
- 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
|
@@ -55,9 +55,7 @@ export function resolveBaseRef(git, options = {}) {
|
|
|
55
55
|
// some-other-branch` invocations, where the local checkout's HEAD is a
|
|
56
56
|
// different branch entirely and the resulting diff would compare the
|
|
57
57
|
// wrong commits.
|
|
58
|
-
const headRef = options.headRef !== undefined && options.headRef.length > 0
|
|
59
|
-
? options.headRef
|
|
60
|
-
: 'HEAD';
|
|
58
|
+
const headRef = options.headRef !== undefined && options.headRef.length > 0 ? options.headRef : 'HEAD';
|
|
61
59
|
const requested = options.lastNCommits;
|
|
62
60
|
const tryDepth = (k) => git.tryRevParse(['--verify', '--quiet', `${headRef}~${k}^{commit}`]).trim();
|
|
63
61
|
// Fast path: requested depth resolves directly.
|
|
@@ -164,9 +162,7 @@ export function resolveBaseRef(git, options = {}) {
|
|
|
164
162
|
// tracking ref (typically `refs/remotes/origin/<branch>`). Returns
|
|
165
163
|
// empty on branches without an upstream — which is normal for a brand
|
|
166
164
|
// new feature branch; fall through.
|
|
167
|
-
const upstream = git
|
|
168
|
-
.tryRevParse(['--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
|
|
169
|
-
.trim();
|
|
165
|
+
const upstream = git.tryRevParse(['--abbrev-ref', '--symbolic-full-name', '@{upstream}']).trim();
|
|
170
166
|
if (upstream.length > 0) {
|
|
171
167
|
return { ref: upstream, source: 'upstream' };
|
|
172
168
|
}
|
|
@@ -190,7 +190,9 @@ export async function runCodexReview(options) {
|
|
|
190
190
|
'--json',
|
|
191
191
|
'--ephemeral',
|
|
192
192
|
];
|
|
193
|
-
const args = options.prompt !== undefined && options.prompt.length > 0
|
|
193
|
+
const args = options.prompt !== undefined && options.prompt.length > 0
|
|
194
|
+
? [...baseArgs, options.prompt]
|
|
195
|
+
: baseArgs;
|
|
194
196
|
// 0.16.3 helix-016.1 #1 fix: pre-flight probe for the codex CLI before
|
|
195
197
|
// we hand control to the long-running review subprocess. The original
|
|
196
198
|
// try/catch around `spawner(...)` only caught synchronous ENOENT; on
|
|
@@ -31,7 +31,7 @@ import { resolveBaseRef } from './base.js';
|
|
|
31
31
|
import { createRealGitExecutor, runCodexReview, CodexNotInstalledError, CodexProtocolError, CodexSubprocessError, CodexTimeoutError, IRON_GATE_DEFAULT_MODEL, IRON_GATE_DEFAULT_REASONING, } from './codex-runner.js';
|
|
32
32
|
import { summarizeReview } from './findings.js';
|
|
33
33
|
import { renderBanner, writeLastReview } from './report.js';
|
|
34
|
-
import { isFlip, lookupVerdict, writeVerdict
|
|
34
|
+
import { isFlip, lookupVerdict, writeVerdict } from './verdict-cache.js';
|
|
35
35
|
/**
|
|
36
36
|
* Parse the raw pre-push stdin text into refspecs. Each line is four
|
|
37
37
|
* whitespace-separated fields. Blank lines and malformed lines are
|
|
@@ -266,7 +266,9 @@ export async function runPushGate(deps) {
|
|
|
266
266
|
}
|
|
267
267
|
if (headSha.length === 0) {
|
|
268
268
|
stderr('PUSH BLOCKED: could not resolve HEAD SHA. Is this a valid git repo?\n');
|
|
269
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, fullPolicy, {
|
|
269
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, fullPolicy, {
|
|
270
|
+
kind: 'head-sha-missing',
|
|
271
|
+
});
|
|
270
272
|
return { status: 'error', exitCode: 2, summary: 'head-sha-missing' };
|
|
271
273
|
}
|
|
272
274
|
// 4b. Auto-narrow probe (J / 0.13.0). When the resolved base is far
|
|
@@ -317,8 +319,7 @@ export async function runPushGate(deps) {
|
|
|
317
319
|
baseFromPushedRemoteTip;
|
|
318
320
|
if (autoNarrowEligible) {
|
|
319
321
|
originalCommitCount = git.revListCount(base.ref, headSha);
|
|
320
|
-
if (originalCommitCount !== null &&
|
|
321
|
-
originalCommitCount > policy.auto_narrow_threshold) {
|
|
322
|
+
if (originalCommitCount !== null && originalCommitCount > policy.auto_narrow_threshold) {
|
|
322
323
|
const fallbackWindow = PUSH_GATE_DEFAULT_LAST_N_COMMITS_FALLBACK;
|
|
323
324
|
const narrowed = resolveBaseRef(git, {
|
|
324
325
|
lastNCommits: fallbackWindow,
|
|
@@ -361,8 +362,8 @@ export async function runPushGate(deps) {
|
|
|
361
362
|
const cacheLookup = policy.cache_ttl_ms > 0 ? lookupVerdict(deps.baseDir, headSha) : { hit: false };
|
|
362
363
|
if (cacheLookup.hit && cacheLookup.entry !== undefined) {
|
|
363
364
|
const cached = cacheLookup.entry;
|
|
364
|
-
const cachedBlocked = cached.verdict === 'blocking'
|
|
365
|
-
|
|
365
|
+
const cachedBlocked = cached.verdict === 'blocking' ||
|
|
366
|
+
(cached.verdict === 'concerns' && policy.concerns_blocks && !isConcernsOverrideSet(env));
|
|
366
367
|
// 0.19.1 P3-3 (code-reviewer): emit EVT_CACHE_HIT (forensic detail
|
|
367
368
|
// for the cache layer specifically) AND EVT_REVIEWED (the canonical
|
|
368
369
|
// verdict event with `cache_hit: true` metadata). Operators
|
|
@@ -421,10 +422,8 @@ export async function runPushGate(deps) {
|
|
|
421
422
|
: {}),
|
|
422
423
|
});
|
|
423
424
|
const summary = summarizeReview(codexResult.reviewText);
|
|
424
|
-
const blocked = summary.verdict === 'blocking'
|
|
425
|
-
|
|
426
|
-
&& policy.concerns_blocks
|
|
427
|
-
&& !isConcernsOverrideSet(env));
|
|
425
|
+
const blocked = summary.verdict === 'blocking' ||
|
|
426
|
+
(summary.verdict === 'concerns' && policy.concerns_blocks && !isConcernsOverrideSet(env));
|
|
428
427
|
const lastReviewPath = path.join(deps.baseDir, '.rea', 'last-review.json');
|
|
429
428
|
const payload = writeLastReviewFn({
|
|
430
429
|
baseDir: deps.baseDir,
|
package/dist/policy/loader.js
CHANGED
|
@@ -63,7 +63,10 @@ const ReviewPolicySchema = z
|
|
|
63
63
|
// (NUL, NL, CR, escape sequences) through the `-c model="<value>"`
|
|
64
64
|
// injection point. Accepts published codex model names; rejects
|
|
65
65
|
// re-quote / TOML-escape edge cases.
|
|
66
|
-
codex_model: z
|
|
66
|
+
codex_model: z
|
|
67
|
+
.string()
|
|
68
|
+
.regex(/^[a-zA-Z0-9._-]{1,64}$/)
|
|
69
|
+
.optional(),
|
|
67
70
|
/**
|
|
68
71
|
* Codex reasoning effort knob (0.13.4+). Pinned via
|
|
69
72
|
* `-c model_reasoning_effort="<level>"` on every invocation. Only
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { Tier, InvocationStatus } from '../policy/types.js';
|
|
25
25
|
import { appendAuditRecord } from '../audit/append.js';
|
|
26
|
-
import { loadFingerprintStore, saveFingerprintStore
|
|
27
|
-
import { classifyServers, updateStore
|
|
26
|
+
import { loadFingerprintStore, saveFingerprintStore } from './fingerprints-store.js';
|
|
27
|
+
import { classifyServers, updateStore } from './tofu.js';
|
|
28
28
|
import { createLogger } from '../gateway/log.js';
|
|
29
29
|
const TOFU_TOOL_NAME = 'rea.tofu';
|
|
30
30
|
const TOFU_SERVER_NAME = 'rea';
|
|
@@ -1,293 +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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
52
33
|
|
|
53
|
-
if
|
|
54
|
-
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
|
|
55
38
|
exit 2
|
|
56
39
|
fi
|
|
57
40
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if
|
|
63
|
-
|
|
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
|
|
64
106
|
fi
|
|
65
107
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
BLOCKED_PATHS=()
|
|
69
|
-
while IFS= read -r entry; do
|
|
70
|
-
[[ -z "$entry" ]] && continue
|
|
71
|
-
BLOCKED_PATHS+=("$entry")
|
|
72
|
-
done < <(policy_list "blocked_paths")
|
|
73
|
-
|
|
74
|
-
if [[ ${#BLOCKED_PATHS[@]} -eq 0 ]]; then
|
|
108
|
+
payload=$(cat)
|
|
109
|
+
if [ -z "$payload" ]; then
|
|
75
110
|
exit 0
|
|
76
111
|
fi
|
|
77
112
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
139
|
fi
|
|
99
|
-
if [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
MATCHED="$entry"
|
|
103
|
-
return 0
|
|
104
|
-
fi
|
|
105
|
-
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
|
|
106
143
|
fi
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
144
|
+
exit 0
|
|
145
|
+
;;
|
|
146
|
+
2)
|
|
147
|
+
if [ "$verdict_check_status" -ne 0 ]; then
|
|
148
|
+
exit 2
|
|
110
149
|
fi
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*/../*)
|
|
126
|
-
local abs="$t"
|
|
127
|
-
[[ "$abs" != /* ]] && abs="$REA_ROOT/$abs"
|
|
128
|
-
local -a raw_parts parts=()
|
|
129
|
-
IFS='/' read -ra raw_parts <<<"$abs"
|
|
130
|
-
for part in "${raw_parts[@]}"; do
|
|
131
|
-
case "$part" in
|
|
132
|
-
''|.) continue ;;
|
|
133
|
-
..) [[ "${#parts[@]}" -gt 0 ]] && unset 'parts[${#parts[@]}-1]' ;;
|
|
134
|
-
*) parts+=("$part") ;;
|
|
135
|
-
esac
|
|
136
|
-
done
|
|
137
|
-
t="/$(IFS=/; printf '%s' "${parts[*]}")"
|
|
138
|
-
if [[ "$t" != "$REA_ROOT" && "$t" != "$REA_ROOT"/* ]]; then
|
|
139
|
-
printf '__rea_outside_root__:%s' "$t"
|
|
140
|
-
return 0
|
|
141
|
-
fi
|
|
142
|
-
;;
|
|
143
|
-
esac
|
|
144
|
-
t=$(normalize_path "$t")
|
|
145
|
-
printf '%s' "$t" | tr '[:upper:]' '[:lower:]'
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
_refuse() {
|
|
149
|
-
local pattern="$1" target="$2" segment="$3"
|
|
150
|
-
{
|
|
151
|
-
printf 'BLOCKED PATH (bash): write denied by policy\n'
|
|
152
|
-
printf '\n'
|
|
153
|
-
printf ' Blocked by: %s\n' "$pattern"
|
|
154
|
-
printf ' Resolved target: %s\n' "$target"
|
|
155
|
-
printf ' Segment: %s\n' "$segment"
|
|
156
|
-
printf '\n'
|
|
157
|
-
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
158
|
-
printf ' Rule: blocked_paths entries are unreachable via Bash redirects\n'
|
|
159
|
-
printf ' too — not just Write/Edit/MultiEdit. To modify, a human\n'
|
|
160
|
-
printf ' must edit directly or update blocked_paths in policy.yaml.\n'
|
|
161
|
-
} >&2
|
|
162
|
-
exit 2
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
# Check a single resolved-target token. Refuses on hit.
|
|
166
|
-
#
|
|
167
|
-
# 0.20.1 helix-021 #2: in addition to the logical post-_normalize_target
|
|
168
|
-
# form, also check the symlink-resolved form. Pre-fix `ln -s . linkroot;
|
|
169
|
-
# printf x > linkroot/.env` had a logical form of `linkroot/.env`
|
|
170
|
-
# (no match against blocked_paths) but a resolved form of `.env`
|
|
171
|
-
# (which DOES match). Refuse on either match. Write-tier
|
|
172
|
-
# `blocked-paths-enforcer.sh` already has this resolution since 0.10.x.
|
|
173
|
-
_check_token() {
|
|
174
|
-
local token="$1" segment="$2"
|
|
175
|
-
[[ -z "$token" ]] && return 0
|
|
176
|
-
local resolved
|
|
177
|
-
resolved=$(_normalize_target "$token")
|
|
178
|
-
if [[ "$resolved" == __rea_outside_root__:* ]]; then
|
|
179
|
-
# Outside REA_ROOT → can't be in blocked_paths (blocked_paths is
|
|
180
|
-
# project-relative). Allow; the protected-paths gate handles
|
|
181
|
-
# outside-root rejection on the protected list itself.
|
|
182
|
-
return 0
|
|
183
|
-
fi
|
|
184
|
-
# Symlink-resolved form via shared helper. Returns empty when the
|
|
185
|
-
# parent doesn't exist (legitimate "creating the parent" case);
|
|
186
|
-
# outside-REA_ROOT sentinel when the symlink walks out of the
|
|
187
|
-
# project (silently allow — same as the logical-path branch above).
|
|
188
|
-
local resolved_symlink
|
|
189
|
-
resolved_symlink=$(rea_resolved_relative_form "$token")
|
|
190
|
-
if [[ "$resolved_symlink" == __rea_outside_root__:* ]]; then
|
|
191
|
-
resolved_symlink=""
|
|
192
|
-
fi
|
|
193
|
-
if _match_blocked "$resolved"; then
|
|
194
|
-
_refuse "$MATCHED" "$resolved" "$segment"
|
|
195
|
-
fi
|
|
196
|
-
if [[ -n "$resolved_symlink" ]] && _match_blocked "$resolved_symlink"; then
|
|
197
|
-
_refuse "$MATCHED" "$resolved_symlink" "$segment"
|
|
198
|
-
fi
|
|
199
|
-
return 0
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
# Scan one segment for redirect / write-utility / node-fs targets and
|
|
203
|
-
# refuse on any hit. Mirrors protected-paths-bash-gate.sh::_check_segment
|
|
204
|
-
# layout, with a few additions to catch discord-ops Round 9 #1's exact
|
|
205
|
-
# Node-interpreter and sed-script-on-target shapes.
|
|
206
|
-
_check_segment() {
|
|
207
|
-
local _raw="$1" segment="$2"
|
|
208
|
-
[[ -z "$segment" ]] && return 0
|
|
209
|
-
|
|
210
|
-
# Same regex set as protected-paths-bash-gate.sh — fd-prefix-aware
|
|
211
|
-
# redirects, cp/mv tail target, sed -i target, dd of=, plus a
|
|
212
|
-
# token-walk for tee/truncate/install/ln. Keeps behavior consistent
|
|
213
|
-
# across the two bash gates.
|
|
214
|
-
local re_redirect='(^|[[:space:]])(&>>|&>|[0-9]+>>|[0-9]+>\||[0-9]+>|>>|>\||>)[[:space:]]*([^[:space:]&|;<>]+)'
|
|
215
|
-
local re_cpmv='(^|[[:space:]])(cp|mv)[[:space:]]+[^&|;<>]+[[:space:]]([^[:space:]&|;<>]+)[[:space:]]*$'
|
|
216
|
-
local re_sed='(^|[[:space:]])sed[[:space:]]+(-[a-zA-Z]*i[a-zA-Z]*[^[:space:]]*)[[:space:]]+[^&|;<>]+[[:space:]]([^[:space:]&|;<>]+)[[:space:]]*$'
|
|
217
|
-
local re_dd='(^|[[:space:]])dd[[:space:]]+[^&|;<>]*of=([^[:space:]&|;<>]+)'
|
|
218
|
-
|
|
219
|
-
if [[ "$segment" =~ $re_redirect ]]; then
|
|
220
|
-
_check_token "${BASH_REMATCH[3]}" "$segment"
|
|
221
|
-
fi
|
|
222
|
-
if [[ "$segment" =~ $re_cpmv ]]; then
|
|
223
|
-
_check_token "${BASH_REMATCH[3]}" "$segment"
|
|
224
|
-
fi
|
|
225
|
-
if [[ "$segment" =~ $re_sed ]]; then
|
|
226
|
-
_check_token "${BASH_REMATCH[3]}" "$segment"
|
|
227
|
-
fi
|
|
228
|
-
if [[ "$segment" =~ $re_dd ]]; then
|
|
229
|
-
_check_token "${BASH_REMATCH[2]}" "$segment"
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
# tee / truncate / install / ln — token-walk identical to
|
|
233
|
-
# protected-paths-bash-gate.sh.
|
|
234
|
-
local _seg_for_walk="$segment"
|
|
235
|
-
_seg_for_walk="${_seg_for_walk#"${_seg_for_walk%%[![:space:]]*}"}"
|
|
236
|
-
local first_tok
|
|
237
|
-
first_tok=$(printf '%s' "$_seg_for_walk" | awk '{print $1}')
|
|
238
|
-
case "$first_tok" in
|
|
239
|
-
tee|truncate|install|ln)
|
|
240
|
-
local found_cmd=""
|
|
241
|
-
# shellcheck disable=SC2086
|
|
242
|
-
set -- $_seg_for_walk
|
|
243
|
-
while [ "$#" -gt 0 ]; do
|
|
244
|
-
local tok="$1"
|
|
245
|
-
shift
|
|
246
|
-
if [[ -z "$found_cmd" ]]; then
|
|
247
|
-
case "$tok" in
|
|
248
|
-
tee|truncate|install|ln) found_cmd="$tok" ;;
|
|
249
|
-
esac
|
|
250
|
-
continue
|
|
251
|
-
fi
|
|
252
|
-
case "$tok" in
|
|
253
|
-
--) continue ;;
|
|
254
|
-
--*=*) continue ;;
|
|
255
|
-
--*)
|
|
256
|
-
case "$tok" in
|
|
257
|
-
--append|--ignore-interrupts|--no-clobber|--force|--no-target-directory|--symbolic|--no-dereference|--reference=*) continue ;;
|
|
258
|
-
*) shift 2>/dev/null || true; continue ;;
|
|
259
|
-
esac
|
|
260
|
-
;;
|
|
261
|
-
-*)
|
|
262
|
-
case "$tok" in
|
|
263
|
-
-s*|-m*|-o*|-g*|-t*) shift 2>/dev/null || true ;;
|
|
264
|
-
esac
|
|
265
|
-
continue
|
|
266
|
-
;;
|
|
267
|
-
*)
|
|
268
|
-
_check_token "$tok" "$segment"
|
|
269
|
-
;;
|
|
270
|
-
esac
|
|
271
|
-
done
|
|
272
|
-
;;
|
|
273
|
-
esac
|
|
274
|
-
|
|
275
|
-
# 0.21.2 helix-022 #2: interpreter scanner factored to
|
|
276
|
-
# _lib/interpreter-scanner.sh and shared with protected-paths-bash-gate.
|
|
277
|
-
# Covers node -e fs.writeFileSync, python -c open(...,'w'),
|
|
278
|
-
# ruby -e File.write, perl -e open(FH,'>...').
|
|
279
|
-
local interp_targets
|
|
280
|
-
interp_targets=$(rea_interpreter_write_targets "$segment")
|
|
281
|
-
if [[ -n "$interp_targets" ]]; then
|
|
282
|
-
while IFS= read -r tgt; do
|
|
283
|
-
[[ -z "$tgt" ]] && continue
|
|
284
|
-
_check_token "$tgt" "$segment"
|
|
285
|
-
done <<<"$interp_targets"
|
|
286
|
-
fi
|
|
287
|
-
|
|
288
|
-
return 0
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
for_each_segment "$CMD" _check_segment
|
|
292
|
-
|
|
293
|
-
exit 0
|
|
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
|
|
153
|
+
fi
|
|
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
|