@kenhkl/agent-moss 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -633,10 +633,15 @@ function loadMC() {
633
633
  try {
634
634
  const rt = (init_runtime_config(), __toCommonJS(runtime_config_exports));
635
635
  if (rules.length > 0 && typeof rt.hasRuntimeConfig === "function" && rt.hasRuntimeConfig()) {
636
- const rtRules = rt.loadRuntimeConfig()?.L2_4?.ltl_rules?.rules || [];
636
+ const ltl = rt.loadRuntimeConfig()?.L2_2?.ltl_rules || {};
637
+ const rtRules = ltl.rules || [];
637
638
  const enabledById = {};
638
639
  for (const r of rtRules) if (r?.id) enabledById[r.id] = r.enabled !== false;
639
- for (const r of rules) if (r?.id && r.id in enabledById) r.enabled = enabledById[r.id];
640
+ const catEnabled = ltl.category_enabled !== false;
641
+ for (const r of rules) {
642
+ if (r?.id && r.id in enabledById) r.enabled = enabledById[r.id];
643
+ if (!catEnabled) r.enabled = false;
644
+ }
640
645
  }
641
646
  } catch {
642
647
  }
@@ -681,7 +686,8 @@ function _loadSensitivePathScope(scope) {
681
686
  path: sp.path,
682
687
  riskLevel: sp.risk_level,
683
688
  desc: sp.desc,
684
- credential: sp.deny_mode === "deny_both" ? true : void 0
689
+ credential: sp.deny_mode === "deny_both" ? true : void 0,
690
+ dirDelOnly: sp.dir_del_only === true ? true : void 0
685
691
  }));
686
692
  }
687
693
  function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWORDS) {
@@ -779,7 +785,7 @@ function runLogicCheck(input) {
779
785
  riskType: "intent_deviation",
780
786
  confidence: 85,
781
787
  source: "logic_rule",
782
- violatedLayers: ["2"]
788
+ violatedLayers: ["L2.1"]
783
789
  };
784
790
  }
785
791
  }
@@ -803,7 +809,7 @@ function runLogicCheck(input) {
803
809
  riskType: "privilege_escalation",
804
810
  confidence: 85,
805
811
  source: "logic_rule",
806
- violatedLayers: ["2"]
812
+ violatedLayers: ["L2.1"]
807
813
  };
808
814
  }
809
815
  }
@@ -855,7 +861,7 @@ function checkComboChain(actionHistory, actionDetailRaw) {
855
861
  riskType: "combo_chain",
856
862
  confidence: 85,
857
863
  source: "logic_rule",
858
- violatedLayers: ["2"]
864
+ violatedLayers: ["L2.1"]
859
865
  };
860
866
  }
861
867
  }
@@ -980,7 +986,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
980
986
  riskType: "read_before_write",
981
987
  confidence: 85,
982
988
  source: "logic_rule",
983
- violatedLayers: ["2"]
989
+ violatedLayers: ["L2.1"]
984
990
  };
985
991
  }
986
992
  return null;
@@ -1000,7 +1006,7 @@ function checkPasswordConsent(actionType, actionDetail, actionHistory) {
1000
1006
  riskType: "consent_missing",
1001
1007
  confidence: 85,
1002
1008
  source: "logic_rule",
1003
- violatedLayers: ["2"]
1009
+ violatedLayers: ["L2.1"]
1004
1010
  };
1005
1011
  }
1006
1012
  function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
@@ -1018,7 +1024,7 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
1018
1024
  riskType: "consent_missing",
1019
1025
  confidence: 85,
1020
1026
  source: "logic_rule",
1021
- violatedLayers: ["2"]
1027
+ violatedLayers: ["L2.1"]
1022
1028
  };
1023
1029
  }
1024
1030
  function containsPrivilegeEscalation(actionDetail) {
@@ -1078,7 +1084,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1078
1084
  riskType: "file_access",
1079
1085
  confidence: 95,
1080
1086
  source: "logic_rule",
1081
- violatedLayers: ["2"],
1087
+ violatedLayers: ["L2.1"],
1082
1088
  isCredential: true
1083
1089
  };
1084
1090
  }
@@ -1094,7 +1100,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1094
1100
  riskType: "file_access",
1095
1101
  confidence: 95,
1096
1102
  source: "logic_rule",
1097
- violatedLayers: ["2"]
1103
+ violatedLayers: ["L2.1"]
1098
1104
  };
1099
1105
  }
1100
1106
  }
@@ -1108,7 +1114,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1108
1114
  riskType: "file_access",
1109
1115
  confidence: 70,
1110
1116
  source: "logic_rule",
1111
- violatedLayers: ["2"]
1117
+ violatedLayers: ["L2.1"]
1112
1118
  };
1113
1119
  }
1114
1120
  var ROOT_SENSITIVE_PATHS, HOME_SENSITIVE_PATHS, CREDENTIAL_PATHS, NON_WRITE_TOOLS, NON_WRITE_TOOL_PREFIXES, WRITE_KEYWORDS, WRITE_COMMAND_RES, READ_ONLY_COMMANDS, DEVNULL_REDIRECT_RE, READ_KEYWORDS, CREATE_KEYWORDS, PASSWORD_MODIFY_PATTERNS, USER_DELETION_PATTERNS, INTENT_DEVIATION_PATTERNS, COMBO_STAGES, COMBO_CHAINS, INLINE_SCRIPT_RE, DIR_TRAVERSAL_KW;
@@ -1120,7 +1126,7 @@ var init_logic_rules = __esm({
1120
1126
  init_rules_loader();
1121
1127
  ROOT_SENSITIVE_PATHS = _loadSensitivePathScope("root");
1122
1128
  HOME_SENSITIVE_PATHS = _loadSensitivePathScope("home");
1123
- CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc }) => ({ path: path6, riskLevel, desc, credential: true }));
1129
+ CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc, dirDelOnly }) => ({ path: path6, riskLevel, desc, credential: true, dirDelOnly }));
1124
1130
  NON_WRITE_TOOLS = /* @__PURE__ */ new Set([
1125
1131
  "ask_user_question",
1126
1132
  "glob",
@@ -1433,6 +1439,7 @@ __export(runtime_config_exports, {
1433
1439
  isLayerEnabled: () => isLayerEnabled,
1434
1440
  loadRuntimeConfig: () => loadRuntimeConfig,
1435
1441
  mergeConfig: () => mergeConfig,
1442
+ pickDeletableRule: () => pickDeletableRule,
1436
1443
  saveRuntimeConfig: () => saveRuntimeConfig,
1437
1444
  updateAgentCategoryEnabled: () => updateAgentCategoryEnabled,
1438
1445
  updateAgentRuleEnabled: () => updateAgentRuleEnabled,
@@ -1471,7 +1478,7 @@ function emptyRuntimeConfig() {
1471
1478
  L1_rules: {},
1472
1479
  L2_rules: {},
1473
1480
  L3_skills: {},
1474
- L2_4: {},
1481
+ L2_2: {},
1475
1482
  agent_rules: {},
1476
1483
  scenario_rules: {}
1477
1484
  };
@@ -1499,7 +1506,7 @@ function loadRuntimeConfig() {
1499
1506
  L1_rules: merged.L1_rules || {},
1500
1507
  L2_rules: merged.L2_rules || {},
1501
1508
  L3_skills: merged.L3_skills || {},
1502
- L2_4: merged.L2_4 || {},
1509
+ L2_2: merged.L2_2 || {},
1503
1510
  agent_rules: merged.agent_rules || {},
1504
1511
  scenario_rules: merged.scenario_rules || {}
1505
1512
  };
@@ -1552,6 +1559,7 @@ function getEnabledL2SensitivePaths(runtime) {
1552
1559
  const out = { path: r.path || "", risk_level: r.risk_level || "", desc: r.desc || "", deny_mode: r.deny_mode || "deny_write" };
1553
1560
  if (r.credential) out.credential = true;
1554
1561
  if (r.read_only) out.read_only = true;
1562
+ if (r.dir_del_only) out.dir_del_only = true;
1555
1563
  return out;
1556
1564
  });
1557
1565
  }
@@ -1597,8 +1605,16 @@ function getDisabledL2RulesWithSkipL3() {
1597
1605
  function _slugify(text) {
1598
1606
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "x";
1599
1607
  }
1608
+ function _fnv1a32(text) {
1609
+ let h = 2166136261;
1610
+ for (const b of Buffer.from(text, "utf-8")) {
1611
+ h ^= b;
1612
+ h = h * 16777619 >>> 0;
1613
+ }
1614
+ return h.toString(16).padStart(8, "0");
1615
+ }
1600
1616
  function _generatePatternId(pattern) {
1601
- return `pat_${_slugify(pattern)}`;
1617
+ return `pat_${_slugify(pattern)}_${_fnv1a32(pattern)}`;
1602
1618
  }
1603
1619
  function _deriveDenyMode(rule) {
1604
1620
  if (rule.deny_mode) return rule.deny_mode;
@@ -1660,6 +1676,7 @@ function _l2SensitivePathsToRules() {
1660
1676
  builtin: true
1661
1677
  };
1662
1678
  _syncDenyModeFields(rule);
1679
+ if (sp.dirDelOnly) rule.dir_del_only = true;
1663
1680
  return rule;
1664
1681
  });
1665
1682
  }
@@ -1876,17 +1893,20 @@ function generateSourceDefaults() {
1876
1893
  user_custom: { category_enabled: true, category_desc: "\u7528\u6237\u81EA\u5B9A\u4E49 L2 \u903B\u8F91\u89C4\u5219", rules: [] }
1877
1894
  },
1878
1895
  L3_skills: _l3SkillsToRules(),
1879
- L2_4: _ltlRulesToRuntime(),
1896
+ L2_2: _ltlRulesToRuntime(),
1880
1897
  agent_rules: _agentRulesToDefaults(),
1881
1898
  scenario_rules: _scenarioRulesToDefaults()
1882
1899
  };
1883
1900
  }
1884
1901
  function _ltlRulesToRuntime() {
1885
1902
  const cands = [
1886
- join3(__dirname4, "..", "rules"),
1887
- join3(__dirname4, "..", "..", "..", "agent_moss", "rules"),
1888
- join3(process.cwd(), "agent_moss", "rules")
1889
- ];
1903
+ getRuntimeConfigDir() && join3(getRuntimeConfigDir(), "rules"),
1904
+ join3(__dirname4, "../rules"),
1905
+ join3(__dirname4, "rules"),
1906
+ join3(__dirname4, "../../../agent_moss/rules"),
1907
+ join3(__dirname4, "../../../../agent_moss/rules"),
1908
+ join3(process.cwd(), "agent_moss/rules")
1909
+ ].filter(Boolean);
1890
1910
  let mc = {};
1891
1911
  for (const dir of cands) {
1892
1912
  const p = join3(dir, "mc_rules.json");
@@ -2064,7 +2084,11 @@ function mergeConfig(userCfg, srcDefaults) {
2064
2084
  );
2065
2085
  _mergeRuleCategories(cfg, srcDefaults, "L1_rules");
2066
2086
  _mergeRuleCategories(cfg, srcDefaults, "L2_rules");
2067
- _mergeRuleCategories(cfg, srcDefaults, "L2_4");
2087
+ if (!cfg.L2_2 && cfg.L2_4) {
2088
+ cfg.L2_2 = cfg.L2_4;
2089
+ delete cfg.L2_4;
2090
+ }
2091
+ _mergeRuleCategories(cfg, srcDefaults, "L2_2");
2068
2092
  _mergeSkillCategories(cfg, srcDefaults, "L3_skills");
2069
2093
  _mergeAgentRules(cfg, srcDefaults);
2070
2094
  _mergeAgentRules(cfg, srcDefaults, "scenario_rules");
@@ -2242,16 +2266,20 @@ function addCustomRule(layer, category, rule) {
2242
2266
  saveRuntimeConfig(runtime);
2243
2267
  return runtime;
2244
2268
  }
2245
- function deleteCustomRule(layer, category, ruleId) {
2246
- const runtime = loadRuntimeConfig();
2247
- const rules = runtime[layer]?.[category]?.rules || [];
2248
- const idx = rules.findIndex((r) => r.id === ruleId);
2269
+ function pickDeletableRule(rules, ruleId) {
2270
+ const idx = rules.findIndex((r) => r.id === ruleId && (!r.builtin || r.source_removed));
2249
2271
  if (idx >= 0) {
2250
- const r = rules[idx];
2251
- if (r.builtin && !r.source_removed) return null;
2252
2272
  rules.splice(idx, 1);
2253
- saveRuntimeConfig(runtime);
2273
+ return rules;
2254
2274
  }
2275
+ if (rules.some((r) => r.id === ruleId)) return null;
2276
+ return rules;
2277
+ }
2278
+ function deleteCustomRule(layer, category, ruleId) {
2279
+ const runtime = loadRuntimeConfig();
2280
+ const rules = runtime[layer]?.[category]?.rules || [];
2281
+ if (pickDeletableRule(rules, ruleId) === null) return null;
2282
+ saveRuntimeConfig(runtime);
2255
2283
  return runtime;
2256
2284
  }
2257
2285
  function addCustomSkill(skillId, category, keywords, content) {
@@ -2684,7 +2712,7 @@ function runHeuristicCheck(input) {
2684
2712
  riskType: "sensitive_action",
2685
2713
  confidence: 90,
2686
2714
  source: "heuristic",
2687
- violatedLayers: ["1.1"]
2715
+ violatedLayers: ["L1.1"]
2688
2716
  };
2689
2717
  }
2690
2718
  }
@@ -2694,8 +2722,10 @@ function runHeuristicCheck(input) {
2694
2722
  const scanDetail = normalizePathSegments(stripLiteralTextSegments(input.aNext.actionDetail));
2695
2723
  const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
2696
2724
  let topRiskType = "";
2725
+ const exemptKeygen = isSshKeygenGenerate(input.aNext.actionDetail);
2697
2726
  for (const rule of DANGEROUS_PATTERNS) {
2698
2727
  if (rule.pattern.test(scanCommand)) {
2728
+ if (exemptKeygen && rule.riskType === "file_access") continue;
2699
2729
  matched.push(rule.description);
2700
2730
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
2701
2731
  highestSeverity = rule.severity;
@@ -2737,10 +2767,26 @@ function runHeuristicCheck(input) {
2737
2767
  const detailL = input.aNext.actionDetail.toLowerCase();
2738
2768
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2739
2769
  const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2740
- if (dangerousResult) return dangerousResult;
2741
- if (!isExcludedType) {
2742
- const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
2743
- if (pathResult) return pathResult;
2770
+ const pathResult = isExcludedType ? null : checkSensitivePath(detailL, input.cwd || "/", agentExt);
2771
+ const homeResult = checkHomeDeletion(actionTypeL, detailL);
2772
+ const mergeResults = [dangerousResult, pathResult, homeResult].filter(Boolean);
2773
+ if (mergeResults.length > 0) {
2774
+ let best = mergeResults[0];
2775
+ for (const r of mergeResults) {
2776
+ if (severityRank(r.riskLevel) > severityRank(best.riskLevel)) best = r;
2777
+ }
2778
+ if (severityRank(highestSeverity) > severityRank(best.riskLevel)) {
2779
+ return {
2780
+ allowed: false,
2781
+ reason: `\u68C0\u6D4B\u5230\u9AD8\u5371\u64CD\u4F5C: ${matched.join(", ")}`,
2782
+ riskLevel: highestSeverity,
2783
+ riskType: highestRiskType,
2784
+ confidence: 95,
2785
+ source: "heuristic",
2786
+ violatedLayers: ["L1.1"]
2787
+ };
2788
+ }
2789
+ return best;
2744
2790
  }
2745
2791
  if (matched.length > 0) {
2746
2792
  return {
@@ -2750,8 +2796,7 @@ function runHeuristicCheck(input) {
2750
2796
  riskType: highestRiskType,
2751
2797
  confidence: 95,
2752
2798
  source: "heuristic",
2753
- violatedLayers: ["1.1"]
2754
- // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2799
+ violatedLayers: ["L1.1"]
2755
2800
  };
2756
2801
  }
2757
2802
  const lateralResult = checkLateralMovement(detailL);
@@ -2879,6 +2924,7 @@ function isSearchActionType(actionType) {
2879
2924
  function checkSensitivePath(command, cwd, agentExt) {
2880
2925
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2881
2926
  const isWriteOpFlag = isWriteOperation("", command);
2927
+ const keygenGenerate = isSshKeygenGenerate(command);
2882
2928
  const pathScanText = normalizePathSegments(stripLiteralTextSegments(command));
2883
2929
  const pathMatches = (text, path6) => {
2884
2930
  const parts = path6.split(/(\(\?!.*?\))/);
@@ -2891,6 +2937,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2891
2937
  };
2892
2938
  for (const sp of ROOT_SENSITIVE_PATHS) {
2893
2939
  if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2940
+ if (keygenGenerate) continue;
2894
2941
  if (sp.path === "/dev/random") {
2895
2942
  if (isWriteOpFlag) {
2896
2943
  continue;
@@ -2902,7 +2949,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2902
2949
  riskType: "file_access",
2903
2950
  confidence: 90,
2904
2951
  source: "heuristic",
2905
- violatedLayers: ["1.1"]
2952
+ violatedLayers: ["L1.1"]
2906
2953
  };
2907
2954
  }
2908
2955
  if (!isWriteOpFlag && !sp.credential) {
@@ -2915,16 +2962,51 @@ function checkSensitivePath(command, cwd, agentExt) {
2915
2962
  riskType: "file_access",
2916
2963
  confidence: 90,
2917
2964
  source: "heuristic",
2918
- violatedLayers: ["1.1"],
2965
+ violatedLayers: ["L1.1"],
2919
2966
  isCredential: !!sp.credential
2920
2967
  };
2921
2968
  }
2922
2969
  }
2923
2970
  for (const sp of HOME_SENSITIVE_PATHS) {
2924
2971
  const expanded = `${expandedCwd}/${sp.path}`;
2925
- const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2926
- const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2927
- if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
2972
+ let pPath = sp.path;
2973
+ if (!sp.path.endsWith(".pub")) pPath = `${sp.path}(?!\\.pub)`;
2974
+ let dirPrefixMatch = null;
2975
+ if (sp.dirDelOnly) {
2976
+ const dirRe = (t) => new RegExp(`(?:^|[\\s/\\\\'"])${t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:/|[\\s'"\\\\]|$)`, "i");
2977
+ dirPrefixMatch = dirRe(sp.path).test(pathScanText) || dirRe(expanded.toLowerCase()).test(pathScanText);
2978
+ }
2979
+ const pathReMatch = sp.dirDelOnly ? !!dirPrefixMatch : pathMatches(pathScanText, pPath);
2980
+ const expandedReMatch = sp.dirDelOnly ? false : pathMatches(pathScanText, expanded + (sp.path.endsWith(".pub") ? "" : "(?!\\.pub)"));
2981
+ if (pathReMatch || expandedReMatch) {
2982
+ if (keygenGenerate) continue;
2983
+ if (sp.dirDelOnly) {
2984
+ if (isDeleteActionForDir(command, sp.path)) {
2985
+ return {
2986
+ allowed: false,
2987
+ reason: `\u5220\u9664\u654F\u611F\u76EE\u5F55: ${sp.desc} (${sp.path})`,
2988
+ riskLevel: sp.riskLevel,
2989
+ riskType: "file_access",
2990
+ confidence: 90,
2991
+ source: "heuristic",
2992
+ violatedLayers: ["L1.1"],
2993
+ isCredential: !!sp.credential
2994
+ };
2995
+ }
2996
+ if (isArchiveReadOfDir(command, sp.path)) {
2997
+ return {
2998
+ allowed: false,
2999
+ reason: `\u5F52\u6863/\u590D\u5236\u8BFB\u53D6\u654F\u611F\u76EE\u5F55: ${sp.desc} (${sp.path})`,
3000
+ riskLevel: sp.riskLevel,
3001
+ riskType: "file_access",
3002
+ confidence: 90,
3003
+ source: "heuristic",
3004
+ violatedLayers: ["L1.1"],
3005
+ isCredential: !!sp.credential
3006
+ };
3007
+ }
3008
+ continue;
3009
+ }
2928
3010
  if (!isWriteOpFlag && !sp.credential) {
2929
3011
  continue;
2930
3012
  }
@@ -2935,7 +3017,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2935
3017
  riskType: "file_access",
2936
3018
  confidence: 90,
2937
3019
  source: "heuristic",
2938
- violatedLayers: ["1.1"],
3020
+ violatedLayers: ["L1.1"],
2939
3021
  isCredential: !!sp.credential
2940
3022
  };
2941
3023
  }
@@ -2958,7 +3040,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2958
3040
  riskType: "file_access",
2959
3041
  confidence: 90,
2960
3042
  source: "heuristic",
2961
- violatedLayers: ["1.1"],
3043
+ violatedLayers: ["L1.1"],
2962
3044
  isCredential: !!sp.credential
2963
3045
  };
2964
3046
  }
@@ -2975,7 +3057,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2975
3057
  riskType: "file_access",
2976
3058
  confidence: 90,
2977
3059
  source: "heuristic",
2978
- violatedLayers: ["1.1"],
3060
+ violatedLayers: ["L1.1"],
2979
3061
  isCredential: true
2980
3062
  };
2981
3063
  }
@@ -2988,18 +3070,142 @@ function checkSensitivePath(command, cwd, agentExt) {
2988
3070
  riskType: "file_access",
2989
3071
  confidence: 90,
2990
3072
  source: "heuristic",
2991
- violatedLayers: ["1.1"]
3073
+ violatedLayers: ["L1.1"]
2992
3074
  };
2993
3075
  }
2994
3076
  return null;
2995
3077
  }
3078
+ function isHomeValue(path6) {
3079
+ let p = path6.trim().toLowerCase();
3080
+ if (p.startsWith("$(") && p.endsWith(")")) {
3081
+ const inner = p.slice(2, -1).trim();
3082
+ return HOME_TARGET_RE.test(" " + inner);
3083
+ }
3084
+ if (/^\/home\/[^/\s]+\/?$/.test(p)) return true;
3085
+ return HOME_TARGET_RE.test(" " + p);
3086
+ }
3087
+ function isDeleteCommand(seg) {
3088
+ const s = seg.trimStart();
3089
+ if (/^(?:rm|rmdir|unlink)\b/i.test(s)) return true;
3090
+ return /删除|移除/.test(s);
3091
+ }
3092
+ function homeEscapeTarget(seg) {
3093
+ const low = seg.toLowerCase();
3094
+ const re = /(?:~|(?:\$home|\$\{home\})|\/home\/[a-z0-9_.-]+)/g;
3095
+ let m;
3096
+ while ((m = re.exec(low)) !== null) {
3097
+ const endIdx = m.index + m[0].length;
3098
+ const tail = low.slice(endIdx);
3099
+ if (!tail) continue;
3100
+ let token = tail.trimStart().split(/\s+/)[0] || "";
3101
+ token = token.split("&&")[0].split("||")[0].split(";")[0].replace(/\/+$/, "");
3102
+ token = token.replace(/^['"]|['"]$/g, "");
3103
+ const segments = token.split("/").filter((s) => s && s !== ".");
3104
+ if (!segments.some((s) => s === "..")) continue;
3105
+ let depth = 0;
3106
+ let minDepth = 0;
3107
+ for (const s of segments) {
3108
+ depth += s === ".." ? -1 : 1;
3109
+ minDepth = Math.min(minDepth, depth);
3110
+ }
3111
+ if (minDepth < 0) return true;
3112
+ if (depth <= 0) return true;
3113
+ }
3114
+ return false;
3115
+ }
3116
+ function checkHomeDeletion(actionType, actionDetail) {
3117
+ if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") return null;
3118
+ const segs = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
3119
+ for (const seg of segs) {
3120
+ if (!isDeleteCommand(seg)) continue;
3121
+ if (HOME_TARGET_RE.test(seg) || homeEscapeTarget(seg)) {
3122
+ return {
3123
+ allowed: false,
3124
+ reason: "\u68C0\u6D4B\u5230\u5220\u9664\u4E3B\u76EE\u5F55 (~/$HOME) \u64CD\u4F5C",
3125
+ riskLevel: "high",
3126
+ riskType: "script_execution",
3127
+ confidence: 90,
3128
+ source: "heuristic",
3129
+ violatedLayers: ["L1.1"]
3130
+ };
3131
+ }
3132
+ }
3133
+ const assigned = {};
3134
+ for (const m of actionDetail.matchAll(HOME_ASSIGN_RE)) assigned[m[1]] = m[2];
3135
+ for (const [varName, path6] of Object.entries(assigned)) {
3136
+ if (!isHomeValue(path6)) continue;
3137
+ const varRefRe = new RegExp(`(?:${escapeRegExp(varName)}|\\$\\{${escapeRegExp(varName)}\\})`);
3138
+ for (const seg of segs) {
3139
+ if (!isDeleteCommand(seg)) continue;
3140
+ if (varRefRe.test(seg)) {
3141
+ return {
3142
+ allowed: false,
3143
+ reason: `\u68C0\u6D4B\u5230\u901A\u8FC7\u53D8\u91CF ${varName}\uFF08=${path6}\uFF0C\u4E3B\u76EE\u5F55\uFF09\u6267\u884C\u5220\u9664`,
3144
+ riskLevel: "high",
3145
+ riskType: "script_execution",
3146
+ confidence: 90,
3147
+ source: "heuristic",
3148
+ violatedLayers: ["L1.1"]
3149
+ };
3150
+ }
3151
+ }
3152
+ }
3153
+ return null;
3154
+ }
3155
+ function escapeRegExp(s) {
3156
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3157
+ }
3158
+ function isDeleteActionForDir(command, dirPath) {
3159
+ const low = command.toLowerCase();
3160
+ const dpath = dirPath.toLowerCase();
3161
+ if (new RegExp(`\\b(?:rm|unlink|rmdir)\\b[^&|;]*?${escapeRegExp(dpath)}`, "i").test(low)) {
3162
+ return true;
3163
+ }
3164
+ if (new RegExp(`(?:\u5220\u9664|\u79FB\u9664)[^\\n;|&]*?${escapeRegExp(dpath)}`).test(low)) {
3165
+ return true;
3166
+ }
3167
+ for (const seg of low.split(/&&|\|\||;|\n/)) {
3168
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
3169
+ if ((fw === "rm" || fw === "unlink" || fw === "rmdir") && low.includes(dpath)) {
3170
+ return true;
3171
+ }
3172
+ }
3173
+ return false;
3174
+ }
3175
+ function isArchiveReadOfDir(command, dirPath) {
3176
+ const low = command.toLowerCase();
3177
+ const dpath = dirPath.toLowerCase();
3178
+ for (const seg of low.split(/&&|\|\||;|\n/)) {
3179
+ const s = seg.trim();
3180
+ if (!s || !s.includes(dpath)) continue;
3181
+ const parts = s.split(/\s+/);
3182
+ const fw = parts[0] || "";
3183
+ if (fw === "zip") return true;
3184
+ if (fw === "tar") {
3185
+ if (/(?:^|\s)-[a-zA-Z]*x|--extract/.test(s)) continue;
3186
+ return true;
3187
+ }
3188
+ if (fw === "cp" || fw === "rsync") {
3189
+ const segPaths = s.match(/[/~][^\s;|&><'"]*/g) || [];
3190
+ if (segPaths.length && segPaths[0].includes(dpath)) return true;
3191
+ }
3192
+ }
3193
+ return false;
3194
+ }
3195
+ function isSshKeygenGenerate(command) {
3196
+ const low = command.toLowerCase();
3197
+ if (!low.includes("ssh-keygen")) return false;
3198
+ if (!/(?:^|\s)-t\s+\S+/.test(low)) return false;
3199
+ if (/(?:^|\s)-\S*[ylpB]\S*/.test(low)) return false;
3200
+ return true;
3201
+ }
2996
3202
  function checkDangerousPatterns(actionType, actionDetail) {
2997
3203
  if (actionType === "file_write" || actionType === "file_edit") return null;
2998
3204
  const scan = stripHeredocContent(actionDetail);
2999
3205
  const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
3000
3206
  const isTempPath = TEMP_DIRS.some(
3001
3207
  (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
3002
- );
3208
+ ) || /(?:^|[\s;]|&&\s*|\|\|\s*)var\/tmp(?:[/\s;]|$)/.test(actionDetail);
3003
3209
  const wildcardDeleteRe = /(?:\brm\b|\b(r?mdir|del|delete|remove|rmdir|unlink)\b|\b删除\b|\b移除\b)[\s\S]{0,40}?[*?]|[*?][\s\S]{0,40}?\b(?:rm|del|delete|remove|rmdir|unlink)\b|rm\s+-[a-zA-Z]*[rf][a-zA-Z]*\s+\S*[*?]/i;
3004
3210
  if (!isTempPath && wildcardDeleteRe.test(scan)) {
3005
3211
  return {
@@ -3009,7 +3215,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3009
3215
  riskType: "script_execution",
3010
3216
  confidence: 90,
3011
3217
  source: "heuristic",
3012
- violatedLayers: ["1.1"]
3218
+ violatedLayers: ["L1.1"]
3013
3219
  };
3014
3220
  }
3015
3221
  const findWriteMatch = actionDetail.match(
@@ -3024,10 +3230,10 @@ function checkDangerousPatterns(actionType, actionDetail) {
3024
3230
  riskType: "script_execution",
3025
3231
  confidence: 90,
3026
3232
  source: "heuristic",
3027
- violatedLayers: ["1.1"]
3233
+ violatedLayers: ["L1.1"]
3028
3234
  };
3029
3235
  }
3030
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
3236
+ if (/>\s*['"]?\/etc\//.test(actionDetail) || />\s*['"]?\/boot\//.test(actionDetail) || />\s*['"]?\/proc\//.test(actionDetail)) {
3031
3237
  return {
3032
3238
  allowed: false,
3033
3239
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -3035,7 +3241,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3035
3241
  riskType: "file_access",
3036
3242
  confidence: 95,
3037
3243
  source: "heuristic",
3038
- violatedLayers: ["1.1"]
3244
+ violatedLayers: ["L1.1"]
3039
3245
  };
3040
3246
  }
3041
3247
  if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
@@ -3046,7 +3252,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3046
3252
  riskType: "file_access",
3047
3253
  confidence: 90,
3048
3254
  source: "heuristic",
3049
- violatedLayers: ["1.1"]
3255
+ violatedLayers: ["L1.1"]
3050
3256
  };
3051
3257
  }
3052
3258
  return null;
@@ -3137,13 +3343,13 @@ function checkLateralMovement(actionDetail) {
3137
3343
  riskType: "lateral_movement",
3138
3344
  confidence: 80,
3139
3345
  source: "heuristic",
3140
- violatedLayers: ["1.1"]
3346
+ violatedLayers: ["L1.1"]
3141
3347
  };
3142
3348
  }
3143
3349
  }
3144
3350
  return null;
3145
3351
  }
3146
- var INJECTION_KEYWORDS, DANGEROUS_PATTERNS, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES;
3352
+ var INJECTION_KEYWORDS, DANGEROUS_PATTERNS, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES, HOME_TARGET_RE, HOME_ASSIGN_RE;
3147
3353
  var init_heuristic = __esm({
3148
3354
  "src/engine/heuristic.ts"() {
3149
3355
  "use strict";
@@ -3173,6 +3379,8 @@ var init_heuristic = __esm({
3173
3379
  })();
3174
3380
  SENSITIVE_EXCLUDED_PREFIXES = ["memory-", "skill-", "skill-manager-", "mailbox-", "app-"];
3175
3381
  SEARCH_ACTION_PREFIXES = ["search-", "web-", "browser-", "query-", "websearch", "web_search"];
3382
+ HOME_TARGET_RE = /(?:^|[\s="'($])(?:~|(?:\$home|\$\{home\}))(?:\/(?:\.\*|\*|\.)?)?(?=[\s"';}|&)]|$)/i;
3383
+ HOME_ASSIGN_RE = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(~[A-Za-z0-9_./-]*|\$HOME[A-Za-z0-9_./-]*|\$\{HOME\}[A-Za-z0-9_./-]*|\/home\/[^\s;|&]+|\$\((?:[^()]|\([^()]*\))*\))(?=\s|;|$|\||&)/gi;
3176
3384
  }
3177
3385
  });
3178
3386
 
@@ -4967,6 +5175,13 @@ function extractScriptPath2(command) {
4967
5175
  return null;
4968
5176
  }
4969
5177
  function extractScriptPathWithArgs(command) {
5178
+ const inline = command.match(/(?:^|\s|&&|\|\|)\s*(?:bash|\/bin\/bash|sh|zsh)\s+-c\s*(['"])([\s\S]*?)\1/i);
5179
+ if (inline) {
5180
+ const inner = inline[2];
5181
+ const realPath = extractScriptPath2(inner);
5182
+ if (realPath) return { path: realPath, args: [] };
5183
+ return { path: null, args: [] };
5184
+ }
4970
5185
  const scriptPath = extractScriptPath2(command);
4971
5186
  if (!scriptPath) return { path: null, args: [] };
4972
5187
  const idx = command.indexOf(scriptPath);
@@ -4980,11 +5195,59 @@ function extractScriptPathWithArgs(command) {
4980
5195
  }
4981
5196
  return { path: scriptPath, args };
4982
5197
  }
5198
+ function looksLikeScriptPath(tok) {
5199
+ const t = tok.trim().replace(/^['"]|['"]$/g, "");
5200
+ if (!t) return false;
5201
+ const low = t.toLowerCase();
5202
+ if (["bash", "sh", "zsh", "dash", "python", "python3", "perl", "ruby", "node", "php", "lua"].includes(low)) return false;
5203
+ if (/\.(sh|bash|py|pl|rb|js|php|lua)$/.test(low)) return true;
5204
+ if (t.includes("/") && !t.startsWith("-")) return true;
5205
+ return false;
5206
+ }
4983
5207
  function extractInlineScriptPath(actionDetail) {
5208
+ const code = extractInlineCode(actionDetail);
5209
+ if (code) {
5210
+ const candidates = [];
5211
+ const osRe = /os\.(?:system|popen)\s*\(\s*(['"])(.*?)\1\s*\)/gi;
5212
+ let m;
5213
+ while ((m = osRe.exec(code)) !== null) candidates.push(m[2]);
5214
+ const subListRe = /subprocess\.(?:run|call|Popen|check_output|check_call)\s*\(\s*\[(.*?)\]/gi;
5215
+ while ((m = subListRe.exec(code)) !== null) candidates.push(m[1]);
5216
+ const subShellRe = /subprocess\.(?:run|call|Popen|check_output|check_call)\s*\(\s*(['"])(.*?)\1[^)]*?shell\s*=\s*true/gi;
5217
+ while ((m = subShellRe.exec(code)) !== null) candidates.push(m[2]);
5218
+ for (const cand of candidates) {
5219
+ const tokens = cand.includes(",") ? cand.split(/\s*,\s*/).map((t) => t.trim().replace(/^['"]|['"]$/g, "")) : cand.split(/\s+/);
5220
+ for (const tok of tokens) {
5221
+ if (looksLikeScriptPath(tok)) {
5222
+ try {
5223
+ const resolved = resolvePath2(tok.replace(/^['"]|['"]$/g, ""));
5224
+ if (existsSync9(resolved)) {
5225
+ const stat = statSync2(resolved);
5226
+ if (stat.isFile()) return resolved;
5227
+ }
5228
+ } catch {
5229
+ }
5230
+ }
5231
+ }
5232
+ }
5233
+ const openRe = /open\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
5234
+ while ((m = openRe.exec(code)) !== null) {
5235
+ const p = m[1];
5236
+ try {
5237
+ const resolved = resolvePath2(p);
5238
+ if (existsSync9(resolved)) {
5239
+ const stat = statSync2(resolved);
5240
+ if (stat.isFile()) return resolved;
5241
+ }
5242
+ } catch {
5243
+ }
5244
+ }
5245
+ return null;
5246
+ }
4984
5247
  const re = /open\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
4985
- let m;
4986
- while ((m = re.exec(actionDetail)) !== null) {
4987
- const p = m[1];
5248
+ let m2;
5249
+ while ((m2 = re.exec(actionDetail)) !== null) {
5250
+ const p = m2[1];
4988
5251
  let resolved;
4989
5252
  try {
4990
5253
  resolved = resolvePath2(p);
@@ -5170,6 +5433,7 @@ var init_script_content_analyzer = __esm({
5170
5433
  init_esm_shims();
5171
5434
  init_heuristic();
5172
5435
  init_logic_rules();
5436
+ init_inline_analyzer();
5173
5437
  init_tracker();
5174
5438
  SUSPICIOUS_PATTERNS2 = [
5175
5439
  { name: "curl POST", pattern: /curl\s+(.*\s)?-X\s+POST/i },
@@ -5634,7 +5898,7 @@ async function analyzeWithLLM(prompt, config) {
5634
5898
  riskType: riskType || "",
5635
5899
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
5636
5900
  source: "llm",
5637
- violatedLayers: ["3"]
5901
+ violatedLayers: ["L3.1"]
5638
5902
  };
5639
5903
  } catch (err) {
5640
5904
  if (abortController.signal.aborted) {
@@ -6220,13 +6484,6 @@ var init_brain = __esm({
6220
6484
  });
6221
6485
 
6222
6486
  // src/engine/formal-engine/decode.ts
6223
- var decode_exports = {};
6224
- __export(decode_exports, {
6225
- allCandidates: () => allCandidates,
6226
- decodeBase64Literals: () => decodeBase64Literals,
6227
- decodeUrlAndNumeric: () => decodeUrlAndNumeric,
6228
- matchStageAny: () => matchStageAny
6229
- });
6230
6487
  function isValidBase64Token(tok) {
6231
6488
  if (tok.length < 8) return false;
6232
6489
  if (!(tok.length % 4 === 0 || tok.endsWith("==") || tok.endsWith("="))) return false;
@@ -6259,6 +6516,13 @@ function decodeBase64Literals(text) {
6259
6516
  }
6260
6517
  return out;
6261
6518
  }
6519
+ function stripStringLiterals(text) {
6520
+ const m = text.match(/\s*([A-Za-z_][\w-]*|\/?[\w./-]+)\b/);
6521
+ if (!m) return text;
6522
+ const cmd = m[1].split("/").pop().toLowerCase();
6523
+ if (!OUTPUT_CMDS.has(cmd)) return text;
6524
+ return text.replace(QUOTED_STR_RE, "");
6525
+ }
6262
6526
  function decodeUrlAndNumeric(text) {
6263
6527
  const out = [];
6264
6528
  let s = text;
@@ -6292,7 +6556,10 @@ function decodeUrlAndNumeric(text) {
6292
6556
  return out;
6293
6557
  }
6294
6558
  function allCandidates(detail, max = 8) {
6295
- const cands = [detail];
6559
+ const shell = stripStringLiterals(detail);
6560
+ const cands = [shell];
6561
+ cands.push(...decodeBase64Literals(shell));
6562
+ cands.push(...decodeUrlAndNumeric(shell));
6296
6563
  cands.push(...decodeBase64Literals(detail));
6297
6564
  cands.push(...decodeUrlAndNumeric(detail));
6298
6565
  const seen = /* @__PURE__ */ new Set();
@@ -6306,7 +6573,31 @@ function allCandidates(detail, max = 8) {
6306
6573
  return out;
6307
6574
  }
6308
6575
  function matchStageAny(detail, stages) {
6309
- for (const cand of allCandidates(detail)) {
6576
+ const cands = allCandidates(detail);
6577
+ if (cands.length === 0) return "";
6578
+ const shell = cands[0];
6579
+ const hits = [];
6580
+ for (const [name, pats] of Object.entries(stages)) {
6581
+ for (const p of pats) {
6582
+ p.lastIndex = 0;
6583
+ if (p.test(shell)) {
6584
+ hits.push(name);
6585
+ break;
6586
+ }
6587
+ }
6588
+ }
6589
+ if (hits.length > 0) {
6590
+ if (hits.length === 1) return hits[0];
6591
+ if (hits.includes("EXEC") && PURPOSE_EXEC_RE.test(shell)) return "EXEC";
6592
+ if (hits.includes("LISTEN") && LISTEN_LEAD_RE.test(shell) && !LISTEN_NOISE_RE.test(shell)) {
6593
+ return "LISTEN";
6594
+ }
6595
+ for (const s of STAGE_PRIORITY) {
6596
+ if (hits.includes(s)) return s;
6597
+ }
6598
+ if (hits.includes("EXEC")) return "EXEC";
6599
+ }
6600
+ for (const cand of cands.slice(1)) {
6310
6601
  for (const [name, pats] of Object.entries(stages)) {
6311
6602
  for (const p of pats) {
6312
6603
  p.lastIndex = 0;
@@ -6316,12 +6607,18 @@ function matchStageAny(detail, stages) {
6316
6607
  }
6317
6608
  return "";
6318
6609
  }
6319
- var B64_TOKEN_RE;
6610
+ var B64_TOKEN_RE, QUOTED_STR_RE, OUTPUT_CMDS, STAGE_PRIORITY, PURPOSE_EXEC_RE, LISTEN_LEAD_RE, LISTEN_NOISE_RE;
6320
6611
  var init_decode = __esm({
6321
6612
  "src/engine/formal-engine/decode.ts"() {
6322
6613
  "use strict";
6323
6614
  init_esm_shims();
6324
6615
  B64_TOKEN_RE = /[A-Za-z0-9+/]+={0,2}/g;
6616
+ QUOTED_STR_RE = /('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`)/g;
6617
+ OUTPUT_CMDS = /* @__PURE__ */ new Set(["echo", "printf", "print"]);
6618
+ STAGE_PRIORITY = ["EXFIL", "PRIVESC", "PERSIST", "SCAN", "LISTEN"];
6619
+ PURPOSE_EXEC_RE = /subprocess\.\w+\s*\(|os\.system\s*\(|shell\s*=\s*True|\beval\s*\(|\bexec\s*\(|os\.popen\s*\(|\/exec\b/i;
6620
+ LISTEN_LEAD_RE = /http\.server|flask\s+run|nohup\b|nc\s+-l|socat\s+TCP-LISTEN/i;
6621
+ LISTEN_NOISE_RE = /shell\s*=\s*True|os\.system|subprocess\.(?:run|call|Popen)|\beval\s*\(|\bexec\s*\(/i;
6325
6622
  }
6326
6623
  });
6327
6624
 
@@ -6357,11 +6654,7 @@ function compileAps(apPatterns) {
6357
6654
  }
6358
6655
  function labelEvent(actionDetail) {
6359
6656
  if (!AP_RE) compileAps(loadMC().ap_patterns || {});
6360
- let candidates = [actionDetail];
6361
- try {
6362
- candidates = (init_decode(), __toCommonJS(decode_exports)).allCandidates(actionDetail);
6363
- } catch {
6364
- }
6657
+ const candidates = allCandidates(actionDetail);
6365
6658
  const hits = /* @__PURE__ */ new Set();
6366
6659
  for (const [name, pats] of Object.entries(AP_RE || {})) {
6367
6660
  for (const c of candidates) {
@@ -6457,6 +6750,7 @@ var init_ltl = __esm({
6457
6750
  init_esm_shims();
6458
6751
  init_rules_loader();
6459
6752
  init_ir();
6753
+ init_decode();
6460
6754
  AP_RE = null;
6461
6755
  }
6462
6756
  });
@@ -6703,7 +6997,7 @@ function checkSensitiveDataFlow(graph) {
6703
6997
  const n = nodes[i];
6704
6998
  if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
6705
6999
  violations.push({
6706
- propertyId: "sensitive-data-exfil",
7000
+ propertyId: "sensitive-data-flow",
6707
7001
  reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${tainted.get(i)} \u2192 ${n.sessionId}#${n.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
6708
7002
  riskLevel: "critical",
6709
7003
  path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
@@ -6713,6 +7007,62 @@ function checkSensitiveDataFlow(graph) {
6713
7007
  }
6714
7008
  return violations;
6715
7009
  }
7010
+ function extractInlineShell(s) {
7011
+ const out = [];
7012
+ let m;
7013
+ INLINE_SHELL_RE.lastIndex = 0;
7014
+ while ((m = INLINE_SHELL_RE.exec(s)) !== null) {
7015
+ let g = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
7016
+ if (/^["']/.test(g) && (/"\s*,/.test(g) || /'\s*,/.test(g))) {
7017
+ g = g.replace(/['"\[\],]+/g, " ");
7018
+ }
7019
+ g = g.trim();
7020
+ if (g) out.push(g);
7021
+ }
7022
+ return out;
7023
+ }
7024
+ function containsPrivesc(shell) {
7025
+ if (!shell) return false;
7026
+ for (const seg of shell.split(/\&\&|\|\||;|\n/)) {
7027
+ const s = seg.trim();
7028
+ if (!s) continue;
7029
+ const toks = s.split(/\s+/);
7030
+ const head = (toks[0] || "").toLowerCase();
7031
+ const rest = toks.slice(1).join(" ");
7032
+ if (head === "chmod" && /(?:u\+s|g\+s|[2-7][0-7]{3})/.test(rest)) return true;
7033
+ if (head === "setcap" && /cap_/.test(rest)) return true;
7034
+ if (head === "pkexec" || head === "doas") return true;
7035
+ if (head === "sudo") {
7036
+ if (/^-u\s+/.test(rest)) {
7037
+ if (/^-u\s+root\b/.test(rest)) return true;
7038
+ continue;
7039
+ }
7040
+ const m = /^(\S+)(.*)$/.exec(rest);
7041
+ if (m) {
7042
+ const nxt = m[1].toLowerCase();
7043
+ const tail = m[2];
7044
+ if (PKG_MGR.has(nxt)) continue;
7045
+ if (SHELLS.has(nxt)) return true;
7046
+ if (nxt === "-i" || nxt === "-s") return true;
7047
+ if (nxt.startsWith("su")) return true;
7048
+ if (nxt.startsWith("/") && /bin\/(?:bash|sh|zsh|dash|fish)$/.test(nxt)) return true;
7049
+ }
7050
+ if (/\/(?:usr\/(?:local\/)?bin|bin)\/(?:bash|sh|zsh|dash|fish)\b/.test(rest)) return true;
7051
+ if (/\bchmod\s+(?:u\+s|g\+s|[2-7][0-7]{3})\b/.test(rest)) return true;
7052
+ if (/\bsetcap\s+cap_\w+/.test(rest)) return true;
7053
+ if (/\bvisudo\b/.test(rest)) return true;
7054
+ if (/\bpasswd\s+root\b/.test(rest)) return true;
7055
+ if (/\buseradd\b/.test(rest)) return true;
7056
+ }
7057
+ if (head === "su") {
7058
+ if (!rest) return true;
7059
+ if (/^-l?\s*$/.test(rest)) return true;
7060
+ if (/^root\b/.test(rest) || /^-l?\s+root\b/.test(rest)) return true;
7061
+ }
7062
+ }
7063
+ if (/\bset(?:uid|euid|gid|egid|reuid|regid|resuid|resgid)\s*\(/i.test(shell)) return true;
7064
+ return false;
7065
+ }
6716
7066
  function isExfilNode(n) {
6717
7067
  if (n.stage === "EXFIL") return true;
6718
7068
  if (EXFIL_RE.test(n.actionDetail) && EXFIL_ACTION_RE.test(n.actionDetail)) return true;
@@ -6727,8 +7077,12 @@ function isCredNode(n) {
6727
7077
  return CRED_STRICT_RE.test(n.actionDetail);
6728
7078
  }
6729
7079
  function isPrivescNode(n) {
6730
- if (n.stage === "PRIVESC") return true;
6731
- return PRIVESC_RE.test(n.actionDetail);
7080
+ const shell = stripStringLiterals(n.actionDetail);
7081
+ if (containsPrivesc(shell)) return true;
7082
+ for (const cmd of extractInlineShell(shell)) {
7083
+ if (containsPrivesc(cmd)) return true;
7084
+ }
7085
+ return false;
6732
7086
  }
6733
7087
  function checkNeverExfilAfterPrivesc(graph) {
6734
7088
  const violations = [];
@@ -6749,7 +7103,7 @@ function checkNeverExfilAfterPrivesc(graph) {
6749
7103
  if (pre) {
6750
7104
  const post = nodes[firstEv];
6751
7105
  return [{
6752
- propertyId: "no-privesc-then-exfil",
7106
+ propertyId: "no-exfil-after-privesc",
6753
7107
  reason: "\u63D0\u6743\u540E\u51FA\u73B0\u5916\u4F20\uFF1A\u63D0\u6743(\u524D) \u2192 \u5916\u4F20(\u540E)\uFF0C\u8FDD\u53CD \u25A1(PRIVESC\u2192\xAC\u25C7EXFIL)",
6754
7108
  riskLevel: "critical",
6755
7109
  path: [
@@ -6765,7 +7119,7 @@ function checkNeverExfilAfterPrivesc(graph) {
6765
7119
  for (let j = i + 1; j < nodes.length; j++) {
6766
7120
  if (isExfilNode(nodes[j])) {
6767
7121
  violations.push({
6768
- propertyId: "no-privesc-then-exfil",
7122
+ propertyId: "no-exfil-after-privesc",
6769
7123
  reason: "\u63D0\u6743\u540E\u51FA\u73B0\u5916\u4F20\uFF1A\u63D0\u6743(\u524D) \u2192 \u5916\u4F20(\u540E)\uFF0C\u8FDD\u53CD \u25A1(PRIVESC\u2192\xAC\u25C7EXFIL)",
6770
7124
  riskLevel: "critical",
6771
7125
  path: [
@@ -6798,7 +7152,7 @@ function checkNeverExfilAfterCred(graph) {
6798
7152
  if (pre) {
6799
7153
  const post = nodes[firstEv];
6800
7154
  return [{
6801
- propertyId: "no-cred-then-exfil",
7155
+ propertyId: "no-exfil-after-credential",
6802
7156
  reason: "\u8BFB\u53D6\u51ED\u636E\u540E\u51FA\u73B0\u5916\u4F20\uFF0C\u8FDD\u53CD \u25A1(CRED\u2192\xAC\u25C7EXFIL)",
6803
7157
  riskLevel: "critical",
6804
7158
  path: [
@@ -6814,7 +7168,7 @@ function checkNeverExfilAfterCred(graph) {
6814
7168
  for (let j = i + 1; j < nodes.length; j++) {
6815
7169
  if (isExfilNode(nodes[j])) {
6816
7170
  violations.push({
6817
- propertyId: "no-cred-then-exfil",
7171
+ propertyId: "no-exfil-after-credential",
6818
7172
  reason: "\u8BFB\u53D6\u51ED\u636E\u540E\u51FA\u73B0\u5916\u4F20\uFF0C\u8FDD\u53CD \u25A1(CRED\u2192\xAC\u25C7EXFIL)",
6819
7173
  riskLevel: "critical",
6820
7174
  path: [
@@ -6881,7 +7235,7 @@ function checkLowIntegrityCommandExec(graph) {
6881
7235
  const n = nodes[i];
6882
7236
  if (lowAt.has(i) && isComputeNode(n)) {
6883
7237
  violations.push({
6884
- propertyId: "low-integrity-controls-command-exec",
7238
+ propertyId: "low-integrity-command-exec",
6885
7239
  reason: `\u4F4E\u5B8C\u6574\u6027\u6570\u636E\u63A7\u5236\u547D\u4EE4\u6267\u884C\uFF1A${lowAt.get(i)} \u2192 ${n.sessionId}#${n.seq} \u6267\u884C\u4E0D\u53EF\u4FE1\u5185\u5BB9`,
6886
7240
  riskLevel: "critical",
6887
7241
  path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
@@ -6927,7 +7281,7 @@ function checkCrossSessionReachability(graph) {
6927
7281
  const srcIdx = Math.min(...sources);
6928
7282
  const srcDesc = nodes[srcIdx].actionDetail.slice(0, 40);
6929
7283
  violations.push({
6930
- propertyId: "cross-session-reachability",
7284
+ propertyId: "low-integrity-reachability",
6931
7285
  reason: `\u4F4E\u5B8C\u6574\u6027\u6E90\u7ECF\u8DE8 session \u6570\u636E\u94FE\u591A\u8DF3\u5230\u8FBE\u5F53\u524D\u6267\u884C\uFF08\u6E90: ${srcDesc} \u2192 ${n.sessionId}#${n.seq}\uFF09`,
6932
7286
  riskLevel: "high",
6933
7287
  path: nodes.slice(-3).map((m) => ({ sessionId: m.sessionId, seq: m.seq, detail: m.actionDetail.slice(0, 60) }))
@@ -6978,7 +7332,7 @@ function checkSensitiveDataAggregation(graph) {
6978
7332
  }
6979
7333
  return violations;
6980
7334
  }
6981
- var CROSS_SESSION_CHAINS, CRED_FILE_RE, EXFIL_RE, EBPF_NET_RE, REMOTE_RE, PRIVESC_RE, EXFIL_ACTION_RE, CRED_STRICT_RE, TOOL_REGISTRY, COMPUTE_RE, STRONG_COMPUTE_TYPES, DL_OR_LOW_RE, ALL_PROPERTIES;
7335
+ var CROSS_SESSION_CHAINS, CRED_FILE_RE, EXFIL_RE, EBPF_NET_RE, REMOTE_RE, EXFIL_ACTION_RE, CRED_STRICT_RE, INLINE_SHELL_RE, PKG_MGR, SHELLS, TOOL_REGISTRY, COMPUTE_RE, STRONG_COMPUTE_TYPES, DL_OR_LOW_RE, ALL_PROPERTIES;
6982
7336
  var init_properties = __esm({
6983
7337
  "src/engine/formal-engine/properties.ts"() {
6984
7338
  "use strict";
@@ -6986,30 +7340,33 @@ var init_properties = __esm({
6986
7340
  init_rules_loader();
6987
7341
  init_ir();
6988
7342
  init_ltl();
7343
+ init_decode();
6989
7344
  CROSS_SESSION_CHAINS = null;
6990
7345
  CRED_FILE_RE = /(known_hosts|\.ssh|id_rsa|\/etc\/shadow|\/etc\/passwd|credentials)/i;
6991
- EXFIL_RE = /https?:\/\/|scp\s+.*@|rsync\s+.*@/i;
7346
+ EXFIL_RE = /https?:\/\/(?!127\.\d+\.\d+\.\d+|0\.0\.0\.0|localhost|\[?::1\]?)[^\s]+|\bscp\s+.*@|\brsync\s+.*@/i;
6992
7347
  EBPF_NET_RE = /connect|sendto|network_connect/i;
6993
7348
  REMOTE_RE = /to\s+\d+\.\d+\.\d+\.\d+|remote_addr/i;
6994
- PRIVESC_RE = /\bsudo\b|\bsu\b|chmod\s+u\s*\+\s*s|pkexec|setuid/i;
6995
7349
  EXFIL_ACTION_RE = /\bcurl\b|\bwget\b|\bscp\b|\brsync\b|\brclone\b|\bftp\b|connect\s+.*\d+\.\d+\.\d+\.\d+|open\s+.*tcp|base64\s+.*\-o/i;
6996
7350
  CRED_STRICT_RE = /(known_hosts|\.ssh|id_rsa|\.pem|\b\.key\b|\/etc\/shadow|\/etc\/passwd|\.aws\/credentials|credentials\.json|netrc|password|secrets?\.ya?ml|credentials?\s*(file|path)?\s*(=|\:)|\bconfig.*(password|token))/i;
7351
+ INLINE_SHELL_RE = /(?:os\.system|os\.popen)\s*\(\s*['"]([^'"]+)['"]|subprocess\.[a-z_]+\(\s*\[([^\]]+)\]|subprocess\.[a-z_]+\(\s*['"]([^'"]+)['"]|(?:bash|sh|zsh)\s+-c\s*['"]([^'"]+)['"]/gi;
7352
+ PKG_MGR = /* @__PURE__ */ new Set(["apt", "apt-get", "aptitude", "yum", "dnf", "pacman", "zypper", "apk", "brew"]);
7353
+ SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "fish", "ksh", "csh", "tcsh", "nologin"]);
6997
7354
  TOOL_REGISTRY = null;
6998
7355
  COMPUTE_RE = /subprocess|os\.system|shell\s*=\s*True|\beval\(|\bexec\(|bash\s+[^\s]|sh\s+-c|python3?\s+|node\s+|\btimeout\s+|nohup\s+|\b\.\/[\w./-]+/i;
6999
7356
  STRONG_COMPUTE_TYPES = /* @__PURE__ */ new Set(["python", "node", "exec", "terminal", "cmd"]);
7000
7357
  DL_OR_LOW_RE = /curl\s+.*-o\b|wget\s+.*\s+-O\b|curl\s+.*\|\s*(bash|sh|python)|wget\s+.*\|\s*(bash|sh|python)|git\s+clone|docker\s+pull|pip\s+install|npm\s+install|base64\s+-d/i;
7001
7358
  ALL_PROPERTIES = [
7002
- { id: "cross-session-chain", check: checkCrossSessionChain },
7003
- { id: "sensitive-data-flow", check: checkSensitiveDataFlow },
7004
- { id: "cross-session-reachability", check: checkCrossSessionReachability },
7005
- { id: "no-privesc-then-exfil", check: checkNeverExfilAfterPrivesc },
7006
- { id: "no-cred-then-exfil", check: checkNeverExfilAfterCred },
7359
+ { id: "cross-session-chain", check: checkCrossSessionChain, shortName: "P1" },
7360
+ { id: "sensitive-data-flow", check: checkSensitiveDataFlow, shortName: "P2" },
7361
+ { id: "low-integrity-reachability", check: checkCrossSessionReachability, shortName: "P3" },
7362
+ { id: "no-exfil-after-privesc", check: checkNeverExfilAfterPrivesc, shortName: "P4" },
7363
+ { id: "no-exfil-after-credential", check: checkNeverExfilAfterCred, shortName: "P5" },
7007
7364
  // L2.2 模型检测(通用 LTL 求值器,mc_rules.json 单一事实源;witness 语义见 ltl.ts)
7008
- { id: "mc-ltl-rules", check: checkMCRules },
7009
- // Rule 5 低完整性控制命令执行(integrity 维度,站点 L2.1 最有区分度的检测之一)
7010
- { id: "low-integrity-command-exec", check: checkLowIntegrityCommandExec },
7011
- // Rule 12 敏感数据聚合外传(egress 聚合 ≥2 事件源)
7012
- { id: "sensitive-data-aggregation", check: checkSensitiveDataAggregation }
7365
+ { id: "mc-ltl-rules", check: checkMCRules, shortName: "P6" },
7366
+ // P7 低完整性控制命令执行(integrity 维度,站点 L2.1 最有区分度的检测之一)
7367
+ { id: "low-integrity-command-exec", check: checkLowIntegrityCommandExec, shortName: "P7" },
7368
+ // P8 敏感数据聚合外传(egress 聚合 ≥2 事件源)
7369
+ { id: "sensitive-data-aggregation", check: checkSensitiveDataAggregation, shortName: "P8" }
7013
7370
  ];
7014
7371
  }
7015
7372
  });
@@ -7082,6 +7439,32 @@ var init_engine = __esm({
7082
7439
  });
7083
7440
 
7084
7441
  // src/engine/formal-engine/stateful.ts
7442
+ function extractInstances(actionDetail, stage) {
7443
+ const out = [];
7444
+ if (PORT_STAGE_RE.test(stage)) {
7445
+ const d = actionDetail;
7446
+ let m;
7447
+ const re = new RegExp(PORT_RE.source, "gi");
7448
+ while ((m = re.exec(d)) !== null) {
7449
+ const p = m[1] || m[2] || m[3];
7450
+ if (p && Number(p) > 0 && Number(p) <= 65535) out.push(`port:${p}`);
7451
+ }
7452
+ if (/SCAN/.test(stage)) {
7453
+ const bareRe = new RegExp(BARE_PORT_RE.source, "g");
7454
+ while ((m = bareRe.exec(d)) !== null) {
7455
+ if (m[1] && Number(m[1]) > 0 && Number(m[1]) <= 65535) out.push(`port:${m[1]}`);
7456
+ }
7457
+ }
7458
+ m = SOCK_RE.exec(d);
7459
+ if (m && m[1]) out.push(`sock:${m[1]}`);
7460
+ m = UNIT_RE.exec(d);
7461
+ if (m && m[1]) out.push(`unit:${m[1]}`);
7462
+ }
7463
+ return out;
7464
+ }
7465
+ function buckKey(chainId, inst) {
7466
+ return inst ? `${chainId}@@${inst}` : chainId;
7467
+ }
7085
7468
  function loadRules() {
7086
7469
  if (STAGES_PATTERNS !== null && CHAINS !== null) {
7087
7470
  return { stages: STAGES_PATTERNS, chains: CHAINS };
@@ -7121,7 +7504,7 @@ function lifecycleFor(detail, stage, isError) {
7121
7504
  if (detail.includes(">>") || detail.includes(">")) return "alive";
7122
7505
  return "";
7123
7506
  }
7124
- var DESTROY_RE, NEVER_ACTIVATED_RE, CRED_FILE_RE2, EXFIL_RE2, STAGES_PATTERNS, CHAINS, MAX_NODES, StatefulChainTracker;
7507
+ var DESTROY_RE, NEVER_ACTIVATED_RE, CRED_FILE_RE2, EXFIL_RE2, PORT_RE, BARE_PORT_RE, SOCK_RE, UNIT_RE, PORT_STAGE_RE, STAGES_PATTERNS, CHAINS, MAX_NODES, StatefulChainTracker;
7125
7508
  var init_stateful = __esm({
7126
7509
  "src/engine/formal-engine/stateful.ts"() {
7127
7510
  "use strict";
@@ -7131,11 +7514,20 @@ var init_stateful = __esm({
7131
7514
  DESTROY_RE = /\b(?:pkill|kill)\b|systemctl\s+stop|service\s+stop|crontab\s+-r|rm\s+-[rf]?\s+/i;
7132
7515
  NEVER_ACTIVATED_RE = /\b(?:not\s+found|no\s+such\s+file|command\s+not\s+found|refused|failed\s+to\s+(?:start|bind|listen)|address\s+already\s+in\s+use)\b/i;
7133
7516
  CRED_FILE_RE2 = /(known_hosts|\.ssh|id_rsa|\/etc\/shadow|\/etc\/passwd|credentials)/i;
7134
- EXFIL_RE2 = /https?:\/\/|scp\s+.*@|rsync\s+.*@/i;
7517
+ EXFIL_RE2 = /https?:\/\/(?!127\.\d+\.\d+\.\d+|0\.0\.0\.0|localhost|\[?::1\]?)|scp\s+.*@|rsync\s+.*@/i;
7518
+ PORT_RE = /(?:-p\s+(\d{1,5})|-p(\d{1,5})|:(\d{1,5})\b)/i;
7519
+ BARE_PORT_RE = /(?<=^|\s)(\d{1,5})(?=\s|$)/g;
7520
+ SOCK_RE = /([\w./-]*(?:\/tmp\/|\/var\/run\/|\.sock\b)[\w./-]*)/;
7521
+ UNIT_RE = /systemctl\s+(?:start|enable|restart)\s+([\w.-]+)/i;
7522
+ PORT_STAGE_RE = /LISTEN|PERSIST|SCAN/;
7135
7523
  STAGES_PATTERNS = null;
7136
7524
  CHAINS = null;
7137
7525
  MAX_NODES = 1e5;
7138
- StatefulChainTracker = class {
7526
+ StatefulChainTracker = class _StatefulChainTracker {
7527
+ /** 本地接收时刻(实时路径无 called_at 时的兜底时间戳,ISO)。 */
7528
+ static now() {
7529
+ return (/* @__PURE__ */ new Date()).toISOString();
7530
+ }
7139
7531
  agentId;
7140
7532
  nodes_ = [];
7141
7533
  ptr = {};
@@ -7157,10 +7549,14 @@ var init_stateful = __esm({
7157
7549
  for (const chain of chains) {
7158
7550
  const stages = chain.stages || [];
7159
7551
  if (stages.length === 0) continue;
7160
- const matched = this.ptr[String(chain.id || "")] || 0;
7552
+ const cid = String(chain.id || "");
7553
+ let matched = this.ptr[cid] || 0;
7554
+ for (const [k, v] of Object.entries(this.ptr)) {
7555
+ if (k.startsWith(`${cid}@@`) && v > matched && v < stages.length) matched = v;
7556
+ }
7161
7557
  if (matched > 0 && matched < stages.length) {
7162
7558
  const pf = {
7163
- chainId: String(chain.id || ""),
7559
+ chainId: cid,
7164
7560
  stages,
7165
7561
  matched,
7166
7562
  riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
@@ -7236,8 +7632,10 @@ var init_stateful = __esm({
7236
7632
  actionDetail: ad,
7237
7633
  stage,
7238
7634
  resources: extractResources2(ad),
7635
+ instances: extractInstances(ad, stage),
7239
7636
  isError,
7240
- lifecycle: lifecycleFor(ad, stage, isError)
7637
+ lifecycle: lifecycleFor(ad, stage, isError),
7638
+ timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
7241
7639
  };
7242
7640
  this.nodes_.push(node);
7243
7641
  this.seqNext += 1;
@@ -7245,11 +7643,19 @@ var init_stateful = __esm({
7245
7643
  }
7246
7644
  recordLifecycle(node) {
7247
7645
  if (node.lifecycle === "dead") {
7248
- let hit = false;
7646
+ const clearedInsts = [];
7647
+ for (const inst of node.instances) {
7648
+ const key = `inst:${inst}`;
7649
+ if (key in this.aliveResources) {
7650
+ delete this.aliveResources[key];
7651
+ clearedInsts.push(inst);
7652
+ }
7653
+ }
7654
+ let pathHit = false;
7249
7655
  for (const r of node.resources) {
7250
7656
  if (r in this.aliveResources) {
7251
7657
  delete this.aliveResources[r];
7252
- hit = true;
7658
+ pathHit = true;
7253
7659
  }
7254
7660
  }
7255
7661
  const detail = node.actionDetail;
@@ -7257,16 +7663,30 @@ var init_stateful = __esm({
7257
7663
  const base = r.split("/").pop() || "";
7258
7664
  if (base && detail.includes(base)) {
7259
7665
  delete this.aliveResources[r];
7260
- hit = true;
7666
+ pathHit = true;
7261
7667
  }
7262
7668
  }
7263
- if (hit) this.resetChainsOnDestroy();
7669
+ if (clearedInsts.length > 0 && !pathHit) {
7670
+ for (const inst of clearedInsts) this.resetChainsForInstance(inst);
7671
+ } else {
7672
+ this.resetChainsOnDestroy();
7673
+ }
7264
7674
  return;
7265
7675
  }
7266
7676
  if (node.lifecycle === "alive") {
7267
7677
  for (const r of node.resources) {
7268
7678
  this.aliveResources[r] = node.seq;
7269
7679
  }
7680
+ for (const inst of node.instances) {
7681
+ this.aliveResources[`inst:${inst}`] = node.seq;
7682
+ }
7683
+ }
7684
+ }
7685
+ /** 只重置指定实例在全部链上的进度(不影响其他端口/资源)。 */
7686
+ resetChainsForInstance(inst) {
7687
+ const key = buckKey("", inst).slice(2);
7688
+ for (const k of Object.keys(this.ptr)) {
7689
+ if (k.endsWith(`@@${inst}`)) this.ptr[k] = 0;
7270
7690
  }
7271
7691
  }
7272
7692
  resetChainsOnDestroy() {
@@ -7274,6 +7694,29 @@ var init_stateful = __esm({
7274
7694
  this.ptr[cid] = 0;
7275
7695
  }
7276
7696
  }
7697
+ /**
7698
+ * 当前节点应推进的链桶集合。
7699
+ * - 有实例节点(LISTEN/PERSIST 带端口/socket/unit)→ 只推自己的实例桶;
7700
+ * 同时为所有链的该实例建立桶(若首次出现)。
7701
+ * - 无实例节点(EXEC/SCAN 等中间段)→ 推全局桶 + 所有当前活跃实例桶
7702
+ * (信息不足时保守推进一切可能,避免漏判)。
7703
+ */
7704
+ targetsForNode(node, chainId) {
7705
+ const insts = node.instances;
7706
+ if (insts.length > 0) {
7707
+ const active = insts.filter((i) => this.aliveResources[`inst:${i}`] !== void 0);
7708
+ if (active.length > 0) return active.map((i) => buckKey(chainId, i));
7709
+ return [chainId];
7710
+ }
7711
+ const targets = [chainId];
7712
+ for (const k of Object.keys(this.aliveResources)) {
7713
+ if (k.startsWith("inst:")) {
7714
+ const inst = k.slice("inst:".length);
7715
+ targets.push(`${chainId}@@${inst}`);
7716
+ }
7717
+ }
7718
+ return targets;
7719
+ }
7277
7720
  advanceChains(node) {
7278
7721
  if (node.lifecycle === "dead") return null;
7279
7722
  if (node.lifecycle === "never_activated") {
@@ -7287,23 +7730,32 @@ var init_stateful = __esm({
7287
7730
  const stages = chain.stages || [];
7288
7731
  if (stages.length === 0) continue;
7289
7732
  const cid = String(chain.id || "");
7290
- const cur = this.ptr[cid] || 0;
7291
- if (cur < stages.length && stages[cur] === stage) {
7292
- this.ptr[cid] = cur + 1;
7293
- if (cur + 1 >= stages.length) {
7294
- return {
7295
- chainId: cid,
7296
- stages,
7297
- riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
7298
- reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
7299
- path: this.chainPath(stages)
7300
- };
7733
+ const targets = this.targetsForNode(node, cid);
7734
+ for (const key of targets) {
7735
+ const cur = this.ptr[key] || 0;
7736
+ if (cur < stages.length && stages[cur] === stage) {
7737
+ this.ptr[key] = cur + 1;
7738
+ if (cur + 1 >= stages.length) {
7739
+ return {
7740
+ chainId: cid,
7741
+ stages,
7742
+ riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
7743
+ reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
7744
+ path: this.chainPath(stages)
7745
+ };
7746
+ }
7301
7747
  }
7302
7748
  }
7303
7749
  }
7304
7750
  return null;
7305
7751
  }
7306
7752
  invalidateChainOnFailure(node) {
7753
+ if (node.instances.length > 0) {
7754
+ for (const inst of node.instances) {
7755
+ this.resetChainsForInstance(inst);
7756
+ }
7757
+ return;
7758
+ }
7307
7759
  const failed = new Set(node.resources);
7308
7760
  if (failed.size === 0) return;
7309
7761
  const { chains } = loadRules();
@@ -7319,7 +7771,7 @@ var init_stateful = __esm({
7319
7771
  let want = stages[0];
7320
7772
  for (const n of this.nodes_) {
7321
7773
  if (n.stage === want) {
7322
- path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage });
7774
+ path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage, timestamp: n.timestamp });
7323
7775
  const idx = stages.indexOf(n.stage);
7324
7776
  if (idx + 1 < stages.length) {
7325
7777
  want = stages[idx + 1];
@@ -7355,7 +7807,7 @@ var init_stateful = __esm({
7355
7807
  if (!this.tainted.has(node.seq)) return [];
7356
7808
  if (EXFIL_RE2.test(node.actionDetail)) {
7357
7809
  return [{
7358
- property_id: "sensitive-data-exfil",
7810
+ property_id: "sensitive-data-flow",
7359
7811
  reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${this.taintSource(node)} \u2192 ${node.sessionId}#${node.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
7360
7812
  risk_level: "critical",
7361
7813
  path: [[node.sessionId, node.seq, node.actionDetail.slice(0, 60)]]
@@ -7397,8 +7849,12 @@ var init_stateful = __esm({
7397
7849
  actionDetail: String(raw.actionDetail || ""),
7398
7850
  stage: String(raw.stage || ""),
7399
7851
  resources: [...raw.resources || []],
7852
+ instances: [...raw.instances || []],
7853
+ // 旧快照无此字段 → 空
7400
7854
  isError: Boolean(raw.isError),
7401
- lifecycle: String(raw.lifecycle || "")
7855
+ lifecycle: String(raw.lifecycle || ""),
7856
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : null
7857
+ // 旧快照无此字段 → null
7402
7858
  });
7403
7859
  }
7404
7860
  this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
@@ -7591,7 +8047,7 @@ async function runAnalysis(input) {
7591
8047
  riskType: node.l1RiskType,
7592
8048
  confidence: 90,
7593
8049
  source: "heuristic_script_scan",
7594
- violatedLayers: ["1.1-script"]
8050
+ violatedLayers: ["L1.2"]
7595
8051
  };
7596
8052
  }
7597
8053
  }
@@ -7709,7 +8165,7 @@ async function runAnalysis(input) {
7709
8165
  riskType: inlineJudgment.riskType,
7710
8166
  confidence: 90,
7711
8167
  source: "logic_rule",
7712
- violatedLayers: ["1.2"]
8168
+ violatedLayers: ["L2.1"]
7713
8169
  };
7714
8170
  }
7715
8171
  return {
@@ -7742,7 +8198,7 @@ async function runAnalysis(input) {
7742
8198
  console.warn(
7743
8199
  `[AgentMoss] \u5F62\u5F0F\u5316\u9A8C\u8BC1 observe\uFF08\u53EA\u8BB0\u5F55\u4E0D\u62E6\u622A\uFF09: property=${v?.propertyId ?? "?"} risk=${v?.riskLevel ?? "?"} path_len=${v?.path?.length ?? 0} reason=${(v?.reason ?? "?").slice(0, 200)}`
7744
8200
  );
7745
- observedFormalLayers.push("2.4");
8201
+ observedFormalLayers.push("L2.2");
7746
8202
  } else {
7747
8203
  return {
7748
8204
  allowed: false,
@@ -7751,7 +8207,7 @@ async function runAnalysis(input) {
7751
8207
  riskType: "formal_verification",
7752
8208
  confidence: 90,
7753
8209
  source: "formal_engine",
7754
- violatedLayers: ["2.4"]
8210
+ violatedLayers: ["L2.2"]
7755
8211
  };
7756
8212
  }
7757
8213
  }
@@ -7768,7 +8224,7 @@ async function runAnalysis(input) {
7768
8224
  riskType: node.l2RiskType,
7769
8225
  confidence: 85,
7770
8226
  source: "logic_rule_script_scan",
7771
- violatedLayers: ["1.2-script"]
8227
+ violatedLayers: ["L2.3"]
7772
8228
  };
7773
8229
  }
7774
8230
  }
@@ -7855,7 +8311,7 @@ async function runAnalysis(input) {
7855
8311
  riskType: "",
7856
8312
  confidence: 60,
7857
8313
  source: "llm_failure_fallback",
7858
- violatedLayers: ["3"]
8314
+ violatedLayers: ["L3.1"]
7859
8315
  };
7860
8316
  }
7861
8317
  if (!llmResult.allowed && llmResult.confidence && llmResult.confidence >= 90 && _brainExtractor) {
@@ -8797,7 +9253,7 @@ var AnalyzeRequestSchema = z.object({
8797
9253
  session_history: z.array(z.object({
8798
9254
  session_id: z.string(),
8799
9255
  action_history: z.array(ActionItemSchema).default([])
8800
- })).default([]).describe("Multi-session historical actions (optional): for L2.4 formal engine cross-session combo-chain / sensitive-data-flow verification. Provided by caller (incl. future agentgate)."),
9256
+ })).default([]).describe("Multi-session historical actions (optional): for L2.2 formal engine cross-session combo-chain / sensitive-data-flow verification. Provided by caller (incl. future agentgate)."),
8801
9257
  a_next: ActionItemSchema.describe("Next action to be executed"),
8802
9258
  reason: z.string().default("").describe("Reason for executing this action"),
8803
9259
  os_type: z.string().default("").describe("OS type: 'linux', 'windows', or empty for auto-detect"),
@@ -8814,7 +9270,9 @@ var AnalyzeResponseSchema = z.object({
8814
9270
  policy: z.string().default(""),
8815
9271
  violated_policy: z.string().default(""),
8816
9272
  confidence: z.number().int().min(0).max(100).default(100),
8817
- analysis_duration_ms: z.number().default(0)
9273
+ analysis_duration_ms: z.number().default(0),
9274
+ // 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
9275
+ violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
8818
9276
  });
8819
9277
  var HealthResponseSchema = z.object({
8820
9278
  status: z.string().default("healthy"),
@@ -9198,7 +9656,7 @@ async function analyzeRoute(c) {
9198
9656
  const judgment = incMatchedToDeny(inc);
9199
9657
  if (judgment) {
9200
9658
  const layers = [...result.violatedLayers];
9201
- if (!layers.includes("2.4-stateful")) layers.push("2.4-stateful");
9659
+ if (!layers.includes("L2.2-stateful")) layers.push("L2.2-stateful");
9202
9660
  console.error(
9203
9661
  `[AgentMoss] stateful \u589E\u91CF\u62E6\u622A: agent=${agentId || "-"} property=${judgment.property_id} risk=${judgment.risk_level}`
9204
9662
  );
@@ -9212,7 +9670,8 @@ async function analyzeRoute(c) {
9212
9670
  policy: "",
9213
9671
  violated_policy: "",
9214
9672
  confidence: 90,
9215
- analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10
9673
+ analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
9674
+ violation_chain: judgment.violation_chain
9216
9675
  })
9217
9676
  );
9218
9677
  }
@@ -9251,17 +9710,20 @@ function getChainStateStoreFor(_agentId) {
9251
9710
  }
9252
9711
  function incMatchedToDeny(inc) {
9253
9712
  if (inc.matched) {
9713
+ const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp]);
9254
9714
  return {
9255
9715
  property_id: "cross-" + inc.matched.chainId,
9256
9716
  reason: inc.matched.reason,
9257
- risk_level: inc.matched.riskLevel
9717
+ risk_level: inc.matched.riskLevel,
9718
+ violation_chain: chain
9258
9719
  };
9259
9720
  }
9260
9721
  if (inc.exfil && inc.exfil.length > 0) {
9261
9722
  return {
9262
- property_id: "sensitive-data-exfil",
9723
+ property_id: "sensitive-data-flow",
9263
9724
  reason: inc.exfil[0].reason,
9264
- risk_level: inc.exfil[0].risk_level
9725
+ risk_level: inc.exfil[0].risk_level,
9726
+ violation_chain: []
9265
9727
  };
9266
9728
  }
9267
9729
  return null;
@@ -9387,7 +9849,7 @@ function createConsoleRouter() {
9387
9849
  const rc = updateLayerEnabled(body.layer_key, body.enabled);
9388
9850
  return c.json({ layers: rc.layers, env_overrides: getEnvOverrides() });
9389
9851
  });
9390
- function getL24View() {
9852
+ function getL22View() {
9391
9853
  const ltlHuman = {
9392
9854
  "mc-ltl-001": "\u5148\u8BFB\u53D6\u4E86\u51ED\u636E/\u5BC6\u94A5\uFF0C\u4E4B\u540E\u51FA\u73B0\u7F51\u7EDC\u5916\u4F20",
9393
9855
  "mc-ltl-002": "\u5148\u4E0B\u8F7D/\u83B7\u53D6\u4E86\u6587\u4EF6\uFF0C\u4E4B\u540E\u6267\u884C\u4E86\u6587\u4EF6",
@@ -9403,9 +9865,16 @@ function createConsoleRouter() {
9403
9865
  };
9404
9866
  const mc = loadMC();
9405
9867
  const rules = mc.rules || [];
9868
+ let catEnabled = true;
9869
+ try {
9870
+ const rt = loadRuntimeConfig();
9871
+ catEnabled = rt.L2_2?.ltl_rules?.category_enabled !== false;
9872
+ } catch {
9873
+ }
9406
9874
  return {
9407
9875
  enabled_via: "L2_logic_rules",
9408
9876
  description: "\u65F6\u5E8F\u4E0E\u6570\u636E\u6D41\u653B\u51FB\u68C0\u6D4B\uFF08L2.2 \u6A21\u578B\u68C0\u6D4B\uFF09",
9877
+ category_enabled: catEnabled,
9409
9878
  ltl_rules: rules.map((r) => ({
9410
9879
  id: r.id,
9411
9880
  type: r.type,
@@ -9418,10 +9887,10 @@ function createConsoleRouter() {
9418
9887
  }))
9419
9888
  };
9420
9889
  }
9421
- function withL24View(rc) {
9422
- if (rc && rc.L2_4 && typeof rc.L2_4 === "object" && !Array.isArray(rc.L2_4)) {
9423
- const cat = rc.L2_4.ltl_rules;
9424
- const rls = cat && cat.rules || [];
9890
+ function withL22View(rc) {
9891
+ const l22 = rc && rc.L2_2;
9892
+ if (l22 && typeof l22 === "object" && !Array.isArray(l22) && l22.ltl_rules && typeof l22.ltl_rules === "object" && !Array.isArray(l22.ltl_rules)) {
9893
+ const rls = l22.ltl_rules.rules || [];
9425
9894
  const ltlHumanLocal = {
9426
9895
  "mc-ltl-001": "\u5148\u8BFB\u53D6\u4E86\u51ED\u636E/\u5BC6\u94A5\uFF0C\u4E4B\u540E\u51FA\u73B0\u7F51\u7EDC\u5916\u4F20",
9427
9896
  "mc-ltl-002": "\u5148\u4E0B\u8F7D/\u83B7\u53D6\u4E86\u6587\u4EF6\uFF0C\u4E4B\u540E\u6267\u884C\u4E86\u6587\u4EF6",
@@ -9435,9 +9904,10 @@ function createConsoleRouter() {
9435
9904
  "mc-ltl-010": "\u5B89\u88C5\u4E86\u6301\u4E45\u5316\u673A\u5236\uFF08crontab/systemd/.bashrc \u7B49\uFF09\uFF0C\u9700\u5BA1\u6279",
9436
9905
  "mc-ctl-001": "\u4EFB\u4F55\u60C5\u51B5\u4E0B\u4E0D\u5141\u8BB8\u6E05\u9664\u65E5\u5FD7\uFF08\u5B89\u5168\u5E95\u7EBF\uFF09"
9437
9906
  };
9438
- rc.L2_4 = {
9907
+ rc.L2_2 = {
9439
9908
  enabled_via: "L2_logic_rules",
9440
9909
  description: "\u65F6\u5E8F\u4E0E\u6570\u636E\u6D41\u653B\u51FB\u68C0\u6D4B\uFF08L2.2 \u6A21\u578B\u68C0\u6D4B\uFF09",
9910
+ category_enabled: l22.ltl_rules.category_enabled !== false,
9441
9911
  ltl_rules: rls.map((r) => ({
9442
9912
  id: r.id,
9443
9913
  type: "ltl",
@@ -9454,27 +9924,27 @@ function createConsoleRouter() {
9454
9924
  }
9455
9925
  console2.get("/api/rules", (c) => {
9456
9926
  const rc = loadRuntimeConfig();
9457
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules, L2_4: getL24View() });
9927
+ return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules, L2_2: getL22View() });
9458
9928
  });
9459
9929
  console2.put("/api/rules/enabled", async (c) => {
9460
9930
  const b = await c.req.json();
9461
9931
  const rc = updateRuleEnabled(b.layer, b.category, b.rule_id, b.enabled);
9462
- return c.json(withL24View(rc));
9932
+ return c.json(withL22View(rc));
9463
9933
  });
9464
9934
  console2.put("/api/rules/deny_mode", async (c) => {
9465
9935
  const b = await c.req.json();
9466
9936
  const rc = updateRuleDenyMode(b.layer, b.category, b.rule_id, b.deny_mode);
9467
- return c.json(withL24View(rc));
9937
+ return c.json(withL22View(rc));
9468
9938
  });
9469
9939
  console2.put("/api/rules/skip_l3", async (c) => {
9470
9940
  const b = await c.req.json();
9471
9941
  const rc = updateRuleSkipL3(b.layer, b.category, b.rule_id, b.skip_l3_on_disabled);
9472
- return c.json(withL24View(rc));
9942
+ return c.json(withL22View(rc));
9473
9943
  });
9474
9944
  console2.put("/api/categories/enabled", async (c) => {
9475
9945
  const b = await c.req.json();
9476
9946
  const rc = updateCategoryEnabled(b.layer, b.category, b.enabled);
9477
- return c.json(rc);
9947
+ return c.json(withL22View(rc));
9478
9948
  });
9479
9949
  console2.post("/api/rules", async (c) => {
9480
9950
  const b = await c.req.json();
@@ -9490,13 +9960,13 @@ function createConsoleRouter() {
9490
9960
  credential: b.credential,
9491
9961
  deny_mode: b.deny_mode
9492
9962
  });
9493
- return c.json(rc);
9963
+ return c.json(withL22View(rc));
9494
9964
  });
9495
9965
  console2.delete("/api/rules", async (c) => {
9496
9966
  const b = await c.req.json();
9497
9967
  deleteCustomRule(b.layer, b.category, b.rule_id);
9498
9968
  const rc = loadRuntimeConfig();
9499
- return c.json(rc);
9969
+ return c.json(withL22View(rc));
9500
9970
  });
9501
9971
  console2.get("/api/skills", (c) => {
9502
9972
  const rc = loadRuntimeConfig();
@@ -9505,23 +9975,23 @@ function createConsoleRouter() {
9505
9975
  console2.put("/api/skills/enabled", async (c) => {
9506
9976
  const b = await c.req.json();
9507
9977
  const rc = updateSkillEnabled(b.skill_id, b.enabled);
9508
- return c.json(rc);
9978
+ return c.json(withL22View(rc));
9509
9979
  });
9510
9980
  console2.put("/api/skill-categories/enabled", async (c) => {
9511
9981
  const b = await c.req.json();
9512
9982
  const rc = updateSkillCategoryEnabled(b.category, b.enabled);
9513
- return c.json(rc);
9983
+ return c.json(withL22View(rc));
9514
9984
  });
9515
9985
  console2.post("/api/skills", async (c) => {
9516
9986
  const b = await c.req.json();
9517
9987
  const rc = addCustomSkill(b.skill_id, b.category || "user_custom", b.keywords || [], b.content);
9518
- return c.json(rc);
9988
+ return c.json(withL22View(rc));
9519
9989
  });
9520
9990
  console2.delete("/api/skills", async (c) => {
9521
9991
  const b = await c.req.json();
9522
9992
  deleteCustomSkill(b.skill_id, b.category);
9523
9993
  const rc = loadRuntimeConfig();
9524
- return c.json(rc);
9994
+ return c.json(withL22View(rc));
9525
9995
  });
9526
9996
  console2.get("/api/skills/:skill_id/content", async (c) => {
9527
9997
  const skillId = c.req.param("skill_id");
@@ -9619,7 +10089,7 @@ function createConsoleRouter() {
9619
10089
  });
9620
10090
  console2.get("/api/config", (c) => {
9621
10091
  const rc = loadRuntimeConfig();
9622
- return c.json({ ...rc, L2_4: getL24View() });
10092
+ return c.json({ ...rc, L2_2: getL22View() });
9623
10093
  });
9624
10094
  console2.get("/api/env-overrides", (c) => {
9625
10095
  return c.json({ overrides: getEnvOverrides() });
@@ -9627,7 +10097,7 @@ function createConsoleRouter() {
9627
10097
  console2.post("/api/reset", (c) => {
9628
10098
  const defaults = generateSourceDefaults();
9629
10099
  saveRuntimeConfig(defaults);
9630
- return c.json(defaults);
10100
+ return c.json(withL22View(defaults));
9631
10101
  });
9632
10102
  console2.get("/api/token-stats", (c) => {
9633
10103
  const days = parseInt(c.req.query("days") || "7", 10);
@@ -9959,4 +10429,14 @@ if (isMain) {
9959
10429
  //! 顺序即 fail-closed 短路优先级:verify 逐个执行,首个返回违规的性质成为
9960
10430
  //! first_violation,后续不再评估。调整顺序影响判定归属,须与 Python
9961
10431
  //! properties.py ALL_PROPERTIES 保持一致并双端测试确认。
10432
+ //!
10433
+ //! shortName(P1~P8 连续短名)与 id(语义化英文)对照(与 Python 一致):
10434
+ //! P1 cross-session-chain 跨会话组合链
10435
+ //! P2 sensitive-data-flow 敏感数据传播
10436
+ //! P3 low-integrity-reachability 低完整性数据流可达
10437
+ //! P4 no-exfil-after-privesc 提权后禁外传
10438
+ //! P5 no-exfil-after-credential 凭据后禁外传
10439
+ //! P6 mc-ltl-rules LTL 模型检测
10440
+ //! P7 low-integrity-command-exec 低完整性命令执行
10441
+ //! P8 sensitive-data-aggregation 敏感数据聚合外传
9962
10442
  //# sourceMappingURL=cli.js.map