@bookedsolid/rea 0.22.0 → 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/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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrapper around `mvdan-sh` — the GopherJS-compiled JS port of
|
|
3
|
+
* the upstream Go bash parser at `mvdan.cc/sh/v3/syntax`.
|
|
4
|
+
*
|
|
5
|
+
* Why a wrapper:
|
|
6
|
+
* 1. The parser instance is mutable (it tracks state across calls
|
|
7
|
+
* to `Parse`). Construct once per process; serialize all parses
|
|
8
|
+
* through it. Multiple concurrent Node CLI invocations get one
|
|
9
|
+
* parser each — fine, it's cheap.
|
|
10
|
+
* 2. The library throws Go-style error objects with `.Error()`
|
|
11
|
+
* methods, not native JS Errors. Normalize them to native Error
|
|
12
|
+
* with a clean message.
|
|
13
|
+
* 3. Callers care only about three outcomes: parsed, parse-failed,
|
|
14
|
+
* or unexpected JS-level throw. Collapse the Go-vs-JS-error
|
|
15
|
+
* ambiguity to a single discriminated union.
|
|
16
|
+
*
|
|
17
|
+
* 0.23.0 — first release of this module. Pinned to mvdan-sh@0.10.1
|
|
18
|
+
* (deprecated upstream but functionally complete; see issue 1145).
|
|
19
|
+
* If we ever migrate to `sh-syntax` (the WASM successor), this
|
|
20
|
+
* wrapper is the only file that changes — everything downstream
|
|
21
|
+
* works against `BashFile`/`BashNode` from our local d.ts shim.
|
|
22
|
+
*/
|
|
23
|
+
import mvdanSh from 'mvdan-sh';
|
|
24
|
+
/**
|
|
25
|
+
* Singleton parser. mvdan-sh's `NewParser` is documented as reusable
|
|
26
|
+
* across calls; spinning up a fresh one per scan adds 1-2ms with no
|
|
27
|
+
* correctness benefit.
|
|
28
|
+
*
|
|
29
|
+
* Imported via the default export — the package ships CommonJS with
|
|
30
|
+
* a single `module.exports = { syntax: ... }` shape; under ESM the
|
|
31
|
+
* named-export `{ syntax }` form fails because the property is
|
|
32
|
+
* dynamically assigned in the GopherJS-generated body and Node's
|
|
33
|
+
* named-exports synthesis cannot see it.
|
|
34
|
+
*/
|
|
35
|
+
const parser = mvdanSh.syntax.NewParser();
|
|
36
|
+
/**
|
|
37
|
+
* Parse a bash command string into an AST. Returns a tagged union so
|
|
38
|
+
* the caller never has to wrap this in try/catch — every failure mode
|
|
39
|
+
* (Go parse error, JS throw, weird native return) is collapsed to
|
|
40
|
+
* `{ ok: false, error }`.
|
|
41
|
+
*
|
|
42
|
+
* Empty / whitespace-only input is a no-op success — the parser
|
|
43
|
+
* returns a `File` with zero `Stmts`, which the walker will yield
|
|
44
|
+
* zero detections for. Equivalent to the bash gates' `[[ -z "$CMD" ]]
|
|
45
|
+
* && exit 0` guard.
|
|
46
|
+
*/
|
|
47
|
+
export function parseBashCommand(src) {
|
|
48
|
+
try {
|
|
49
|
+
const file = parser.Parse(src, 'rea-bash-scanner.sh');
|
|
50
|
+
// Defensive: GopherJS sometimes returns a node missing the Stmts
|
|
51
|
+
// field on certain edge-case inputs (we have not reproduced one,
|
|
52
|
+
// but the file-IO layer the original Go uses is mocked out and
|
|
53
|
+
// failure modes are not perfectly characterized). Treat that as
|
|
54
|
+
// "parse failure" rather than "empty file" so we fail closed.
|
|
55
|
+
if (file === null || file === undefined) {
|
|
56
|
+
return { ok: false, error: 'parser returned null file' };
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, file };
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
// Go errors come through as objects with `.Error()` methods. Native
|
|
62
|
+
// JS errors have `.message`. Anything else gets stringified.
|
|
63
|
+
const error = goErrorMessage(e) ?? jsErrorMessage(e) ?? String(e);
|
|
64
|
+
return { ok: false, error };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function goErrorMessage(e) {
|
|
68
|
+
if (typeof e === 'object' && e !== null && 'Error' in e) {
|
|
69
|
+
const fn = e.Error;
|
|
70
|
+
if (typeof fn === 'function') {
|
|
71
|
+
try {
|
|
72
|
+
const msg = fn.call(e);
|
|
73
|
+
if (typeof msg === 'string')
|
|
74
|
+
return msg;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Fall through to other strategies.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function jsErrorMessage(e) {
|
|
84
|
+
if (e instanceof Error)
|
|
85
|
+
return e.message;
|
|
86
|
+
if (typeof e === 'object' && e !== null && 'message' in e) {
|
|
87
|
+
const m = e.message;
|
|
88
|
+
if (typeof m === 'string')
|
|
89
|
+
return m;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protected-paths policy composition. Mirrors the bash semantics in
|
|
3
|
+
* `hooks/_lib/protected-paths.sh` byte-for-byte:
|
|
4
|
+
*
|
|
5
|
+
* 1. Build the effective protected set:
|
|
6
|
+
* - If policy.protected_writes is set: that list, plus kill-switch
|
|
7
|
+
* invariants always added.
|
|
8
|
+
* - Else: the historical default (REA_PROTECTED_PATTERNS_FULL).
|
|
9
|
+
* Then subtract policy.protected_paths_relax — but kill-switch
|
|
10
|
+
* invariants in the relax list are silently dropped from the
|
|
11
|
+
* relax set with a stderr advisory (not from the protected set).
|
|
12
|
+
*
|
|
13
|
+
* 2. The match check:
|
|
14
|
+
* a. Explicit `protected_writes` overrides win FIRST (helix-020 G2).
|
|
15
|
+
* Matched against the path BEFORE the extension-surface check.
|
|
16
|
+
* b. Extension-surface paths (`.husky/{commit-msg,pre-push,
|
|
17
|
+
* pre-commit}.d/<fragment>`) are NOT protected by default
|
|
18
|
+
* (helix-018 Option B / 0.16.4).
|
|
19
|
+
* c. Default protected list applies, with kill-switch invariants
|
|
20
|
+
* always enforced.
|
|
21
|
+
*
|
|
22
|
+
* 3. Pattern matching:
|
|
23
|
+
* - case-insensitive (macOS APFS — helix-015 #2)
|
|
24
|
+
* - trailing `/` is a prefix-match
|
|
25
|
+
* - everything else is exact-match
|
|
26
|
+
*
|
|
27
|
+
* 4. Path normalization runs BEFORE matching:
|
|
28
|
+
* - URL decode, backslash → slash, leading `./` strip
|
|
29
|
+
* - `..` walk-up via the parser-friendly equivalent of
|
|
30
|
+
* `cd -P / pwd -P` (we rely on `node:fs.realpathSync` for the
|
|
31
|
+
* symlink resolution; non-existent parents walk up to the
|
|
32
|
+
* nearest existing ancestor — helix-022 #1)
|
|
33
|
+
* - case-insensitive lowercase comparison
|
|
34
|
+
* - sentinel `__rea_unresolved_expansion__` for $-substitution
|
|
35
|
+
* - sentinel `__rea_outside_root__` for paths escaping REA_ROOT
|
|
36
|
+
*/
|
|
37
|
+
import type { Policy } from '../../policy/types.js';
|
|
38
|
+
import type { DetectedWrite } from './walker.js';
|
|
39
|
+
import { type Verdict } from './verdict.js';
|
|
40
|
+
/**
|
|
41
|
+
* Inputs the protected-scan composer needs. The caller collects these
|
|
42
|
+
* from the environment + policy file once and passes them in. Keeps
|
|
43
|
+
* the scanner function pure / testable.
|
|
44
|
+
*/
|
|
45
|
+
export interface ProtectedScanContext {
|
|
46
|
+
reaRoot: string;
|
|
47
|
+
policy: Pick<Policy, 'protected_writes' | 'protected_paths_relax'>;
|
|
48
|
+
/**
|
|
49
|
+
* Stderr sink for advisory messages (e.g. "kill-switch invariant in
|
|
50
|
+
* protected_paths_relax"). Defaults to no-op so unit tests don't
|
|
51
|
+
* pollute stdout.
|
|
52
|
+
*/
|
|
53
|
+
stderr?: (line: string) => void;
|
|
54
|
+
}
|
|
55
|
+
interface EffectivePatterns {
|
|
56
|
+
/** Full effective protected set (default OR override + invariants − relax). */
|
|
57
|
+
full: string[];
|
|
58
|
+
/** Subset that came from explicit `protected_writes` (overrides the extension-surface allow-list). */
|
|
59
|
+
override: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compute the effective protected pattern sets from policy. Pure — no
|
|
63
|
+
* filesystem access.
|
|
64
|
+
*/
|
|
65
|
+
export declare function computeEffectivePatterns(ctx: ProtectedScanContext): EffectivePatterns;
|
|
66
|
+
/**
|
|
67
|
+
* Run a list of detected writes against the protected-paths policy.
|
|
68
|
+
* Returns the FIRST blocking verdict, or allow if every detection is
|
|
69
|
+
* clean.
|
|
70
|
+
*
|
|
71
|
+
* Order: walk detections in order of appearance. The walker emits
|
|
72
|
+
* them in source order, so the operator sees the EARLIEST violation
|
|
73
|
+
* in the error message.
|
|
74
|
+
*/
|
|
75
|
+
export declare function scanForProtectedViolations(ctx: ProtectedScanContext, detections: readonly DetectedWrite[]): Verdict;
|
|
76
|
+
export {};
|