@bastani/atomic 0.8.28-alpha.2 → 0.8.28-alpha.3
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.md +7 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +18 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring.d.ts +5 -2
- package/dist/builtin/workflows/src/extension/dispatcher.ts +2 -0
- package/dist/builtin/workflows/src/extension/index.ts +8 -0
- package/dist/builtin/workflows/src/extension/render-result.ts +5 -2
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +18 -0
- package/dist/builtin/workflows/src/runs/background/status.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +1251 -110
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +34 -10
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +10 -2
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +28 -9
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +9 -3
- package/dist/builtin/workflows/src/shared/store-types.ts +10 -3
- package/dist/builtin/workflows/src/shared/store.ts +29 -7
- package/dist/builtin/workflows/src/shared/types.ts +12 -10
- package/dist/builtin/workflows/src/tui/run-detail.ts +12 -0
- package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
- package/dist/builtin/workflows/src/tui/status-list.ts +15 -1
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +1 -1
- package/dist/builtin/workflows/src/tui/widget.ts +12 -3
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +3 -3
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +12 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/inline-input.d.ts +28 -0
- package/dist/core/tools/ask-user-question/state/inline-input.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/inline-input.js +56 -0
- package/dist/core/tools/ask-user-question/state/inline-input.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/key-router.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/key-router.js +30 -4
- package/dist/core/tools/ask-user-question/state/key-router.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js +9 -8
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts +3 -2
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.js +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +2 -0
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/projections.js +2 -0
- package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/state-reducer.js +36 -24
- package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/state.d.ts +8 -0
- package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +6 -0
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js +19 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +3 -2
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.js +15 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/types.d.ts +2 -1
- package/dist/core/tools/ask-user-question/tool/types.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/types.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts +5 -2
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js +2 -0
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +1 -0
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +2 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts +3 -3
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/props-adapter.js +11 -4
- package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
- package/dist/core/tools/bash-policy.d.ts +62 -0
- package/dist/core/tools/bash-policy.d.ts.map +1 -0
- package/dist/core/tools/bash-policy.js +1069 -0
- package/dist/core/tools/bash-policy.js.map +1 -0
- package/dist/core/tools/bash.d.ts +5 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +7 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/sdk.md +42 -0
- package/docs/security.md +2 -0
- package/docs/workflows.md +127 -15
- package/package.json +1 -1
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
const UNSUPPORTED_CONTROL_HEADS = new Set([
|
|
2
|
+
"!",
|
|
3
|
+
"[[",
|
|
4
|
+
"]]",
|
|
5
|
+
"case",
|
|
6
|
+
"coproc",
|
|
7
|
+
"do",
|
|
8
|
+
"done",
|
|
9
|
+
"elif",
|
|
10
|
+
"else",
|
|
11
|
+
"esac",
|
|
12
|
+
"fi",
|
|
13
|
+
"for",
|
|
14
|
+
"function",
|
|
15
|
+
"if",
|
|
16
|
+
"in",
|
|
17
|
+
"select",
|
|
18
|
+
"then",
|
|
19
|
+
"time",
|
|
20
|
+
"until",
|
|
21
|
+
"while",
|
|
22
|
+
"{",
|
|
23
|
+
"}",
|
|
24
|
+
]);
|
|
25
|
+
const DIAGNOSTIC_TEXT_LIMIT = 220;
|
|
26
|
+
function truncateDiagnostic(text) {
|
|
27
|
+
if (text.length <= DIAGNOSTIC_TEXT_LIMIT)
|
|
28
|
+
return text;
|
|
29
|
+
return `${text.slice(0, DIAGNOSTIC_TEXT_LIMIT - 1)}…`;
|
|
30
|
+
}
|
|
31
|
+
const BASH_POLICY_TOP_LEVEL_KEYS = ["default", "allow", "deny", "match"];
|
|
32
|
+
const BASH_POLICY_TOP_LEVEL_KEY_SET = new Set(BASH_POLICY_TOP_LEVEL_KEYS);
|
|
33
|
+
function hasOwnRuleKey(value, key) {
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
35
|
+
}
|
|
36
|
+
function hasOnlyRuleKeys(value, allowedKeys) {
|
|
37
|
+
return Object.keys(value).every((key) => allowedKeys.includes(key));
|
|
38
|
+
}
|
|
39
|
+
function unknownBashPolicyTopLevelKeys(policy) {
|
|
40
|
+
return Object.keys(policy).filter((key) => !BASH_POLICY_TOP_LEVEL_KEY_SET.has(key));
|
|
41
|
+
}
|
|
42
|
+
function escapeRegexLiteral(value) {
|
|
43
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
44
|
+
}
|
|
45
|
+
function escapeGlobClassCharacter(char) {
|
|
46
|
+
if (char === "\\")
|
|
47
|
+
return "\\\\";
|
|
48
|
+
if (char === "]")
|
|
49
|
+
return "\\]";
|
|
50
|
+
if (char === "[")
|
|
51
|
+
return "\\[";
|
|
52
|
+
return char;
|
|
53
|
+
}
|
|
54
|
+
function escapeEscapedGlobClassCharacter(char) {
|
|
55
|
+
if (char === "\\")
|
|
56
|
+
return "\\\\";
|
|
57
|
+
if (char === "-")
|
|
58
|
+
return "\\-";
|
|
59
|
+
if (char === "^")
|
|
60
|
+
return "\\^";
|
|
61
|
+
if (char === "]")
|
|
62
|
+
return "\\]";
|
|
63
|
+
if (char === "[")
|
|
64
|
+
return "\\[";
|
|
65
|
+
return char;
|
|
66
|
+
}
|
|
67
|
+
function readGlobBracketClass(pattern, openIndex) {
|
|
68
|
+
let cursor = openIndex + 1;
|
|
69
|
+
let negated = false;
|
|
70
|
+
let hasContent = false;
|
|
71
|
+
let content = "";
|
|
72
|
+
if (cursor >= pattern.length)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (pattern[cursor] === "!" || pattern[cursor] === "^") {
|
|
75
|
+
negated = true;
|
|
76
|
+
cursor += 1;
|
|
77
|
+
}
|
|
78
|
+
if (pattern[cursor] === "]") {
|
|
79
|
+
content += "\\]";
|
|
80
|
+
hasContent = true;
|
|
81
|
+
cursor += 1;
|
|
82
|
+
}
|
|
83
|
+
for (; cursor < pattern.length; cursor += 1) {
|
|
84
|
+
const char = pattern[cursor];
|
|
85
|
+
if (char === "]") {
|
|
86
|
+
if (!hasContent)
|
|
87
|
+
return undefined;
|
|
88
|
+
return { regexSource: `[${negated ? "^" : ""}${content}]`, closeIndex: cursor };
|
|
89
|
+
}
|
|
90
|
+
hasContent = true;
|
|
91
|
+
if (char === "\\" && cursor + 1 < pattern.length) {
|
|
92
|
+
cursor += 1;
|
|
93
|
+
content += escapeEscapedGlobClassCharacter(pattern[cursor]);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
content += escapeGlobClassCharacter(char);
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
function compileCommandStringGlob(pattern) {
|
|
101
|
+
let source = "^";
|
|
102
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
103
|
+
const char = pattern[index];
|
|
104
|
+
if (char === "*") {
|
|
105
|
+
source += ".*";
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (char === "?") {
|
|
109
|
+
source += ".";
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (char === "[") {
|
|
113
|
+
const bracket = readGlobBracketClass(pattern, index);
|
|
114
|
+
if (bracket !== undefined) {
|
|
115
|
+
source += bracket.regexSource;
|
|
116
|
+
index = bracket.closeIndex;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (char === "\\" && index + 1 < pattern.length) {
|
|
121
|
+
index += 1;
|
|
122
|
+
source += escapeRegexLiteral(pattern[index]);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
source += escapeRegexLiteral(char);
|
|
126
|
+
}
|
|
127
|
+
return new RegExp(`${source}$`);
|
|
128
|
+
}
|
|
129
|
+
function compileRule(rule, listName, index) {
|
|
130
|
+
if (typeof rule === "string") {
|
|
131
|
+
if (rule.length === 0) {
|
|
132
|
+
return { ok: false, message: `${listName}[${index}] exact rule must not be empty` };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, rule: { kind: "exact", source: rule, value: rule } };
|
|
135
|
+
}
|
|
136
|
+
if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
|
|
137
|
+
return { ok: false, message: `${listName}[${index}] rule must be a non-empty string or an object rule` };
|
|
138
|
+
}
|
|
139
|
+
const hasPrefix = hasOwnRuleKey(rule, "prefix");
|
|
140
|
+
const hasGlob = hasOwnRuleKey(rule, "glob");
|
|
141
|
+
const hasRegex = hasOwnRuleKey(rule, "regex");
|
|
142
|
+
const variantCount = (hasPrefix ? 1 : 0) + (hasGlob ? 1 : 0) + (hasRegex ? 1 : 0);
|
|
143
|
+
if (variantCount !== 1) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
message: `${listName}[${index}] must specify exactly one of prefix, glob, or regex`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (hasPrefix) {
|
|
150
|
+
if (!hasOnlyRuleKeys(rule, ["prefix"])) {
|
|
151
|
+
return { ok: false, message: `${listName}[${index}] prefix rule must only contain prefix` };
|
|
152
|
+
}
|
|
153
|
+
if (typeof rule.prefix !== "string") {
|
|
154
|
+
return { ok: false, message: `${listName}[${index}].prefix must be a string` };
|
|
155
|
+
}
|
|
156
|
+
if (rule.prefix.length === 0) {
|
|
157
|
+
return { ok: false, message: `${listName}[${index}].prefix must not be empty` };
|
|
158
|
+
}
|
|
159
|
+
return { ok: true, rule: { kind: "prefix", source: rule, value: rule.prefix } };
|
|
160
|
+
}
|
|
161
|
+
if (hasGlob) {
|
|
162
|
+
if (!hasOnlyRuleKeys(rule, ["glob"])) {
|
|
163
|
+
return { ok: false, message: `${listName}[${index}] glob rule must only contain glob` };
|
|
164
|
+
}
|
|
165
|
+
if (typeof rule.glob !== "string") {
|
|
166
|
+
return { ok: false, message: `${listName}[${index}].glob must be a string` };
|
|
167
|
+
}
|
|
168
|
+
if (rule.glob.length === 0) {
|
|
169
|
+
return { ok: false, message: `${listName}[${index}].glob must not be empty` };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return { ok: true, rule: { kind: "glob", source: rule, value: compileCommandStringGlob(rule.glob) } };
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return { ok: false, message: `${listName}[${index}].glob is not a valid command string glob` };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!hasOnlyRuleKeys(rule, ["regex", "flags"])) {
|
|
179
|
+
return { ok: false, message: `${listName}[${index}] regex rule must only contain regex and optional flags` };
|
|
180
|
+
}
|
|
181
|
+
if (typeof rule.regex !== "string") {
|
|
182
|
+
return { ok: false, message: `${listName}[${index}].regex must be a string` };
|
|
183
|
+
}
|
|
184
|
+
if (rule.regex.length === 0) {
|
|
185
|
+
return { ok: false, message: `${listName}[${index}].regex must not be empty` };
|
|
186
|
+
}
|
|
187
|
+
const hasFlags = hasOwnRuleKey(rule, "flags");
|
|
188
|
+
if (hasFlags && typeof rule.flags !== "string") {
|
|
189
|
+
return { ok: false, message: `${listName}[${index}].flags must be a string when present` };
|
|
190
|
+
}
|
|
191
|
+
const flags = hasFlags && typeof rule.flags === "string" ? rule.flags : "";
|
|
192
|
+
if (/[gy]/.test(flags)) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
message: `${listName}[${index}].flags must not include stateful g or y regex flags`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
return { ok: true, rule: { kind: "regex", source: rule, value: new RegExp(rule.regex, flags) } };
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return { ok: false, message: `${listName}[${index}].regex is not a valid JavaScript RegExp` };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function compileRules(rules, listName) {
|
|
206
|
+
const compiled = [];
|
|
207
|
+
if (rules === undefined)
|
|
208
|
+
return { ok: true, rules: compiled };
|
|
209
|
+
for (let index = 0; index < rules.length; index += 1) {
|
|
210
|
+
const rule = rules[index];
|
|
211
|
+
const result = compileRule(rule, listName, index);
|
|
212
|
+
if (!result.ok)
|
|
213
|
+
return result;
|
|
214
|
+
compiled.push(result.rule);
|
|
215
|
+
}
|
|
216
|
+
return { ok: true, rules: compiled };
|
|
217
|
+
}
|
|
218
|
+
function compilePolicy(policy) {
|
|
219
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
220
|
+
return { ok: false, message: "bash policy must be a non-null object" };
|
|
221
|
+
}
|
|
222
|
+
const unknownKeys = unknownBashPolicyTopLevelKeys(policy);
|
|
223
|
+
if (unknownKeys.length > 0) {
|
|
224
|
+
const formattedKeys = unknownKeys.map((key) => JSON.stringify(key)).join(", ");
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
message: `bash policy contains unknown top-level key${unknownKeys.length === 1 ? "" : "s"} ${formattedKeys}; allowed keys are default, allow, deny, and match`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const defaultDecision = policy.default === undefined ? "allow" : policy.default;
|
|
231
|
+
if (defaultDecision !== "allow" && defaultDecision !== "deny") {
|
|
232
|
+
return { ok: false, message: `bash policy default must be "allow" or "deny"` };
|
|
233
|
+
}
|
|
234
|
+
const match = policy.match === undefined ? "segments" : policy.match;
|
|
235
|
+
if (match !== "whole" && match !== "segments") {
|
|
236
|
+
return { ok: false, message: `bash policy match must be "whole" or "segments"` };
|
|
237
|
+
}
|
|
238
|
+
if (policy.allow !== undefined && !Array.isArray(policy.allow)) {
|
|
239
|
+
return { ok: false, message: "bash policy allow must be an array" };
|
|
240
|
+
}
|
|
241
|
+
if (policy.deny !== undefined && !Array.isArray(policy.deny)) {
|
|
242
|
+
return { ok: false, message: "bash policy deny must be an array" };
|
|
243
|
+
}
|
|
244
|
+
const allow = compileRules(policy.allow, "allow");
|
|
245
|
+
if (!allow.ok)
|
|
246
|
+
return allow;
|
|
247
|
+
const deny = compileRules(policy.deny, "deny");
|
|
248
|
+
if (!deny.ok)
|
|
249
|
+
return deny;
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
policy: {
|
|
253
|
+
defaultDecision,
|
|
254
|
+
match,
|
|
255
|
+
allow: allow.rules,
|
|
256
|
+
deny: deny.rules,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export function validateBashCommandPolicy(policy) {
|
|
261
|
+
const compiled = compilePolicy(policy);
|
|
262
|
+
if (!compiled.ok) {
|
|
263
|
+
throw new Error(`Invalid bash command policy: ${compiled.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function shellError(reason, offset, source) {
|
|
267
|
+
return { ok: false, error: { reason, offset, source } };
|
|
268
|
+
}
|
|
269
|
+
function isWhitespace(char) {
|
|
270
|
+
return char === " " || char === "\t" || char === "\n" || char === "\r";
|
|
271
|
+
}
|
|
272
|
+
function lineTerminatorLengthAt(input, index) {
|
|
273
|
+
const char = input[index];
|
|
274
|
+
if (char === "\r")
|
|
275
|
+
return input[index + 1] === "\n" ? 2 : 1;
|
|
276
|
+
if (char === "\n")
|
|
277
|
+
return 1;
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
function previousNonWhitespace(input, index) {
|
|
281
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
282
|
+
const char = input[i];
|
|
283
|
+
if (!isWhitespace(char))
|
|
284
|
+
return char;
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
function isRedirectionAmpersand(input, index) {
|
|
289
|
+
const previous = previousNonWhitespace(input, index);
|
|
290
|
+
const next = input[index + 1];
|
|
291
|
+
return previous === ">" || previous === "<" || next === ">";
|
|
292
|
+
}
|
|
293
|
+
function operatorLengthAt(input, index) {
|
|
294
|
+
const char = input[index];
|
|
295
|
+
const next = input[index + 1];
|
|
296
|
+
if (char === "|" && input[index - 1] === ">")
|
|
297
|
+
return 0;
|
|
298
|
+
if (char === "|" && next === "&")
|
|
299
|
+
return 2;
|
|
300
|
+
if (char === "&" && next === "&")
|
|
301
|
+
return 2;
|
|
302
|
+
if (char === "|" && next === "|")
|
|
303
|
+
return 2;
|
|
304
|
+
if (char === "|")
|
|
305
|
+
return 1;
|
|
306
|
+
if (char === ";")
|
|
307
|
+
return 1;
|
|
308
|
+
if (char === "&" && !isRedirectionAmpersand(input, index))
|
|
309
|
+
return 1;
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
function isHereDocumentAt(input, index) {
|
|
313
|
+
return input[index] === "<" && input[index + 1] === "<";
|
|
314
|
+
}
|
|
315
|
+
function isCommandSubstitutionAt(input, index) {
|
|
316
|
+
return input[index] === "$" && input[index + 1] === "(";
|
|
317
|
+
}
|
|
318
|
+
function isProcessSubstitutionAt(input, index) {
|
|
319
|
+
const char = input[index];
|
|
320
|
+
return (char === "<" || char === ">") && input[index + 1] === "(";
|
|
321
|
+
}
|
|
322
|
+
function findClosingBacktick(input, openIndex) {
|
|
323
|
+
for (let i = openIndex + 1; i < input.length; i += 1) {
|
|
324
|
+
const char = input[i];
|
|
325
|
+
if (char === "\\") {
|
|
326
|
+
if (i + 1 >= input.length) {
|
|
327
|
+
return { ok: false, reason: "trailing escape in backtick command substitution", offset: i };
|
|
328
|
+
}
|
|
329
|
+
i += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (char === "`")
|
|
333
|
+
return { ok: true, closeIndex: i };
|
|
334
|
+
}
|
|
335
|
+
return { ok: false, reason: "unclosed backtick command substitution", offset: openIndex };
|
|
336
|
+
}
|
|
337
|
+
function findClosingParen(input, openIndex, construct) {
|
|
338
|
+
let quote = "none";
|
|
339
|
+
let depth = 1;
|
|
340
|
+
for (let i = openIndex + 1; i < input.length; i += 1) {
|
|
341
|
+
const char = input[i];
|
|
342
|
+
if (quote === "single") {
|
|
343
|
+
if (char === "'")
|
|
344
|
+
quote = "none";
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (char === "\\") {
|
|
348
|
+
if (i + 1 >= input.length) {
|
|
349
|
+
return { ok: false, reason: `trailing escape in ${construct}`, offset: i };
|
|
350
|
+
}
|
|
351
|
+
i += 1;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (quote === "double") {
|
|
355
|
+
if (char === "\"") {
|
|
356
|
+
quote = "none";
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {
|
|
360
|
+
depth += 1;
|
|
361
|
+
i += 1;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (char === "`") {
|
|
365
|
+
const close = findClosingBacktick(input, i);
|
|
366
|
+
if (!close.ok)
|
|
367
|
+
return close;
|
|
368
|
+
i = close.closeIndex;
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (char === "'") {
|
|
373
|
+
quote = "single";
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (char === "\"") {
|
|
377
|
+
quote = "double";
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {
|
|
381
|
+
depth += 1;
|
|
382
|
+
i += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (char === "`") {
|
|
386
|
+
const close = findClosingBacktick(input, i);
|
|
387
|
+
if (!close.ok)
|
|
388
|
+
return close;
|
|
389
|
+
i = close.closeIndex;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (char === "(") {
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
reason: `unsupported shell grouping parentheses in ${construct}`,
|
|
396
|
+
offset: i,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (char === ")") {
|
|
400
|
+
depth -= 1;
|
|
401
|
+
if (depth === 0)
|
|
402
|
+
return { ok: true, closeIndex: i };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (quote === "single")
|
|
406
|
+
return { ok: false, reason: `unclosed single quote in ${construct}`, offset: openIndex };
|
|
407
|
+
if (quote === "double")
|
|
408
|
+
return { ok: false, reason: `unclosed double quote in ${construct}`, offset: openIndex };
|
|
409
|
+
return { ok: false, reason: `unclosed ${construct}`, offset: openIndex };
|
|
410
|
+
}
|
|
411
|
+
function readShellWord(input, start) {
|
|
412
|
+
let quote = "none";
|
|
413
|
+
let sawSingleQuote = false;
|
|
414
|
+
let sawDoubleQuote = false;
|
|
415
|
+
let sawEscape = false;
|
|
416
|
+
let sawParameterExpansion = false;
|
|
417
|
+
let sawCommandSubstitution = false;
|
|
418
|
+
let sawProcessSubstitution = false;
|
|
419
|
+
let sawBacktick = false;
|
|
420
|
+
let sawGlobPattern = false;
|
|
421
|
+
let sawBraceExpansion = false;
|
|
422
|
+
let sawTildePrefix = false;
|
|
423
|
+
const metadata = () => ({
|
|
424
|
+
sawSingleQuote,
|
|
425
|
+
sawDoubleQuote,
|
|
426
|
+
sawEscape,
|
|
427
|
+
sawParameterExpansion,
|
|
428
|
+
sawCommandSubstitution,
|
|
429
|
+
sawProcessSubstitution,
|
|
430
|
+
sawBacktick,
|
|
431
|
+
sawGlobPattern,
|
|
432
|
+
sawBraceExpansion,
|
|
433
|
+
sawTildePrefix,
|
|
434
|
+
});
|
|
435
|
+
for (let i = start; i < input.length; i += 1) {
|
|
436
|
+
const char = input[i];
|
|
437
|
+
if (quote === "single") {
|
|
438
|
+
if (char === "'")
|
|
439
|
+
quote = "none";
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (char === "\\") {
|
|
443
|
+
sawEscape = true;
|
|
444
|
+
if (i + 1 >= input.length)
|
|
445
|
+
return { ok: false, reason: "trailing escape in shell word", offset: i };
|
|
446
|
+
i += 1;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (quote === "double") {
|
|
450
|
+
if (char === "\"") {
|
|
451
|
+
quote = "none";
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {
|
|
455
|
+
if (isCommandSubstitutionAt(input, i)) {
|
|
456
|
+
sawCommandSubstitution = true;
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
sawProcessSubstitution = true;
|
|
460
|
+
}
|
|
461
|
+
const close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? "command substitution `$(`" : "process substitution");
|
|
462
|
+
if (!close.ok)
|
|
463
|
+
return { ok: false, reason: close.reason, offset: close.offset };
|
|
464
|
+
i = close.closeIndex;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (char === "`") {
|
|
468
|
+
sawBacktick = true;
|
|
469
|
+
const close = findClosingBacktick(input, i);
|
|
470
|
+
if (!close.ok)
|
|
471
|
+
return { ok: false, reason: close.reason, offset: close.offset };
|
|
472
|
+
i = close.closeIndex;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (char === "$")
|
|
476
|
+
sawParameterExpansion = true;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (isWhitespace(char)) {
|
|
480
|
+
return { ok: true, word: input.slice(start, i), end: i, metadata: metadata() };
|
|
481
|
+
}
|
|
482
|
+
if (char === "'") {
|
|
483
|
+
sawSingleQuote = true;
|
|
484
|
+
quote = "single";
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (char === "\"") {
|
|
488
|
+
sawDoubleQuote = true;
|
|
489
|
+
quote = "double";
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (i === start && char === "~") {
|
|
493
|
+
sawTildePrefix = true;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {
|
|
497
|
+
if (isCommandSubstitutionAt(input, i)) {
|
|
498
|
+
sawCommandSubstitution = true;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
sawProcessSubstitution = true;
|
|
502
|
+
}
|
|
503
|
+
const close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? "command substitution `$(`" : "process substitution");
|
|
504
|
+
if (!close.ok)
|
|
505
|
+
return { ok: false, reason: close.reason, offset: close.offset };
|
|
506
|
+
i = close.closeIndex;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (char === "`") {
|
|
510
|
+
sawBacktick = true;
|
|
511
|
+
const close = findClosingBacktick(input, i);
|
|
512
|
+
if (!close.ok)
|
|
513
|
+
return { ok: false, reason: close.reason, offset: close.offset };
|
|
514
|
+
i = close.closeIndex;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (char === "$") {
|
|
518
|
+
sawParameterExpansion = true;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (char === "*" || char === "?" || char === "[" || char === "]") {
|
|
522
|
+
sawGlobPattern = true;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (char === "{" || char === "}") {
|
|
526
|
+
sawBraceExpansion = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (quote === "single")
|
|
530
|
+
return { ok: false, reason: "unclosed single quote in shell word", offset: start };
|
|
531
|
+
if (quote === "double")
|
|
532
|
+
return { ok: false, reason: "unclosed double quote in shell word", offset: start };
|
|
533
|
+
return { ok: true, word: input.slice(start), end: input.length, metadata: metadata() };
|
|
534
|
+
}
|
|
535
|
+
function isAsciiDigit(char) {
|
|
536
|
+
return char !== undefined && char >= "0" && char <= "9";
|
|
537
|
+
}
|
|
538
|
+
function leadingRedirectionTokenAt(input, index) {
|
|
539
|
+
let operatorStart = index;
|
|
540
|
+
while (isAsciiDigit(input[operatorStart]))
|
|
541
|
+
operatorStart += 1;
|
|
542
|
+
const hasDescriptorPrefix = operatorStart > index;
|
|
543
|
+
const char = input[operatorStart];
|
|
544
|
+
const next = input[operatorStart + 1];
|
|
545
|
+
const afterNext = input[operatorStart + 2];
|
|
546
|
+
if (char === undefined)
|
|
547
|
+
return undefined;
|
|
548
|
+
if (hasDescriptorPrefix && char !== "<" && char !== ">")
|
|
549
|
+
return undefined;
|
|
550
|
+
if (!hasDescriptorPrefix && (char === "<" || char === ">") && next === "(") {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
let operator;
|
|
554
|
+
if (char === "&" && next === ">") {
|
|
555
|
+
operator = afterNext === ">" ? "&>>" : "&>";
|
|
556
|
+
}
|
|
557
|
+
else if (char === "<") {
|
|
558
|
+
if (next === "<")
|
|
559
|
+
operator = "<<";
|
|
560
|
+
else if (next === "&")
|
|
561
|
+
operator = "<&";
|
|
562
|
+
else if (next === ">")
|
|
563
|
+
operator = "<>";
|
|
564
|
+
else
|
|
565
|
+
operator = "<";
|
|
566
|
+
}
|
|
567
|
+
else if (char === ">") {
|
|
568
|
+
if (next === ">")
|
|
569
|
+
operator = ">>";
|
|
570
|
+
else if (next === "|")
|
|
571
|
+
operator = ">|";
|
|
572
|
+
else if (next === "&")
|
|
573
|
+
operator = ">&";
|
|
574
|
+
else
|
|
575
|
+
operator = ">";
|
|
576
|
+
}
|
|
577
|
+
return operator === undefined ? undefined : `${input.slice(index, operatorStart)}${operator}`;
|
|
578
|
+
}
|
|
579
|
+
function isEnvAssignmentWord(word) {
|
|
580
|
+
return /^[A-Za-z_][A-Za-z0-9_]*(?:\+)?=/.test(word);
|
|
581
|
+
}
|
|
582
|
+
function attachedRedirectionOperatorAt(input, index) {
|
|
583
|
+
const char = input[index];
|
|
584
|
+
const next = input[index + 1];
|
|
585
|
+
const afterNext = input[index + 2];
|
|
586
|
+
if ((char === "<" || char === ">") && next === "(") {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
if (char === "&" && next === ">") {
|
|
590
|
+
return afterNext === ">" ? "&>>" : "&>";
|
|
591
|
+
}
|
|
592
|
+
if (char === "<") {
|
|
593
|
+
if (next === "<")
|
|
594
|
+
return "<<";
|
|
595
|
+
if (next === "&")
|
|
596
|
+
return "<&";
|
|
597
|
+
if (next === ">")
|
|
598
|
+
return "<>";
|
|
599
|
+
return "<";
|
|
600
|
+
}
|
|
601
|
+
if (char === ">") {
|
|
602
|
+
if (next === ">")
|
|
603
|
+
return ">>";
|
|
604
|
+
if (next === "|")
|
|
605
|
+
return ">|";
|
|
606
|
+
if (next === "&")
|
|
607
|
+
return ">&";
|
|
608
|
+
return ">";
|
|
609
|
+
}
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
function attachedCommandHeadRedirection(input, start, end) {
|
|
613
|
+
let quote = "none";
|
|
614
|
+
for (let i = start; i < end; i += 1) {
|
|
615
|
+
const char = input[i];
|
|
616
|
+
if (quote === "single") {
|
|
617
|
+
if (char === "'")
|
|
618
|
+
quote = "none";
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (char === "\\") {
|
|
622
|
+
i += 1;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (quote === "double") {
|
|
626
|
+
if (char === "\"") {
|
|
627
|
+
quote = "none";
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {
|
|
631
|
+
const close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? "command substitution `$(`" : "process substitution");
|
|
632
|
+
if (close.ok)
|
|
633
|
+
i = close.closeIndex;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (char === "`") {
|
|
637
|
+
const close = findClosingBacktick(input, i);
|
|
638
|
+
if (close.ok)
|
|
639
|
+
i = close.closeIndex;
|
|
640
|
+
}
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (char === "'") {
|
|
644
|
+
quote = "single";
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (char === "\"") {
|
|
648
|
+
quote = "double";
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {
|
|
652
|
+
const close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? "command substitution `$(`" : "process substitution");
|
|
653
|
+
if (close.ok)
|
|
654
|
+
i = close.closeIndex;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (char === "`") {
|
|
658
|
+
const close = findClosingBacktick(input, i);
|
|
659
|
+
if (close.ok)
|
|
660
|
+
i = close.closeIndex;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const operator = attachedRedirectionOperatorAt(input, i);
|
|
664
|
+
if (operator !== undefined && i > start) {
|
|
665
|
+
return { token: operator, offset: i };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
function validateLiteralCommandHead(head, metadata) {
|
|
671
|
+
if (head.length === 0) {
|
|
672
|
+
return { ok: false, reason: "empty command heads are not supported by bash policy segments mode" };
|
|
673
|
+
}
|
|
674
|
+
if (metadata.sawSingleQuote || metadata.sawDoubleQuote) {
|
|
675
|
+
return { ok: false, reason: "quoted or quote-constructed command heads are not supported by bash policy segments mode" };
|
|
676
|
+
}
|
|
677
|
+
if (metadata.sawEscape) {
|
|
678
|
+
return { ok: false, reason: "escape-constructed command heads are not supported by bash policy segments mode" };
|
|
679
|
+
}
|
|
680
|
+
if (metadata.sawCommandSubstitution || metadata.sawProcessSubstitution || metadata.sawBacktick) {
|
|
681
|
+
return { ok: false, reason: "command, process, and backtick substitutions are not supported in command heads by bash policy segments mode" };
|
|
682
|
+
}
|
|
683
|
+
if (metadata.sawParameterExpansion) {
|
|
684
|
+
return { ok: false, reason: "parameter-expanded command heads are not supported by bash policy segments mode" };
|
|
685
|
+
}
|
|
686
|
+
if (metadata.sawTildePrefix) {
|
|
687
|
+
return { ok: false, reason: "tilde-expanded command heads are not supported by bash policy segments mode" };
|
|
688
|
+
}
|
|
689
|
+
if (metadata.sawGlobPattern) {
|
|
690
|
+
return { ok: false, reason: "glob-expanded command heads are not supported by bash policy segments mode" };
|
|
691
|
+
}
|
|
692
|
+
if (metadata.sawBraceExpansion) {
|
|
693
|
+
return { ok: false, reason: "brace-expanded command heads are not supported by bash policy segments mode" };
|
|
694
|
+
}
|
|
695
|
+
return { ok: true };
|
|
696
|
+
}
|
|
697
|
+
function buildSegment(rawSegment, absoluteStart, absoluteEnd, source) {
|
|
698
|
+
let cursor = 0;
|
|
699
|
+
while (cursor < rawSegment.length && isWhitespace(rawSegment[cursor]))
|
|
700
|
+
cursor += 1;
|
|
701
|
+
if (cursor >= rawSegment.length)
|
|
702
|
+
return { ok: true };
|
|
703
|
+
while (cursor < rawSegment.length) {
|
|
704
|
+
while (cursor < rawSegment.length && isWhitespace(rawSegment[cursor]))
|
|
705
|
+
cursor += 1;
|
|
706
|
+
if (cursor >= rawSegment.length)
|
|
707
|
+
return { ok: true };
|
|
708
|
+
const leadingRedirection = leadingRedirectionTokenAt(rawSegment, cursor);
|
|
709
|
+
if (leadingRedirection !== undefined) {
|
|
710
|
+
return {
|
|
711
|
+
ok: false,
|
|
712
|
+
error: {
|
|
713
|
+
reason: `leading shell redirection ${JSON.stringify(leadingRedirection)} is not supported by bash policy segments mode`,
|
|
714
|
+
offset: absoluteStart + cursor,
|
|
715
|
+
source,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const word = readShellWord(rawSegment, cursor);
|
|
720
|
+
if (!word.ok) {
|
|
721
|
+
return {
|
|
722
|
+
ok: false,
|
|
723
|
+
error: { reason: word.reason, offset: absoluteStart + word.offset, source },
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const attachedRedirection = attachedCommandHeadRedirection(rawSegment, cursor, word.end);
|
|
727
|
+
if (attachedRedirection !== undefined) {
|
|
728
|
+
return {
|
|
729
|
+
ok: false,
|
|
730
|
+
error: {
|
|
731
|
+
reason: `attached shell redirection ${JSON.stringify(attachedRedirection.token)} in the command head is not supported by bash policy segments mode`,
|
|
732
|
+
offset: absoluteStart + attachedRedirection.offset,
|
|
733
|
+
source,
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
if (isEnvAssignmentWord(word.word)) {
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
error: {
|
|
741
|
+
reason: "environment assignment words are not supported by bash policy segments mode",
|
|
742
|
+
offset: absoluteStart + cursor,
|
|
743
|
+
source,
|
|
744
|
+
},
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
const target = rawSegment.slice(cursor).trim();
|
|
748
|
+
const head = word.word;
|
|
749
|
+
if (UNSUPPORTED_CONTROL_HEADS.has(head)) {
|
|
750
|
+
return {
|
|
751
|
+
ok: false,
|
|
752
|
+
error: {
|
|
753
|
+
reason: `unsupported shell reserved or compound syntax starting with ${JSON.stringify(head)}`,
|
|
754
|
+
offset: absoluteStart + cursor,
|
|
755
|
+
source,
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const literalHead = validateLiteralCommandHead(head, word.metadata);
|
|
760
|
+
if (!literalHead.ok) {
|
|
761
|
+
return {
|
|
762
|
+
ok: false,
|
|
763
|
+
error: {
|
|
764
|
+
reason: literalHead.reason,
|
|
765
|
+
offset: absoluteStart + cursor,
|
|
766
|
+
source,
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
ok: true,
|
|
772
|
+
segment: {
|
|
773
|
+
raw: rawSegment.trim(),
|
|
774
|
+
target,
|
|
775
|
+
head,
|
|
776
|
+
start: absoluteStart + cursor,
|
|
777
|
+
end: absoluteEnd,
|
|
778
|
+
source,
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return { ok: true };
|
|
783
|
+
}
|
|
784
|
+
function parseSegmentsInSource(input, baseOffset, source) {
|
|
785
|
+
const segments = [];
|
|
786
|
+
let quote = "none";
|
|
787
|
+
let segmentStart = 0;
|
|
788
|
+
let nestedForCurrentSegment = [];
|
|
789
|
+
const commitSegment = (end) => {
|
|
790
|
+
const built = buildSegment(input.slice(segmentStart, end), baseOffset + segmentStart, baseOffset + end, source);
|
|
791
|
+
if (!built.ok)
|
|
792
|
+
return { ok: false, error: built.error };
|
|
793
|
+
if (built.segment)
|
|
794
|
+
segments.push(built.segment);
|
|
795
|
+
segments.push(...nestedForCurrentSegment);
|
|
796
|
+
nestedForCurrentSegment = [];
|
|
797
|
+
return undefined;
|
|
798
|
+
};
|
|
799
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
800
|
+
const char = input[i];
|
|
801
|
+
if (quote === "single") {
|
|
802
|
+
if (char === "'")
|
|
803
|
+
quote = "none";
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (char === "\\") {
|
|
807
|
+
if (i + 1 >= input.length) {
|
|
808
|
+
return shellError("trailing escape", baseOffset + i, source);
|
|
809
|
+
}
|
|
810
|
+
i += 1;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (quote === "double") {
|
|
814
|
+
if (char === "\"") {
|
|
815
|
+
quote = "none";
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (isCommandSubstitutionAt(input, i)) {
|
|
819
|
+
const close = findClosingParen(input, i + 1, "command substitution `$(`");
|
|
820
|
+
if (!close.ok)
|
|
821
|
+
return shellError(close.reason, baseOffset + close.offset, source);
|
|
822
|
+
const nested = parseSegmentsInSource(input.slice(i + 2, close.closeIndex), baseOffset + i + 2, "command-substitution");
|
|
823
|
+
if (!nested.ok)
|
|
824
|
+
return nested;
|
|
825
|
+
nestedForCurrentSegment.push(...nested.segments);
|
|
826
|
+
i = close.closeIndex;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (char === "`") {
|
|
830
|
+
const close = findClosingBacktick(input, i);
|
|
831
|
+
if (!close.ok)
|
|
832
|
+
return shellError(close.reason, baseOffset + close.offset, source);
|
|
833
|
+
const nested = parseSegmentsInSource(input.slice(i + 1, close.closeIndex), baseOffset + i + 1, "backtick");
|
|
834
|
+
if (!nested.ok)
|
|
835
|
+
return nested;
|
|
836
|
+
nestedForCurrentSegment.push(...nested.segments);
|
|
837
|
+
i = close.closeIndex;
|
|
838
|
+
}
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (char === "'") {
|
|
842
|
+
quote = "single";
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (char === "\"") {
|
|
846
|
+
quote = "double";
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (isHereDocumentAt(input, i)) {
|
|
850
|
+
return shellError("here-documents are not supported by bash policy segments mode", baseOffset + i, source);
|
|
851
|
+
}
|
|
852
|
+
if (isCommandSubstitutionAt(input, i)) {
|
|
853
|
+
const close = findClosingParen(input, i + 1, "command substitution `$(`");
|
|
854
|
+
if (!close.ok)
|
|
855
|
+
return shellError(close.reason, baseOffset + close.offset, source);
|
|
856
|
+
const nested = parseSegmentsInSource(input.slice(i + 2, close.closeIndex), baseOffset + i + 2, "command-substitution");
|
|
857
|
+
if (!nested.ok)
|
|
858
|
+
return nested;
|
|
859
|
+
nestedForCurrentSegment.push(...nested.segments);
|
|
860
|
+
i = close.closeIndex;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (isProcessSubstitutionAt(input, i)) {
|
|
864
|
+
const close = findClosingParen(input, i + 1, "process substitution");
|
|
865
|
+
if (!close.ok)
|
|
866
|
+
return shellError(close.reason, baseOffset + close.offset, source);
|
|
867
|
+
const nested = parseSegmentsInSource(input.slice(i + 2, close.closeIndex), baseOffset + i + 2, "process-substitution");
|
|
868
|
+
if (!nested.ok)
|
|
869
|
+
return nested;
|
|
870
|
+
nestedForCurrentSegment.push(...nested.segments);
|
|
871
|
+
i = close.closeIndex;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (char === "`") {
|
|
875
|
+
const close = findClosingBacktick(input, i);
|
|
876
|
+
if (!close.ok)
|
|
877
|
+
return shellError(close.reason, baseOffset + close.offset, source);
|
|
878
|
+
const nested = parseSegmentsInSource(input.slice(i + 1, close.closeIndex), baseOffset + i + 1, "backtick");
|
|
879
|
+
if (!nested.ok)
|
|
880
|
+
return nested;
|
|
881
|
+
nestedForCurrentSegment.push(...nested.segments);
|
|
882
|
+
i = close.closeIndex;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (char === "(" || char === ")") {
|
|
886
|
+
return shellError("unsupported shell grouping parentheses", baseOffset + i, source);
|
|
887
|
+
}
|
|
888
|
+
const lineTerminatorLength = lineTerminatorLengthAt(input, i);
|
|
889
|
+
if (lineTerminatorLength > 0) {
|
|
890
|
+
const committed = commitSegment(i);
|
|
891
|
+
if (committed)
|
|
892
|
+
return committed;
|
|
893
|
+
i += lineTerminatorLength - 1;
|
|
894
|
+
segmentStart = i + 1;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const operatorLength = operatorLengthAt(input, i);
|
|
898
|
+
if (operatorLength > 0) {
|
|
899
|
+
const committed = commitSegment(i);
|
|
900
|
+
if (committed)
|
|
901
|
+
return committed;
|
|
902
|
+
i += operatorLength - 1;
|
|
903
|
+
segmentStart = i + 1;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (quote === "single")
|
|
907
|
+
return shellError("unclosed single quote", baseOffset + input.length, source);
|
|
908
|
+
if (quote === "double")
|
|
909
|
+
return shellError("unclosed double quote", baseOffset + input.length, source);
|
|
910
|
+
const committed = commitSegment(input.length);
|
|
911
|
+
if (committed)
|
|
912
|
+
return committed;
|
|
913
|
+
return { ok: true, segments };
|
|
914
|
+
}
|
|
915
|
+
export function parseBashCommandSegments(command) {
|
|
916
|
+
return parseSegmentsInSource(command, 0, "top-level");
|
|
917
|
+
}
|
|
918
|
+
function wholeCommandTarget(command) {
|
|
919
|
+
const target = command;
|
|
920
|
+
const trimmed = command.trimStart();
|
|
921
|
+
const leading = command.length - trimmed.length;
|
|
922
|
+
const firstSpace = trimmed.search(/\s/);
|
|
923
|
+
const head = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
|
|
924
|
+
return {
|
|
925
|
+
raw: command,
|
|
926
|
+
target,
|
|
927
|
+
head,
|
|
928
|
+
start: leading,
|
|
929
|
+
end: command.length,
|
|
930
|
+
source: "top-level",
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function ruleMatches(rule, target) {
|
|
934
|
+
switch (rule.kind) {
|
|
935
|
+
case "exact":
|
|
936
|
+
return target === rule.value;
|
|
937
|
+
case "prefix":
|
|
938
|
+
return target.startsWith(rule.value);
|
|
939
|
+
case "glob":
|
|
940
|
+
return rule.value.test(target);
|
|
941
|
+
case "regex":
|
|
942
|
+
return rule.value.test(target);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function firstMatchingRule(rules, target) {
|
|
946
|
+
for (const rule of rules) {
|
|
947
|
+
if (ruleMatches(rule, target))
|
|
948
|
+
return rule.source;
|
|
949
|
+
}
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
function invalidPolicyMode(policy) {
|
|
953
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy))
|
|
954
|
+
return "segments";
|
|
955
|
+
return policy.match === "whole" ? "whole" : "segments";
|
|
956
|
+
}
|
|
957
|
+
function isNoRuleDefaultAllowPolicy(policy) {
|
|
958
|
+
return policy.defaultDecision === "allow" && policy.allow.length === 0 && policy.deny.length === 0;
|
|
959
|
+
}
|
|
960
|
+
export function evaluateBashCommandPolicy(command, policy) {
|
|
961
|
+
if (policy === undefined) {
|
|
962
|
+
return { allowed: true, mode: "segments", targets: [] };
|
|
963
|
+
}
|
|
964
|
+
const compiled = compilePolicy(policy);
|
|
965
|
+
if (!compiled.ok) {
|
|
966
|
+
return {
|
|
967
|
+
allowed: false,
|
|
968
|
+
mode: invalidPolicyMode(policy),
|
|
969
|
+
targets: [],
|
|
970
|
+
rejection: {
|
|
971
|
+
reason: "invalid-policy",
|
|
972
|
+
message: compiled.message,
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
const activePolicy = compiled.policy;
|
|
977
|
+
if (isNoRuleDefaultAllowPolicy(activePolicy)) {
|
|
978
|
+
return { allowed: true, mode: activePolicy.match, targets: [] };
|
|
979
|
+
}
|
|
980
|
+
const targetsResult = activePolicy.match === "whole"
|
|
981
|
+
? { ok: true, segments: [wholeCommandTarget(command)] }
|
|
982
|
+
: parseBashCommandSegments(command);
|
|
983
|
+
if (!targetsResult.ok) {
|
|
984
|
+
return {
|
|
985
|
+
allowed: false,
|
|
986
|
+
mode: activePolicy.match,
|
|
987
|
+
targets: [],
|
|
988
|
+
rejection: {
|
|
989
|
+
reason: "unsupported-shell-syntax",
|
|
990
|
+
message: targetsResult.error.reason,
|
|
991
|
+
parseError: targetsResult.error,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
for (const target of targetsResult.segments) {
|
|
996
|
+
const denyRule = firstMatchingRule(activePolicy.deny, target.target);
|
|
997
|
+
if (denyRule !== undefined) {
|
|
998
|
+
return {
|
|
999
|
+
allowed: false,
|
|
1000
|
+
mode: activePolicy.match,
|
|
1001
|
+
targets: targetsResult.segments,
|
|
1002
|
+
rejection: {
|
|
1003
|
+
reason: "matched-deny",
|
|
1004
|
+
message: `command ${JSON.stringify(target.head)} matched a deny rule`,
|
|
1005
|
+
target,
|
|
1006
|
+
matchedRule: denyRule,
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
const allowRule = firstMatchingRule(activePolicy.allow, target.target);
|
|
1011
|
+
if (allowRule !== undefined || activePolicy.defaultDecision === "allow") {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
allowed: false,
|
|
1016
|
+
mode: activePolicy.match,
|
|
1017
|
+
targets: targetsResult.segments,
|
|
1018
|
+
rejection: {
|
|
1019
|
+
reason: "default-deny",
|
|
1020
|
+
message: `command ${JSON.stringify(target.head)} is not permitted by default-deny bash policy`,
|
|
1021
|
+
target,
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return { allowed: true, mode: activePolicy.match, targets: targetsResult.segments };
|
|
1026
|
+
}
|
|
1027
|
+
function formatRule(rule) {
|
|
1028
|
+
if (typeof rule === "string")
|
|
1029
|
+
return JSON.stringify(rule);
|
|
1030
|
+
if ("prefix" in rule)
|
|
1031
|
+
return `{ prefix: ${JSON.stringify(rule.prefix)} }`;
|
|
1032
|
+
if ("glob" in rule)
|
|
1033
|
+
return `{ glob: ${JSON.stringify(rule.glob)} }`;
|
|
1034
|
+
return rule.flags === undefined
|
|
1035
|
+
? `{ regex: ${JSON.stringify(rule.regex)} }`
|
|
1036
|
+
: `{ regex: ${JSON.stringify(rule.regex)}, flags: ${JSON.stringify(rule.flags)} }`;
|
|
1037
|
+
}
|
|
1038
|
+
export function formatBashCommandPolicyRejection(decision, policyLabel = "bash command policy") {
|
|
1039
|
+
const lines = [`Bash command blocked by ${policyLabel}.`, ""];
|
|
1040
|
+
const rejection = decision.rejection;
|
|
1041
|
+
if (rejection.reason === "unsupported-shell-syntax") {
|
|
1042
|
+
lines.push("The command uses shell syntax that Atomic cannot safely parse in `segments` mode.", `Reason: ${rejection.message}.`);
|
|
1043
|
+
if (rejection.parseError) {
|
|
1044
|
+
lines.push(`Parser source: ${rejection.parseError.source} at offset ${rejection.parseError.offset}.`);
|
|
1045
|
+
}
|
|
1046
|
+
lines.push("Use match: \"whole\" only if the caller intentionally accepts raw-command matching semantics.");
|
|
1047
|
+
}
|
|
1048
|
+
else if (rejection.reason === "invalid-policy") {
|
|
1049
|
+
lines.push("The configured bash command policy is invalid.", `Reason: ${rejection.message}.`);
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
const target = rejection.target;
|
|
1053
|
+
if (target) {
|
|
1054
|
+
lines.push(`Command head: \`${truncateDiagnostic(target.head)}\``, `Rejected ${decision.mode === "whole" ? "command" : "segment"}: \`${truncateDiagnostic(target.target)}\``, `Segment source: ${target.source}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (rejection.reason === "matched-deny") {
|
|
1057
|
+
lines.push("Reason: matched a deny rule; deny rules take precedence over allow rules.");
|
|
1058
|
+
if (rejection.matchedRule !== undefined) {
|
|
1059
|
+
lines.push(`Matched deny rule: ${formatRule(rejection.matchedRule)}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
lines.push("Reason: no allow rule matched and the policy default is deny.");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
lines.push(`Policy mode: ${decision.mode}.`, "", "No shell process was started.");
|
|
1067
|
+
return lines.join("\n");
|
|
1068
|
+
}
|
|
1069
|
+
//# sourceMappingURL=bash-policy.js.map
|