@bookedsolid/rea 0.33.0 → 0.34.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/hook.js +21 -0
- package/dist/hooks/_lib/segments.d.ts +102 -0
- package/dist/hooks/_lib/segments.js +290 -0
- package/dist/hooks/dangerous-bash-interceptor/index.d.ts +103 -0
- package/dist/hooks/dangerous-bash-interceptor/index.js +669 -0
- package/dist/hooks/local-review-gate/index.d.ts +145 -0
- package/dist/hooks/local-review-gate/index.js +374 -0
- package/dist/hooks/secret-scanner/index.d.ts +143 -0
- package/dist/hooks/secret-scanner/index.js +404 -0
- package/hooks/dangerous-bash-interceptor.sh +168 -386
- package/hooks/local-review-gate.sh +523 -410
- package/hooks/secret-scanner.sh +210 -200
- package/package.json +1 -1
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +196 -0
- package/templates/local-review-gate.dogfood-staged.sh +573 -0
- package/templates/secret-scanner.dogfood-staged.sh +240 -0
package/dist/cli/hook.js
CHANGED
|
@@ -43,6 +43,9 @@ import { runHookEnvFileProtection } from '../hooks/env-file-protection/index.js'
|
|
|
43
43
|
import { runHookDependencyAuditGate } from '../hooks/dependency-audit-gate/index.js';
|
|
44
44
|
import { runHookChangesetSecurityGate } from '../hooks/changeset-security-gate/index.js';
|
|
45
45
|
import { runHookArchitectureReviewGate } from '../hooks/architecture-review-gate/index.js';
|
|
46
|
+
import { runHookDangerousBashInterceptor } from '../hooks/dangerous-bash-interceptor/index.js';
|
|
47
|
+
import { runHookLocalReviewGate } from '../hooks/local-review-gate/index.js';
|
|
48
|
+
import { runHookSecretScanner } from '../hooks/secret-scanner/index.js';
|
|
46
49
|
import { loadPolicy } from '../policy/loader.js';
|
|
47
50
|
import { appendAuditRecord, InvocationStatus, Tier } from '../audit/append.js';
|
|
48
51
|
import { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from '../audit/codex-event.js';
|
|
@@ -1001,6 +1004,24 @@ export function registerHookCommand(program) {
|
|
|
1001
1004
|
.action(async () => {
|
|
1002
1005
|
await runHookArchitectureReviewGate();
|
|
1003
1006
|
});
|
|
1007
|
+
hook
|
|
1008
|
+
.command('dangerous-bash-interceptor')
|
|
1009
|
+
.description('Node-binary port of `hooks/dangerous-bash-interceptor.sh` (0.34.0). PreToolUse Bash gate that blocks destructive commands. Catalog of 17 HIGH (H1-H17) + 1 MEDIUM (M1) rules: force-push, --no-verify, HUSKY=0, rm -rf broad targets, curl|sh pipe-RCE, REA_BYPASS, alias/function-with-bypass, psql DROP, context_protection delegate enforcement. Exit 2 on HIGH match, 0 on MEDIUM-only advisory or pass-through.')
|
|
1010
|
+
.action(async () => {
|
|
1011
|
+
await runHookDangerousBashInterceptor();
|
|
1012
|
+
});
|
|
1013
|
+
hook
|
|
1014
|
+
.command('local-review-gate')
|
|
1015
|
+
.description('Node-binary port of `hooks/local-review-gate.sh` (0.34.0). PreToolUse Bash gate refusing `git push` (and optionally `git commit`) until a recent `rea.local_review` audit entry covers HEAD. Honors `policy.review.local_review.{mode=off|enforced, refuse_at=push|commit|both, bypass_env_var}`. Mode=off short-circuits silently; bypass var (default REA_SKIP_LOCAL_REVIEW) accepts process-env (global) or per-segment inline `VAR="<reason>" git push` shapes. CTO directive 2026-05-05 enforcement.')
|
|
1016
|
+
.action(async () => {
|
|
1017
|
+
await runHookLocalReviewGate();
|
|
1018
|
+
});
|
|
1019
|
+
hook
|
|
1020
|
+
.command('secret-scanner')
|
|
1021
|
+
.description('Node-binary port of `hooks/secret-scanner.sh` (0.34.0). PreToolUse Write/Edit/MultiEdit/NotebookEdit pre-write credential gate. Catalog of 12 HIGH + 5 MEDIUM patterns (AWS, Anthropic, GitHub, Stripe live/test, Supabase JWT, generic SECRET=, private-key armor, DB connection strings). awk-style line filter strips shell comments and `process.env.VAR` RHS assignments; `is_placeholder` filter drops `<your_key>`/`test_token`/`aaaaaaa` shapes. HIGH match → exit 2; MEDIUM-only → exit 0 with advisory. Suffix-excludes `.env.example`/`.env.sample`.')
|
|
1022
|
+
.action(async () => {
|
|
1023
|
+
await runHookSecretScanner();
|
|
1024
|
+
});
|
|
1004
1025
|
hook
|
|
1005
1026
|
.command('policy-get')
|
|
1006
1027
|
.description('Read a value from `.rea/policy.yaml` via the canonical YAML parser. Used by bash-tier hooks (`hooks/_lib/policy-read.sh::policy_nested_scalar`) so inline AND block YAML forms agree at a single source of truth. Default scalar mode: prints raw value or empty. With `--json`: emits JSON (scalar or object/array; missing path → `null`). Unparseable YAML → empty / null, exit 1.')
|
|
@@ -123,3 +123,105 @@ export declare function anySegmentMatches(cmd: string, regexSource: string): boo
|
|
|
123
123
|
* (any-utility OR any-env) were AND'd across segments.
|
|
124
124
|
*/
|
|
125
125
|
export declare function anySegmentMatchesBoth(cmd: string, regexA: string, regexB: string): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Returns true if any segment's RAW text (env-var prefixes intact, only
|
|
128
|
+
* leading whitespace trimmed) matches the regex source. Mirrors
|
|
129
|
+
* `any_segment_raw_matches` in the bash counterpart — used by checks
|
|
130
|
+
* where the env-prefix itself IS the signal (`HUSKY=0 git`, `REA_BYPASS=`,
|
|
131
|
+
* `alias … = HUSKY=0`).
|
|
132
|
+
*
|
|
133
|
+
* 0.34.0 port — dangerous-bash-interceptor (H10, H15, H16) and
|
|
134
|
+
* local-review-gate (env-prefix git push detection) call into this.
|
|
135
|
+
* Note: callers anchor with `^` in the regex source when they want
|
|
136
|
+
* "starts at segment head"; we do not prepend `^` here.
|
|
137
|
+
*/
|
|
138
|
+
export declare function anySegmentRawMatches(cmd: string, regexSource: string): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Returns true if any segment's RAW text contains a match for the
|
|
141
|
+
* regex source. Mirrors `any_segment_matches` in the bash counterpart —
|
|
142
|
+
* used by content-scan style checks. The regex matches anywhere in the
|
|
143
|
+
* segment (not anchored). Useful for `(psql|pgcli)[^|&;]*DROP[[:space:]]+(TABLE|…)`
|
|
144
|
+
* style patterns that must match across the whole segment but only
|
|
145
|
+
* within a single segment (a heredoc body in segment N or commit
|
|
146
|
+
* message in segment 1 must NOT poison segment N+1).
|
|
147
|
+
*
|
|
148
|
+
* 0.34.0 port — dangerous-bash-interceptor H6 calls into this.
|
|
149
|
+
*/
|
|
150
|
+
export declare function anySegmentContains(cmd: string, regexSource: string): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Iterate over every segment of `cmd` and invoke `callback(raw, head)`
|
|
153
|
+
* for each. Mirrors `for_each_segment` in the bash counterpart —
|
|
154
|
+
* dangerous-bash-interceptor H1 uses this to walk each push segment
|
|
155
|
+
* independently (since one segment may include `--force-with-lease`
|
|
156
|
+
* while another carries an unsafe `--force`).
|
|
157
|
+
*
|
|
158
|
+
* The callback receives the raw segment (env-prefix preserved) and the
|
|
159
|
+
* prefix-stripped head. Return value is ignored.
|
|
160
|
+
*
|
|
161
|
+
* 0.34.0 port.
|
|
162
|
+
*/
|
|
163
|
+
export declare function forEachSegment(cmd: string, callback: (raw: string, head: string) => void): void;
|
|
164
|
+
/**
|
|
165
|
+
* Quote-aware mask of in-quote separators. Mirrors `quote_masked_cmd`
|
|
166
|
+
* in the bash counterpart — produces a string where in-quote `|` / `;`
|
|
167
|
+
* / `&` characters are replaced with multi-byte sentinels so a caller's
|
|
168
|
+
* regex can match real (unquoted) instances of those bytes without
|
|
169
|
+
* false-positiving on quoted commit-message bodies (`git commit -m
|
|
170
|
+
* "curl|sh later"`).
|
|
171
|
+
*
|
|
172
|
+
* 0.34.0 port — dangerous-bash-interceptor H12 (`curl|sh` detection)
|
|
173
|
+
* uses this to scan the WHOLE command (not split into segments)
|
|
174
|
+
* without quoted-mention false positives.
|
|
175
|
+
*
|
|
176
|
+
* Implementation uses the same sentinel-byte alphabet the bash helper
|
|
177
|
+
* uses. Sentinels are public so callers can `.test()` against the
|
|
178
|
+
* masked output without accidentally tripping on them.
|
|
179
|
+
*/
|
|
180
|
+
export declare const INQUOTE_PIPE_SENTINEL = "__REA_INQUOTE_PIPE_a8f2c1__";
|
|
181
|
+
export declare const INQUOTE_SEMI_SENTINEL = "__REA_INQUOTE_SC_a8f2c1__";
|
|
182
|
+
export declare const INQUOTE_AMP_SENTINEL = "__REA_INQUOTE_AMP_a8f2c1__";
|
|
183
|
+
export declare function quoteMaskedCmd(cmd: string): string;
|
|
184
|
+
/**
|
|
185
|
+
* Walk the nested-shell unwrap chain and emit `cmd` PLUS each inner
|
|
186
|
+
* payload as a separate string. Mirrors `_rea_unwrap_nested_shells`
|
|
187
|
+
* in the bash counterpart.
|
|
188
|
+
*
|
|
189
|
+
* Used by dangerous-bash-interceptor H12 (`curl|sh` detection) so a
|
|
190
|
+
* payload like `zsh -c "curl https://x | sh"` is scanned for the pipe
|
|
191
|
+
* shape even though the literal `|` is inside quotes at the outer
|
|
192
|
+
* level. The H12 check then runs `quoteMaskedCmd` against each
|
|
193
|
+
* emitted line independently.
|
|
194
|
+
*
|
|
195
|
+
* Depth-bounded at MAX_NESTED_DEPTH (8) — same as `splitSegments`.
|
|
196
|
+
*
|
|
197
|
+
* 0.34.0 port.
|
|
198
|
+
*/
|
|
199
|
+
export declare function unwrapNestedShells(cmd: string): string[];
|
|
200
|
+
/**
|
|
201
|
+
* Return every segment of `cmd` whose prefix-stripped head matches the
|
|
202
|
+
* head-anchored regex source. Mirrors `find_all_segments_starting_with`
|
|
203
|
+
* in the bash counterpart.
|
|
204
|
+
*
|
|
205
|
+
* Returns each match as `{ raw, head }` so callers (local-review-gate's
|
|
206
|
+
* round-25 P1-B sweep) can validate per-segment bypass against the
|
|
207
|
+
* raw (env-prefix-intact) form.
|
|
208
|
+
*
|
|
209
|
+
* Case-INSENSITIVE. Empty array on no matches.
|
|
210
|
+
*
|
|
211
|
+
* 0.34.0 port.
|
|
212
|
+
*/
|
|
213
|
+
export declare function findAllSegmentsStartingWith(cmd: string, regexSource: string): CommandSegment[];
|
|
214
|
+
/**
|
|
215
|
+
* Return every segment of `cmd` whose RAW text (env-prefix intact,
|
|
216
|
+
* leading whitespace trimmed) matches the regex source. Mirrors
|
|
217
|
+
* `find_all_segments_raw_matches` in the bash counterpart.
|
|
218
|
+
*
|
|
219
|
+
* Companion to `findAllSegmentsStartingWith` for the env-prefix shapes
|
|
220
|
+
* the prefix-stripper bails on (quoted-value env-vars like
|
|
221
|
+
* `REA_SKIP="urgent fix"`).
|
|
222
|
+
*
|
|
223
|
+
* Case-INSENSITIVE. Empty array on no matches.
|
|
224
|
+
*
|
|
225
|
+
* 0.34.0 port.
|
|
226
|
+
*/
|
|
227
|
+
export declare function findAllSegmentsRawMatches(cmd: string, regexSource: string): CommandSegment[];
|
|
@@ -764,3 +764,293 @@ export function anySegmentMatchesBoth(cmd, regexA, regexB) {
|
|
|
764
764
|
}
|
|
765
765
|
return false;
|
|
766
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Returns true if any segment's RAW text (env-var prefixes intact, only
|
|
769
|
+
* leading whitespace trimmed) matches the regex source. Mirrors
|
|
770
|
+
* `any_segment_raw_matches` in the bash counterpart — used by checks
|
|
771
|
+
* where the env-prefix itself IS the signal (`HUSKY=0 git`, `REA_BYPASS=`,
|
|
772
|
+
* `alias … = HUSKY=0`).
|
|
773
|
+
*
|
|
774
|
+
* 0.34.0 port — dangerous-bash-interceptor (H10, H15, H16) and
|
|
775
|
+
* local-review-gate (env-prefix git push detection) call into this.
|
|
776
|
+
* Note: callers anchor with `^` in the regex source when they want
|
|
777
|
+
* "starts at segment head"; we do not prepend `^` here.
|
|
778
|
+
*/
|
|
779
|
+
export function anySegmentRawMatches(cmd, regexSource) {
|
|
780
|
+
const re = new RegExp(regexSource, 'i');
|
|
781
|
+
for (const seg of splitSegments(cmd)) {
|
|
782
|
+
const trimmed = seg.raw.replace(/^\s+/, '');
|
|
783
|
+
if (re.test(trimmed))
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Returns true if any segment's RAW text contains a match for the
|
|
790
|
+
* regex source. Mirrors `any_segment_matches` in the bash counterpart —
|
|
791
|
+
* used by content-scan style checks. The regex matches anywhere in the
|
|
792
|
+
* segment (not anchored). Useful for `(psql|pgcli)[^|&;]*DROP[[:space:]]+(TABLE|…)`
|
|
793
|
+
* style patterns that must match across the whole segment but only
|
|
794
|
+
* within a single segment (a heredoc body in segment N or commit
|
|
795
|
+
* message in segment 1 must NOT poison segment N+1).
|
|
796
|
+
*
|
|
797
|
+
* 0.34.0 port — dangerous-bash-interceptor H6 calls into this.
|
|
798
|
+
*/
|
|
799
|
+
export function anySegmentContains(cmd, regexSource) {
|
|
800
|
+
const re = new RegExp(regexSource, 'i');
|
|
801
|
+
for (const seg of splitSegments(cmd)) {
|
|
802
|
+
if (re.test(seg.head))
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Iterate over every segment of `cmd` and invoke `callback(raw, head)`
|
|
809
|
+
* for each. Mirrors `for_each_segment` in the bash counterpart —
|
|
810
|
+
* dangerous-bash-interceptor H1 uses this to walk each push segment
|
|
811
|
+
* independently (since one segment may include `--force-with-lease`
|
|
812
|
+
* while another carries an unsafe `--force`).
|
|
813
|
+
*
|
|
814
|
+
* The callback receives the raw segment (env-prefix preserved) and the
|
|
815
|
+
* prefix-stripped head. Return value is ignored.
|
|
816
|
+
*
|
|
817
|
+
* 0.34.0 port.
|
|
818
|
+
*/
|
|
819
|
+
export function forEachSegment(cmd, callback) {
|
|
820
|
+
for (const seg of splitSegments(cmd)) {
|
|
821
|
+
callback(seg.raw, seg.head);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Quote-aware mask of in-quote separators. Mirrors `quote_masked_cmd`
|
|
826
|
+
* in the bash counterpart — produces a string where in-quote `|` / `;`
|
|
827
|
+
* / `&` characters are replaced with multi-byte sentinels so a caller's
|
|
828
|
+
* regex can match real (unquoted) instances of those bytes without
|
|
829
|
+
* false-positiving on quoted commit-message bodies (`git commit -m
|
|
830
|
+
* "curl|sh later"`).
|
|
831
|
+
*
|
|
832
|
+
* 0.34.0 port — dangerous-bash-interceptor H12 (`curl|sh` detection)
|
|
833
|
+
* uses this to scan the WHOLE command (not split into segments)
|
|
834
|
+
* without quoted-mention false positives.
|
|
835
|
+
*
|
|
836
|
+
* Implementation uses the same sentinel-byte alphabet the bash helper
|
|
837
|
+
* uses. Sentinels are public so callers can `.test()` against the
|
|
838
|
+
* masked output without accidentally tripping on them.
|
|
839
|
+
*/
|
|
840
|
+
export const INQUOTE_PIPE_SENTINEL = '__REA_INQUOTE_PIPE_a8f2c1__';
|
|
841
|
+
export const INQUOTE_SEMI_SENTINEL = '__REA_INQUOTE_SC_a8f2c1__';
|
|
842
|
+
export const INQUOTE_AMP_SENTINEL = '__REA_INQUOTE_AMP_a8f2c1__';
|
|
843
|
+
export function quoteMaskedCmd(cmd) {
|
|
844
|
+
// 4-state walker mirroring the bash awk:
|
|
845
|
+
// 0 = plain
|
|
846
|
+
// 1 = inside "…" (backslash escapes next char)
|
|
847
|
+
// 2 = inside '…' (no escapes)
|
|
848
|
+
// 3 = inside $'…' (ANSI-C; backslash escapes next char)
|
|
849
|
+
// In modes 1/2/3, in-quote `|`/`;`/`&` are replaced with sentinels.
|
|
850
|
+
// The opening `$'` is preserved verbatim (caller code that detects
|
|
851
|
+
// ANSI-C envelopes still sees them).
|
|
852
|
+
let out = '';
|
|
853
|
+
let i = 0;
|
|
854
|
+
const n = cmd.length;
|
|
855
|
+
let mode = 0;
|
|
856
|
+
while (i < n) {
|
|
857
|
+
const ch = cmd[i];
|
|
858
|
+
if (mode === 0) {
|
|
859
|
+
if (ch === '$' && i + 1 < n && cmd[i + 1] === "'") {
|
|
860
|
+
mode = 3;
|
|
861
|
+
out += "$'";
|
|
862
|
+
i += 2;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (ch === '"') {
|
|
866
|
+
mode = 1;
|
|
867
|
+
out += ch;
|
|
868
|
+
i += 1;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (ch === "'") {
|
|
872
|
+
mode = 2;
|
|
873
|
+
out += ch;
|
|
874
|
+
i += 1;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (ch === '\\' && i + 1 < n) {
|
|
878
|
+
out += ch + cmd[i + 1];
|
|
879
|
+
i += 2;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
out += ch;
|
|
883
|
+
i += 1;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (mode === 3) {
|
|
887
|
+
if (ch === '\\' && i + 1 < n) {
|
|
888
|
+
out += ch + cmd[i + 1];
|
|
889
|
+
i += 2;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (ch === "'") {
|
|
893
|
+
mode = 0;
|
|
894
|
+
out += ch;
|
|
895
|
+
i += 1;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (ch === '|') {
|
|
899
|
+
out += INQUOTE_PIPE_SENTINEL;
|
|
900
|
+
i += 1;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (ch === ';') {
|
|
904
|
+
out += INQUOTE_SEMI_SENTINEL;
|
|
905
|
+
i += 1;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (ch === '&') {
|
|
909
|
+
out += INQUOTE_AMP_SENTINEL;
|
|
910
|
+
i += 1;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
out += ch;
|
|
914
|
+
i += 1;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (mode === 2) {
|
|
918
|
+
if (ch === "'") {
|
|
919
|
+
mode = 0;
|
|
920
|
+
out += ch;
|
|
921
|
+
i += 1;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (ch === '|') {
|
|
925
|
+
out += INQUOTE_PIPE_SENTINEL;
|
|
926
|
+
i += 1;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (ch === ';') {
|
|
930
|
+
out += INQUOTE_SEMI_SENTINEL;
|
|
931
|
+
i += 1;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (ch === '&') {
|
|
935
|
+
out += INQUOTE_AMP_SENTINEL;
|
|
936
|
+
i += 1;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
out += ch;
|
|
940
|
+
i += 1;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
// mode === 1
|
|
944
|
+
if (ch === '\\' && i + 1 < n) {
|
|
945
|
+
out += ch + cmd[i + 1];
|
|
946
|
+
i += 2;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (ch === '"') {
|
|
950
|
+
mode = 0;
|
|
951
|
+
out += ch;
|
|
952
|
+
i += 1;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (ch === '|') {
|
|
956
|
+
out += INQUOTE_PIPE_SENTINEL;
|
|
957
|
+
i += 1;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (ch === ';') {
|
|
961
|
+
out += INQUOTE_SEMI_SENTINEL;
|
|
962
|
+
i += 1;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (ch === '&') {
|
|
966
|
+
out += INQUOTE_AMP_SENTINEL;
|
|
967
|
+
i += 1;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
out += ch;
|
|
971
|
+
i += 1;
|
|
972
|
+
}
|
|
973
|
+
return out;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Walk the nested-shell unwrap chain and emit `cmd` PLUS each inner
|
|
977
|
+
* payload as a separate string. Mirrors `_rea_unwrap_nested_shells`
|
|
978
|
+
* in the bash counterpart.
|
|
979
|
+
*
|
|
980
|
+
* Used by dangerous-bash-interceptor H12 (`curl|sh` detection) so a
|
|
981
|
+
* payload like `zsh -c "curl https://x | sh"` is scanned for the pipe
|
|
982
|
+
* shape even though the literal `|` is inside quotes at the outer
|
|
983
|
+
* level. The H12 check then runs `quoteMaskedCmd` against each
|
|
984
|
+
* emitted line independently.
|
|
985
|
+
*
|
|
986
|
+
* Depth-bounded at MAX_NESTED_DEPTH (8) — same as `splitSegments`.
|
|
987
|
+
*
|
|
988
|
+
* 0.34.0 port.
|
|
989
|
+
*/
|
|
990
|
+
export function unwrapNestedShells(cmd) {
|
|
991
|
+
const out = [cmd];
|
|
992
|
+
unwrapNestedShellsRecursive(cmd, 0, out);
|
|
993
|
+
return out;
|
|
994
|
+
}
|
|
995
|
+
function unwrapNestedShellsRecursive(cmd, depth, acc) {
|
|
996
|
+
if (depth >= MAX_NESTED_DEPTH)
|
|
997
|
+
return;
|
|
998
|
+
// Walk segments so a heredoc-style or multi-line command gets each
|
|
999
|
+
// segment's inner payload extracted independently.
|
|
1000
|
+
const masked = maskQuotedSeparators(cmd);
|
|
1001
|
+
const rawSegs = splitOnUnquotedSeparators(masked);
|
|
1002
|
+
for (const raw of rawSegs) {
|
|
1003
|
+
const unmaskedRaw = unmask(raw);
|
|
1004
|
+
const head = stripSegmentPrefix(unmaskedRaw);
|
|
1005
|
+
const inner = extractNestedShellPayload(head);
|
|
1006
|
+
if (inner !== null) {
|
|
1007
|
+
acc.push(inner);
|
|
1008
|
+
unwrapNestedShellsRecursive(inner, depth + 1, acc);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Return every segment of `cmd` whose prefix-stripped head matches the
|
|
1014
|
+
* head-anchored regex source. Mirrors `find_all_segments_starting_with`
|
|
1015
|
+
* in the bash counterpart.
|
|
1016
|
+
*
|
|
1017
|
+
* Returns each match as `{ raw, head }` so callers (local-review-gate's
|
|
1018
|
+
* round-25 P1-B sweep) can validate per-segment bypass against the
|
|
1019
|
+
* raw (env-prefix-intact) form.
|
|
1020
|
+
*
|
|
1021
|
+
* Case-INSENSITIVE. Empty array on no matches.
|
|
1022
|
+
*
|
|
1023
|
+
* 0.34.0 port.
|
|
1024
|
+
*/
|
|
1025
|
+
export function findAllSegmentsStartingWith(cmd, regexSource) {
|
|
1026
|
+
const re = new RegExp(`^${regexSource}`, 'i');
|
|
1027
|
+
const out = [];
|
|
1028
|
+
for (const seg of splitSegments(cmd)) {
|
|
1029
|
+
if (re.test(seg.head))
|
|
1030
|
+
out.push(seg);
|
|
1031
|
+
}
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Return every segment of `cmd` whose RAW text (env-prefix intact,
|
|
1036
|
+
* leading whitespace trimmed) matches the regex source. Mirrors
|
|
1037
|
+
* `find_all_segments_raw_matches` in the bash counterpart.
|
|
1038
|
+
*
|
|
1039
|
+
* Companion to `findAllSegmentsStartingWith` for the env-prefix shapes
|
|
1040
|
+
* the prefix-stripper bails on (quoted-value env-vars like
|
|
1041
|
+
* `REA_SKIP="urgent fix"`).
|
|
1042
|
+
*
|
|
1043
|
+
* Case-INSENSITIVE. Empty array on no matches.
|
|
1044
|
+
*
|
|
1045
|
+
* 0.34.0 port.
|
|
1046
|
+
*/
|
|
1047
|
+
export function findAllSegmentsRawMatches(cmd, regexSource) {
|
|
1048
|
+
const re = new RegExp(regexSource, 'i');
|
|
1049
|
+
const out = [];
|
|
1050
|
+
for (const seg of splitSegments(cmd)) {
|
|
1051
|
+
const trimmed = seg.raw.replace(/^\s+/, '');
|
|
1052
|
+
if (re.test(trimmed))
|
|
1053
|
+
out.push(seg);
|
|
1054
|
+
}
|
|
1055
|
+
return out;
|
|
1056
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-binary port of `hooks/dangerous-bash-interceptor.sh`.
|
|
3
|
+
*
|
|
4
|
+
* 0.34.0 Phase 2 port #1 (tier-2 medium-complexity hooks with enforcer
|
|
5
|
+
* logic). This is the agent-runaway gate — it refuses destructive Bash
|
|
6
|
+
* commands before Claude Code dispatches them. Every refusal class in
|
|
7
|
+
* the 414-LOC bash body must be preserved byte-for-byte; the bypass
|
|
8
|
+
* corpus pinned across 0.13–0.27 demands it.
|
|
9
|
+
*
|
|
10
|
+
* Behavioral contract — preserves the bash hook byte-for-byte:
|
|
11
|
+
*
|
|
12
|
+
* 1. HALT check → exit 2 with the shared banner.
|
|
13
|
+
* 2. Read stdin, extract `tool_input.command`. Non-Bash payloads or
|
|
14
|
+
* empty command → exit 0.
|
|
15
|
+
* 3. Compute smart exclusion flags:
|
|
16
|
+
* - `CMD_IS_REBASE_SAFE` → segments that begin with
|
|
17
|
+
* `git rebase --abort|--continue` skip the H2 rebase advisory.
|
|
18
|
+
* - `CMD_IS_CLEAN_DRY` → segments that begin with
|
|
19
|
+
* `git clean -n|--dry-run` skip the H5 destructive-clean check.
|
|
20
|
+
* 4. Run every HIGH check (H1–H17, M1) against the command. Each
|
|
21
|
+
* check returns 0..N matches; matches are accumulated into the
|
|
22
|
+
* violations table. The accumulator preserves the original bash
|
|
23
|
+
* hook's first-match-wins-per-check semantics — H1 fires once
|
|
24
|
+
* per command even if multiple push segments are unsafe.
|
|
25
|
+
* 5. If any HIGH match → emit "BASH INTERCEPTED" banner + exit 2.
|
|
26
|
+
* Else if MEDIUM-only → emit "BASH ADVISORY" banner + exit 0.
|
|
27
|
+
* Else exit 0 silently.
|
|
28
|
+
*
|
|
29
|
+
* The pattern catalog is in `RULES` below. Each rule is a self-
|
|
30
|
+
* contained closure with a stable identifier (`H1`, `H2`, …) so a
|
|
31
|
+
* future rule addition lands as a one-line array push, not a rewrite.
|
|
32
|
+
* Identifiers match the bash hook's `add_high "H<N>: …"` shape so
|
|
33
|
+
* audit/log consumers grepping for `H12` continue to work.
|
|
34
|
+
*
|
|
35
|
+
* Key parity choices:
|
|
36
|
+
*
|
|
37
|
+
* - Segment-anchored detection via `anySegmentStartsWith` (and
|
|
38
|
+
* `forEachSegment` for per-segment work). The bash 0.15.0 fix
|
|
39
|
+
* (segment-aware instead of full-command grep) is reproduced here.
|
|
40
|
+
* - Env-var-prefix shapes (H10 `HUSKY=0 git`, H15 `REA_BYPASS=…`,
|
|
41
|
+
* H16 alias/function defs) use `anySegmentRawMatches` since the
|
|
42
|
+
* prefix IS the signal — `stripSegmentPrefix` would eat it.
|
|
43
|
+
* - H12 (`curl|sh` pipe-RCE) scans the whole command via
|
|
44
|
+
* `quoteMaskedCmd` because pipe-RCE is a multi-segment property
|
|
45
|
+
* (`|` is the separator that joins curl to sh). The bash hook's
|
|
46
|
+
* `_rea_unwrap_nested_shells` is mirrored via `unwrapNestedShells`
|
|
47
|
+
* so inner payloads of `bash -c "curl … | sh"` are also scanned.
|
|
48
|
+
* - H17 (context-protection) reads
|
|
49
|
+
* `policy.context_protection.delegate_to_subagent` via the canonical
|
|
50
|
+
* YAML loader (matches the bash hook's 0.16.0 fix J.2).
|
|
51
|
+
*/
|
|
52
|
+
import type { Buffer } from 'node:buffer';
|
|
53
|
+
export interface DangerousBashOptions {
|
|
54
|
+
reaRoot?: string;
|
|
55
|
+
stdinOverride?: string | Buffer;
|
|
56
|
+
stderrWrite?: (s: string) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface DangerousBashResult {
|
|
59
|
+
exitCode: number;
|
|
60
|
+
stderr: string;
|
|
61
|
+
/** Test seam — violations the run accumulated, in catalog order. */
|
|
62
|
+
violations: Violation[];
|
|
63
|
+
}
|
|
64
|
+
export interface Violation {
|
|
65
|
+
severity: 'HIGH' | 'MEDIUM';
|
|
66
|
+
/** Stable identifier (`H1`, `H10`, `M1`, …) — matches bash labels. */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Banner headline. */
|
|
69
|
+
label: string;
|
|
70
|
+
/** Banner explanation paragraph. */
|
|
71
|
+
detail: string;
|
|
72
|
+
/** Suggested alternatives. */
|
|
73
|
+
alternatives: string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Rule descriptor + execution closure. The closure receives the raw
|
|
77
|
+
* command + the active exclusion flags and returns 0..N violations
|
|
78
|
+
* for that rule.
|
|
79
|
+
*/
|
|
80
|
+
interface RuleContext {
|
|
81
|
+
cmd: string;
|
|
82
|
+
cmdIsRebaseSafe: boolean;
|
|
83
|
+
cmdIsCleanDry: boolean;
|
|
84
|
+
delegatePatterns: string[];
|
|
85
|
+
}
|
|
86
|
+
interface Rule {
|
|
87
|
+
id: string;
|
|
88
|
+
severity: 'HIGH' | 'MEDIUM';
|
|
89
|
+
run: (ctx: RuleContext) => Violation[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Pure executor. Returns `{ exitCode, stderr, violations }`; the CLI
|
|
93
|
+
* wrapper translates them into `process.stderr.write` + `process.exit`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function runDangerousBashInterceptor(options?: DangerousBashOptions): Promise<DangerousBashResult>;
|
|
96
|
+
/**
|
|
97
|
+
* CLI entry point — `rea hook dangerous-bash-interceptor`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function runHookDangerousBashInterceptor(options?: DangerousBashOptions): Promise<void>;
|
|
100
|
+
export declare const __INTERNAL_FOR_TESTS: {
|
|
101
|
+
RULES: readonly Rule[];
|
|
102
|
+
};
|
|
103
|
+
export {};
|