@principles/core 1.296.2 → 1.297.1
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 +0 -1
- package/dist/runtime-v2/adapter/index.d.ts +0 -2
- package/dist/runtime-v2/adapter/index.d.ts.map +1 -1
- package/dist/runtime-v2/adapter/index.js +0 -1
- package/dist/runtime-v2/adapter/index.js.map +1 -1
- package/dist/runtime-v2/adapter/output-schema-registry.d.ts +3 -3
- package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
- package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +1 -2
- package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
- package/dist/runtime-v2/cli/probe.d.ts +4 -15
- package/dist/runtime-v2/cli/probe.d.ts.map +1 -1
- package/dist/runtime-v2/cli/probe.js +24 -44
- package/dist/runtime-v2/cli/probe.js.map +1 -1
- package/dist/runtime-v2/config/index.d.ts +1 -1
- package/dist/runtime-v2/config/index.d.ts.map +1 -1
- package/dist/runtime-v2/config/index.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-agent-binding.d.ts +10 -17
- package/dist/runtime-v2/config/pd-config-agent-binding.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-agent-binding.js +23 -27
- package/dist/runtime-v2/config/pd-config-agent-binding.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-defaults.d.ts +3 -5
- package/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-defaults.js +17 -20
- package/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-effective.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-effective.js +4 -1
- package/dist/runtime-v2/config/pd-config-effective.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-redaction.js +4 -7
- package/dist/runtime-v2/config/pd-config-redaction.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-types.d.ts +5 -1
- package/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-types.js +6 -0
- package/dist/runtime-v2/config/pd-config-types.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-validate.js +6 -0
- package/dist/runtime-v2/config/pd-config-validate.js.map +1 -1
- package/dist/runtime-v2/golden-trace.d.ts.map +1 -1
- package/dist/runtime-v2/golden-trace.js +8 -1
- package/dist/runtime-v2/golden-trace.js.map +1 -1
- package/dist/runtime-v2/index.d.ts +4 -4
- package/dist/runtime-v2/index.d.ts.map +1 -1
- package/dist/runtime-v2/index.js +2 -3
- package/dist/runtime-v2/index.js.map +1 -1
- package/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/artificer-intent-preflight.js +26 -95
- package/dist/runtime-v2/internalization/artificer-intent-preflight.js.map +1 -1
- package/dist/runtime-v2/internalization/bash-risk.d.ts +32 -0
- package/dist/runtime-v2/internalization/bash-risk.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/bash-risk.js +785 -0
- package/dist/runtime-v2/internalization/bash-risk.js.map +1 -0
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts +25 -2
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.js +177 -14
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.js.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-runner.js +14 -2
- package/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
- package/dist/runtime-v2/internalization/internalization-orchestrator.d.ts +47 -0
- package/dist/runtime-v2/internalization/internalization-orchestrator.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/internalization-orchestrator.js +164 -3
- package/dist/runtime-v2/internalization/internalization-orchestrator.js.map +1 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +9 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
- package/dist/runtime-v2/internalization/pitask-metadata.d.ts +39 -0
- package/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/pitask-metadata.js +59 -0
- package/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
- package/dist/runtime-v2/internalization/rule-source-correction.d.ts +60 -0
- package/dist/runtime-v2/internalization/rule-source-correction.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/rule-source-correction.js +214 -0
- package/dist/runtime-v2/internalization/rule-source-correction.js.map +1 -0
- package/dist/runtime-v2/internalization/rulecode-verification-obligations.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/rulecode-verification-obligations.js +65 -0
- package/dist/runtime-v2/internalization/rulecode-verification-obligations.js.map +1 -1
- package/dist/runtime-v2/internalization/scribe-output.d.ts +14 -2
- package/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/scribe-output.js +57 -2
- package/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
- package/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +13 -1
- package/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/scribe-prompt-builder.js +15 -3
- package/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
- package/dist/runtime-v2/internalization/scribe-runner.d.ts +34 -0
- package/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/scribe-runner.js +128 -4
- package/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
- package/dist/runtime-v2/internalization/textual-basis-anchor.d.ts +72 -0
- package/dist/runtime-v2/internalization/textual-basis-anchor.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/textual-basis-anchor.js +100 -0
- package/dist/runtime-v2/internalization/textual-basis-anchor.js.map +1 -0
- package/dist/runtime-v2/pain-signal-runtime-factory.d.ts +9 -7
- package/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
- package/dist/runtime-v2/pain-signal-runtime-factory.js +59 -121
- package/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
- package/dist/runtime-v2/runtime-protocol.d.ts +2 -3
- package/dist/runtime-v2/runtime-protocol.d.ts.map +1 -1
- package/dist/runtime-v2/runtime-protocol.js +15 -10
- package/dist/runtime-v2/runtime-protocol.js.map +1 -1
- package/dist/runtime-v2/store/lifecycle/lease-manager.d.ts.map +1 -1
- package/dist/runtime-v2/store/lifecycle/lease-manager.js +11 -2
- package/dist/runtime-v2/store/lifecycle/lease-manager.js.map +1 -1
- package/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts.map +1 -1
- package/dist/runtime-v2/tools/artificer-l2-tool-contract.js +26 -0
- package/dist/runtime-v2/tools/artificer-l2-tool-contract.js.map +1 -1
- package/dist/telemetry-event.d.ts +2 -2
- package/dist/telemetry-event.d.ts.map +1 -1
- package/dist/telemetry-event.js +5 -0
- package/dist/telemetry-event.js.map +1 -1
- package/dist/workflow-funnel-loader.d.ts +0 -2
- package/dist/workflow-funnel-loader.d.ts.map +1 -1
- package/dist/workflow-funnel-loader.js.map +1 -1
- package/package.json +5 -1
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.d.ts +0 -72
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.d.ts.map +0 -1
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.js +0 -837
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.js.map +0 -1
- package/dist/runtime-v2/utils/cli-process-runner.d.ts +0 -35
- package/dist/runtime-v2/utils/cli-process-runner.d.ts.map +0 -1
- package/dist/runtime-v2/utils/cli-process-runner.js +0 -263
- package/dist/runtime-v2/utils/cli-process-runner.js.map +0 -1
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash-risk.ts — PRI-1047/R8: 宿主 bash 命令风险分类的单一实现(P4)。
|
|
3
|
+
*
|
|
4
|
+
* 契约(SPEC-PRI-1047 §R8 修订,Owner 评审 2026-10-27):
|
|
5
|
+
* safe = 正面证据确认整条命令链只读(显式只读白名单 + 参数形状核验
|
|
6
|
+
* + 结构核验:管道/组合/换行/重定向/命令替换全部确认)。
|
|
7
|
+
* normal = 确认发生变更(已知变更命令/子命令、写文件选项、输出重定向、
|
|
8
|
+
* 内联任意代码执行)。
|
|
9
|
+
* dangerous= 确认高破坏性变更(rm -rf / chmod / chown)。
|
|
10
|
+
* unknown = 无法可靠解释(未识别命令/子命令/全局选项、不支持的结构、
|
|
11
|
+
* 解释器执行不可分析输入)。unknown 不获得只读豁免,
|
|
12
|
+
* 但与审批事实 unknown/missing 的中性降级是两个概念。
|
|
13
|
+
*
|
|
14
|
+
* 能力边界(如实声明,不假装完整 shell 语义):
|
|
15
|
+
* - 仅 POSIX 风格 tokenizer(单/双引号、结构算子、重定向、$()/``);
|
|
16
|
+
* 不实现变量展开、glob 语义、进程替换(>( <( → unknown)。
|
|
17
|
+
* - 只读是显式白名单子集;清单外的命令一律 unknown(不是 safe)。
|
|
18
|
+
* - git 仅识别显式全局选项集;未识别的子命令前选项 → unknown。
|
|
19
|
+
* - 命令替换递归深度上限 3。
|
|
20
|
+
*
|
|
21
|
+
* 三方共用(保持零调用方改动):host-runtime 生产 gate(无 provider 时的
|
|
22
|
+
* 确定性默认)、OpenClaw 插件 enrichment、golden-trace 回放推导。
|
|
23
|
+
*/
|
|
24
|
+
const LITERAL_OPERATOR_MARKER = '\u0001';
|
|
25
|
+
const SHELL_OPERATORS = '|;&<>()';
|
|
26
|
+
function literalShellChar(ch) {
|
|
27
|
+
return SHELL_OPERATORS.includes(ch) ? `${LITERAL_OPERATOR_MARKER}${ch}` : ch;
|
|
28
|
+
}
|
|
29
|
+
function restoreLiteralShellChars(token) {
|
|
30
|
+
return token.replaceAll(LITERAL_OPERATOR_MARKER, '');
|
|
31
|
+
}
|
|
32
|
+
// ── 结构层 tokenizer ─────────────────────────────────────────────────────────
|
|
33
|
+
/** 引号感知的 tokenizer;无法解析(引号未闭合等)返回 null。 */
|
|
34
|
+
function tokenize(input) {
|
|
35
|
+
if (input.includes(LITERAL_OPERATOR_MARKER))
|
|
36
|
+
return null;
|
|
37
|
+
const tokens = [];
|
|
38
|
+
let current = '';
|
|
39
|
+
let hasCurrent = false;
|
|
40
|
+
let i = 0;
|
|
41
|
+
const n = input.length;
|
|
42
|
+
let state = 'plain';
|
|
43
|
+
while (i < n) {
|
|
44
|
+
const ch = input[i];
|
|
45
|
+
if (ch === undefined)
|
|
46
|
+
break;
|
|
47
|
+
if (state === 'single') {
|
|
48
|
+
if (ch === "'")
|
|
49
|
+
state = 'plain';
|
|
50
|
+
else
|
|
51
|
+
current += literalShellChar(ch);
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (state === 'double') {
|
|
56
|
+
if (ch === '"')
|
|
57
|
+
state = 'plain';
|
|
58
|
+
else if (ch === '\\' && i + 1 < n && '"\\$`'.includes(input[i + 1] ?? '')) {
|
|
59
|
+
current += literalShellChar(input[i + 1] ?? '');
|
|
60
|
+
i += 2;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
current += literalShellChar(ch);
|
|
65
|
+
i += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === "'") {
|
|
69
|
+
state = 'single';
|
|
70
|
+
hasCurrent = true;
|
|
71
|
+
i += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === '"') {
|
|
75
|
+
state = 'double';
|
|
76
|
+
hasCurrent = true;
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === '\\' && i + 1 < n) {
|
|
81
|
+
current += literalShellChar(input[i + 1] ?? '');
|
|
82
|
+
hasCurrent = true;
|
|
83
|
+
i += 2;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === '\r' || ch === '\n') {
|
|
87
|
+
if (hasCurrent || current) {
|
|
88
|
+
tokens.push(current);
|
|
89
|
+
current = '';
|
|
90
|
+
hasCurrent = false;
|
|
91
|
+
}
|
|
92
|
+
// A physical newline terminates a simple command, unlike horizontal space.
|
|
93
|
+
if (ch === '\r' && input[i + 1] === '\n')
|
|
94
|
+
i += 1;
|
|
95
|
+
tokens.push('\n');
|
|
96
|
+
i += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (' \t'.includes(ch)) {
|
|
100
|
+
if (hasCurrent || current) {
|
|
101
|
+
tokens.push(current);
|
|
102
|
+
current = '';
|
|
103
|
+
hasCurrent = false;
|
|
104
|
+
}
|
|
105
|
+
i += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Structural operators become standalone tokens (newline included).
|
|
109
|
+
if ('|;&<>()'.includes(ch)) {
|
|
110
|
+
if (hasCurrent || current) {
|
|
111
|
+
tokens.push(current);
|
|
112
|
+
current = '';
|
|
113
|
+
hasCurrent = false;
|
|
114
|
+
}
|
|
115
|
+
let op = ch;
|
|
116
|
+
const next = input[i + 1];
|
|
117
|
+
if (ch === '<' && next === '>')
|
|
118
|
+
op = '<>';
|
|
119
|
+
else if ((ch === '|' || ch === '&' || ch === '>' || ch === '<') && next === ch)
|
|
120
|
+
op += next;
|
|
121
|
+
if ((ch === '>' || ch === '<') && next === '(')
|
|
122
|
+
op += next; // process substitution marker
|
|
123
|
+
tokens.push(op);
|
|
124
|
+
i += op.length;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
current += ch;
|
|
128
|
+
hasCurrent = true;
|
|
129
|
+
i += 1;
|
|
130
|
+
}
|
|
131
|
+
if (state !== 'plain')
|
|
132
|
+
return null;
|
|
133
|
+
if (hasCurrent || current)
|
|
134
|
+
tokens.push(current);
|
|
135
|
+
return tokens;
|
|
136
|
+
}
|
|
137
|
+
const REDIRECT_OUT_OPS = new Set(['>', '>>', '<>', '&>', '&>>', '2>', '2>>']);
|
|
138
|
+
/**
|
|
139
|
+
* 把 token 流切成段。遇到 `|`、`;`、`&&`、`||` 结束当前段;重定向算子吞掉
|
|
140
|
+
* 其目标 token。无法解释的结构(`(`、`)`、`>(`、`<(`、裸 `&`)标记
|
|
141
|
+
* unsupported。命令替换在 tokenize 前已被提取(见 classifyCommandRisk)。
|
|
142
|
+
*/
|
|
143
|
+
function splitSegments(tokens) {
|
|
144
|
+
const segments = [];
|
|
145
|
+
let args = [];
|
|
146
|
+
let hasOut = false;
|
|
147
|
+
let hasIn = false;
|
|
148
|
+
let unsupported = false;
|
|
149
|
+
const flush = (allowEmpty = false) => {
|
|
150
|
+
if (args.length === 0 && !hasOut && !hasIn && !unsupported) {
|
|
151
|
+
if (allowEmpty)
|
|
152
|
+
unsupported = true;
|
|
153
|
+
else
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
segments.push({
|
|
157
|
+
command: args[0] === undefined ? null : restoreLiteralShellChars(args[0]),
|
|
158
|
+
hasOutputRedirect: hasOut,
|
|
159
|
+
hasInputRedirect: hasIn,
|
|
160
|
+
unsupported,
|
|
161
|
+
args: args.slice(1).map(restoreLiteralShellChars),
|
|
162
|
+
substitutions: [],
|
|
163
|
+
});
|
|
164
|
+
args = [];
|
|
165
|
+
hasOut = false;
|
|
166
|
+
hasIn = false;
|
|
167
|
+
unsupported = false;
|
|
168
|
+
};
|
|
169
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
170
|
+
const token = tokens[i];
|
|
171
|
+
if (token === '\n') {
|
|
172
|
+
flush();
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (token === '|' || token === ';' || token === '&&' || token === '||') {
|
|
176
|
+
flush(true);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (REDIRECT_OUT_OPS.has(token)) {
|
|
180
|
+
hasOut = true;
|
|
181
|
+
i += 1; // swallow the redirect target token
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (token === '<' || token === '<<' || token === '<<<') {
|
|
185
|
+
hasIn = true;
|
|
186
|
+
i += 1; // swallow the target word (heredoc body is data)
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (token.startsWith('2>') || token.startsWith('&>') || token.startsWith('>>') || token.startsWith('>')) {
|
|
190
|
+
// Glued forms like `2>x`, `>x`, `>>x`.
|
|
191
|
+
hasOut = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (token === '(' || token === ')' || token === '>(' || token === '<(' || token === '&' || token === '>' || token === '<' || token === '>>') {
|
|
195
|
+
unsupported = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
args.push(token);
|
|
199
|
+
}
|
|
200
|
+
flush();
|
|
201
|
+
return segments;
|
|
202
|
+
}
|
|
203
|
+
// ── 命令级白名单/已知集合 ────────────────────────────────────────────────────
|
|
204
|
+
/** Commands whose stdout-only forms have a bounded option grammar below. */
|
|
205
|
+
const STDOUT_QUERY_COMMANDS = new Set(['cat', 'ls', 'printf', 'head', 'tail', 'grep', 'rg', 'echo', 'wc']);
|
|
206
|
+
/** git 显式全局选项集(带参 vs 布尔)。 */
|
|
207
|
+
const GIT_GLOBAL_WITH_VALUE = new Set(['-C', '--git-dir', '--work-tree']);
|
|
208
|
+
const GIT_GLOBAL_BOOLEAN = new Set([
|
|
209
|
+
'--no-pager', '-P', '--no-replace-objects', '--bare', '--literal-pathspecs',
|
|
210
|
+
'--glob-pathspecs', '--noglob-pathspecs', '--icase-pathspecs', '--no-optional-locks', '--no-lazy-fetch',
|
|
211
|
+
'--no-advice',
|
|
212
|
+
]);
|
|
213
|
+
/** git 查询子命令(需再核写文件选项)。 */
|
|
214
|
+
const GIT_READONLY_SUBCOMMANDS = new Set(['status', 'log', 'show', 'diff', 'grep']);
|
|
215
|
+
const GIT_MUTATING_SUBCOMMANDS = new Set([
|
|
216
|
+
'add', 'am', 'apply', 'bisect', 'checkout', 'cherry-pick', 'clean', 'clone', 'commit', 'fetch', 'gc',
|
|
217
|
+
'init', 'merge', 'mv', 'pull', 'push', 'rebase', 'reset', 'restore', 'revert', 'rm', 'sparse-checkout',
|
|
218
|
+
'submodule', 'switch', 'worktree', 'update-index', 'hash-object',
|
|
219
|
+
]);
|
|
220
|
+
/** git 查询子命令中的写文件选项(--output=FILE / --output FILE / -o FILE)。 */
|
|
221
|
+
function gitArgsHaveWriteOption(args) {
|
|
222
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
223
|
+
const a = args[i];
|
|
224
|
+
if (a === '--output' || a === '-o' || a.startsWith('-o') && a.length > 2)
|
|
225
|
+
return true;
|
|
226
|
+
if (a.startsWith('--output='))
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
/** 列表形状子命令(branch/tag/remote/stash/config)逐个判定;返回 true/false/'unknown'。 */
|
|
232
|
+
function isReadonlyGitListShape(sub, args) {
|
|
233
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
234
|
+
const flags = args.filter((a) => a.startsWith('-'));
|
|
235
|
+
if (sub === 'branch' || sub === 'tag') {
|
|
236
|
+
const writes = ['-d', '-D', '-m', '-M', '-c', '-C', '--delete', '--move', '--copy', '--unset-upstream', '--set-upstream-to', '--set-upstream'];
|
|
237
|
+
if (flags.some((f) => writes.some((w) => f === w || f.startsWith(`${w}=`))))
|
|
238
|
+
return false;
|
|
239
|
+
const allowed = sub === 'branch'
|
|
240
|
+
? new Set(['-a', '-r', '-v', '-vv', '--all', '--remotes', '--verbose', '--no-abbrev', '--no-color', '--color', '--contains', '--no-contains', '--merged', '--no-merged', '--list', '--show-current'])
|
|
241
|
+
: new Set(['-l', '--list', '-n', '--contains', '--no-contains', '--merged', '--no-merged', '--sort', '--format', '--column', '--no-column', '--points-at', '--format']);
|
|
242
|
+
if (flags.some((f) => !allowed.has(f) && ![...allowed].some((w) => f.startsWith(`${w}=`))))
|
|
243
|
+
return 'unknown';
|
|
244
|
+
if (positional.length === 0 || flags.includes('--list') || flags.includes('-l'))
|
|
245
|
+
return true;
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (sub === 'remote') {
|
|
249
|
+
if (positional.length === 0 && flags.every((f) => f === '-v' || f === '--verbose'))
|
|
250
|
+
return true;
|
|
251
|
+
return false; // add/rename/remove/set-url/prune/rm → 变更
|
|
252
|
+
}
|
|
253
|
+
if (sub === 'stash') {
|
|
254
|
+
// stash list / stash show are queries; other verbs mutate or are unsupported.
|
|
255
|
+
if (args.length === 0 || positional[0] === 'push' || positional[0] === 'pop' || positional[0] === 'drop' || positional[0] === 'clear')
|
|
256
|
+
return false;
|
|
257
|
+
if (positional[0] === 'list' || positional[0] === 'show')
|
|
258
|
+
return flags.length === 0 ? true : 'unknown';
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (sub === 'config') {
|
|
262
|
+
// 仅查询形状:--list/-l/--get*/--get-regexp + 可选 pattern。
|
|
263
|
+
const queryFlags = ['--list', '-l', '--get', '--get-all', '--get-regexp', '--get-urlmatch'];
|
|
264
|
+
const hasQuery = flags.some((f) => queryFlags.includes(f));
|
|
265
|
+
if (flags.some((f) => !queryFlags.includes(f)))
|
|
266
|
+
return 'unknown';
|
|
267
|
+
if (hasQuery && !positional.some((p) => p.includes('=')))
|
|
268
|
+
return true;
|
|
269
|
+
return 'unknown';
|
|
270
|
+
}
|
|
271
|
+
return 'unknown';
|
|
272
|
+
}
|
|
273
|
+
function isReadonlyGitQueryShape(sub, args) {
|
|
274
|
+
const allowed = {
|
|
275
|
+
status: ['--short', '--branch', '--porcelain', '--untracked-files', '--ignore-submodules', '-s', '-b'],
|
|
276
|
+
log: ['--oneline', '--stat', '--format', '--pretty', '--max-count', '-n', '--grep', '--author', '--since', '--until', '--date', '--graph', '--decorate', '--no-decorate', '--abbrev-commit', '--reverse', '--name-only', '--name-status'],
|
|
277
|
+
show: ['--stat', '--format', '--pretty', '--no-ext-diff', '--no-textconv'],
|
|
278
|
+
diff: ['--stat', '--name-only', '--name-status', '--check', '--cached', '--staged', '--patch', '-p', '--no-ext-diff', '--no-textconv'],
|
|
279
|
+
grep: ['--no-index', '--cached', '--untracked', '--ignore-case', '--line-number', '--regexp', '-n', '-i', '-e'],
|
|
280
|
+
};
|
|
281
|
+
const withValue = {
|
|
282
|
+
status: ['--porcelain', '--untracked-files', '--ignore-submodules'],
|
|
283
|
+
log: ['--format', '--pretty', '--max-count', '-n', '--grep', '--author', '--since', '--until', '--date'],
|
|
284
|
+
show: ['--format', '--pretty'],
|
|
285
|
+
diff: [],
|
|
286
|
+
grep: ['--regexp', '-e'],
|
|
287
|
+
};
|
|
288
|
+
if (allowed[sub] === undefined)
|
|
289
|
+
return 'unknown';
|
|
290
|
+
let positionalOnly = false;
|
|
291
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
292
|
+
const token = args[i];
|
|
293
|
+
if (token === '--') {
|
|
294
|
+
positionalOnly = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (positionalOnly || token === '-' || !token.startsWith('-'))
|
|
298
|
+
continue;
|
|
299
|
+
if (sub === 'log' && /^-\d+$/.test(token))
|
|
300
|
+
continue;
|
|
301
|
+
const name = token.startsWith('--') && token.includes('=') ? token.slice(0, token.indexOf('=')) : token;
|
|
302
|
+
if (!allowed[sub]?.includes(name))
|
|
303
|
+
return 'unknown';
|
|
304
|
+
if (withValue[sub]?.includes(name) && !token.includes('=')) {
|
|
305
|
+
if (args[i + 1] === undefined)
|
|
306
|
+
return 'unknown';
|
|
307
|
+
i += 1;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function isReadonlyStdoutShape(command, args) {
|
|
313
|
+
const noValue = {
|
|
314
|
+
cat: { long: ['--number', '--show-ends', '--show-tabs', '--show-nonprinting'], short: 'nvet' },
|
|
315
|
+
ls: { long: ['--all', '--almost-all', '--directory', '--long', '--human-readable'], short: '1adhlrt' },
|
|
316
|
+
head: { long: ['--quiet', '--silent', '--verbose'], short: 'qv' },
|
|
317
|
+
tail: { long: ['--follow', '--quiet', '--silent', '--verbose'], short: 'fqv' },
|
|
318
|
+
grep: { long: ['--line-number', '--ignore-case', '--word-regexp', '--invert-match', '--recursive', '--files-with-matches', '--count', '--extended-regexp', '--fixed-strings'], short: 'EinwvrclF' },
|
|
319
|
+
rg: { long: ['--line-number', '--hidden', '--column', '--fixed-strings', '--ignore-case', '--smart-case', '--invert-match', '--word-regexp', '--files', '--count', '--only-matching'], short: 'nchFivw' },
|
|
320
|
+
printf: { long: [], short: '' },
|
|
321
|
+
echo: { long: [], short: '' },
|
|
322
|
+
wc: { long: [], short: 'lwmLc' },
|
|
323
|
+
};
|
|
324
|
+
const withValue = {
|
|
325
|
+
cat: { long: [], short: '' },
|
|
326
|
+
ls: { long: ['--color', '--sort', '--width'], short: 'w' },
|
|
327
|
+
head: { long: ['--lines', '--bytes'], short: 'nc' },
|
|
328
|
+
tail: { long: ['--lines', '--bytes'], short: 'nc' },
|
|
329
|
+
grep: { long: ['--regexp', '--file', '--max-count', '--after-context', '--before-context', '--context', '--include', '--exclude', '--exclude-dir'], short: 'efABmC' },
|
|
330
|
+
rg: { long: ['--glob', '--type', '--regexp', '--max-count', '--max-depth', '--after-context', '--before-context', '--context'], short: 'egmC' },
|
|
331
|
+
printf: { long: [], short: '' },
|
|
332
|
+
echo: { long: [], short: '' },
|
|
333
|
+
wc: { long: [], short: '' },
|
|
334
|
+
};
|
|
335
|
+
const allowed = noValue[command];
|
|
336
|
+
const values = withValue[command];
|
|
337
|
+
if (allowed === undefined || values === undefined)
|
|
338
|
+
return 'unknown';
|
|
339
|
+
if (command === 'printf' || command === 'echo') {
|
|
340
|
+
if (command === 'echo')
|
|
341
|
+
return true;
|
|
342
|
+
const format = args[0] === '--' ? args[1] : args[0];
|
|
343
|
+
const unverifiedOption = args[0] !== '--' && args[0]?.startsWith('-');
|
|
344
|
+
return unverifiedOption || /%[^%]*n/.test(format ?? '') ? 'unknown' : true;
|
|
345
|
+
}
|
|
346
|
+
let positionalOnly = false;
|
|
347
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
348
|
+
const token = args[i];
|
|
349
|
+
if (positionalOnly || token === '-' || !token.startsWith('-'))
|
|
350
|
+
continue;
|
|
351
|
+
if (token === '--') {
|
|
352
|
+
positionalOnly = true;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (token.startsWith('--')) {
|
|
356
|
+
const equal = token.indexOf('=');
|
|
357
|
+
const name = equal === -1 ? token : token.slice(0, equal);
|
|
358
|
+
if (allowed.long.includes(name))
|
|
359
|
+
continue;
|
|
360
|
+
if (!values.long.includes(name))
|
|
361
|
+
return 'unknown';
|
|
362
|
+
if (equal === -1) {
|
|
363
|
+
if (args[i + 1] === undefined)
|
|
364
|
+
return 'unknown';
|
|
365
|
+
i += 1;
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const letters = token.slice(1);
|
|
370
|
+
if ((command === 'head' || command === 'tail') && /^\d+$/.test(letters))
|
|
371
|
+
continue;
|
|
372
|
+
for (let j = 0; j < letters.length; j += 1) {
|
|
373
|
+
const letter = letters[j];
|
|
374
|
+
if (values.short.includes(letter)) {
|
|
375
|
+
if (j + 1 === letters.length) {
|
|
376
|
+
if (args[i + 1] === undefined)
|
|
377
|
+
return 'unknown';
|
|
378
|
+
i += 1;
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
if (!allowed.short.includes(letter))
|
|
383
|
+
return 'unknown';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (command === 'uniq' && args.filter((a) => !a.startsWith('-')).length > 1)
|
|
387
|
+
return false;
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
function isReadonlyGit(args) {
|
|
391
|
+
let i = 0;
|
|
392
|
+
// 跳过显式全局选项,找到子命令。
|
|
393
|
+
while (i < args.length) {
|
|
394
|
+
const a = args[i];
|
|
395
|
+
if (GIT_GLOBAL_WITH_VALUE.has(a)) {
|
|
396
|
+
i += 2;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (a.startsWith('--git-dir=') || a.startsWith('--work-tree=')) {
|
|
400
|
+
i += 1;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (GIT_GLOBAL_BOOLEAN.has(a)) {
|
|
404
|
+
i += 1;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
break; // first non-global token = subcommand
|
|
408
|
+
}
|
|
409
|
+
const sub = args[i];
|
|
410
|
+
if (sub === undefined)
|
|
411
|
+
return 'unknown'; // `git` bare prints help — but conservative: unknown
|
|
412
|
+
const rest = args.slice(i + 1);
|
|
413
|
+
if (GIT_READONLY_SUBCOMMANDS.has(sub)) {
|
|
414
|
+
if (gitArgsHaveWriteOption(rest))
|
|
415
|
+
return false;
|
|
416
|
+
return isReadonlyGitQueryShape(sub, rest);
|
|
417
|
+
}
|
|
418
|
+
if (['branch', 'tag', 'remote', 'stash', 'config'].includes(sub)) {
|
|
419
|
+
return isReadonlyGitListShape(sub, rest);
|
|
420
|
+
}
|
|
421
|
+
if (sub.startsWith('-'))
|
|
422
|
+
return 'unknown'; // unrecognized global option → cannot confirm
|
|
423
|
+
if (GIT_MUTATING_SUBCOMMANDS.has(sub))
|
|
424
|
+
return false;
|
|
425
|
+
if (sub === 'reflog') {
|
|
426
|
+
if (rest[0] === 'delete' || rest[0] === 'expire' || rest[0] === 'drop')
|
|
427
|
+
return false;
|
|
428
|
+
return rest.length === 0 || rest.length === 1 && rest[0] === 'show' ? true : 'unknown';
|
|
429
|
+
}
|
|
430
|
+
return 'unknown';
|
|
431
|
+
}
|
|
432
|
+
/** 只读命令 + 参数形状约束;未识别命令 → 'unknown'(不默认 safe 也不默认变更)。 */
|
|
433
|
+
function isReadonlyWithArgShape(command, args) {
|
|
434
|
+
// find supports only simple predicates and stdout listing; actions stay unknown.
|
|
435
|
+
if (command === 'find') {
|
|
436
|
+
const takesValue = new Set(['-name', '-iname', '-type', '-maxdepth', '-mindepth']);
|
|
437
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
438
|
+
const token = args[i];
|
|
439
|
+
if (!token.startsWith('-'))
|
|
440
|
+
continue;
|
|
441
|
+
if (!takesValue.has(token) && token !== '-print')
|
|
442
|
+
return 'unknown';
|
|
443
|
+
if (takesValue.has(token)) {
|
|
444
|
+
if (args[i + 1] === undefined)
|
|
445
|
+
return 'unknown';
|
|
446
|
+
i += 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
// env:裸 env / 仅赋值与 flag → 打印环境(只读);带命令 → 不可确认。
|
|
452
|
+
if (command === 'env')
|
|
453
|
+
return args.length === 0 ? true : 'unknown';
|
|
454
|
+
// git:全局选项核验 + 子命令白名单。
|
|
455
|
+
if (command === 'git')
|
|
456
|
+
return isReadonlyGit(args);
|
|
457
|
+
// sort/uniq are excluded from the read-only subset because their output
|
|
458
|
+
// operand/options can name a file; classify their explicit write form only.
|
|
459
|
+
if (command === 'sort')
|
|
460
|
+
return args.some((a) => a === '-o' || a === '--output' || a.startsWith('--output=') || a.startsWith('-o')) ? false : 'unknown';
|
|
461
|
+
if (command === 'uniq')
|
|
462
|
+
return args.filter((a) => !a.startsWith('-')).length > 1 ? false : 'unknown';
|
|
463
|
+
if (STDOUT_QUERY_COMMANDS.has(command))
|
|
464
|
+
return isReadonlyStdoutShape(command, args);
|
|
465
|
+
// Positive evidence is limited to the audited command families above.
|
|
466
|
+
return 'unknown';
|
|
467
|
+
}
|
|
468
|
+
/** 确认变更(非破坏级)的已知命令/解释器内联代码。 */
|
|
469
|
+
const KNOWN_MUTATING = new Set([
|
|
470
|
+
'npm', 'yarn', 'pnpm', 'pip', 'pip3', 'cargo', 'gem', 'brew', 'apt', 'apt-get', 'choco', 'winget',
|
|
471
|
+
'docker', 'kubectl', 'make', 'cmake', 'gcc', 'g++', 'touch', 'tee', 'truncate', 'shred', 'patch', 'ln',
|
|
472
|
+
'kill', 'pkill', 'killall', 'dd', 'mount', 'umount', 'useradd', 'userdel', 'groupadd', 'systemctl',
|
|
473
|
+
'service', 'crontab', 'scp', 'rsync', 'cp', 'mv', 'rm', 'mkdir', 'rmdir', 'install', 'chgrp', 'openssl',
|
|
474
|
+
'gzip', 'gunzip', 'tar', 'zip', 'unzip', 'chmod', 'chown', 'aws', 'gcloud', 'az', 'ssh-keygen', 'watch',
|
|
475
|
+
]);
|
|
476
|
+
/** 危险级:显式破坏模式。 */
|
|
477
|
+
function isDangerousShape(command, args) {
|
|
478
|
+
if (command === 'rm')
|
|
479
|
+
return args.some((a) => /^-[a-zA-Z]*[rf][a-zA-Z]*$/.test(a) && /[rf]/.test(a));
|
|
480
|
+
return command === 'chmod' || command === 'chown' || command === 'mkfs' || command === 'fdisk' || command === 'shred';
|
|
481
|
+
}
|
|
482
|
+
/** 内联任意代码执行(确认具备变更能力)与原地写入特判。 */
|
|
483
|
+
function isInlineCodeExecution(command, args) {
|
|
484
|
+
if (command === 'node' || command === 'deno' || command === 'bun')
|
|
485
|
+
return args.some((a) => a === '-e' || a === '--eval' || a === 'eval');
|
|
486
|
+
if (command === 'python' || command === 'python3')
|
|
487
|
+
return args.some((a) => a === '-c' || a === '--command');
|
|
488
|
+
if (command === 'ruby' || command === 'perl')
|
|
489
|
+
return args.some((a) => a === '-e' || a === '--eval');
|
|
490
|
+
// sed/perl 原地写入是确认的文件变更;流式形态则不可确认(归 unknown)。
|
|
491
|
+
if (command === 'sed' || command === 'perl') {
|
|
492
|
+
return args.some((a) => a === '-i' || a === '--in-place' || a.startsWith('-i') && /^-[ip]/.test(a));
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
// ── 段级与链级判定 ──────────────────────────────────────────────────────────
|
|
497
|
+
const SEVERITY = { normal: 2, dangerous: 3, unknown: 1 };
|
|
498
|
+
function worstLevel(a, b) {
|
|
499
|
+
if (a === 'safe')
|
|
500
|
+
return b;
|
|
501
|
+
if (b === 'safe')
|
|
502
|
+
return a;
|
|
503
|
+
return SEVERITY[a] >= SEVERITY[b] ? a : b;
|
|
504
|
+
}
|
|
505
|
+
/** 命令替换提取:把 $(…) 与 `…` 的内部文本收集出来并替换为占位(保留结构)。 */
|
|
506
|
+
function extractBalanced(input, start, close) {
|
|
507
|
+
let depth = 0;
|
|
508
|
+
let inner = '';
|
|
509
|
+
for (let i = start - 1; i < input.length; i += 1) {
|
|
510
|
+
const ch = input[i];
|
|
511
|
+
if (ch === '(')
|
|
512
|
+
depth += 1;
|
|
513
|
+
if (ch === ')') {
|
|
514
|
+
depth -= 1;
|
|
515
|
+
if (depth === 0)
|
|
516
|
+
return { inner, end: i + 1 };
|
|
517
|
+
}
|
|
518
|
+
if (depth >= 1 && i >= start)
|
|
519
|
+
inner += ch;
|
|
520
|
+
}
|
|
521
|
+
void close;
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
function findDoubleEnd(input, start) {
|
|
525
|
+
for (let i = start; i < input.length; i += 1) {
|
|
526
|
+
if (input[i] === '"')
|
|
527
|
+
return i;
|
|
528
|
+
}
|
|
529
|
+
return -1;
|
|
530
|
+
}
|
|
531
|
+
function extractSubstitutions(input) {
|
|
532
|
+
const subs = [];
|
|
533
|
+
let out = '';
|
|
534
|
+
let unknownExpansion = false;
|
|
535
|
+
let i = 0;
|
|
536
|
+
const n = input.length;
|
|
537
|
+
let depth = 0;
|
|
538
|
+
let current = '';
|
|
539
|
+
let mode = 'none';
|
|
540
|
+
while (i < n) {
|
|
541
|
+
const ch = input[i];
|
|
542
|
+
if (mode === 'single') {
|
|
543
|
+
if (ch === "'")
|
|
544
|
+
mode = 'none';
|
|
545
|
+
out += ch;
|
|
546
|
+
i += 1;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (mode === 'double') {
|
|
550
|
+
if (ch === '"') {
|
|
551
|
+
mode = 'none';
|
|
552
|
+
out += ch;
|
|
553
|
+
i += 1;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (ch === '\\' && i + 1 < n && '"\\$`'.includes(input[i + 1] ?? '')) {
|
|
557
|
+
out += ch + (input[i + 1] ?? '');
|
|
558
|
+
i += 2;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (ch === '$' && input[i + 1] === '(') {
|
|
562
|
+
// command substitution inside double quotes
|
|
563
|
+
const sub = extractBalanced(input, i + 2, ')');
|
|
564
|
+
if (sub === null)
|
|
565
|
+
return null;
|
|
566
|
+
subs.push(sub.inner);
|
|
567
|
+
out += '"SUB"';
|
|
568
|
+
i = sub.end;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (ch === '`') {
|
|
572
|
+
let end = i + 1;
|
|
573
|
+
while (end < n) {
|
|
574
|
+
if (input[end] === '\\') {
|
|
575
|
+
end += 2;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (input[end] === '`')
|
|
579
|
+
break;
|
|
580
|
+
end += 1;
|
|
581
|
+
}
|
|
582
|
+
if (end >= n)
|
|
583
|
+
return null;
|
|
584
|
+
subs.push(input.slice(i + 1, end));
|
|
585
|
+
out += '"SUB"';
|
|
586
|
+
i = end + 1;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (ch === '$')
|
|
590
|
+
unknownExpansion = true;
|
|
591
|
+
out += ch;
|
|
592
|
+
i += 1;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (mode === 'paren') {
|
|
596
|
+
if (ch === "'") {
|
|
597
|
+
current += ch;
|
|
598
|
+
i += 1;
|
|
599
|
+
const close = input.indexOf("'", i);
|
|
600
|
+
if (close === -1)
|
|
601
|
+
return null;
|
|
602
|
+
current += input.slice(i, close + 1);
|
|
603
|
+
i = close + 1;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (ch === '"') {
|
|
607
|
+
current += ch;
|
|
608
|
+
i += 1;
|
|
609
|
+
const close = findDoubleEnd(input, i);
|
|
610
|
+
if (close === -1)
|
|
611
|
+
return null;
|
|
612
|
+
current += input.slice(i, close + 1);
|
|
613
|
+
i = close + 1;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (ch === '(') {
|
|
617
|
+
depth += 1;
|
|
618
|
+
current += ch;
|
|
619
|
+
i += 1;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (ch === ')') {
|
|
623
|
+
depth -= 1;
|
|
624
|
+
if (depth === 0) {
|
|
625
|
+
mode = 'none';
|
|
626
|
+
subs.push(current);
|
|
627
|
+
out += 'SUB';
|
|
628
|
+
i += 1;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
current += ch;
|
|
632
|
+
i += 1;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
current += ch;
|
|
636
|
+
i += 1;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (mode === 'backtick') {
|
|
640
|
+
if (ch === '\\' && i + 1 < n) {
|
|
641
|
+
current += ch + (input[i + 1] ?? '');
|
|
642
|
+
i += 2;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (ch === '`') {
|
|
646
|
+
mode = 'none';
|
|
647
|
+
subs.push(current);
|
|
648
|
+
out += 'SUB';
|
|
649
|
+
i += 1;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
current += ch;
|
|
653
|
+
i += 1;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (ch === "'" && depth === 0) {
|
|
657
|
+
mode = 'single';
|
|
658
|
+
out += ch;
|
|
659
|
+
i += 1;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (ch === '"') {
|
|
663
|
+
mode = 'double';
|
|
664
|
+
out += ch;
|
|
665
|
+
i += 1;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (ch === '\\') {
|
|
669
|
+
if (i + 1 >= n || input[i + 1] === '\n' || input[i + 1] === '\r')
|
|
670
|
+
return null;
|
|
671
|
+
out += ch + (input[i + 1] ?? '');
|
|
672
|
+
i += 2;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (ch === '$' && input[i + 1] === '(') {
|
|
676
|
+
mode = 'paren';
|
|
677
|
+
depth = 1;
|
|
678
|
+
current = '';
|
|
679
|
+
i += 2;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (ch === '$' || ch === '*' || ch === '?' || ch === '[' || ch === '{' || ch === '}')
|
|
683
|
+
unknownExpansion = true;
|
|
684
|
+
if (ch === '`') {
|
|
685
|
+
mode = 'backtick';
|
|
686
|
+
current = '';
|
|
687
|
+
i += 1;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (ch === '>' && input[i + 1] === '(')
|
|
691
|
+
return null; // process substitution unsupported
|
|
692
|
+
if (ch === '<' && input[i + 1] === '(')
|
|
693
|
+
return null;
|
|
694
|
+
out += ch;
|
|
695
|
+
i += 1;
|
|
696
|
+
}
|
|
697
|
+
if (mode !== 'none' && mode !== 'double' && mode !== 'single')
|
|
698
|
+
return null;
|
|
699
|
+
if (mode === 'double' || mode === 'single')
|
|
700
|
+
return null; // unterminated quote
|
|
701
|
+
return { text: out, subs, unknownExpansion };
|
|
702
|
+
}
|
|
703
|
+
function classifySegment(segment, subLevels) {
|
|
704
|
+
let level = 'safe';
|
|
705
|
+
for (const s of subLevels)
|
|
706
|
+
level = worstLevel(level, s);
|
|
707
|
+
if (segment.unsupported)
|
|
708
|
+
return worstLevel(level, 'unknown');
|
|
709
|
+
if (segment.hasOutputRedirect)
|
|
710
|
+
return worstLevel(level, 'normal');
|
|
711
|
+
const { command } = segment;
|
|
712
|
+
if (command === null || command === '')
|
|
713
|
+
return 'unknown';
|
|
714
|
+
// 原地写入(sed -i 等)在 isInlineCodeExecution 内特判为确认变更。
|
|
715
|
+
if (isInlineCodeExecution(command, segment.args))
|
|
716
|
+
return worstLevel(level, 'normal');
|
|
717
|
+
// 解释器执行不可分析输入:无法确认只读。
|
|
718
|
+
if (['sh', 'bash', 'zsh', 'dash', 'ksh', 'powershell', 'pwsh', 'cmd', 'eval', 'source', '.', 'xargs', 'awk', 'sed', 'perl', 'expect', 'ssh'].includes(command)) {
|
|
719
|
+
return worstLevel(level, 'unknown');
|
|
720
|
+
}
|
|
721
|
+
if (isDangerousShape(command, segment.args))
|
|
722
|
+
return 'dangerous';
|
|
723
|
+
if (KNOWN_MUTATING.has(command))
|
|
724
|
+
return worstLevel(level, 'normal');
|
|
725
|
+
const readonly = isReadonlyWithArgShape(command, segment.args);
|
|
726
|
+
if (readonly === true)
|
|
727
|
+
return level;
|
|
728
|
+
if (readonly === false)
|
|
729
|
+
return worstLevel(level, 'normal');
|
|
730
|
+
return worstLevel(level, 'unknown');
|
|
731
|
+
}
|
|
732
|
+
function classifyCommandChain(input, depth) {
|
|
733
|
+
if (depth > 3)
|
|
734
|
+
return 'unknown';
|
|
735
|
+
const extracted = extractSubstitutions(input);
|
|
736
|
+
if (extracted === null)
|
|
737
|
+
return 'unknown';
|
|
738
|
+
const subLevels = extracted.subs.map((sub) => classifyCommandChain(sub, depth + 1));
|
|
739
|
+
const tokens = tokenize(extracted.text);
|
|
740
|
+
if (tokens === null)
|
|
741
|
+
return 'unknown';
|
|
742
|
+
if (tokens.length === 0)
|
|
743
|
+
return 'unknown';
|
|
744
|
+
const segments = splitSegments(tokens);
|
|
745
|
+
if (segments === null)
|
|
746
|
+
return 'unknown';
|
|
747
|
+
let level = 'safe';
|
|
748
|
+
for (const segment of segments) {
|
|
749
|
+
const segLevel = classifySegment(segment, subLevels);
|
|
750
|
+
level = worstLevel(level, segLevel);
|
|
751
|
+
}
|
|
752
|
+
if (extracted.unknownExpansion || extracted.subs.length > 0)
|
|
753
|
+
level = worstLevel(level, 'unknown');
|
|
754
|
+
return level;
|
|
755
|
+
}
|
|
756
|
+
/** 纯命令文本分类;空串 → unknown(与既有行为一致)。 */
|
|
757
|
+
export function classifyCommandRisk(command) {
|
|
758
|
+
if (command.trim() === '')
|
|
759
|
+
return 'unknown';
|
|
760
|
+
try {
|
|
761
|
+
return classifyCommandChain(command, 0);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return 'unknown';
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* 按 (工具是否 execute 类, 参数) 分类。非 execute 类工具的 bashRisk 恒为
|
|
769
|
+
* unknown——该字段只描述 shell 命令。
|
|
770
|
+
*/
|
|
771
|
+
export function classifyBashRisk(isExecuteKind, params) {
|
|
772
|
+
if (!isExecuteKind)
|
|
773
|
+
return 'unknown';
|
|
774
|
+
try {
|
|
775
|
+
const p = typeof params === 'object' && params !== null && !Array.isArray(params) ? params : {};
|
|
776
|
+
const command = typeof p.command === 'string' ? p.command : typeof p.args === 'string' ? p.args : '';
|
|
777
|
+
if (command === '')
|
|
778
|
+
return 'unknown';
|
|
779
|
+
return classifyCommandRisk(command);
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
return 'unknown';
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
//# sourceMappingURL=bash-risk.js.map
|