@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/server.js CHANGED
@@ -632,10 +632,15 @@ function loadMC() {
632
632
  try {
633
633
  const rt = (init_runtime_config(), __toCommonJS(runtime_config_exports));
634
634
  if (rules.length > 0 && typeof rt.hasRuntimeConfig === "function" && rt.hasRuntimeConfig()) {
635
- const rtRules = rt.loadRuntimeConfig()?.L2_4?.ltl_rules?.rules || [];
635
+ const ltl = rt.loadRuntimeConfig()?.L2_2?.ltl_rules || {};
636
+ const rtRules = ltl.rules || [];
636
637
  const enabledById = {};
637
638
  for (const r of rtRules) if (r?.id) enabledById[r.id] = r.enabled !== false;
638
- for (const r of rules) if (r?.id && r.id in enabledById) r.enabled = enabledById[r.id];
639
+ const catEnabled = ltl.category_enabled !== false;
640
+ for (const r of rules) {
641
+ if (r?.id && r.id in enabledById) r.enabled = enabledById[r.id];
642
+ if (!catEnabled) r.enabled = false;
643
+ }
639
644
  }
640
645
  } catch {
641
646
  }
@@ -680,7 +685,8 @@ function _loadSensitivePathScope(scope) {
680
685
  path: sp.path,
681
686
  riskLevel: sp.risk_level,
682
687
  desc: sp.desc,
683
- credential: sp.deny_mode === "deny_both" ? true : void 0
688
+ credential: sp.deny_mode === "deny_both" ? true : void 0,
689
+ dirDelOnly: sp.dir_del_only === true ? true : void 0
684
690
  }));
685
691
  }
686
692
  function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWORDS) {
@@ -778,7 +784,7 @@ function runLogicCheck(input) {
778
784
  riskType: "intent_deviation",
779
785
  confidence: 85,
780
786
  source: "logic_rule",
781
- violatedLayers: ["2"]
787
+ violatedLayers: ["L2.1"]
782
788
  };
783
789
  }
784
790
  }
@@ -802,7 +808,7 @@ function runLogicCheck(input) {
802
808
  riskType: "privilege_escalation",
803
809
  confidence: 85,
804
810
  source: "logic_rule",
805
- violatedLayers: ["2"]
811
+ violatedLayers: ["L2.1"]
806
812
  };
807
813
  }
808
814
  }
@@ -854,7 +860,7 @@ function checkComboChain(actionHistory, actionDetailRaw) {
854
860
  riskType: "combo_chain",
855
861
  confidence: 85,
856
862
  source: "logic_rule",
857
- violatedLayers: ["2"]
863
+ violatedLayers: ["L2.1"]
858
864
  };
859
865
  }
860
866
  }
@@ -979,7 +985,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
979
985
  riskType: "read_before_write",
980
986
  confidence: 85,
981
987
  source: "logic_rule",
982
- violatedLayers: ["2"]
988
+ violatedLayers: ["L2.1"]
983
989
  };
984
990
  }
985
991
  return null;
@@ -999,7 +1005,7 @@ function checkPasswordConsent(actionType, actionDetail, actionHistory) {
999
1005
  riskType: "consent_missing",
1000
1006
  confidence: 85,
1001
1007
  source: "logic_rule",
1002
- violatedLayers: ["2"]
1008
+ violatedLayers: ["L2.1"]
1003
1009
  };
1004
1010
  }
1005
1011
  function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
@@ -1017,7 +1023,7 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
1017
1023
  riskType: "consent_missing",
1018
1024
  confidence: 85,
1019
1025
  source: "logic_rule",
1020
- violatedLayers: ["2"]
1026
+ violatedLayers: ["L2.1"]
1021
1027
  };
1022
1028
  }
1023
1029
  function containsPrivilegeEscalation(actionDetail) {
@@ -1077,7 +1083,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1077
1083
  riskType: "file_access",
1078
1084
  confidence: 95,
1079
1085
  source: "logic_rule",
1080
- violatedLayers: ["2"],
1086
+ violatedLayers: ["L2.1"],
1081
1087
  isCredential: true
1082
1088
  };
1083
1089
  }
@@ -1093,7 +1099,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1093
1099
  riskType: "file_access",
1094
1100
  confidence: 95,
1095
1101
  source: "logic_rule",
1096
- violatedLayers: ["2"]
1102
+ violatedLayers: ["L2.1"]
1097
1103
  };
1098
1104
  }
1099
1105
  }
@@ -1107,7 +1113,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1107
1113
  riskType: "file_access",
1108
1114
  confidence: 70,
1109
1115
  source: "logic_rule",
1110
- violatedLayers: ["2"]
1116
+ violatedLayers: ["L2.1"]
1111
1117
  };
1112
1118
  }
1113
1119
  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;
@@ -1119,7 +1125,7 @@ var init_logic_rules = __esm({
1119
1125
  init_rules_loader();
1120
1126
  ROOT_SENSITIVE_PATHS = _loadSensitivePathScope("root");
1121
1127
  HOME_SENSITIVE_PATHS = _loadSensitivePathScope("home");
1122
- CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc }) => ({ path: path6, riskLevel, desc, credential: true }));
1128
+ CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc, dirDelOnly }) => ({ path: path6, riskLevel, desc, credential: true, dirDelOnly }));
1123
1129
  NON_WRITE_TOOLS = /* @__PURE__ */ new Set([
1124
1130
  "ask_user_question",
1125
1131
  "glob",
@@ -1432,6 +1438,7 @@ __export(runtime_config_exports, {
1432
1438
  isLayerEnabled: () => isLayerEnabled,
1433
1439
  loadRuntimeConfig: () => loadRuntimeConfig,
1434
1440
  mergeConfig: () => mergeConfig,
1441
+ pickDeletableRule: () => pickDeletableRule,
1435
1442
  saveRuntimeConfig: () => saveRuntimeConfig,
1436
1443
  updateAgentCategoryEnabled: () => updateAgentCategoryEnabled,
1437
1444
  updateAgentRuleEnabled: () => updateAgentRuleEnabled,
@@ -1470,7 +1477,7 @@ function emptyRuntimeConfig() {
1470
1477
  L1_rules: {},
1471
1478
  L2_rules: {},
1472
1479
  L3_skills: {},
1473
- L2_4: {},
1480
+ L2_2: {},
1474
1481
  agent_rules: {},
1475
1482
  scenario_rules: {}
1476
1483
  };
@@ -1498,7 +1505,7 @@ function loadRuntimeConfig() {
1498
1505
  L1_rules: merged.L1_rules || {},
1499
1506
  L2_rules: merged.L2_rules || {},
1500
1507
  L3_skills: merged.L3_skills || {},
1501
- L2_4: merged.L2_4 || {},
1508
+ L2_2: merged.L2_2 || {},
1502
1509
  agent_rules: merged.agent_rules || {},
1503
1510
  scenario_rules: merged.scenario_rules || {}
1504
1511
  };
@@ -1551,6 +1558,7 @@ function getEnabledL2SensitivePaths(runtime) {
1551
1558
  const out = { path: r.path || "", risk_level: r.risk_level || "", desc: r.desc || "", deny_mode: r.deny_mode || "deny_write" };
1552
1559
  if (r.credential) out.credential = true;
1553
1560
  if (r.read_only) out.read_only = true;
1561
+ if (r.dir_del_only) out.dir_del_only = true;
1554
1562
  return out;
1555
1563
  });
1556
1564
  }
@@ -1596,8 +1604,16 @@ function getDisabledL2RulesWithSkipL3() {
1596
1604
  function _slugify(text) {
1597
1605
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "x";
1598
1606
  }
1607
+ function _fnv1a32(text) {
1608
+ let h = 2166136261;
1609
+ for (const b of Buffer.from(text, "utf-8")) {
1610
+ h ^= b;
1611
+ h = h * 16777619 >>> 0;
1612
+ }
1613
+ return h.toString(16).padStart(8, "0");
1614
+ }
1599
1615
  function _generatePatternId(pattern) {
1600
- return `pat_${_slugify(pattern)}`;
1616
+ return `pat_${_slugify(pattern)}_${_fnv1a32(pattern)}`;
1601
1617
  }
1602
1618
  function _deriveDenyMode(rule) {
1603
1619
  if (rule.deny_mode) return rule.deny_mode;
@@ -1659,6 +1675,7 @@ function _l2SensitivePathsToRules() {
1659
1675
  builtin: true
1660
1676
  };
1661
1677
  _syncDenyModeFields(rule);
1678
+ if (sp.dirDelOnly) rule.dir_del_only = true;
1662
1679
  return rule;
1663
1680
  });
1664
1681
  }
@@ -1875,17 +1892,20 @@ function generateSourceDefaults() {
1875
1892
  user_custom: { category_enabled: true, category_desc: "\u7528\u6237\u81EA\u5B9A\u4E49 L2 \u903B\u8F91\u89C4\u5219", rules: [] }
1876
1893
  },
1877
1894
  L3_skills: _l3SkillsToRules(),
1878
- L2_4: _ltlRulesToRuntime(),
1895
+ L2_2: _ltlRulesToRuntime(),
1879
1896
  agent_rules: _agentRulesToDefaults(),
1880
1897
  scenario_rules: _scenarioRulesToDefaults()
1881
1898
  };
1882
1899
  }
1883
1900
  function _ltlRulesToRuntime() {
1884
1901
  const cands = [
1885
- join3(__dirname4, "..", "rules"),
1886
- join3(__dirname4, "..", "..", "..", "agent_moss", "rules"),
1887
- join3(process.cwd(), "agent_moss", "rules")
1888
- ];
1902
+ getRuntimeConfigDir() && join3(getRuntimeConfigDir(), "rules"),
1903
+ join3(__dirname4, "../rules"),
1904
+ join3(__dirname4, "rules"),
1905
+ join3(__dirname4, "../../../agent_moss/rules"),
1906
+ join3(__dirname4, "../../../../agent_moss/rules"),
1907
+ join3(process.cwd(), "agent_moss/rules")
1908
+ ].filter(Boolean);
1889
1909
  let mc = {};
1890
1910
  for (const dir of cands) {
1891
1911
  const p = join3(dir, "mc_rules.json");
@@ -2063,7 +2083,11 @@ function mergeConfig(userCfg, srcDefaults) {
2063
2083
  );
2064
2084
  _mergeRuleCategories(cfg, srcDefaults, "L1_rules");
2065
2085
  _mergeRuleCategories(cfg, srcDefaults, "L2_rules");
2066
- _mergeRuleCategories(cfg, srcDefaults, "L2_4");
2086
+ if (!cfg.L2_2 && cfg.L2_4) {
2087
+ cfg.L2_2 = cfg.L2_4;
2088
+ delete cfg.L2_4;
2089
+ }
2090
+ _mergeRuleCategories(cfg, srcDefaults, "L2_2");
2067
2091
  _mergeSkillCategories(cfg, srcDefaults, "L3_skills");
2068
2092
  _mergeAgentRules(cfg, srcDefaults);
2069
2093
  _mergeAgentRules(cfg, srcDefaults, "scenario_rules");
@@ -2241,16 +2265,20 @@ function addCustomRule(layer, category, rule) {
2241
2265
  saveRuntimeConfig(runtime);
2242
2266
  return runtime;
2243
2267
  }
2244
- function deleteCustomRule(layer, category, ruleId) {
2245
- const runtime = loadRuntimeConfig();
2246
- const rules = runtime[layer]?.[category]?.rules || [];
2247
- const idx = rules.findIndex((r) => r.id === ruleId);
2268
+ function pickDeletableRule(rules, ruleId) {
2269
+ const idx = rules.findIndex((r) => r.id === ruleId && (!r.builtin || r.source_removed));
2248
2270
  if (idx >= 0) {
2249
- const r = rules[idx];
2250
- if (r.builtin && !r.source_removed) return null;
2251
2271
  rules.splice(idx, 1);
2252
- saveRuntimeConfig(runtime);
2272
+ return rules;
2253
2273
  }
2274
+ if (rules.some((r) => r.id === ruleId)) return null;
2275
+ return rules;
2276
+ }
2277
+ function deleteCustomRule(layer, category, ruleId) {
2278
+ const runtime = loadRuntimeConfig();
2279
+ const rules = runtime[layer]?.[category]?.rules || [];
2280
+ if (pickDeletableRule(rules, ruleId) === null) return null;
2281
+ saveRuntimeConfig(runtime);
2254
2282
  return runtime;
2255
2283
  }
2256
2284
  function addCustomSkill(skillId, category, keywords, content) {
@@ -2683,7 +2711,7 @@ function runHeuristicCheck(input) {
2683
2711
  riskType: "sensitive_action",
2684
2712
  confidence: 90,
2685
2713
  source: "heuristic",
2686
- violatedLayers: ["1.1"]
2714
+ violatedLayers: ["L1.1"]
2687
2715
  };
2688
2716
  }
2689
2717
  }
@@ -2693,8 +2721,10 @@ function runHeuristicCheck(input) {
2693
2721
  const scanDetail = normalizePathSegments(stripLiteralTextSegments(input.aNext.actionDetail));
2694
2722
  const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
2695
2723
  let topRiskType = "";
2724
+ const exemptKeygen = isSshKeygenGenerate(input.aNext.actionDetail);
2696
2725
  for (const rule of DANGEROUS_PATTERNS) {
2697
2726
  if (rule.pattern.test(scanCommand)) {
2727
+ if (exemptKeygen && rule.riskType === "file_access") continue;
2698
2728
  matched.push(rule.description);
2699
2729
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
2700
2730
  highestSeverity = rule.severity;
@@ -2736,10 +2766,26 @@ function runHeuristicCheck(input) {
2736
2766
  const detailL = input.aNext.actionDetail.toLowerCase();
2737
2767
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2738
2768
  const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2739
- if (dangerousResult) return dangerousResult;
2740
- if (!isExcludedType) {
2741
- const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
2742
- if (pathResult) return pathResult;
2769
+ const pathResult = isExcludedType ? null : checkSensitivePath(detailL, input.cwd || "/", agentExt);
2770
+ const homeResult = checkHomeDeletion(actionTypeL, detailL);
2771
+ const mergeResults = [dangerousResult, pathResult, homeResult].filter(Boolean);
2772
+ if (mergeResults.length > 0) {
2773
+ let best = mergeResults[0];
2774
+ for (const r of mergeResults) {
2775
+ if (severityRank(r.riskLevel) > severityRank(best.riskLevel)) best = r;
2776
+ }
2777
+ if (severityRank(highestSeverity) > severityRank(best.riskLevel)) {
2778
+ return {
2779
+ allowed: false,
2780
+ reason: `\u68C0\u6D4B\u5230\u9AD8\u5371\u64CD\u4F5C: ${matched.join(", ")}`,
2781
+ riskLevel: highestSeverity,
2782
+ riskType: highestRiskType,
2783
+ confidence: 95,
2784
+ source: "heuristic",
2785
+ violatedLayers: ["L1.1"]
2786
+ };
2787
+ }
2788
+ return best;
2743
2789
  }
2744
2790
  if (matched.length > 0) {
2745
2791
  return {
@@ -2749,8 +2795,7 @@ function runHeuristicCheck(input) {
2749
2795
  riskType: highestRiskType,
2750
2796
  confidence: 95,
2751
2797
  source: "heuristic",
2752
- violatedLayers: ["1.1"]
2753
- // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2798
+ violatedLayers: ["L1.1"]
2754
2799
  };
2755
2800
  }
2756
2801
  const lateralResult = checkLateralMovement(detailL);
@@ -2878,6 +2923,7 @@ function isSearchActionType(actionType) {
2878
2923
  function checkSensitivePath(command, cwd, agentExt) {
2879
2924
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2880
2925
  const isWriteOpFlag = isWriteOperation("", command);
2926
+ const keygenGenerate = isSshKeygenGenerate(command);
2881
2927
  const pathScanText = normalizePathSegments(stripLiteralTextSegments(command));
2882
2928
  const pathMatches = (text, path6) => {
2883
2929
  const parts = path6.split(/(\(\?!.*?\))/);
@@ -2890,6 +2936,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2890
2936
  };
2891
2937
  for (const sp of ROOT_SENSITIVE_PATHS) {
2892
2938
  if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2939
+ if (keygenGenerate) continue;
2893
2940
  if (sp.path === "/dev/random") {
2894
2941
  if (isWriteOpFlag) {
2895
2942
  continue;
@@ -2901,7 +2948,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2901
2948
  riskType: "file_access",
2902
2949
  confidence: 90,
2903
2950
  source: "heuristic",
2904
- violatedLayers: ["1.1"]
2951
+ violatedLayers: ["L1.1"]
2905
2952
  };
2906
2953
  }
2907
2954
  if (!isWriteOpFlag && !sp.credential) {
@@ -2914,16 +2961,51 @@ function checkSensitivePath(command, cwd, agentExt) {
2914
2961
  riskType: "file_access",
2915
2962
  confidence: 90,
2916
2963
  source: "heuristic",
2917
- violatedLayers: ["1.1"],
2964
+ violatedLayers: ["L1.1"],
2918
2965
  isCredential: !!sp.credential
2919
2966
  };
2920
2967
  }
2921
2968
  }
2922
2969
  for (const sp of HOME_SENSITIVE_PATHS) {
2923
2970
  const expanded = `${expandedCwd}/${sp.path}`;
2924
- const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2925
- const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2926
- if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
2971
+ let pPath = sp.path;
2972
+ if (!sp.path.endsWith(".pub")) pPath = `${sp.path}(?!\\.pub)`;
2973
+ let dirPrefixMatch = null;
2974
+ if (sp.dirDelOnly) {
2975
+ const dirRe = (t) => new RegExp(`(?:^|[\\s/\\\\'"])${t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:/|[\\s'"\\\\]|$)`, "i");
2976
+ dirPrefixMatch = dirRe(sp.path).test(pathScanText) || dirRe(expanded.toLowerCase()).test(pathScanText);
2977
+ }
2978
+ const pathReMatch = sp.dirDelOnly ? !!dirPrefixMatch : pathMatches(pathScanText, pPath);
2979
+ const expandedReMatch = sp.dirDelOnly ? false : pathMatches(pathScanText, expanded + (sp.path.endsWith(".pub") ? "" : "(?!\\.pub)"));
2980
+ if (pathReMatch || expandedReMatch) {
2981
+ if (keygenGenerate) continue;
2982
+ if (sp.dirDelOnly) {
2983
+ if (isDeleteActionForDir(command, sp.path)) {
2984
+ return {
2985
+ allowed: false,
2986
+ reason: `\u5220\u9664\u654F\u611F\u76EE\u5F55: ${sp.desc} (${sp.path})`,
2987
+ riskLevel: sp.riskLevel,
2988
+ riskType: "file_access",
2989
+ confidence: 90,
2990
+ source: "heuristic",
2991
+ violatedLayers: ["L1.1"],
2992
+ isCredential: !!sp.credential
2993
+ };
2994
+ }
2995
+ if (isArchiveReadOfDir(command, sp.path)) {
2996
+ return {
2997
+ allowed: false,
2998
+ reason: `\u5F52\u6863/\u590D\u5236\u8BFB\u53D6\u654F\u611F\u76EE\u5F55: ${sp.desc} (${sp.path})`,
2999
+ riskLevel: sp.riskLevel,
3000
+ riskType: "file_access",
3001
+ confidence: 90,
3002
+ source: "heuristic",
3003
+ violatedLayers: ["L1.1"],
3004
+ isCredential: !!sp.credential
3005
+ };
3006
+ }
3007
+ continue;
3008
+ }
2927
3009
  if (!isWriteOpFlag && !sp.credential) {
2928
3010
  continue;
2929
3011
  }
@@ -2934,7 +3016,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2934
3016
  riskType: "file_access",
2935
3017
  confidence: 90,
2936
3018
  source: "heuristic",
2937
- violatedLayers: ["1.1"],
3019
+ violatedLayers: ["L1.1"],
2938
3020
  isCredential: !!sp.credential
2939
3021
  };
2940
3022
  }
@@ -2957,7 +3039,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2957
3039
  riskType: "file_access",
2958
3040
  confidence: 90,
2959
3041
  source: "heuristic",
2960
- violatedLayers: ["1.1"],
3042
+ violatedLayers: ["L1.1"],
2961
3043
  isCredential: !!sp.credential
2962
3044
  };
2963
3045
  }
@@ -2974,7 +3056,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2974
3056
  riskType: "file_access",
2975
3057
  confidence: 90,
2976
3058
  source: "heuristic",
2977
- violatedLayers: ["1.1"],
3059
+ violatedLayers: ["L1.1"],
2978
3060
  isCredential: true
2979
3061
  };
2980
3062
  }
@@ -2987,18 +3069,142 @@ function checkSensitivePath(command, cwd, agentExt) {
2987
3069
  riskType: "file_access",
2988
3070
  confidence: 90,
2989
3071
  source: "heuristic",
2990
- violatedLayers: ["1.1"]
3072
+ violatedLayers: ["L1.1"]
2991
3073
  };
2992
3074
  }
2993
3075
  return null;
2994
3076
  }
3077
+ function isHomeValue(path6) {
3078
+ let p = path6.trim().toLowerCase();
3079
+ if (p.startsWith("$(") && p.endsWith(")")) {
3080
+ const inner = p.slice(2, -1).trim();
3081
+ return HOME_TARGET_RE.test(" " + inner);
3082
+ }
3083
+ if (/^\/home\/[^/\s]+\/?$/.test(p)) return true;
3084
+ return HOME_TARGET_RE.test(" " + p);
3085
+ }
3086
+ function isDeleteCommand(seg) {
3087
+ const s = seg.trimStart();
3088
+ if (/^(?:rm|rmdir|unlink)\b/i.test(s)) return true;
3089
+ return /删除|移除/.test(s);
3090
+ }
3091
+ function homeEscapeTarget(seg) {
3092
+ const low = seg.toLowerCase();
3093
+ const re = /(?:~|(?:\$home|\$\{home\})|\/home\/[a-z0-9_.-]+)/g;
3094
+ let m;
3095
+ while ((m = re.exec(low)) !== null) {
3096
+ const endIdx = m.index + m[0].length;
3097
+ const tail = low.slice(endIdx);
3098
+ if (!tail) continue;
3099
+ let token = tail.trimStart().split(/\s+/)[0] || "";
3100
+ token = token.split("&&")[0].split("||")[0].split(";")[0].replace(/\/+$/, "");
3101
+ token = token.replace(/^['"]|['"]$/g, "");
3102
+ const segments = token.split("/").filter((s) => s && s !== ".");
3103
+ if (!segments.some((s) => s === "..")) continue;
3104
+ let depth = 0;
3105
+ let minDepth = 0;
3106
+ for (const s of segments) {
3107
+ depth += s === ".." ? -1 : 1;
3108
+ minDepth = Math.min(minDepth, depth);
3109
+ }
3110
+ if (minDepth < 0) return true;
3111
+ if (depth <= 0) return true;
3112
+ }
3113
+ return false;
3114
+ }
3115
+ function checkHomeDeletion(actionType, actionDetail) {
3116
+ if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") return null;
3117
+ const segs = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
3118
+ for (const seg of segs) {
3119
+ if (!isDeleteCommand(seg)) continue;
3120
+ if (HOME_TARGET_RE.test(seg) || homeEscapeTarget(seg)) {
3121
+ return {
3122
+ allowed: false,
3123
+ reason: "\u68C0\u6D4B\u5230\u5220\u9664\u4E3B\u76EE\u5F55 (~/$HOME) \u64CD\u4F5C",
3124
+ riskLevel: "high",
3125
+ riskType: "script_execution",
3126
+ confidence: 90,
3127
+ source: "heuristic",
3128
+ violatedLayers: ["L1.1"]
3129
+ };
3130
+ }
3131
+ }
3132
+ const assigned = {};
3133
+ for (const m of actionDetail.matchAll(HOME_ASSIGN_RE)) assigned[m[1]] = m[2];
3134
+ for (const [varName, path6] of Object.entries(assigned)) {
3135
+ if (!isHomeValue(path6)) continue;
3136
+ const varRefRe = new RegExp(`(?:${escapeRegExp(varName)}|\\$\\{${escapeRegExp(varName)}\\})`);
3137
+ for (const seg of segs) {
3138
+ if (!isDeleteCommand(seg)) continue;
3139
+ if (varRefRe.test(seg)) {
3140
+ return {
3141
+ allowed: false,
3142
+ reason: `\u68C0\u6D4B\u5230\u901A\u8FC7\u53D8\u91CF ${varName}\uFF08=${path6}\uFF0C\u4E3B\u76EE\u5F55\uFF09\u6267\u884C\u5220\u9664`,
3143
+ riskLevel: "high",
3144
+ riskType: "script_execution",
3145
+ confidence: 90,
3146
+ source: "heuristic",
3147
+ violatedLayers: ["L1.1"]
3148
+ };
3149
+ }
3150
+ }
3151
+ }
3152
+ return null;
3153
+ }
3154
+ function escapeRegExp(s) {
3155
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3156
+ }
3157
+ function isDeleteActionForDir(command, dirPath) {
3158
+ const low = command.toLowerCase();
3159
+ const dpath = dirPath.toLowerCase();
3160
+ if (new RegExp(`\\b(?:rm|unlink|rmdir)\\b[^&|;]*?${escapeRegExp(dpath)}`, "i").test(low)) {
3161
+ return true;
3162
+ }
3163
+ if (new RegExp(`(?:\u5220\u9664|\u79FB\u9664)[^\\n;|&]*?${escapeRegExp(dpath)}`).test(low)) {
3164
+ return true;
3165
+ }
3166
+ for (const seg of low.split(/&&|\|\||;|\n/)) {
3167
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
3168
+ if ((fw === "rm" || fw === "unlink" || fw === "rmdir") && low.includes(dpath)) {
3169
+ return true;
3170
+ }
3171
+ }
3172
+ return false;
3173
+ }
3174
+ function isArchiveReadOfDir(command, dirPath) {
3175
+ const low = command.toLowerCase();
3176
+ const dpath = dirPath.toLowerCase();
3177
+ for (const seg of low.split(/&&|\|\||;|\n/)) {
3178
+ const s = seg.trim();
3179
+ if (!s || !s.includes(dpath)) continue;
3180
+ const parts = s.split(/\s+/);
3181
+ const fw = parts[0] || "";
3182
+ if (fw === "zip") return true;
3183
+ if (fw === "tar") {
3184
+ if (/(?:^|\s)-[a-zA-Z]*x|--extract/.test(s)) continue;
3185
+ return true;
3186
+ }
3187
+ if (fw === "cp" || fw === "rsync") {
3188
+ const segPaths = s.match(/[/~][^\s;|&><'"]*/g) || [];
3189
+ if (segPaths.length && segPaths[0].includes(dpath)) return true;
3190
+ }
3191
+ }
3192
+ return false;
3193
+ }
3194
+ function isSshKeygenGenerate(command) {
3195
+ const low = command.toLowerCase();
3196
+ if (!low.includes("ssh-keygen")) return false;
3197
+ if (!/(?:^|\s)-t\s+\S+/.test(low)) return false;
3198
+ if (/(?:^|\s)-\S*[ylpB]\S*/.test(low)) return false;
3199
+ return true;
3200
+ }
2995
3201
  function checkDangerousPatterns(actionType, actionDetail) {
2996
3202
  if (actionType === "file_write" || actionType === "file_edit") return null;
2997
3203
  const scan = stripHeredocContent(actionDetail);
2998
3204
  const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
2999
3205
  const isTempPath = TEMP_DIRS.some(
3000
3206
  (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
3001
- );
3207
+ ) || /(?:^|[\s;]|&&\s*|\|\|\s*)var\/tmp(?:[/\s;]|$)/.test(actionDetail);
3002
3208
  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;
3003
3209
  if (!isTempPath && wildcardDeleteRe.test(scan)) {
3004
3210
  return {
@@ -3008,7 +3214,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3008
3214
  riskType: "script_execution",
3009
3215
  confidence: 90,
3010
3216
  source: "heuristic",
3011
- violatedLayers: ["1.1"]
3217
+ violatedLayers: ["L1.1"]
3012
3218
  };
3013
3219
  }
3014
3220
  const findWriteMatch = actionDetail.match(
@@ -3023,10 +3229,10 @@ function checkDangerousPatterns(actionType, actionDetail) {
3023
3229
  riskType: "script_execution",
3024
3230
  confidence: 90,
3025
3231
  source: "heuristic",
3026
- violatedLayers: ["1.1"]
3232
+ violatedLayers: ["L1.1"]
3027
3233
  };
3028
3234
  }
3029
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
3235
+ if (/>\s*['"]?\/etc\//.test(actionDetail) || />\s*['"]?\/boot\//.test(actionDetail) || />\s*['"]?\/proc\//.test(actionDetail)) {
3030
3236
  return {
3031
3237
  allowed: false,
3032
3238
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -3034,7 +3240,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3034
3240
  riskType: "file_access",
3035
3241
  confidence: 95,
3036
3242
  source: "heuristic",
3037
- violatedLayers: ["1.1"]
3243
+ violatedLayers: ["L1.1"]
3038
3244
  };
3039
3245
  }
3040
3246
  if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
@@ -3045,7 +3251,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3045
3251
  riskType: "file_access",
3046
3252
  confidence: 90,
3047
3253
  source: "heuristic",
3048
- violatedLayers: ["1.1"]
3254
+ violatedLayers: ["L1.1"]
3049
3255
  };
3050
3256
  }
3051
3257
  return null;
@@ -3136,13 +3342,13 @@ function checkLateralMovement(actionDetail) {
3136
3342
  riskType: "lateral_movement",
3137
3343
  confidence: 80,
3138
3344
  source: "heuristic",
3139
- violatedLayers: ["1.1"]
3345
+ violatedLayers: ["L1.1"]
3140
3346
  };
3141
3347
  }
3142
3348
  }
3143
3349
  return null;
3144
3350
  }
3145
- var INJECTION_KEYWORDS, DANGEROUS_PATTERNS, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES;
3351
+ var INJECTION_KEYWORDS, DANGEROUS_PATTERNS, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES, HOME_TARGET_RE, HOME_ASSIGN_RE;
3146
3352
  var init_heuristic = __esm({
3147
3353
  "src/engine/heuristic.ts"() {
3148
3354
  "use strict";
@@ -3172,6 +3378,8 @@ var init_heuristic = __esm({
3172
3378
  })();
3173
3379
  SENSITIVE_EXCLUDED_PREFIXES = ["memory-", "skill-", "skill-manager-", "mailbox-", "app-"];
3174
3380
  SEARCH_ACTION_PREFIXES = ["search-", "web-", "browser-", "query-", "websearch", "web_search"];
3381
+ HOME_TARGET_RE = /(?:^|[\s="'($])(?:~|(?:\$home|\$\{home\}))(?:\/(?:\.\*|\*|\.)?)?(?=[\s"';}|&)]|$)/i;
3382
+ 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;
3175
3383
  }
3176
3384
  });
3177
3385
 
@@ -3350,112 +3558,6 @@ var init_token_stats = __esm({
3350
3558
  }
3351
3559
  });
3352
3560
 
3353
- // src/engine/formal-engine/decode.ts
3354
- var decode_exports = {};
3355
- __export(decode_exports, {
3356
- allCandidates: () => allCandidates,
3357
- decodeBase64Literals: () => decodeBase64Literals,
3358
- decodeUrlAndNumeric: () => decodeUrlAndNumeric,
3359
- matchStageAny: () => matchStageAny
3360
- });
3361
- function isValidBase64Token(tok) {
3362
- if (tok.length < 8) return false;
3363
- if (!(tok.length % 4 === 0 || tok.endsWith("==") || tok.endsWith("="))) return false;
3364
- return true;
3365
- }
3366
- function isPrintable(s) {
3367
- for (let i = 0; i < s.length; i++) {
3368
- const c = s.charCodeAt(i);
3369
- if (c < 32 || c > 126) return false;
3370
- }
3371
- return true;
3372
- }
3373
- function decodeBase64Literals(text) {
3374
- const out = [];
3375
- const seen = /* @__PURE__ */ new Set();
3376
- for (const m of text.matchAll(B64_TOKEN_RE)) {
3377
- const tok = m[0];
3378
- if (!isValidBase64Token(tok)) continue;
3379
- if (!/[A-Z]/.test(tok) || !/[a-z]/.test(tok)) continue;
3380
- try {
3381
- const padded = tok + "=".repeat((4 - tok.length % 4) % 4);
3382
- const bin = Buffer.from(padded, "base64");
3383
- const s = bin.toString("utf8");
3384
- if (s && isPrintable(s) && !seen.has(s)) {
3385
- seen.add(s);
3386
- out.push(s);
3387
- }
3388
- } catch {
3389
- }
3390
- }
3391
- return out;
3392
- }
3393
- function decodeUrlAndNumeric(text) {
3394
- const out = [];
3395
- let s = text;
3396
- for (let i = 0; i < 3; i++) {
3397
- let nxt = s;
3398
- try {
3399
- nxt = decodeURIComponent(s);
3400
- } catch {
3401
- break;
3402
- }
3403
- if (nxt === s) break;
3404
- s = nxt;
3405
- }
3406
- if (s !== text) out.push(s);
3407
- if (/\\[0-7]{3}/.test(text)) {
3408
- let expanded = "";
3409
- try {
3410
- expanded = text.replace(/\\[0-7]{3}/g, (m) => String.fromCharCode(parseInt(m.slice(1), 8)));
3411
- } catch {
3412
- }
3413
- if (expanded && expanded !== text) out.push(expanded);
3414
- }
3415
- if (/\\x[0-9a-fA-F]{2}/.test(text)) {
3416
- let expanded = "";
3417
- try {
3418
- expanded = text.replace(/\\x[0-9a-fA-F]{2}/g, (m) => String.fromCharCode(parseInt(m.slice(2), 16)));
3419
- } catch {
3420
- }
3421
- if (expanded && expanded !== text) out.push(expanded);
3422
- }
3423
- return out;
3424
- }
3425
- function allCandidates(detail, max = 8) {
3426
- const cands = [detail];
3427
- cands.push(...decodeBase64Literals(detail));
3428
- cands.push(...decodeUrlAndNumeric(detail));
3429
- const seen = /* @__PURE__ */ new Set();
3430
- const out = [];
3431
- for (const c of cands) {
3432
- if (seen.has(c) || c.length > 4096) continue;
3433
- seen.add(c);
3434
- out.push(c);
3435
- if (out.length >= max) break;
3436
- }
3437
- return out;
3438
- }
3439
- function matchStageAny(detail, stages) {
3440
- for (const cand of allCandidates(detail)) {
3441
- for (const [name, pats] of Object.entries(stages)) {
3442
- for (const p of pats) {
3443
- p.lastIndex = 0;
3444
- if (p.test(cand)) return name;
3445
- }
3446
- }
3447
- }
3448
- return "";
3449
- }
3450
- var B64_TOKEN_RE;
3451
- var init_decode = __esm({
3452
- "src/engine/formal-engine/decode.ts"() {
3453
- "use strict";
3454
- init_esm_shims();
3455
- B64_TOKEN_RE = /[A-Za-z0-9+/]+={0,2}/g;
3456
- }
3457
- });
3458
-
3459
3561
  // node_modules/better-sqlite3/lib/util.js
3460
3562
  var require_util = __commonJS({
3461
3563
  "node_modules/better-sqlite3/lib/util.js"(exports) {
@@ -4283,7 +4385,7 @@ var AnalyzeRequestSchema = z.object({
4283
4385
  session_history: z.array(z.object({
4284
4386
  session_id: z.string(),
4285
4387
  action_history: z.array(ActionItemSchema).default([])
4286
- })).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)."),
4388
+ })).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)."),
4287
4389
  a_next: ActionItemSchema.describe("Next action to be executed"),
4288
4390
  reason: z.string().default("").describe("Reason for executing this action"),
4289
4391
  os_type: z.string().default("").describe("OS type: 'linux', 'windows', or empty for auto-detect"),
@@ -4300,7 +4402,9 @@ var AnalyzeResponseSchema = z.object({
4300
4402
  policy: z.string().default(""),
4301
4403
  violated_policy: z.string().default(""),
4302
4404
  confidence: z.number().int().min(0).max(100).default(100),
4303
- analysis_duration_ms: z.number().default(0)
4405
+ analysis_duration_ms: z.number().default(0),
4406
+ // 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
4407
+ violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
4304
4408
  });
4305
4409
  var HealthResponseSchema = z.object({
4306
4410
  status: z.string().default("healthy"),
@@ -4563,6 +4667,7 @@ ${result.scriptContent}
4563
4667
  init_esm_shims();
4564
4668
  init_heuristic();
4565
4669
  init_logic_rules();
4670
+ init_inline_analyzer();
4566
4671
  import { statSync as statSync2, existsSync as existsSync9 } from "fs";
4567
4672
  import { resolve as resolve8, join as join6, isAbsolute as isAbsolute2 } from "path";
4568
4673
  import { homedir as homedir4 } from "os";
@@ -6177,6 +6282,13 @@ function extractScriptPath2(command) {
6177
6282
  return null;
6178
6283
  }
6179
6284
  function extractScriptPathWithArgs(command) {
6285
+ const inline = command.match(/(?:^|\s|&&|\|\|)\s*(?:bash|\/bin\/bash|sh|zsh)\s+-c\s*(['"])([\s\S]*?)\1/i);
6286
+ if (inline) {
6287
+ const inner = inline[2];
6288
+ const realPath = extractScriptPath2(inner);
6289
+ if (realPath) return { path: realPath, args: [] };
6290
+ return { path: null, args: [] };
6291
+ }
6180
6292
  const scriptPath = extractScriptPath2(command);
6181
6293
  if (!scriptPath) return { path: null, args: [] };
6182
6294
  const idx = command.indexOf(scriptPath);
@@ -6190,11 +6302,59 @@ function extractScriptPathWithArgs(command) {
6190
6302
  }
6191
6303
  return { path: scriptPath, args };
6192
6304
  }
6305
+ function looksLikeScriptPath(tok) {
6306
+ const t = tok.trim().replace(/^['"]|['"]$/g, "");
6307
+ if (!t) return false;
6308
+ const low = t.toLowerCase();
6309
+ if (["bash", "sh", "zsh", "dash", "python", "python3", "perl", "ruby", "node", "php", "lua"].includes(low)) return false;
6310
+ if (/\.(sh|bash|py|pl|rb|js|php|lua)$/.test(low)) return true;
6311
+ if (t.includes("/") && !t.startsWith("-")) return true;
6312
+ return false;
6313
+ }
6193
6314
  function extractInlineScriptPath(actionDetail) {
6315
+ const code = extractInlineCode(actionDetail);
6316
+ if (code) {
6317
+ const candidates = [];
6318
+ const osRe = /os\.(?:system|popen)\s*\(\s*(['"])(.*?)\1\s*\)/gi;
6319
+ let m;
6320
+ while ((m = osRe.exec(code)) !== null) candidates.push(m[2]);
6321
+ const subListRe = /subprocess\.(?:run|call|Popen|check_output|check_call)\s*\(\s*\[(.*?)\]/gi;
6322
+ while ((m = subListRe.exec(code)) !== null) candidates.push(m[1]);
6323
+ const subShellRe = /subprocess\.(?:run|call|Popen|check_output|check_call)\s*\(\s*(['"])(.*?)\1[^)]*?shell\s*=\s*true/gi;
6324
+ while ((m = subShellRe.exec(code)) !== null) candidates.push(m[2]);
6325
+ for (const cand of candidates) {
6326
+ const tokens = cand.includes(",") ? cand.split(/\s*,\s*/).map((t) => t.trim().replace(/^['"]|['"]$/g, "")) : cand.split(/\s+/);
6327
+ for (const tok of tokens) {
6328
+ if (looksLikeScriptPath(tok)) {
6329
+ try {
6330
+ const resolved = resolvePath2(tok.replace(/^['"]|['"]$/g, ""));
6331
+ if (existsSync9(resolved)) {
6332
+ const stat = statSync2(resolved);
6333
+ if (stat.isFile()) return resolved;
6334
+ }
6335
+ } catch {
6336
+ }
6337
+ }
6338
+ }
6339
+ }
6340
+ const openRe = /open\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
6341
+ while ((m = openRe.exec(code)) !== null) {
6342
+ const p = m[1];
6343
+ try {
6344
+ const resolved = resolvePath2(p);
6345
+ if (existsSync9(resolved)) {
6346
+ const stat = statSync2(resolved);
6347
+ if (stat.isFile()) return resolved;
6348
+ }
6349
+ } catch {
6350
+ }
6351
+ }
6352
+ return null;
6353
+ }
6194
6354
  const re = /open\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
6195
- let m;
6196
- while ((m = re.exec(actionDetail)) !== null) {
6197
- const p = m[1];
6355
+ let m2;
6356
+ while ((m2 = re.exec(actionDetail)) !== null) {
6357
+ const p = m2[1];
6198
6358
  let resolved;
6199
6359
  try {
6200
6360
  resolved = resolvePath2(p);
@@ -6579,7 +6739,7 @@ async function analyzeWithLLM(prompt, config) {
6579
6739
  riskType: riskType || "",
6580
6740
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
6581
6741
  source: "llm",
6582
- violatedLayers: ["3"]
6742
+ violatedLayers: ["L3.1"]
6583
6743
  };
6584
6744
  } catch (err) {
6585
6745
  if (abortController.signal.aborted) {
@@ -7140,7 +7300,139 @@ init_esm_shims();
7140
7300
  // src/engine/formal-engine/builder.ts
7141
7301
  init_esm_shims();
7142
7302
  init_rules_loader();
7143
- init_decode();
7303
+
7304
+ // src/engine/formal-engine/decode.ts
7305
+ init_esm_shims();
7306
+ var B64_TOKEN_RE = /[A-Za-z0-9+/]+={0,2}/g;
7307
+ function isValidBase64Token(tok) {
7308
+ if (tok.length < 8) return false;
7309
+ if (!(tok.length % 4 === 0 || tok.endsWith("==") || tok.endsWith("="))) return false;
7310
+ return true;
7311
+ }
7312
+ function isPrintable(s) {
7313
+ for (let i = 0; i < s.length; i++) {
7314
+ const c = s.charCodeAt(i);
7315
+ if (c < 32 || c > 126) return false;
7316
+ }
7317
+ return true;
7318
+ }
7319
+ function decodeBase64Literals(text) {
7320
+ const out = [];
7321
+ const seen = /* @__PURE__ */ new Set();
7322
+ for (const m of text.matchAll(B64_TOKEN_RE)) {
7323
+ const tok = m[0];
7324
+ if (!isValidBase64Token(tok)) continue;
7325
+ if (!/[A-Z]/.test(tok) || !/[a-z]/.test(tok)) continue;
7326
+ try {
7327
+ const padded = tok + "=".repeat((4 - tok.length % 4) % 4);
7328
+ const bin = Buffer.from(padded, "base64");
7329
+ const s = bin.toString("utf8");
7330
+ if (s && isPrintable(s) && !seen.has(s)) {
7331
+ seen.add(s);
7332
+ out.push(s);
7333
+ }
7334
+ } catch {
7335
+ }
7336
+ }
7337
+ return out;
7338
+ }
7339
+ var QUOTED_STR_RE = /('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`)/g;
7340
+ var OUTPUT_CMDS = /* @__PURE__ */ new Set(["echo", "printf", "print"]);
7341
+ function stripStringLiterals(text) {
7342
+ const m = text.match(/\s*([A-Za-z_][\w-]*|\/?[\w./-]+)\b/);
7343
+ if (!m) return text;
7344
+ const cmd = m[1].split("/").pop().toLowerCase();
7345
+ if (!OUTPUT_CMDS.has(cmd)) return text;
7346
+ return text.replace(QUOTED_STR_RE, "");
7347
+ }
7348
+ function decodeUrlAndNumeric(text) {
7349
+ const out = [];
7350
+ let s = text;
7351
+ for (let i = 0; i < 3; i++) {
7352
+ let nxt = s;
7353
+ try {
7354
+ nxt = decodeURIComponent(s);
7355
+ } catch {
7356
+ break;
7357
+ }
7358
+ if (nxt === s) break;
7359
+ s = nxt;
7360
+ }
7361
+ if (s !== text) out.push(s);
7362
+ if (/\\[0-7]{3}/.test(text)) {
7363
+ let expanded = "";
7364
+ try {
7365
+ expanded = text.replace(/\\[0-7]{3}/g, (m) => String.fromCharCode(parseInt(m.slice(1), 8)));
7366
+ } catch {
7367
+ }
7368
+ if (expanded && expanded !== text) out.push(expanded);
7369
+ }
7370
+ if (/\\x[0-9a-fA-F]{2}/.test(text)) {
7371
+ let expanded = "";
7372
+ try {
7373
+ expanded = text.replace(/\\x[0-9a-fA-F]{2}/g, (m) => String.fromCharCode(parseInt(m.slice(2), 16)));
7374
+ } catch {
7375
+ }
7376
+ if (expanded && expanded !== text) out.push(expanded);
7377
+ }
7378
+ return out;
7379
+ }
7380
+ function allCandidates(detail, max = 8) {
7381
+ const shell = stripStringLiterals(detail);
7382
+ const cands = [shell];
7383
+ cands.push(...decodeBase64Literals(shell));
7384
+ cands.push(...decodeUrlAndNumeric(shell));
7385
+ cands.push(...decodeBase64Literals(detail));
7386
+ cands.push(...decodeUrlAndNumeric(detail));
7387
+ const seen = /* @__PURE__ */ new Set();
7388
+ const out = [];
7389
+ for (const c of cands) {
7390
+ if (seen.has(c) || c.length > 4096) continue;
7391
+ seen.add(c);
7392
+ out.push(c);
7393
+ if (out.length >= max) break;
7394
+ }
7395
+ return out;
7396
+ }
7397
+ var STAGE_PRIORITY = ["EXFIL", "PRIVESC", "PERSIST", "SCAN", "LISTEN"];
7398
+ var PURPOSE_EXEC_RE = /subprocess\.\w+\s*\(|os\.system\s*\(|shell\s*=\s*True|\beval\s*\(|\bexec\s*\(|os\.popen\s*\(|\/exec\b/i;
7399
+ var LISTEN_LEAD_RE = /http\.server|flask\s+run|nohup\b|nc\s+-l|socat\s+TCP-LISTEN/i;
7400
+ var LISTEN_NOISE_RE = /shell\s*=\s*True|os\.system|subprocess\.(?:run|call|Popen)|\beval\s*\(|\bexec\s*\(/i;
7401
+ function matchStageAny(detail, stages) {
7402
+ const cands = allCandidates(detail);
7403
+ if (cands.length === 0) return "";
7404
+ const shell = cands[0];
7405
+ const hits = [];
7406
+ for (const [name, pats] of Object.entries(stages)) {
7407
+ for (const p of pats) {
7408
+ p.lastIndex = 0;
7409
+ if (p.test(shell)) {
7410
+ hits.push(name);
7411
+ break;
7412
+ }
7413
+ }
7414
+ }
7415
+ if (hits.length > 0) {
7416
+ if (hits.length === 1) return hits[0];
7417
+ if (hits.includes("EXEC") && PURPOSE_EXEC_RE.test(shell)) return "EXEC";
7418
+ if (hits.includes("LISTEN") && LISTEN_LEAD_RE.test(shell) && !LISTEN_NOISE_RE.test(shell)) {
7419
+ return "LISTEN";
7420
+ }
7421
+ for (const s of STAGE_PRIORITY) {
7422
+ if (hits.includes(s)) return s;
7423
+ }
7424
+ if (hits.includes("EXEC")) return "EXEC";
7425
+ }
7426
+ for (const cand of cands.slice(1)) {
7427
+ for (const [name, pats] of Object.entries(stages)) {
7428
+ for (const p of pats) {
7429
+ p.lastIndex = 0;
7430
+ if (p.test(cand)) return name;
7431
+ }
7432
+ }
7433
+ }
7434
+ return "";
7435
+ }
7144
7436
 
7145
7437
  // src/engine/formal-engine/ltl.ts
7146
7438
  init_esm_shims();
@@ -7174,11 +7466,7 @@ function compileAps(apPatterns) {
7174
7466
  }
7175
7467
  function labelEvent(actionDetail) {
7176
7468
  if (!AP_RE) compileAps(loadMC().ap_patterns || {});
7177
- let candidates = [actionDetail];
7178
- try {
7179
- candidates = (init_decode(), __toCommonJS(decode_exports)).allCandidates(actionDetail);
7180
- } catch {
7181
- }
7469
+ const candidates = allCandidates(actionDetail);
7182
7470
  const hits = /* @__PURE__ */ new Set();
7183
7471
  for (const [name, pats] of Object.entries(AP_RE || {})) {
7184
7472
  for (const c of candidates) {
@@ -7416,7 +7704,7 @@ function loadCrossSessionChains() {
7416
7704
  return CROSS_SESSION_CHAINS;
7417
7705
  }
7418
7706
  var CRED_FILE_RE = /(known_hosts|\.ssh|id_rsa|\/etc\/shadow|\/etc\/passwd|credentials)/i;
7419
- var EXFIL_RE = /https?:\/\/|scp\s+.*@|rsync\s+.*@/i;
7707
+ var EXFIL_RE = /https?:\/\/(?!127\.\d+\.\d+\.\d+|0\.0\.0\.0|localhost|\[?::1\]?)[^\s]+|\bscp\s+.*@|\brsync\s+.*@/i;
7420
7708
  function checkCrossSessionChain(graph) {
7421
7709
  const violations = [];
7422
7710
  const nodes = allNodes(graph);
@@ -7505,7 +7793,7 @@ function checkSensitiveDataFlow(graph) {
7505
7793
  const n = nodes[i];
7506
7794
  if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
7507
7795
  violations.push({
7508
- propertyId: "sensitive-data-exfil",
7796
+ propertyId: "sensitive-data-flow",
7509
7797
  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`,
7510
7798
  riskLevel: "critical",
7511
7799
  path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
@@ -7517,9 +7805,67 @@ function checkSensitiveDataFlow(graph) {
7517
7805
  }
7518
7806
  var EBPF_NET_RE = /connect|sendto|network_connect/i;
7519
7807
  var REMOTE_RE = /to\s+\d+\.\d+\.\d+\.\d+|remote_addr/i;
7520
- var PRIVESC_RE = /\bsudo\b|\bsu\b|chmod\s+u\s*\+\s*s|pkexec|setuid/i;
7521
7808
  var 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;
7522
7809
  var 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;
7810
+ var INLINE_SHELL_RE = /(?:os\.system|os\.popen)\s*\(\s*['"]([^'"]+)['"]|subprocess\.[a-z_]+\(\s*\[([^\]]+)\]|subprocess\.[a-z_]+\(\s*['"]([^'"]+)['"]|(?:bash|sh|zsh)\s+-c\s*['"]([^'"]+)['"]/gi;
7811
+ function extractInlineShell(s) {
7812
+ const out = [];
7813
+ let m;
7814
+ INLINE_SHELL_RE.lastIndex = 0;
7815
+ while ((m = INLINE_SHELL_RE.exec(s)) !== null) {
7816
+ let g = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
7817
+ if (/^["']/.test(g) && (/"\s*,/.test(g) || /'\s*,/.test(g))) {
7818
+ g = g.replace(/['"\[\],]+/g, " ");
7819
+ }
7820
+ g = g.trim();
7821
+ if (g) out.push(g);
7822
+ }
7823
+ return out;
7824
+ }
7825
+ var PKG_MGR = /* @__PURE__ */ new Set(["apt", "apt-get", "aptitude", "yum", "dnf", "pacman", "zypper", "apk", "brew"]);
7826
+ var SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "fish", "ksh", "csh", "tcsh", "nologin"]);
7827
+ function containsPrivesc(shell) {
7828
+ if (!shell) return false;
7829
+ for (const seg of shell.split(/\&\&|\|\||;|\n/)) {
7830
+ const s = seg.trim();
7831
+ if (!s) continue;
7832
+ const toks = s.split(/\s+/);
7833
+ const head = (toks[0] || "").toLowerCase();
7834
+ const rest = toks.slice(1).join(" ");
7835
+ if (head === "chmod" && /(?:u\+s|g\+s|[2-7][0-7]{3})/.test(rest)) return true;
7836
+ if (head === "setcap" && /cap_/.test(rest)) return true;
7837
+ if (head === "pkexec" || head === "doas") return true;
7838
+ if (head === "sudo") {
7839
+ if (/^-u\s+/.test(rest)) {
7840
+ if (/^-u\s+root\b/.test(rest)) return true;
7841
+ continue;
7842
+ }
7843
+ const m = /^(\S+)(.*)$/.exec(rest);
7844
+ if (m) {
7845
+ const nxt = m[1].toLowerCase();
7846
+ const tail = m[2];
7847
+ if (PKG_MGR.has(nxt)) continue;
7848
+ if (SHELLS.has(nxt)) return true;
7849
+ if (nxt === "-i" || nxt === "-s") return true;
7850
+ if (nxt.startsWith("su")) return true;
7851
+ if (nxt.startsWith("/") && /bin\/(?:bash|sh|zsh|dash|fish)$/.test(nxt)) return true;
7852
+ }
7853
+ if (/\/(?:usr\/(?:local\/)?bin|bin)\/(?:bash|sh|zsh|dash|fish)\b/.test(rest)) return true;
7854
+ if (/\bchmod\s+(?:u\+s|g\+s|[2-7][0-7]{3})\b/.test(rest)) return true;
7855
+ if (/\bsetcap\s+cap_\w+/.test(rest)) return true;
7856
+ if (/\bvisudo\b/.test(rest)) return true;
7857
+ if (/\bpasswd\s+root\b/.test(rest)) return true;
7858
+ if (/\buseradd\b/.test(rest)) return true;
7859
+ }
7860
+ if (head === "su") {
7861
+ if (!rest) return true;
7862
+ if (/^-l?\s*$/.test(rest)) return true;
7863
+ if (/^root\b/.test(rest) || /^-l?\s+root\b/.test(rest)) return true;
7864
+ }
7865
+ }
7866
+ if (/\bset(?:uid|euid|gid|egid|reuid|regid|resuid|resgid)\s*\(/i.test(shell)) return true;
7867
+ return false;
7868
+ }
7523
7869
  function isExfilNode(n) {
7524
7870
  if (n.stage === "EXFIL") return true;
7525
7871
  if (EXFIL_RE.test(n.actionDetail) && EXFIL_ACTION_RE.test(n.actionDetail)) return true;
@@ -7534,8 +7880,12 @@ function isCredNode(n) {
7534
7880
  return CRED_STRICT_RE.test(n.actionDetail);
7535
7881
  }
7536
7882
  function isPrivescNode(n) {
7537
- if (n.stage === "PRIVESC") return true;
7538
- return PRIVESC_RE.test(n.actionDetail);
7883
+ const shell = stripStringLiterals(n.actionDetail);
7884
+ if (containsPrivesc(shell)) return true;
7885
+ for (const cmd of extractInlineShell(shell)) {
7886
+ if (containsPrivesc(cmd)) return true;
7887
+ }
7888
+ return false;
7539
7889
  }
7540
7890
  function checkNeverExfilAfterPrivesc(graph) {
7541
7891
  const violations = [];
@@ -7556,7 +7906,7 @@ function checkNeverExfilAfterPrivesc(graph) {
7556
7906
  if (pre) {
7557
7907
  const post = nodes[firstEv];
7558
7908
  return [{
7559
- propertyId: "no-privesc-then-exfil",
7909
+ propertyId: "no-exfil-after-privesc",
7560
7910
  reason: "\u63D0\u6743\u540E\u51FA\u73B0\u5916\u4F20\uFF1A\u63D0\u6743(\u524D) \u2192 \u5916\u4F20(\u540E)\uFF0C\u8FDD\u53CD \u25A1(PRIVESC\u2192\xAC\u25C7EXFIL)",
7561
7911
  riskLevel: "critical",
7562
7912
  path: [
@@ -7572,7 +7922,7 @@ function checkNeverExfilAfterPrivesc(graph) {
7572
7922
  for (let j = i + 1; j < nodes.length; j++) {
7573
7923
  if (isExfilNode(nodes[j])) {
7574
7924
  violations.push({
7575
- propertyId: "no-privesc-then-exfil",
7925
+ propertyId: "no-exfil-after-privesc",
7576
7926
  reason: "\u63D0\u6743\u540E\u51FA\u73B0\u5916\u4F20\uFF1A\u63D0\u6743(\u524D) \u2192 \u5916\u4F20(\u540E)\uFF0C\u8FDD\u53CD \u25A1(PRIVESC\u2192\xAC\u25C7EXFIL)",
7577
7927
  riskLevel: "critical",
7578
7928
  path: [
@@ -7605,7 +7955,7 @@ function checkNeverExfilAfterCred(graph) {
7605
7955
  if (pre) {
7606
7956
  const post = nodes[firstEv];
7607
7957
  return [{
7608
- propertyId: "no-cred-then-exfil",
7958
+ propertyId: "no-exfil-after-credential",
7609
7959
  reason: "\u8BFB\u53D6\u51ED\u636E\u540E\u51FA\u73B0\u5916\u4F20\uFF0C\u8FDD\u53CD \u25A1(CRED\u2192\xAC\u25C7EXFIL)",
7610
7960
  riskLevel: "critical",
7611
7961
  path: [
@@ -7621,7 +7971,7 @@ function checkNeverExfilAfterCred(graph) {
7621
7971
  for (let j = i + 1; j < nodes.length; j++) {
7622
7972
  if (isExfilNode(nodes[j])) {
7623
7973
  violations.push({
7624
- propertyId: "no-cred-then-exfil",
7974
+ propertyId: "no-exfil-after-credential",
7625
7975
  reason: "\u8BFB\u53D6\u51ED\u636E\u540E\u51FA\u73B0\u5916\u4F20\uFF0C\u8FDD\u53CD \u25A1(CRED\u2192\xAC\u25C7EXFIL)",
7626
7976
  riskLevel: "critical",
7627
7977
  path: [
@@ -7691,7 +8041,7 @@ function checkLowIntegrityCommandExec(graph) {
7691
8041
  const n = nodes[i];
7692
8042
  if (lowAt.has(i) && isComputeNode(n)) {
7693
8043
  violations.push({
7694
- propertyId: "low-integrity-controls-command-exec",
8044
+ propertyId: "low-integrity-command-exec",
7695
8045
  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`,
7696
8046
  riskLevel: "critical",
7697
8047
  path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
@@ -7738,7 +8088,7 @@ function checkCrossSessionReachability(graph) {
7738
8088
  const srcIdx = Math.min(...sources);
7739
8089
  const srcDesc = nodes[srcIdx].actionDetail.slice(0, 40);
7740
8090
  violations.push({
7741
- propertyId: "cross-session-reachability",
8091
+ propertyId: "low-integrity-reachability",
7742
8092
  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`,
7743
8093
  riskLevel: "high",
7744
8094
  path: nodes.slice(-3).map((m) => ({ sessionId: m.sessionId, seq: m.seq, detail: m.actionDetail.slice(0, 60) }))
@@ -7790,17 +8140,17 @@ function checkSensitiveDataAggregation(graph) {
7790
8140
  return violations;
7791
8141
  }
7792
8142
  var ALL_PROPERTIES = [
7793
- { id: "cross-session-chain", check: checkCrossSessionChain },
7794
- { id: "sensitive-data-flow", check: checkSensitiveDataFlow },
7795
- { id: "cross-session-reachability", check: checkCrossSessionReachability },
7796
- { id: "no-privesc-then-exfil", check: checkNeverExfilAfterPrivesc },
7797
- { id: "no-cred-then-exfil", check: checkNeverExfilAfterCred },
8143
+ { id: "cross-session-chain", check: checkCrossSessionChain, shortName: "P1" },
8144
+ { id: "sensitive-data-flow", check: checkSensitiveDataFlow, shortName: "P2" },
8145
+ { id: "low-integrity-reachability", check: checkCrossSessionReachability, shortName: "P3" },
8146
+ { id: "no-exfil-after-privesc", check: checkNeverExfilAfterPrivesc, shortName: "P4" },
8147
+ { id: "no-exfil-after-credential", check: checkNeverExfilAfterCred, shortName: "P5" },
7798
8148
  // L2.2 模型检测(通用 LTL 求值器,mc_rules.json 单一事实源;witness 语义见 ltl.ts)
7799
- { id: "mc-ltl-rules", check: checkMCRules },
7800
- // Rule 5 低完整性控制命令执行(integrity 维度,站点 L2.1 最有区分度的检测之一)
7801
- { id: "low-integrity-command-exec", check: checkLowIntegrityCommandExec },
7802
- // Rule 12 敏感数据聚合外传(egress 聚合 ≥2 事件源)
7803
- { id: "sensitive-data-aggregation", check: checkSensitiveDataAggregation }
8149
+ { id: "mc-ltl-rules", check: checkMCRules, shortName: "P6" },
8150
+ // P7 低完整性控制命令执行(integrity 维度,站点 L2.1 最有区分度的检测之一)
8151
+ { id: "low-integrity-command-exec", check: checkLowIntegrityCommandExec, shortName: "P7" },
8152
+ // P8 敏感数据聚合外传(egress 聚合 ≥2 事件源)
8153
+ { id: "sensitive-data-aggregation", check: checkSensitiveDataAggregation, shortName: "P8" }
7804
8154
  ];
7805
8155
 
7806
8156
  // src/engine/formal-engine/engine.ts
@@ -7863,11 +8213,41 @@ var FormalEngine = class {
7863
8213
  // src/engine/formal-engine/stateful.ts
7864
8214
  init_esm_shims();
7865
8215
  init_rules_loader();
7866
- init_decode();
7867
8216
  var DESTROY_RE = /\b(?:pkill|kill)\b|systemctl\s+stop|service\s+stop|crontab\s+-r|rm\s+-[rf]?\s+/i;
7868
8217
  var 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;
7869
8218
  var CRED_FILE_RE2 = /(known_hosts|\.ssh|id_rsa|\/etc\/shadow|\/etc\/passwd|credentials)/i;
7870
- var EXFIL_RE2 = /https?:\/\/|scp\s+.*@|rsync\s+.*@/i;
8219
+ var EXFIL_RE2 = /https?:\/\/(?!127\.\d+\.\d+\.\d+|0\.0\.0\.0|localhost|\[?::1\]?)|scp\s+.*@|rsync\s+.*@/i;
8220
+ var PORT_RE = /(?:-p\s+(\d{1,5})|-p(\d{1,5})|:(\d{1,5})\b)/i;
8221
+ var BARE_PORT_RE = /(?<=^|\s)(\d{1,5})(?=\s|$)/g;
8222
+ var SOCK_RE = /([\w./-]*(?:\/tmp\/|\/var\/run\/|\.sock\b)[\w./-]*)/;
8223
+ var UNIT_RE = /systemctl\s+(?:start|enable|restart)\s+([\w.-]+)/i;
8224
+ var PORT_STAGE_RE = /LISTEN|PERSIST|SCAN/;
8225
+ function extractInstances(actionDetail, stage) {
8226
+ const out = [];
8227
+ if (PORT_STAGE_RE.test(stage)) {
8228
+ const d = actionDetail;
8229
+ let m;
8230
+ const re = new RegExp(PORT_RE.source, "gi");
8231
+ while ((m = re.exec(d)) !== null) {
8232
+ const p = m[1] || m[2] || m[3];
8233
+ if (p && Number(p) > 0 && Number(p) <= 65535) out.push(`port:${p}`);
8234
+ }
8235
+ if (/SCAN/.test(stage)) {
8236
+ const bareRe = new RegExp(BARE_PORT_RE.source, "g");
8237
+ while ((m = bareRe.exec(d)) !== null) {
8238
+ if (m[1] && Number(m[1]) > 0 && Number(m[1]) <= 65535) out.push(`port:${m[1]}`);
8239
+ }
8240
+ }
8241
+ m = SOCK_RE.exec(d);
8242
+ if (m && m[1]) out.push(`sock:${m[1]}`);
8243
+ m = UNIT_RE.exec(d);
8244
+ if (m && m[1]) out.push(`unit:${m[1]}`);
8245
+ }
8246
+ return out;
8247
+ }
8248
+ function buckKey(chainId, inst) {
8249
+ return inst ? `${chainId}@@${inst}` : chainId;
8250
+ }
7871
8251
  var STAGES_PATTERNS = null;
7872
8252
  var CHAINS = null;
7873
8253
  function loadRules() {
@@ -7910,7 +8290,11 @@ function lifecycleFor(detail, stage, isError) {
7910
8290
  return "";
7911
8291
  }
7912
8292
  var MAX_NODES = 1e5;
7913
- var StatefulChainTracker = class {
8293
+ var StatefulChainTracker = class _StatefulChainTracker {
8294
+ /** 本地接收时刻(实时路径无 called_at 时的兜底时间戳,ISO)。 */
8295
+ static now() {
8296
+ return (/* @__PURE__ */ new Date()).toISOString();
8297
+ }
7914
8298
  agentId;
7915
8299
  nodes_ = [];
7916
8300
  ptr = {};
@@ -7932,10 +8316,14 @@ var StatefulChainTracker = class {
7932
8316
  for (const chain of chains) {
7933
8317
  const stages = chain.stages || [];
7934
8318
  if (stages.length === 0) continue;
7935
- const matched = this.ptr[String(chain.id || "")] || 0;
8319
+ const cid = String(chain.id || "");
8320
+ let matched = this.ptr[cid] || 0;
8321
+ for (const [k, v] of Object.entries(this.ptr)) {
8322
+ if (k.startsWith(`${cid}@@`) && v > matched && v < stages.length) matched = v;
8323
+ }
7936
8324
  if (matched > 0 && matched < stages.length) {
7937
8325
  const pf = {
7938
- chainId: String(chain.id || ""),
8326
+ chainId: cid,
7939
8327
  stages,
7940
8328
  matched,
7941
8329
  riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
@@ -8011,8 +8399,10 @@ var StatefulChainTracker = class {
8011
8399
  actionDetail: ad,
8012
8400
  stage,
8013
8401
  resources: extractResources2(ad),
8402
+ instances: extractInstances(ad, stage),
8014
8403
  isError,
8015
- lifecycle: lifecycleFor(ad, stage, isError)
8404
+ lifecycle: lifecycleFor(ad, stage, isError),
8405
+ timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
8016
8406
  };
8017
8407
  this.nodes_.push(node);
8018
8408
  this.seqNext += 1;
@@ -8020,11 +8410,19 @@ var StatefulChainTracker = class {
8020
8410
  }
8021
8411
  recordLifecycle(node) {
8022
8412
  if (node.lifecycle === "dead") {
8023
- let hit = false;
8413
+ const clearedInsts = [];
8414
+ for (const inst of node.instances) {
8415
+ const key = `inst:${inst}`;
8416
+ if (key in this.aliveResources) {
8417
+ delete this.aliveResources[key];
8418
+ clearedInsts.push(inst);
8419
+ }
8420
+ }
8421
+ let pathHit = false;
8024
8422
  for (const r of node.resources) {
8025
8423
  if (r in this.aliveResources) {
8026
8424
  delete this.aliveResources[r];
8027
- hit = true;
8425
+ pathHit = true;
8028
8426
  }
8029
8427
  }
8030
8428
  const detail = node.actionDetail;
@@ -8032,16 +8430,30 @@ var StatefulChainTracker = class {
8032
8430
  const base = r.split("/").pop() || "";
8033
8431
  if (base && detail.includes(base)) {
8034
8432
  delete this.aliveResources[r];
8035
- hit = true;
8433
+ pathHit = true;
8036
8434
  }
8037
8435
  }
8038
- if (hit) this.resetChainsOnDestroy();
8436
+ if (clearedInsts.length > 0 && !pathHit) {
8437
+ for (const inst of clearedInsts) this.resetChainsForInstance(inst);
8438
+ } else {
8439
+ this.resetChainsOnDestroy();
8440
+ }
8039
8441
  return;
8040
8442
  }
8041
8443
  if (node.lifecycle === "alive") {
8042
8444
  for (const r of node.resources) {
8043
8445
  this.aliveResources[r] = node.seq;
8044
8446
  }
8447
+ for (const inst of node.instances) {
8448
+ this.aliveResources[`inst:${inst}`] = node.seq;
8449
+ }
8450
+ }
8451
+ }
8452
+ /** 只重置指定实例在全部链上的进度(不影响其他端口/资源)。 */
8453
+ resetChainsForInstance(inst) {
8454
+ const key = buckKey("", inst).slice(2);
8455
+ for (const k of Object.keys(this.ptr)) {
8456
+ if (k.endsWith(`@@${inst}`)) this.ptr[k] = 0;
8045
8457
  }
8046
8458
  }
8047
8459
  resetChainsOnDestroy() {
@@ -8049,6 +8461,29 @@ var StatefulChainTracker = class {
8049
8461
  this.ptr[cid] = 0;
8050
8462
  }
8051
8463
  }
8464
+ /**
8465
+ * 当前节点应推进的链桶集合。
8466
+ * - 有实例节点(LISTEN/PERSIST 带端口/socket/unit)→ 只推自己的实例桶;
8467
+ * 同时为所有链的该实例建立桶(若首次出现)。
8468
+ * - 无实例节点(EXEC/SCAN 等中间段)→ 推全局桶 + 所有当前活跃实例桶
8469
+ * (信息不足时保守推进一切可能,避免漏判)。
8470
+ */
8471
+ targetsForNode(node, chainId) {
8472
+ const insts = node.instances;
8473
+ if (insts.length > 0) {
8474
+ const active = insts.filter((i) => this.aliveResources[`inst:${i}`] !== void 0);
8475
+ if (active.length > 0) return active.map((i) => buckKey(chainId, i));
8476
+ return [chainId];
8477
+ }
8478
+ const targets = [chainId];
8479
+ for (const k of Object.keys(this.aliveResources)) {
8480
+ if (k.startsWith("inst:")) {
8481
+ const inst = k.slice("inst:".length);
8482
+ targets.push(`${chainId}@@${inst}`);
8483
+ }
8484
+ }
8485
+ return targets;
8486
+ }
8052
8487
  advanceChains(node) {
8053
8488
  if (node.lifecycle === "dead") return null;
8054
8489
  if (node.lifecycle === "never_activated") {
@@ -8062,23 +8497,32 @@ var StatefulChainTracker = class {
8062
8497
  const stages = chain.stages || [];
8063
8498
  if (stages.length === 0) continue;
8064
8499
  const cid = String(chain.id || "");
8065
- const cur = this.ptr[cid] || 0;
8066
- if (cur < stages.length && stages[cur] === stage) {
8067
- this.ptr[cid] = cur + 1;
8068
- if (cur + 1 >= stages.length) {
8069
- return {
8070
- chainId: cid,
8071
- stages,
8072
- riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
8073
- reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
8074
- path: this.chainPath(stages)
8075
- };
8500
+ const targets = this.targetsForNode(node, cid);
8501
+ for (const key of targets) {
8502
+ const cur = this.ptr[key] || 0;
8503
+ if (cur < stages.length && stages[cur] === stage) {
8504
+ this.ptr[key] = cur + 1;
8505
+ if (cur + 1 >= stages.length) {
8506
+ return {
8507
+ chainId: cid,
8508
+ stages,
8509
+ riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
8510
+ reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
8511
+ path: this.chainPath(stages)
8512
+ };
8513
+ }
8076
8514
  }
8077
8515
  }
8078
8516
  }
8079
8517
  return null;
8080
8518
  }
8081
8519
  invalidateChainOnFailure(node) {
8520
+ if (node.instances.length > 0) {
8521
+ for (const inst of node.instances) {
8522
+ this.resetChainsForInstance(inst);
8523
+ }
8524
+ return;
8525
+ }
8082
8526
  const failed = new Set(node.resources);
8083
8527
  if (failed.size === 0) return;
8084
8528
  const { chains } = loadRules();
@@ -8094,7 +8538,7 @@ var StatefulChainTracker = class {
8094
8538
  let want = stages[0];
8095
8539
  for (const n of this.nodes_) {
8096
8540
  if (n.stage === want) {
8097
- path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage });
8541
+ path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage, timestamp: n.timestamp });
8098
8542
  const idx = stages.indexOf(n.stage);
8099
8543
  if (idx + 1 < stages.length) {
8100
8544
  want = stages[idx + 1];
@@ -8130,7 +8574,7 @@ var StatefulChainTracker = class {
8130
8574
  if (!this.tainted.has(node.seq)) return [];
8131
8575
  if (EXFIL_RE2.test(node.actionDetail)) {
8132
8576
  return [{
8133
- property_id: "sensitive-data-exfil",
8577
+ property_id: "sensitive-data-flow",
8134
8578
  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`,
8135
8579
  risk_level: "critical",
8136
8580
  path: [[node.sessionId, node.seq, node.actionDetail.slice(0, 60)]]
@@ -8172,8 +8616,12 @@ var StatefulChainTracker = class {
8172
8616
  actionDetail: String(raw.actionDetail || ""),
8173
8617
  stage: String(raw.stage || ""),
8174
8618
  resources: [...raw.resources || []],
8619
+ instances: [...raw.instances || []],
8620
+ // 旧快照无此字段 → 空
8175
8621
  isError: Boolean(raw.isError),
8176
- lifecycle: String(raw.lifecycle || "")
8622
+ lifecycle: String(raw.lifecycle || ""),
8623
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : null
8624
+ // 旧快照无此字段 → null
8177
8625
  });
8178
8626
  }
8179
8627
  this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
@@ -8402,7 +8850,7 @@ async function runAnalysis(input) {
8402
8850
  riskType: node.l1RiskType,
8403
8851
  confidence: 90,
8404
8852
  source: "heuristic_script_scan",
8405
- violatedLayers: ["1.1-script"]
8853
+ violatedLayers: ["L1.2"]
8406
8854
  };
8407
8855
  }
8408
8856
  }
@@ -8520,7 +8968,7 @@ async function runAnalysis(input) {
8520
8968
  riskType: inlineJudgment.riskType,
8521
8969
  confidence: 90,
8522
8970
  source: "logic_rule",
8523
- violatedLayers: ["1.2"]
8971
+ violatedLayers: ["L2.1"]
8524
8972
  };
8525
8973
  }
8526
8974
  return {
@@ -8553,7 +9001,7 @@ async function runAnalysis(input) {
8553
9001
  console.warn(
8554
9002
  `[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)}`
8555
9003
  );
8556
- observedFormalLayers.push("2.4");
9004
+ observedFormalLayers.push("L2.2");
8557
9005
  } else {
8558
9006
  return {
8559
9007
  allowed: false,
@@ -8562,7 +9010,7 @@ async function runAnalysis(input) {
8562
9010
  riskType: "formal_verification",
8563
9011
  confidence: 90,
8564
9012
  source: "formal_engine",
8565
- violatedLayers: ["2.4"]
9013
+ violatedLayers: ["L2.2"]
8566
9014
  };
8567
9015
  }
8568
9016
  }
@@ -8579,7 +9027,7 @@ async function runAnalysis(input) {
8579
9027
  riskType: node.l2RiskType,
8580
9028
  confidence: 85,
8581
9029
  source: "logic_rule_script_scan",
8582
- violatedLayers: ["1.2-script"]
9030
+ violatedLayers: ["L2.3"]
8583
9031
  };
8584
9032
  }
8585
9033
  }
@@ -8666,7 +9114,7 @@ async function runAnalysis(input) {
8666
9114
  riskType: "",
8667
9115
  confidence: 60,
8668
9116
  source: "llm_failure_fallback",
8669
- violatedLayers: ["3"]
9117
+ violatedLayers: ["L3.1"]
8670
9118
  };
8671
9119
  }
8672
9120
  if (!llmResult.allowed && llmResult.confidence && llmResult.confidence >= 90 && _brainExtractor) {
@@ -9035,7 +9483,7 @@ async function analyzeRoute(c) {
9035
9483
  const judgment = incMatchedToDeny(inc);
9036
9484
  if (judgment) {
9037
9485
  const layers = [...result.violatedLayers];
9038
- if (!layers.includes("2.4-stateful")) layers.push("2.4-stateful");
9486
+ if (!layers.includes("L2.2-stateful")) layers.push("L2.2-stateful");
9039
9487
  console.error(
9040
9488
  `[AgentMoss] stateful \u589E\u91CF\u62E6\u622A: agent=${agentId || "-"} property=${judgment.property_id} risk=${judgment.risk_level}`
9041
9489
  );
@@ -9049,7 +9497,8 @@ async function analyzeRoute(c) {
9049
9497
  policy: "",
9050
9498
  violated_policy: "",
9051
9499
  confidence: 90,
9052
- analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10
9500
+ analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
9501
+ violation_chain: judgment.violation_chain
9053
9502
  })
9054
9503
  );
9055
9504
  }
@@ -9088,17 +9537,20 @@ function getChainStateStoreFor(_agentId) {
9088
9537
  }
9089
9538
  function incMatchedToDeny(inc) {
9090
9539
  if (inc.matched) {
9540
+ const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp]);
9091
9541
  return {
9092
9542
  property_id: "cross-" + inc.matched.chainId,
9093
9543
  reason: inc.matched.reason,
9094
- risk_level: inc.matched.riskLevel
9544
+ risk_level: inc.matched.riskLevel,
9545
+ violation_chain: chain
9095
9546
  };
9096
9547
  }
9097
9548
  if (inc.exfil && inc.exfil.length > 0) {
9098
9549
  return {
9099
- property_id: "sensitive-data-exfil",
9550
+ property_id: "sensitive-data-flow",
9100
9551
  reason: inc.exfil[0].reason,
9101
- risk_level: inc.exfil[0].risk_level
9552
+ risk_level: inc.exfil[0].risk_level,
9553
+ violation_chain: []
9102
9554
  };
9103
9555
  }
9104
9556
  return null;
@@ -9224,7 +9676,7 @@ function createConsoleRouter() {
9224
9676
  const rc = updateLayerEnabled(body.layer_key, body.enabled);
9225
9677
  return c.json({ layers: rc.layers, env_overrides: getEnvOverrides() });
9226
9678
  });
9227
- function getL24View() {
9679
+ function getL22View() {
9228
9680
  const ltlHuman = {
9229
9681
  "mc-ltl-001": "\u5148\u8BFB\u53D6\u4E86\u51ED\u636E/\u5BC6\u94A5\uFF0C\u4E4B\u540E\u51FA\u73B0\u7F51\u7EDC\u5916\u4F20",
9230
9682
  "mc-ltl-002": "\u5148\u4E0B\u8F7D/\u83B7\u53D6\u4E86\u6587\u4EF6\uFF0C\u4E4B\u540E\u6267\u884C\u4E86\u6587\u4EF6",
@@ -9240,9 +9692,16 @@ function createConsoleRouter() {
9240
9692
  };
9241
9693
  const mc = loadMC();
9242
9694
  const rules = mc.rules || [];
9695
+ let catEnabled = true;
9696
+ try {
9697
+ const rt = loadRuntimeConfig();
9698
+ catEnabled = rt.L2_2?.ltl_rules?.category_enabled !== false;
9699
+ } catch {
9700
+ }
9243
9701
  return {
9244
9702
  enabled_via: "L2_logic_rules",
9245
9703
  description: "\u65F6\u5E8F\u4E0E\u6570\u636E\u6D41\u653B\u51FB\u68C0\u6D4B\uFF08L2.2 \u6A21\u578B\u68C0\u6D4B\uFF09",
9704
+ category_enabled: catEnabled,
9246
9705
  ltl_rules: rules.map((r) => ({
9247
9706
  id: r.id,
9248
9707
  type: r.type,
@@ -9255,10 +9714,10 @@ function createConsoleRouter() {
9255
9714
  }))
9256
9715
  };
9257
9716
  }
9258
- function withL24View(rc) {
9259
- if (rc && rc.L2_4 && typeof rc.L2_4 === "object" && !Array.isArray(rc.L2_4)) {
9260
- const cat = rc.L2_4.ltl_rules;
9261
- const rls = cat && cat.rules || [];
9717
+ function withL22View(rc) {
9718
+ const l22 = rc && rc.L2_2;
9719
+ if (l22 && typeof l22 === "object" && !Array.isArray(l22) && l22.ltl_rules && typeof l22.ltl_rules === "object" && !Array.isArray(l22.ltl_rules)) {
9720
+ const rls = l22.ltl_rules.rules || [];
9262
9721
  const ltlHumanLocal = {
9263
9722
  "mc-ltl-001": "\u5148\u8BFB\u53D6\u4E86\u51ED\u636E/\u5BC6\u94A5\uFF0C\u4E4B\u540E\u51FA\u73B0\u7F51\u7EDC\u5916\u4F20",
9264
9723
  "mc-ltl-002": "\u5148\u4E0B\u8F7D/\u83B7\u53D6\u4E86\u6587\u4EF6\uFF0C\u4E4B\u540E\u6267\u884C\u4E86\u6587\u4EF6",
@@ -9272,9 +9731,10 @@ function createConsoleRouter() {
9272
9731
  "mc-ltl-010": "\u5B89\u88C5\u4E86\u6301\u4E45\u5316\u673A\u5236\uFF08crontab/systemd/.bashrc \u7B49\uFF09\uFF0C\u9700\u5BA1\u6279",
9273
9732
  "mc-ctl-001": "\u4EFB\u4F55\u60C5\u51B5\u4E0B\u4E0D\u5141\u8BB8\u6E05\u9664\u65E5\u5FD7\uFF08\u5B89\u5168\u5E95\u7EBF\uFF09"
9274
9733
  };
9275
- rc.L2_4 = {
9734
+ rc.L2_2 = {
9276
9735
  enabled_via: "L2_logic_rules",
9277
9736
  description: "\u65F6\u5E8F\u4E0E\u6570\u636E\u6D41\u653B\u51FB\u68C0\u6D4B\uFF08L2.2 \u6A21\u578B\u68C0\u6D4B\uFF09",
9737
+ category_enabled: l22.ltl_rules.category_enabled !== false,
9278
9738
  ltl_rules: rls.map((r) => ({
9279
9739
  id: r.id,
9280
9740
  type: "ltl",
@@ -9291,27 +9751,27 @@ function createConsoleRouter() {
9291
9751
  }
9292
9752
  console2.get("/api/rules", (c) => {
9293
9753
  const rc = loadRuntimeConfig();
9294
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules, L2_4: getL24View() });
9754
+ return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules, L2_2: getL22View() });
9295
9755
  });
9296
9756
  console2.put("/api/rules/enabled", async (c) => {
9297
9757
  const b = await c.req.json();
9298
9758
  const rc = updateRuleEnabled(b.layer, b.category, b.rule_id, b.enabled);
9299
- return c.json(withL24View(rc));
9759
+ return c.json(withL22View(rc));
9300
9760
  });
9301
9761
  console2.put("/api/rules/deny_mode", async (c) => {
9302
9762
  const b = await c.req.json();
9303
9763
  const rc = updateRuleDenyMode(b.layer, b.category, b.rule_id, b.deny_mode);
9304
- return c.json(withL24View(rc));
9764
+ return c.json(withL22View(rc));
9305
9765
  });
9306
9766
  console2.put("/api/rules/skip_l3", async (c) => {
9307
9767
  const b = await c.req.json();
9308
9768
  const rc = updateRuleSkipL3(b.layer, b.category, b.rule_id, b.skip_l3_on_disabled);
9309
- return c.json(withL24View(rc));
9769
+ return c.json(withL22View(rc));
9310
9770
  });
9311
9771
  console2.put("/api/categories/enabled", async (c) => {
9312
9772
  const b = await c.req.json();
9313
9773
  const rc = updateCategoryEnabled(b.layer, b.category, b.enabled);
9314
- return c.json(rc);
9774
+ return c.json(withL22View(rc));
9315
9775
  });
9316
9776
  console2.post("/api/rules", async (c) => {
9317
9777
  const b = await c.req.json();
@@ -9327,13 +9787,13 @@ function createConsoleRouter() {
9327
9787
  credential: b.credential,
9328
9788
  deny_mode: b.deny_mode
9329
9789
  });
9330
- return c.json(rc);
9790
+ return c.json(withL22View(rc));
9331
9791
  });
9332
9792
  console2.delete("/api/rules", async (c) => {
9333
9793
  const b = await c.req.json();
9334
9794
  deleteCustomRule(b.layer, b.category, b.rule_id);
9335
9795
  const rc = loadRuntimeConfig();
9336
- return c.json(rc);
9796
+ return c.json(withL22View(rc));
9337
9797
  });
9338
9798
  console2.get("/api/skills", (c) => {
9339
9799
  const rc = loadRuntimeConfig();
@@ -9342,23 +9802,23 @@ function createConsoleRouter() {
9342
9802
  console2.put("/api/skills/enabled", async (c) => {
9343
9803
  const b = await c.req.json();
9344
9804
  const rc = updateSkillEnabled(b.skill_id, b.enabled);
9345
- return c.json(rc);
9805
+ return c.json(withL22View(rc));
9346
9806
  });
9347
9807
  console2.put("/api/skill-categories/enabled", async (c) => {
9348
9808
  const b = await c.req.json();
9349
9809
  const rc = updateSkillCategoryEnabled(b.category, b.enabled);
9350
- return c.json(rc);
9810
+ return c.json(withL22View(rc));
9351
9811
  });
9352
9812
  console2.post("/api/skills", async (c) => {
9353
9813
  const b = await c.req.json();
9354
9814
  const rc = addCustomSkill(b.skill_id, b.category || "user_custom", b.keywords || [], b.content);
9355
- return c.json(rc);
9815
+ return c.json(withL22View(rc));
9356
9816
  });
9357
9817
  console2.delete("/api/skills", async (c) => {
9358
9818
  const b = await c.req.json();
9359
9819
  deleteCustomSkill(b.skill_id, b.category);
9360
9820
  const rc = loadRuntimeConfig();
9361
- return c.json(rc);
9821
+ return c.json(withL22View(rc));
9362
9822
  });
9363
9823
  console2.get("/api/skills/:skill_id/content", async (c) => {
9364
9824
  const skillId = c.req.param("skill_id");
@@ -9456,7 +9916,7 @@ function createConsoleRouter() {
9456
9916
  });
9457
9917
  console2.get("/api/config", (c) => {
9458
9918
  const rc = loadRuntimeConfig();
9459
- return c.json({ ...rc, L2_4: getL24View() });
9919
+ return c.json({ ...rc, L2_2: getL22View() });
9460
9920
  });
9461
9921
  console2.get("/api/env-overrides", (c) => {
9462
9922
  return c.json({ overrides: getEnvOverrides() });
@@ -9464,7 +9924,7 @@ function createConsoleRouter() {
9464
9924
  console2.post("/api/reset", (c) => {
9465
9925
  const defaults = generateSourceDefaults();
9466
9926
  saveRuntimeConfig(defaults);
9467
- return c.json(defaults);
9927
+ return c.json(withL22View(defaults));
9468
9928
  });
9469
9929
  console2.get("/api/token-stats", (c) => {
9470
9930
  const days = parseInt(c.req.query("days") || "7", 10);
@@ -9536,4 +9996,14 @@ export {
9536
9996
  //! 顺序即 fail-closed 短路优先级:verify 逐个执行,首个返回违规的性质成为
9537
9997
  //! first_violation,后续不再评估。调整顺序影响判定归属,须与 Python
9538
9998
  //! properties.py ALL_PROPERTIES 保持一致并双端测试确认。
9999
+ //!
10000
+ //! shortName(P1~P8 连续短名)与 id(语义化英文)对照(与 Python 一致):
10001
+ //! P1 cross-session-chain 跨会话组合链
10002
+ //! P2 sensitive-data-flow 敏感数据传播
10003
+ //! P3 low-integrity-reachability 低完整性数据流可达
10004
+ //! P4 no-exfil-after-privesc 提权后禁外传
10005
+ //! P5 no-exfil-after-credential 凭据后禁外传
10006
+ //! P6 mc-ltl-rules LTL 模型检测
10007
+ //! P7 low-integrity-command-exec 低完整性命令执行
10008
+ //! P8 sensitive-data-aggregation 敏感数据聚合外传
9539
10009
  //# sourceMappingURL=server.js.map