@aliou/pi-guardrails 0.11.2 → 0.12.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 +72 -167
- package/extensions/guardrails/commands/examples/index.ts +520 -0
- package/extensions/guardrails/commands/onboarding/config.ts +54 -0
- package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts} +5 -31
- package/extensions/guardrails/commands/settings/add-rule-wizard.ts +267 -0
- package/extensions/guardrails/commands/settings/examples.ts +399 -0
- package/extensions/guardrails/commands/settings/index.ts +596 -0
- package/extensions/guardrails/commands/settings/path-list-editor.ts +158 -0
- package/extensions/guardrails/commands/settings/scope-picker-submenu.ts +69 -0
- package/extensions/guardrails/commands/settings/utils.ts +108 -0
- package/extensions/guardrails/components/onboarding-choice-step.ts +140 -0
- package/extensions/guardrails/components/onboarding-finish-step.ts +50 -0
- package/extensions/guardrails/components/onboarding-intro-step.ts +30 -0
- package/extensions/guardrails/components/onboarding-types.ts +10 -0
- package/extensions/guardrails/components/onboarding-wizard.ts +116 -0
- package/{src → extensions/guardrails}/components/pattern-editor.ts +11 -10
- package/extensions/guardrails/index.ts +106 -0
- package/extensions/guardrails/rules.test.ts +107 -0
- package/extensions/guardrails/rules.ts +119 -0
- package/extensions/guardrails/targets.test.ts +44 -0
- package/extensions/guardrails/targets.ts +66 -0
- package/extensions/path-access/grants.test.ts +47 -0
- package/extensions/path-access/grants.ts +68 -0
- package/extensions/path-access/index.ts +143 -0
- package/extensions/path-access/prompt.ts +196 -0
- package/extensions/path-access/rules.test.ts +46 -0
- package/extensions/path-access/rules.ts +37 -0
- package/extensions/path-access/targets.test.ts +40 -0
- package/extensions/path-access/targets.ts +19 -0
- package/extensions/permission-gate/grants.ts +21 -0
- package/extensions/permission-gate/index.ts +122 -0
- package/extensions/permission-gate/prompt.ts +222 -0
- package/extensions/permission-gate/rules.test.ts +132 -0
- package/extensions/permission-gate/rules.ts +72 -0
- package/package.json +18 -20
- package/schema.json +286 -0
- package/src/core/check.test.ts +169 -0
- package/src/core/check.ts +38 -0
- package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts} +134 -2
- package/src/{hooks/permission-gate/dangerous-commands.ts → core/commands/dangerous.ts} +119 -1
- package/src/core/commands/index.ts +15 -0
- package/src/core/index.ts +13 -0
- package/src/{utils/path-access.test.ts → core/paths/access.test.ts} +1 -5
- package/src/core/paths/index.ts +14 -0
- package/src/{utils → core/shell}/command-args.test.ts +31 -20
- package/src/core/shell/index.ts +2 -0
- package/src/core/types.ts +55 -0
- package/src/shared/config/defaults.ts +118 -0
- package/src/shared/config/index.ts +17 -0
- package/src/shared/config/loader.ts +64 -0
- package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
- package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
- package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
- package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
- package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
- package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
- package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
- package/src/shared/config/migration/index.ts +44 -0
- package/src/shared/config/migration/version.ts +7 -0
- package/src/shared/config/types.ts +141 -0
- package/src/shared/events.ts +100 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/matching.test.ts +86 -0
- package/src/{utils → shared}/matching.ts +4 -4
- package/src/{utils → shared/paths}/bash-paths.test.ts +11 -2
- package/src/{utils → shared/paths}/bash-paths.ts +4 -4
- package/src/shared/paths/index.ts +1 -0
- package/src/shared/warnings.ts +17 -0
- package/docs/defaults.md +0 -140
- package/docs/examples.md +0 -170
- package/src/commands/onboarding.ts +0 -390
- package/src/commands/settings-command.ts +0 -1616
- package/src/config.ts +0 -392
- package/src/hooks/index.ts +0 -11
- package/src/hooks/path-access.ts +0 -395
- package/src/hooks/permission-gate/index.test.ts +0 -332
- package/src/hooks/permission-gate/index.ts +0 -595
- package/src/hooks/policies.ts +0 -322
- package/src/index.ts +0 -96
- package/src/lib/executor.ts +0 -280
- package/src/lib/index.ts +0 -16
- package/src/lib/model-resolver.ts +0 -47
- package/src/lib/timing.ts +0 -42
- package/src/lib/types.ts +0 -115
- package/src/utils/events.ts +0 -32
- package/src/utils/migration.test.ts +0 -58
- package/src/utils/migration.ts +0 -340
- package/src/utils/warnings.ts +0 -7
- /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
- /package/src/{utils → core/paths}/path.test.ts +0 -0
- /package/src/{utils → core/paths}/path.ts +0 -0
- /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
- /package/src/{utils → core/shell}/command-args.ts +0 -0
- /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { parse } from "@aliou/sh";
|
|
2
|
+
import { walkCommands, wordToString } from "../shell/ast";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Dangerous command matchers for the permission gate.
|
|
3
6
|
*
|
|
@@ -8,6 +11,29 @@
|
|
|
8
11
|
|
|
9
12
|
export type StructuralMatcher = (words: string[]) => string | undefined;
|
|
10
13
|
|
|
14
|
+
export interface CommandPattern {
|
|
15
|
+
pattern: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
regex?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CompiledCommandPattern {
|
|
21
|
+
test: (input: string) => boolean;
|
|
22
|
+
source: CommandPattern;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DangerousCommandMatch {
|
|
26
|
+
description: string;
|
|
27
|
+
pattern: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DangerousCommandCheckOptions {
|
|
31
|
+
command: string;
|
|
32
|
+
patterns: readonly CompiledCommandPattern[];
|
|
33
|
+
useBuiltinMatchers: boolean;
|
|
34
|
+
fallbackPatterns: readonly CommandPattern[];
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
/**
|
|
12
38
|
* Helper to check if any word starts with a given prefix.
|
|
13
39
|
*/
|
|
@@ -332,7 +358,7 @@ export const BUILTIN_KEYWORD_PATTERNS = new Set([
|
|
|
332
358
|
*/
|
|
333
359
|
export function matchDangerousCommand(
|
|
334
360
|
words: string[],
|
|
335
|
-
):
|
|
361
|
+
): DangerousCommandMatch | undefined {
|
|
336
362
|
for (const matcher of BUILTIN_MATCHERS) {
|
|
337
363
|
const description = matcher(words);
|
|
338
364
|
if (description) {
|
|
@@ -343,3 +369,95 @@ export function matchDangerousCommand(
|
|
|
343
369
|
}
|
|
344
370
|
return undefined;
|
|
345
371
|
}
|
|
372
|
+
|
|
373
|
+
export function compileCommandPattern(
|
|
374
|
+
config: CommandPattern,
|
|
375
|
+
): CompiledCommandPattern {
|
|
376
|
+
if (config.regex) {
|
|
377
|
+
try {
|
|
378
|
+
const re = new RegExp(config.pattern);
|
|
379
|
+
return { test: (input) => re.test(input), source: config };
|
|
380
|
+
} catch {
|
|
381
|
+
return { test: () => false, source: config };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
test: (input) => input.includes(config.pattern),
|
|
387
|
+
source: config,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function compileCommandPatterns(
|
|
392
|
+
configs: readonly CommandPattern[],
|
|
393
|
+
): CompiledCommandPattern[] {
|
|
394
|
+
return configs.map(compileCommandPattern);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function matchBuiltinDangerous(
|
|
398
|
+
words: string[],
|
|
399
|
+
): DangerousCommandMatch | undefined {
|
|
400
|
+
if (words.length === 0) return undefined;
|
|
401
|
+
for (const matcher of BUILTIN_MATCHERS) {
|
|
402
|
+
const description = matcher(words);
|
|
403
|
+
if (description) return { description, pattern: "(structural)" };
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function checkDangerousCommand({
|
|
409
|
+
command,
|
|
410
|
+
patterns,
|
|
411
|
+
useBuiltinMatchers,
|
|
412
|
+
fallbackPatterns,
|
|
413
|
+
}: DangerousCommandCheckOptions): DangerousCommandMatch | undefined {
|
|
414
|
+
let parsedSuccessfully = false;
|
|
415
|
+
|
|
416
|
+
if (useBuiltinMatchers) {
|
|
417
|
+
try {
|
|
418
|
+
const { ast } = parse(command);
|
|
419
|
+
parsedSuccessfully = true;
|
|
420
|
+
let match: DangerousCommandMatch | undefined;
|
|
421
|
+
walkCommands(ast, (cmd) => {
|
|
422
|
+
const words = (cmd.words ?? []).map(wordToString);
|
|
423
|
+
const result = matchBuiltinDangerous(words);
|
|
424
|
+
if (result) {
|
|
425
|
+
match = result;
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
});
|
|
430
|
+
if (match) return match;
|
|
431
|
+
} catch {
|
|
432
|
+
for (const pattern of fallbackPatterns) {
|
|
433
|
+
if (command.includes(pattern.pattern)) {
|
|
434
|
+
return {
|
|
435
|
+
description: pattern.description ?? pattern.pattern,
|
|
436
|
+
pattern: pattern.pattern,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
for (const compiled of patterns) {
|
|
444
|
+
const source = compiled.source;
|
|
445
|
+
if (
|
|
446
|
+
useBuiltinMatchers &&
|
|
447
|
+
parsedSuccessfully &&
|
|
448
|
+
!source.regex &&
|
|
449
|
+
BUILTIN_KEYWORD_PATTERNS.has(source.pattern)
|
|
450
|
+
) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (compiled.test(command)) {
|
|
455
|
+
return {
|
|
456
|
+
description: source.description ?? source.pattern,
|
|
457
|
+
pattern: source.pattern,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CommandPattern,
|
|
3
|
+
CompiledCommandPattern,
|
|
4
|
+
DangerousCommandCheckOptions,
|
|
5
|
+
DangerousCommandMatch,
|
|
6
|
+
StructuralMatcher,
|
|
7
|
+
} from "./dangerous";
|
|
8
|
+
export {
|
|
9
|
+
BUILTIN_KEYWORD_PATTERNS,
|
|
10
|
+
BUILTIN_MATCHERS,
|
|
11
|
+
checkDangerousCommand,
|
|
12
|
+
compileCommandPattern,
|
|
13
|
+
compileCommandPatterns,
|
|
14
|
+
matchDangerousCommand,
|
|
15
|
+
} from "./dangerous";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { checkAction, resolveDecision } from "./check";
|
|
2
|
+
export * from "./commands";
|
|
3
|
+
export * from "./paths";
|
|
4
|
+
export * from "./shell";
|
|
5
|
+
export type {
|
|
6
|
+
Action,
|
|
7
|
+
Decision,
|
|
8
|
+
Grant,
|
|
9
|
+
PermissionState,
|
|
10
|
+
Rule,
|
|
11
|
+
RuleResult,
|
|
12
|
+
Safety,
|
|
13
|
+
} from "./types";
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { assert, describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
checkPathAccess,
|
|
4
|
-
isPathAllowed,
|
|
5
|
-
type PathAccessState,
|
|
6
|
-
} from "./path-access";
|
|
2
|
+
import { checkPathAccess, isPathAllowed, type PathAccessState } from "./access";
|
|
7
3
|
|
|
8
4
|
describe("isPathAllowed", () => {
|
|
9
5
|
describe("when allowedPaths is empty", () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
checkPathAccess,
|
|
3
|
+
isPathAllowed,
|
|
4
|
+
type PathAccessState,
|
|
5
|
+
type PathDecision,
|
|
6
|
+
} from "./access";
|
|
7
|
+
export {
|
|
8
|
+
expandHomePath,
|
|
9
|
+
isWithinBoundary,
|
|
10
|
+
maybePathLike,
|
|
11
|
+
normalizeForDisplay,
|
|
12
|
+
resolveFromCwd,
|
|
13
|
+
toStorageForm,
|
|
14
|
+
} from "./path";
|
|
@@ -12,37 +12,45 @@ describe("classifyCommandArgs", () => {
|
|
|
12
12
|
]);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
it("normalizes command basenames", () => {
|
|
16
|
+
expect(tokens("/usr/bin/awk", ["/aaa/{print}", "./input"])).toEqual([
|
|
17
|
+
"./input",
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
15
21
|
it("ignores awk inline program and keeps file operands", () => {
|
|
16
22
|
expect(tokens("awk", ["/aaa/{print}", "./input"])).toEqual(["./input"]);
|
|
17
23
|
});
|
|
18
24
|
|
|
19
|
-
it(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
]);
|
|
25
|
+
it.each([
|
|
26
|
+
["-f as separate option", ["-f", "./prog.awk", "./input"]],
|
|
27
|
+
["-f as joined option", ["-f./prog.awk", "./input"]],
|
|
28
|
+
])("keeps awk program files with %s", (_label, args) => {
|
|
29
|
+
expect(tokens("awk", args)).toEqual(["./prog.awk", "./input"]);
|
|
24
30
|
});
|
|
25
31
|
|
|
26
32
|
it("ignores sed inline scripts and keeps file operands", () => {
|
|
27
33
|
expect(tokens("sed", ["s#/old#/new#g", "./file"])).toEqual(["./file"]);
|
|
28
34
|
});
|
|
29
35
|
|
|
30
|
-
it(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
it.each([
|
|
37
|
+
["-f as separate option", ["-f", "./script.sed", "./file"]],
|
|
38
|
+
["--file as long option", ["--file", "./script.sed", "./file"]],
|
|
39
|
+
["-f as joined option", ["-f./script.sed", "./file"]],
|
|
40
|
+
])("keeps sed script files with %s", (_label, args) => {
|
|
41
|
+
expect(tokens("sed", args)).toEqual(["./script.sed", "./file"]);
|
|
35
42
|
});
|
|
36
43
|
|
|
37
44
|
it("ignores grep patterns and keeps file operands", () => {
|
|
38
45
|
expect(tokens("grep", ["/api/v1", "./src"])).toEqual(["./src"]);
|
|
39
46
|
});
|
|
40
47
|
|
|
41
|
-
it(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
it.each([
|
|
49
|
+
["-f as separate option", ["-f", "./patterns", "./src"]],
|
|
50
|
+
["--file as long option", ["--file", "./patterns", "./src"]],
|
|
51
|
+
["-f as joined option", ["-f./patterns", "./src"]],
|
|
52
|
+
])("keeps grep pattern files with %s", (_label, args) => {
|
|
53
|
+
expect(tokens("grep", args)).toEqual(["./patterns", "./src"]);
|
|
46
54
|
});
|
|
47
55
|
|
|
48
56
|
it("keeps find roots and ignores expression patterns", () => {
|
|
@@ -57,11 +65,14 @@ describe("classifyCommandArgs", () => {
|
|
|
57
65
|
]);
|
|
58
66
|
});
|
|
59
67
|
|
|
60
|
-
it(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
68
|
+
it.each([
|
|
69
|
+
["-f as separate option", ["-f", "./filter.jq", "./data.json"]],
|
|
70
|
+
[
|
|
71
|
+
"--from-file as long option",
|
|
72
|
+
["--from-file", "./filter.jq", "./data.json"],
|
|
73
|
+
],
|
|
74
|
+
])("keeps jq filter files with %s", (_label, args) => {
|
|
75
|
+
expect(tokens("jq", args)).toEqual(["./filter.jq", "./data.json"]);
|
|
65
76
|
});
|
|
66
77
|
|
|
67
78
|
it("ignores interpreter inline code", () => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type Action =
|
|
2
|
+
| {
|
|
3
|
+
kind: "file";
|
|
4
|
+
path: string;
|
|
5
|
+
origin?: string;
|
|
6
|
+
}
|
|
7
|
+
| {
|
|
8
|
+
kind: "command";
|
|
9
|
+
command: string;
|
|
10
|
+
origin?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RuleResult<TMeta = null> =
|
|
14
|
+
| {
|
|
15
|
+
kind: "pass";
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
kind: "match";
|
|
19
|
+
reason: string;
|
|
20
|
+
metadata: TMeta;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type Safety<TMeta = null> =
|
|
24
|
+
| {
|
|
25
|
+
kind: "safe";
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
kind: "dangerous";
|
|
29
|
+
action: Action;
|
|
30
|
+
key: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
metadata: TMeta;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type Rule<TMeta = null> = {
|
|
36
|
+
key: string;
|
|
37
|
+
check: (action: Action) => RuleResult<TMeta> | Promise<RuleResult<TMeta>>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type PermissionState = "granted" | "prompt" | "denied";
|
|
41
|
+
|
|
42
|
+
export type Grant = "once" | "always" | "never";
|
|
43
|
+
|
|
44
|
+
export type Decision<TMeta = null> =
|
|
45
|
+
| {
|
|
46
|
+
kind: "allow";
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
kind: "deny";
|
|
50
|
+
reason: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
kind: "prompt";
|
|
54
|
+
risk: Safety<TMeta> & { kind: "dangerous" };
|
|
55
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { CURRENT_VERSION } from "./migration";
|
|
2
|
+
import type { ResolvedConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_CONFIG: ResolvedConfig = {
|
|
5
|
+
version: CURRENT_VERSION,
|
|
6
|
+
enabled: true,
|
|
7
|
+
applyBuiltinDefaults: true,
|
|
8
|
+
features: {
|
|
9
|
+
policies: true,
|
|
10
|
+
permissionGate: true,
|
|
11
|
+
pathAccess: false,
|
|
12
|
+
},
|
|
13
|
+
pathAccess: {
|
|
14
|
+
mode: "ask",
|
|
15
|
+
allowedPaths: [],
|
|
16
|
+
},
|
|
17
|
+
policies: {
|
|
18
|
+
rules: [
|
|
19
|
+
{
|
|
20
|
+
id: "secret-files",
|
|
21
|
+
description: "Files containing secrets",
|
|
22
|
+
patterns: [
|
|
23
|
+
{ pattern: ".env" },
|
|
24
|
+
{ pattern: ".env.local" },
|
|
25
|
+
{ pattern: ".env.production" },
|
|
26
|
+
{ pattern: ".env.prod" },
|
|
27
|
+
{ pattern: ".dev.vars" },
|
|
28
|
+
],
|
|
29
|
+
allowedPatterns: [
|
|
30
|
+
{ pattern: "*.example.env" },
|
|
31
|
+
{ pattern: "*.sample.env" },
|
|
32
|
+
{ pattern: "*.test.env" },
|
|
33
|
+
{ pattern: ".env.example" },
|
|
34
|
+
{ pattern: ".env.sample" },
|
|
35
|
+
{ pattern: ".env.test" },
|
|
36
|
+
],
|
|
37
|
+
protection: "noAccess",
|
|
38
|
+
onlyIfExists: true,
|
|
39
|
+
blockMessage:
|
|
40
|
+
"Accessing {file} is not allowed. This file contains secrets. " +
|
|
41
|
+
"Explain to the user why you want to access this file, and if changes are needed ask the user to make them.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "home-ssh",
|
|
45
|
+
description: "SSH directory and keys",
|
|
46
|
+
enabled: false,
|
|
47
|
+
patterns: [
|
|
48
|
+
{ pattern: "~/.ssh/**" },
|
|
49
|
+
{ pattern: "~/.ssh/*_rsa" },
|
|
50
|
+
{ pattern: "~/.ssh/*_ed25519" },
|
|
51
|
+
{ pattern: "~/.ssh/*.pem" },
|
|
52
|
+
],
|
|
53
|
+
allowedPatterns: [{ pattern: "~/.ssh/*.pub" }],
|
|
54
|
+
protection: "noAccess",
|
|
55
|
+
onlyIfExists: true,
|
|
56
|
+
blockMessage:
|
|
57
|
+
"Accessing {file} is not allowed. This file is part of your SSH configuration and may contain private keys or sensitive host information.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "home-config",
|
|
61
|
+
description: "Sensitive user configuration directories",
|
|
62
|
+
enabled: false,
|
|
63
|
+
patterns: [
|
|
64
|
+
{ pattern: "~/.config/gh/**" },
|
|
65
|
+
{ pattern: "~/.config/gcloud/**" },
|
|
66
|
+
{ pattern: "~/.config/op/**" },
|
|
67
|
+
{ pattern: "~/.config/sops/**" },
|
|
68
|
+
],
|
|
69
|
+
protection: "noAccess",
|
|
70
|
+
onlyIfExists: true,
|
|
71
|
+
blockMessage:
|
|
72
|
+
"Accessing {file} is not allowed. This file is in a sensitive user configuration directory and may contain credentials or tokens.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "home-gpg",
|
|
76
|
+
description: "GPG keys and configuration",
|
|
77
|
+
enabled: false,
|
|
78
|
+
patterns: [
|
|
79
|
+
{ pattern: "~/.gnupg/**" },
|
|
80
|
+
{ pattern: "~/*.gpg" },
|
|
81
|
+
{ pattern: "~/.gpg-agent.conf" },
|
|
82
|
+
],
|
|
83
|
+
protection: "noAccess",
|
|
84
|
+
onlyIfExists: true,
|
|
85
|
+
blockMessage:
|
|
86
|
+
"Accessing {file} is not allowed. This file is part of your GPG configuration and may contain private keys or trust settings.",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
permissionGate: {
|
|
91
|
+
patterns: [
|
|
92
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
93
|
+
{ pattern: "sudo", description: "superuser command" },
|
|
94
|
+
{ pattern: "dd of=", description: "disk write operation" },
|
|
95
|
+
{ pattern: "mkfs.", description: "filesystem format" },
|
|
96
|
+
{
|
|
97
|
+
pattern: "chmod -R 777",
|
|
98
|
+
description: "insecure recursive permissions",
|
|
99
|
+
},
|
|
100
|
+
{ pattern: "chown -R", description: "recursive ownership change" },
|
|
101
|
+
{ pattern: "doas", description: "privileged command execution" },
|
|
102
|
+
{ pattern: "pkexec", description: "privileged command execution" },
|
|
103
|
+
{ pattern: "shred", description: "secure file overwrite" },
|
|
104
|
+
{ pattern: "wipefs", description: "filesystem signature wipe" },
|
|
105
|
+
{ pattern: "blkdiscard", description: "block device discard" },
|
|
106
|
+
{ pattern: "fdisk", description: "disk partitioning" },
|
|
107
|
+
{ pattern: "parted", description: "disk partitioning" },
|
|
108
|
+
{
|
|
109
|
+
pattern: "docker run --privileged",
|
|
110
|
+
description: "container with privileged mode",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
useBuiltinMatchers: true,
|
|
114
|
+
requireConfirmation: true,
|
|
115
|
+
allowedPatterns: [],
|
|
116
|
+
autoDenyPatterns: [],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { DEFAULT_CONFIG } from "./defaults";
|
|
2
|
+
export { configLoader } from "./loader";
|
|
3
|
+
export {
|
|
4
|
+
CURRENT_VERSION,
|
|
5
|
+
globalConfigMigrations,
|
|
6
|
+
migrations,
|
|
7
|
+
} from "./migration";
|
|
8
|
+
export type {
|
|
9
|
+
DangerousPattern,
|
|
10
|
+
GuardrailsConfig,
|
|
11
|
+
PathAccessConfig,
|
|
12
|
+
PathAccessMode,
|
|
13
|
+
PatternConfig,
|
|
14
|
+
PolicyRule,
|
|
15
|
+
Protection,
|
|
16
|
+
ResolvedConfig,
|
|
17
|
+
} from "./types";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { buildSchemaUrl, ConfigLoader } from "@aliou/pi-utils-settings";
|
|
2
|
+
import pkg from "../../../package.json" with { type: "json" };
|
|
3
|
+
import { DEFAULT_CONFIG } from "./defaults";
|
|
4
|
+
import { migrations } from "./migration";
|
|
5
|
+
import type { GuardrailsConfig, PolicyRule, ResolvedConfig } from "./types";
|
|
6
|
+
|
|
7
|
+
export const configLoader = new ConfigLoader<GuardrailsConfig, ResolvedConfig>(
|
|
8
|
+
"guardrails",
|
|
9
|
+
DEFAULT_CONFIG,
|
|
10
|
+
{
|
|
11
|
+
scopes: ["global", "local", "memory"],
|
|
12
|
+
migrations,
|
|
13
|
+
schemaUrl: buildSchemaUrl(pkg.name, pkg.version),
|
|
14
|
+
afterMerge: (resolved, global, local, memory) => {
|
|
15
|
+
const ruleMap = new Map<string, PolicyRule>();
|
|
16
|
+
|
|
17
|
+
if (resolved.applyBuiltinDefaults) {
|
|
18
|
+
for (const rule of DEFAULT_CONFIG.policies.rules) {
|
|
19
|
+
ruleMap.set(rule.id, rule);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (global?.policies?.rules) {
|
|
23
|
+
for (const rule of global.policies.rules) {
|
|
24
|
+
ruleMap.set(rule.id, rule);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (local?.policies?.rules) {
|
|
28
|
+
for (const rule of local.policies.rules) {
|
|
29
|
+
ruleMap.set(rule.id, rule);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (memory?.policies?.rules) {
|
|
33
|
+
for (const rule of memory.policies.rules) {
|
|
34
|
+
ruleMap.set(rule.id, rule);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
resolved.policies.rules = [...ruleMap.values()];
|
|
38
|
+
|
|
39
|
+
const customPatterns =
|
|
40
|
+
memory?.permissionGate?.customPatterns ??
|
|
41
|
+
local?.permissionGate?.customPatterns ??
|
|
42
|
+
global?.permissionGate?.customPatterns;
|
|
43
|
+
if (customPatterns) {
|
|
44
|
+
resolved.permissionGate.patterns = customPatterns;
|
|
45
|
+
resolved.permissionGate.useBuiltinMatchers = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const mergedPaths = new Set<string>();
|
|
49
|
+
for (const paths of [
|
|
50
|
+
global?.pathAccess?.allowedPaths,
|
|
51
|
+
local?.pathAccess?.allowedPaths,
|
|
52
|
+
memory?.pathAccess?.allowedPaths,
|
|
53
|
+
]) {
|
|
54
|
+
for (const path of paths ?? []) {
|
|
55
|
+
const trimmed = path.trim();
|
|
56
|
+
if (trimmed) mergedPaths.add(trimmed);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
resolved.pathAccess.allowedPaths = [...mergedPaths];
|
|
60
|
+
|
|
61
|
+
return resolved;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { copyFile, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { addPendingWarning } from "../../warnings";
|
|
4
|
+
import type {
|
|
5
|
+
DangerousPattern,
|
|
6
|
+
GuardrailsConfig,
|
|
7
|
+
PatternConfig,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { CURRENT_VERSION } from "./version";
|
|
10
|
+
|
|
11
|
+
export function shouldRun(config: GuardrailsConfig): boolean {
|
|
12
|
+
return config.version === undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function run(
|
|
16
|
+
config: GuardrailsConfig,
|
|
17
|
+
filePath: string,
|
|
18
|
+
): Promise<GuardrailsConfig> {
|
|
19
|
+
await backupConfig(filePath);
|
|
20
|
+
return migrateV0(config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function migrateV0(config: GuardrailsConfig): GuardrailsConfig {
|
|
24
|
+
const migrated = structuredClone(config);
|
|
25
|
+
|
|
26
|
+
if (migrated.envFiles) {
|
|
27
|
+
if (migrated.envFiles.protectedPatterns) {
|
|
28
|
+
migrated.envFiles.protectedPatterns = migrateStringArray(
|
|
29
|
+
migrated.envFiles.protectedPatterns,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (migrated.envFiles.allowedPatterns) {
|
|
33
|
+
migrated.envFiles.allowedPatterns = migrateStringArray(
|
|
34
|
+
migrated.envFiles.allowedPatterns,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (migrated.envFiles.protectedDirectories) {
|
|
38
|
+
migrated.envFiles.protectedDirectories = migrateStringArray(
|
|
39
|
+
migrated.envFiles.protectedDirectories,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (migrated.permissionGate) {
|
|
45
|
+
if (migrated.permissionGate.patterns) {
|
|
46
|
+
migrated.permissionGate.patterns = migrateDangerousPatterns(
|
|
47
|
+
migrated.permissionGate.patterns,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (migrated.permissionGate.customPatterns) {
|
|
51
|
+
migrated.permissionGate.customPatterns = migrateDangerousPatterns(
|
|
52
|
+
migrated.permissionGate.customPatterns,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (migrated.permissionGate.allowedPatterns) {
|
|
56
|
+
migrated.permissionGate.allowedPatterns = migrateStringArray(
|
|
57
|
+
migrated.permissionGate.allowedPatterns,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (migrated.permissionGate.autoDenyPatterns) {
|
|
61
|
+
migrated.permissionGate.autoDenyPatterns = migrateStringArray(
|
|
62
|
+
migrated.permissionGate.autoDenyPatterns,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
migrated.version = CURRENT_VERSION;
|
|
68
|
+
return migrated;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function migrateStringArray(
|
|
72
|
+
items: (string | PatternConfig)[],
|
|
73
|
+
): PatternConfig[] {
|
|
74
|
+
return items.map((item) => {
|
|
75
|
+
if (typeof item === "string") return { pattern: item, regex: true };
|
|
76
|
+
if (item.regex === undefined) return { ...item, regex: true };
|
|
77
|
+
return item;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function migrateDangerousPatterns(
|
|
82
|
+
items: (DangerousPattern | { pattern: string; description: string })[],
|
|
83
|
+
): DangerousPattern[] {
|
|
84
|
+
return items.map((item) => {
|
|
85
|
+
if ("regex" in item && item.regex !== undefined) {
|
|
86
|
+
return item as DangerousPattern;
|
|
87
|
+
}
|
|
88
|
+
return { ...item, regex: true };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function backupConfig(configPath: string): Promise<void> {
|
|
93
|
+
const dir = dirname(configPath);
|
|
94
|
+
const basename = configPath.split("/").pop() ?? "guardrails.json";
|
|
95
|
+
const backupName = basename.replace(".json", ".v0.json");
|
|
96
|
+
const backupPath = resolve(dir, backupName);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await stat(backupPath);
|
|
100
|
+
} catch {
|
|
101
|
+
try {
|
|
102
|
+
await copyFile(configPath, backupPath);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
addPendingWarning(`guardrails: could not back up config: ${err}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { addPendingWarning } from "../../warnings";
|
|
2
|
+
import type { GuardrailsConfig } from "../types";
|
|
3
|
+
import { CURRENT_VERSION } from "./version";
|
|
4
|
+
|
|
5
|
+
const REMOVED_FEATURE_KEYS = [
|
|
6
|
+
"preventBrew",
|
|
7
|
+
"preventPython",
|
|
8
|
+
"enforcePackageManager",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export function shouldRun(config: GuardrailsConfig): boolean {
|
|
12
|
+
const raw = config as Record<string, unknown>;
|
|
13
|
+
const features = raw.features as Record<string, unknown> | undefined;
|
|
14
|
+
if (features) {
|
|
15
|
+
for (const key of REMOVED_FEATURE_KEYS) {
|
|
16
|
+
if (key in features) return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return "packageManager" in raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function run(config: GuardrailsConfig): GuardrailsConfig {
|
|
23
|
+
addPendingWarning(
|
|
24
|
+
"[guardrails] preventBrew, preventPython, enforcePackageManager, and packageManager " +
|
|
25
|
+
"have been removed from guardrails and moved to @aliou/pi-toolchain. " +
|
|
26
|
+
"These fields will be stripped from your config.",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const cleaned = structuredClone(config) as Record<string, unknown>;
|
|
30
|
+
const features = cleaned.features as Record<string, unknown> | undefined;
|
|
31
|
+
if (features) {
|
|
32
|
+
for (const key of REMOVED_FEATURE_KEYS) {
|
|
33
|
+
delete features[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
delete cleaned.packageManager;
|
|
37
|
+
cleaned.version = CURRENT_VERSION;
|
|
38
|
+
return cleaned as GuardrailsConfig;
|
|
39
|
+
}
|