dsh-perm-gate 2.1.2 → 4.0.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/CHANGELOG.ja.md +82 -0
- package/CHANGELOG.ko.md +80 -0
- package/CHANGELOG.md +225 -0
- package/INSTALL.ja.md +15 -9
- package/INSTALL.ko.md +16 -10
- package/INSTALL.md +17 -9
- package/INSTALL.zh.md +13 -8
- package/README.ja.md +17 -5
- package/README.ko.md +18 -6
- package/README.md +33 -3
- package/README.zh.md +11 -3
- package/docs/baseline-guard.md +56 -0
- package/docs/rules-format.en.md +294 -0
- package/docs/rules-format.md +279 -0
- package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
- package/dsh.plugin.json +2 -2
- package/examples/permissions.example.yaml +16 -1
- package/lib/allowlist.d.ts +27 -0
- package/lib/allowlist.js +103 -1
- package/lib/cli.js +11 -14
- package/lib/client.js +646 -95
- package/lib/client.js.map +1 -1
- package/lib/command-dispatcher.d.ts +22 -0
- package/lib/command-dispatcher.js +97 -0
- package/lib/command-semantics.d.ts +62 -0
- package/lib/command-semantics.js +6 -0
- package/lib/compiler.d.ts +11 -0
- package/lib/compiler.js +16 -0
- package/lib/config.d.ts +73 -12
- package/lib/config.js +56 -14
- package/lib/dry-run.d.ts +87 -0
- package/lib/dry-run.js +110 -0
- package/lib/engine.js +52 -2
- package/lib/evaluate.js +76 -2
- package/lib/events.d.ts +91 -2
- package/lib/events.js +220 -0
- package/lib/index.js +111 -26
- package/lib/parsers/git.d.ts +43 -0
- package/lib/parsers/git.js +404 -0
- package/lib/parsers/shell-cmds.d.ts +32 -0
- package/lib/parsers/shell-cmds.js +293 -0
- package/lib/preset.d.ts +13 -0
- package/lib/preset.js +17 -0
- package/lib/risk.d.ts +17 -3
- package/lib/risk.js +17 -3
- package/lib/rule-chain.js +13 -22
- package/lib/rule-dims.d.ts +35 -0
- package/lib/rule-dims.js +36 -0
- package/lib/rule.d.ts +36 -1
- package/lib/rule.js +56 -8
- package/lib/rules-view.d.ts +52 -0
- package/lib/rules-view.js +126 -0
- package/lib/runtime.d.ts +121 -7
- package/lib/runtime.js +204 -25
- package/lib/shell.d.ts +14 -0
- package/lib/shell.js +17 -0
- package/package.json +13 -5
package/lib/allowlist.js
CHANGED
|
@@ -2,11 +2,113 @@
|
|
|
2
2
|
* Durable allowlist (whitelist) writes for dsh-perm-gate.
|
|
3
3
|
*
|
|
4
4
|
* The "allow everywhere" approval action appends a matched command pattern to
|
|
5
|
-
* the rules
|
|
5
|
+
* the rules' `allow` list and reloads, so a human-approved command is
|
|
6
6
|
* persistently whitelisted across sessions — not just granted once.
|
|
7
|
+
*
|
|
8
|
+
* Two storage backs, per the dual-source migration:
|
|
9
|
+
* - **settings** (`appendAllowToSettings` / `replaceAllowInSettings`) — the
|
|
10
|
+
* `dsh-perm-gate-rules` namespace; the primary write path once the settings
|
|
11
|
+
* service is available.
|
|
12
|
+
* - **rules file** (`appendAllowCommand` / `replaceAllowCommands`) — the legacy
|
|
13
|
+
* path, kept as the fallback when no settings scope is wired (and as the
|
|
14
|
+
* migration seed: the first settings write carries the file document along so
|
|
15
|
+
* file-defined deny/ask rules are not shadowed).
|
|
7
16
|
*/
|
|
8
17
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
9
18
|
import { parse, stringify } from 'yaml';
|
|
19
|
+
import { isRulesConfigured } from './config.js';
|
|
20
|
+
function isRecord(v) {
|
|
21
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
/** Accept both the bare rules form and the file's `permissions:`-wrapped form. */
|
|
24
|
+
function unwrapPermissions(seed) {
|
|
25
|
+
if (!isRecord(seed))
|
|
26
|
+
return {};
|
|
27
|
+
return isRecord(seed['permissions']) ? seed['permissions'] : seed;
|
|
28
|
+
}
|
|
29
|
+
/** The file document a first write should migrate into the namespace, or bare defaults. */
|
|
30
|
+
function seedOrDefaults(seed) {
|
|
31
|
+
const root = unwrapPermissions(seed);
|
|
32
|
+
return {
|
|
33
|
+
defaultAction: typeof root['defaultAction'] === 'string' ? root['defaultAction'] : 'ask',
|
|
34
|
+
deny: Array.isArray(root['deny']) ? root['deny'] : [],
|
|
35
|
+
allow: Array.isArray(root['allow']) ? root['allow'] : [],
|
|
36
|
+
ask: Array.isArray(root['ask']) ? root['ask'] : [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function allowEntriesOf(section) {
|
|
40
|
+
const allow = section['allow'];
|
|
41
|
+
return Array.isArray(allow) ? allow : [];
|
|
42
|
+
}
|
|
43
|
+
function commandEntries(patterns, reason) {
|
|
44
|
+
return patterns.map((pattern) => ({ command: [pattern], reason }));
|
|
45
|
+
}
|
|
46
|
+
/** The allow-list command patterns currently in the settings rules document (read-only). */
|
|
47
|
+
export function listAllowFromRulesDoc(doc) {
|
|
48
|
+
const section = unwrapPermissions(doc);
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const entry of allowEntriesOf(section)) {
|
|
51
|
+
if (!isRecord(entry))
|
|
52
|
+
continue;
|
|
53
|
+
const command = entry['command'];
|
|
54
|
+
if (typeof command === 'string') {
|
|
55
|
+
out.push(command);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(command))
|
|
59
|
+
continue;
|
|
60
|
+
for (const c of command)
|
|
61
|
+
if (typeof c === 'string')
|
|
62
|
+
out.push(c);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Append one command pattern as a new `allow` entry in the settings rules
|
|
68
|
+
* namespace. When the namespace is still unconfigured (bare defaults) and a
|
|
69
|
+
* file document is supplied, the namespace is seeded from it first — the
|
|
70
|
+
* implicit migration — so the first settings write cannot shadow file-defined
|
|
71
|
+
* deny/ask rules. Returns false when the scope rejects the write.
|
|
72
|
+
*/
|
|
73
|
+
export async function appendAllowToSettings(scope, pattern, reason = 'permissive allow-everywhere', seed) {
|
|
74
|
+
try {
|
|
75
|
+
const current = scope.get();
|
|
76
|
+
const section = isRulesConfigured(current) && isRecord(current)
|
|
77
|
+
? { ...current }
|
|
78
|
+
: seedOrDefaults(seed);
|
|
79
|
+
const allow = allowEntriesOf(section);
|
|
80
|
+
allow.push({ command: [pattern], reason });
|
|
81
|
+
section['allow'] = allow;
|
|
82
|
+
await scope.update(section);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Replace the settings rules namespace's `allow` whitelist with exactly the
|
|
91
|
+
* given command patterns (one allow entry per pattern); empty clears it.
|
|
92
|
+
* Other sections (deny / ask / defaultAction) are preserved — seeded from the
|
|
93
|
+
* file document on the namespace's first write. Returns false when the scope
|
|
94
|
+
* rejects the write.
|
|
95
|
+
*/
|
|
96
|
+
export async function replaceAllowInSettings(scope, patterns, reason = 'permissive allowlist', seed) {
|
|
97
|
+
try {
|
|
98
|
+
if (patterns === undefined)
|
|
99
|
+
return false;
|
|
100
|
+
const current = scope.get();
|
|
101
|
+
const section = isRulesConfigured(current) && isRecord(current)
|
|
102
|
+
? { ...current }
|
|
103
|
+
: seedOrDefaults(seed);
|
|
104
|
+
section['allow'] = commandEntries(patterns, reason);
|
|
105
|
+
await scope.update(section);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
10
112
|
/**
|
|
11
113
|
* Append one command pattern as a new `allow` entry in the rules file.
|
|
12
114
|
* Returns false (leaving the file untouched) on any I/O or shape error.
|
package/lib/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parseArgs } from 'node:util';
|
|
12
12
|
import { resolve } from 'node:path';
|
|
13
|
-
import {
|
|
13
|
+
import { createDryRunRuntime, runDryRun } from './dry-run.js';
|
|
14
14
|
function main() {
|
|
15
15
|
const { values } = parseArgs({
|
|
16
16
|
options: {
|
|
@@ -23,11 +23,7 @@ function main() {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
const rulesFile = values.rules ? resolve(values.rules) : undefined;
|
|
26
|
-
const runtime =
|
|
27
|
-
rulesFile,
|
|
28
|
-
caseInsensitivePaths: true,
|
|
29
|
-
permissive: values.permissive || undefined,
|
|
30
|
-
});
|
|
26
|
+
const runtime = createDryRunRuntime({ rulesFile, permissive: values.permissive });
|
|
31
27
|
if (values.list) {
|
|
32
28
|
process.stdout.write(JSON.stringify({
|
|
33
29
|
rulesFile,
|
|
@@ -53,15 +49,16 @@ function main() {
|
|
|
53
49
|
process.exitCode = 2;
|
|
54
50
|
return;
|
|
55
51
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
52
|
+
// `hostView` reproduces this command's historic output exactly: it reports the
|
|
53
|
+
// host-facing decision path (whose `allow` is a bare passthrough with no
|
|
54
|
+
// reason) and its audit count. The panel reports the pure policy verdict
|
|
55
|
+
// instead, because a session-less caller must not inherit the session layers.
|
|
56
|
+
const result = runDryRun({ tool, args, rulesFile, cwd: values.cwd, hostView: true }, runtime);
|
|
57
|
+
const host = result.host;
|
|
61
58
|
process.stdout.write(JSON.stringify({
|
|
62
|
-
decision:
|
|
63
|
-
reason:
|
|
64
|
-
audited:
|
|
59
|
+
decision: host?.verdict ?? result.verdict,
|
|
60
|
+
reason: host?.reason ?? result.reason,
|
|
61
|
+
audited: host?.audited ?? 0,
|
|
65
62
|
}, null, 2) + '\n');
|
|
66
63
|
}
|
|
67
64
|
main();
|