dsh-rule-engine 0.5.11 → 0.5.13

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.
@@ -247,6 +247,25 @@ const READONLY_CMD_RE =
247
247
  /(?:\b(?:Get-Content|Get-ChildItem|Get-Item|Get-Command|Get-Date|Select-String|Find-String|Test-Path|Get-Process|Get-Service|Get-ItemProperty|Get-Variable|Get-FileHash|cat|type|dir|ls|grep|findstr|more|netstat|where|echo|Write-Output|Write-Host|Get-Location|pwd|cwd|Get-Help|help|Select-Object|Out-String|Format-Table|Format-List|Format-Wide|Measure-Object|Sort-Object|Where-Object|Group-Object|ForEach-Object|Out-Null|ConvertTo-Json|ConvertFrom-Json|Join-Path|Split-Path|Resolve-Path|New-Object|Add-Type|Get-Member|Get-ChildItemProperty|Get-CimInstance|Get-WmiObject)\b)|(?:\bgit\s+(?:-[^\s]+\s+[^\s]+\s+|--[^\s]+\s+)*\b(?:status|log|diff|show|branch|tag|remote|rev-parse|diff-tree|ls-files)(?:\s|$))|(?:\bdsh\s+(?:--version|--help|--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bdsh\s+--profile\s+[^\s]+\s+(?:--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bnode\s+(?:--check|--test|--version)(?:\s|$))|(?:\bnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bpnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bgh\s+(?:auth\s+status|repo\s+view)(?:\s|$))|(?:\bnpm\s+(?:ls|view)(?:\s|$))/i;
248
248
  const MUTATING_CMD_RE =
249
249
  /(?:Set-Content|Add-Content|Out-File|Tee-Object|Remove-Item|Move-Item|Copy-Item|Rename-Item|New-Item|Clear-Content|Start-Process|Invoke-Expression|Invoke-Item|Export-Csv|Export-Clixml|Export-ModuleMember|Compress-Archive|Expand-Archive|Add-Type\s+-Output(?:Assembly|Type)|git\s+(?:push|commit)|rm\s+-r|rmdir\s+\/s|del\s+\/s|(?:^|[^0-9])>>|(?:^|[^0-9])>|(?:curl|wget|iwr|Invoke-WebRequest|Invoke-RestMethod)\s+(?=[^\n;|]*\s(?:-o|--output|-OutFile|OutFile)\s))/i;
250
+ // 0.5.12(F4):PowerShell 别名展开表——写/删除/移动/执行别名 → 标准 cmdlet 名
251
+ // 供 normalizeAliases 做命令规范化(MUTATING_CMD_RE 覆盖完整 cmdlet;别名先展开再判)
252
+ const PS_ALIAS_MAP = [
253
+ [/^rm\b(?![a-z])/i, "Remove-Item"], [/^rd\b(?![a-z])/i, "Remove-Item"], [/^ri\b(?![a-z])/i, "Remove-Item"],
254
+ [/^del\b(?![a-z])/i, "Remove-Item"], [/^erase\b(?![a-z])/i, "Remove-Item"],
255
+ [/^cp\b(?![a-z])/i, "Copy-Item"], [/^copy\b(?![a-z])/i, "Copy-Item"],
256
+ [/^mv\b(?![a-z])/i, "Move-Item"], [/^move\b(?![a-z])/i, "Move-Item"],
257
+ [/^ren\b(?![a-z])/i, "Rename-Item"], [/^iex\b(?![a-z])/i, "Invoke-Expression"],
258
+ [/^gc\b(?![a-z])/i, "Get-Content"], [/^gci\b(?![a-z])/i, "Get-ChildItem"], [/^gi\b(?![a-z])/i, "Get-Item"],
259
+ [/^gp\b(?![a-z])/i, "Get-ItemProperty"], [/^gv\b(?![a-z])/i, "Get-Variable"], [/^gl\b(?![a-z])/i, "Get-Location"]
260
+ ];
261
+ /** 规范化命令文本中的 PS 别名 → 标准 cmdlet 大写(只变换命令头,不动参数/字符串) */
262
+ export function normalizeAliases(command) {
263
+ const s = String(command || "");
264
+ return s.replace(/(?:^|[\s;|&{(])\s*(rm|rd|ri|del|erase|cp|copy|mv|move|ren|iex|gc|gci|gi|gp|gv|gl)(?=\s+)/gi, (m, name) => {
265
+ const hit = PS_ALIAS_MAP.find(([re]) => re.test(name));
266
+ return hit ? `${m.slice(0, m.length - name.length)}${hit[1]}` : m;
267
+ });
268
+ }
250
269
  // .NET 静态/实例写方法(MUTATING 补充:Set-Content 等 cmdlet 之外的写路径)
251
270
  const MUTATING_METHOD_RE =
252
271
  /(?:\[[\w.]+\]::\s*(?:Write|Set|Remove|Delete|Move|Copy|Create|Save|Append)\w*\s*\(|\.(?:Write(?:AllText|Line|Bytes|Text)?|SetContent|MoveTo?|CopyTo|Delete|Remove|SaveAs?2?\s*\(|CreateDirectory|CreateFile|WriteLine)\s*\()/i;
@@ -255,7 +274,10 @@ const MUTATING_METHOD_RE =
255
274
  // 保守边界:内容属性赋值($doc.Text=... / $doc.Content.Text=...)是写文档,不豁免。
256
275
  const COM_CONTENT_ASSIGN_RE = /\.(?:Text|Content|Value|Range|Paragraphs|Sections|Documents|Document)\s*=/i;
257
276
  const ALLOW_SEG_RE =
258
- /(?:\$[A-Za-z_][\w$]*(?:(?:\.[\w$]+)|(?:\[[^\]]+\]))*\s*=)|(?:\$[\w$.]+\.(?:Content|Range|Text|Value|Paragraphs|Sections|Length|Count|Name|Selected)\b)|(?:\$[\w$.]+$)|(?:\b(?:Documents|Document)\.(?:Open|Item)\s*\([^)]*,\s*\d+\s*,\s*\$?true\s*\))/;
277
+ /(?:\$[A-Za-z_][\w$]*(?:(?:\.[\w$]+)|(?:\[[^\]]+\]))*\s*=)|(?:\$[\w$.]+\.(?:Content|Range|Text|Value|Paragraphs|Sections|Length|Count|Name|Selected|LinkType|Target|FullName|Extension|Directory|PSPath|Property|Attributes|Mode|LastWriteTime|Exists)\b)|(?:\$[\w$.]+$)|(?:\b(?:Documents|Document)\.(?:Open|Item)\s*\([^)]*,\s*\d+\s*,\s*\$?true\s*\))|(?:\(!?\$[\w$.]+(?:\.\w+)?\)|\([^()\r\n]*\$[\w$.]+\.[\w.]+\s*\))/;
278
+ // 0.5.12(F4):纯字符串/纯输出段(整段是引号包裹字符串或 $() 子表达式)——不是命令,放行。
279
+ // 保守边界:只匹配"整段即字符串"(^...$ 锚定),不让 node -e "..." / curl -o "..." 借道。
280
+ const PURE_STRING_SEG_RE = /^(?:"[^"\r\n]*"|'[^'\r\n]*'|`[^`\r\n]*`|\$\(\s*[^()\r\n]*\s*\))+$/;
259
281
  // 剥离 try/catch/finally 控制块:块内语句与主语句同样接受只读判定(Try-* 不是 cmdlet)。
260
282
  // 注意:不做全量 [{}] 替换——if/else 子表达式($(if(...){...}else{...}))会被切碎误判。
261
283
  function stripControlBlocks(command) {
@@ -289,7 +311,10 @@ function splitCommandSegments(command) {
289
311
  * ③ 其它不认识的段 → 保守判非只读(不放大白名单,宁紧勿松)。 */
290
312
  export function isReadOnlyCommand(command) {
291
313
  if (typeof command !== "string") return false;
292
- const segments = splitCommandSegments(stripControlBlocks(command));
314
+ // 0.5.12(F4):先做别名规范化(rm→Remove-Item、gc→Get-Content…),
315
+ // 保证 MUTATING/READONLY 词表只看标准 cmdlet(别名双向缺口:写漏 rm/del/iex、读漏 gc/gci)
316
+ const normalized = normalizeAliases(command);
317
+ const segments = splitCommandSegments(stripControlBlocks(normalized));
293
318
  if (segments.length === 0) return false;
294
319
  return segments.every((seg) => {
295
320
  const s = String(seg).trim();
@@ -300,6 +325,7 @@ export function isReadOnlyCommand(command) {
300
325
  if (COM_CONTENT_ASSIGN_RE.test(s)) return false; // $doc.Text=... 是写文档
301
326
  if (READONLY_CMD_RE.test(s)) return true;
302
327
  if (ALLOW_SEG_RE.test(s)) return true;
328
+ if (PURE_STRING_SEG_RE.test(s)) return true; // 0.5.12(F4):纯字符串/输出段(非命令)
303
329
  return false; // 看不清的段:保守按非只读处理
304
330
  });
305
331
  }
@@ -362,6 +388,9 @@ export function isAnalysisOp(toolName, args) {
362
388
  }
363
389
  }
364
390
  if (name === "node" || name === "pwsh" || name === "bash") {
391
+ // ㊄ 验证通道(2026-08-31,用户定稿):验证类命令(测试/体检/探针/审计/dry-run/只读查询)
392
+ // 独立放行——调研/验证灵活性与"执行类"严格授权分层;清单见 isVerificationCommand(执行类不匹配)。
393
+ if (isVerificationCommand(cmd)) return true;
365
394
  // ③ 临时区写:写命令(不含删除/移动——删除/移动属 13A 红线,不走分析豁免)+ 提取路径全部命中临时区 + 无受保护名
366
395
  if (isMutationCommand(cmd) && !/(?:Remove-Item|Move-Item|Rename-Item|del\s|rm\s|git\s+push|git\s+commit)/i.test(cmd)) {
367
396
  const paths = extractAnalysisScratchPaths(cmd);
@@ -550,11 +579,21 @@ export function isAuditCommand(command) {
550
579
  * v0.5.7 P0-1(用户拍板 2026-08-26):验证类命令判定——运行测试/冷加载探针/挂载审计/语法检查。
551
580
  * 与"修/改/写"授权伴生放行使用:用户授权修改后,验证是变更的正常闭环,不再被
552
581
  * write vs command 类型不匹配误拦(今天实弹连卡 4 次)。
582
+ * 2026-08-31(验证通道放行面,用户定稿):清单扩展 + 由 isAnalysisOp 独立调用——
583
+ * ① 单测通配(任意 *.test.mjs)、② 验证/门禁脚本(check-tool-coverage / publish-aptitude-check)、
584
+ * ③ --dry-run(发布脚本 dry-run=只读推导,发布链无 dry-run 不匹配)、④ gh api 只读 GET、npm whoami。
585
+ * 安全边界:执行类脚本(release-plugin 无 --dry-run / inject / uninject / install / publish / commit /
586
+ * push / Remove-Item 等)一律不匹配——执行类仍走授权语义严格版(0.5.11 防借道回归不放松)。
553
587
  */
554
588
  export function isVerificationCommand(command) {
555
589
  if (typeof command !== "string") return false;
556
- if (/\b(?:node|npm|npx)\s+[^\n]*(?:run-all\.mjs|check-plugin-load\.mjs|audit-mount-consistency\.mjs|loader-smoke\.e2e\.mjs|verify-all\.mjs|health-audit\.mjs|--check|--test)\b/i.test(command)) return true;
590
+ if (/\b(?:node|npm|npx)\s+[^\n]*(?:run-all\.mjs|check-plugin-load\.mjs|audit-mount-consistency\.mjs|loader-smoke\.e2e\.mjs|verify-all\.mjs|health-audit\.mjs|check-tool-coverage\.mjs|publish-aptitude-check\.mjs|\.test\.mjs\b|--check|--test|--dry-run)\b/i.test(command)) return true;
557
591
  if (/\b(?:npm|pnpm)\s+(?:test|run\s+test)\b/i.test(command)) return true;
592
+ if (/\bgh\s+api\b/i.test(command)) {
593
+ // 只读 GET 形态;写形态(--method/-X 显式方法、-F/--field 表单)不豁免(gg api 写=执行类)
594
+ return !/(?:--method|-X)\s+(?:POST|PUT|PATCH|DELETE)|\s-(?:F|f)\s|--field\s/i.test(command);
595
+ }
596
+ if (/\bnpm\s+whoami\b/i.test(command)) return true;
558
597
  return false;
559
598
  }
560
599