@kenhkl/agent-moss 0.13.0 → 0.14.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,7 +632,7 @@ 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 rtRules = rt.loadRuntimeConfig()?.L2_2?.ltl_rules?.rules || [];
636
636
  const enabledById = {};
637
637
  for (const r of rtRules) if (r?.id) enabledById[r.id] = r.enabled !== false;
638
638
  for (const r of rules) if (r?.id && r.id in enabledById) r.enabled = enabledById[r.id];
@@ -680,7 +680,8 @@ function _loadSensitivePathScope(scope) {
680
680
  path: sp.path,
681
681
  riskLevel: sp.risk_level,
682
682
  desc: sp.desc,
683
- credential: sp.deny_mode === "deny_both" ? true : void 0
683
+ credential: sp.deny_mode === "deny_both" ? true : void 0,
684
+ dirDelOnly: sp.dir_del_only === true ? true : void 0
684
685
  }));
685
686
  }
686
687
  function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWORDS) {
@@ -778,7 +779,7 @@ function runLogicCheck(input) {
778
779
  riskType: "intent_deviation",
779
780
  confidence: 85,
780
781
  source: "logic_rule",
781
- violatedLayers: ["2"]
782
+ violatedLayers: ["L2.1"]
782
783
  };
783
784
  }
784
785
  }
@@ -802,7 +803,7 @@ function runLogicCheck(input) {
802
803
  riskType: "privilege_escalation",
803
804
  confidence: 85,
804
805
  source: "logic_rule",
805
- violatedLayers: ["2"]
806
+ violatedLayers: ["L2.1"]
806
807
  };
807
808
  }
808
809
  }
@@ -854,7 +855,7 @@ function checkComboChain(actionHistory, actionDetailRaw) {
854
855
  riskType: "combo_chain",
855
856
  confidence: 85,
856
857
  source: "logic_rule",
857
- violatedLayers: ["2"]
858
+ violatedLayers: ["L2.1"]
858
859
  };
859
860
  }
860
861
  }
@@ -979,7 +980,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
979
980
  riskType: "read_before_write",
980
981
  confidence: 85,
981
982
  source: "logic_rule",
982
- violatedLayers: ["2"]
983
+ violatedLayers: ["L2.1"]
983
984
  };
984
985
  }
985
986
  return null;
@@ -999,7 +1000,7 @@ function checkPasswordConsent(actionType, actionDetail, actionHistory) {
999
1000
  riskType: "consent_missing",
1000
1001
  confidence: 85,
1001
1002
  source: "logic_rule",
1002
- violatedLayers: ["2"]
1003
+ violatedLayers: ["L2.1"]
1003
1004
  };
1004
1005
  }
1005
1006
  function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
@@ -1017,7 +1018,7 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
1017
1018
  riskType: "consent_missing",
1018
1019
  confidence: 85,
1019
1020
  source: "logic_rule",
1020
- violatedLayers: ["2"]
1021
+ violatedLayers: ["L2.1"]
1021
1022
  };
1022
1023
  }
1023
1024
  function containsPrivilegeEscalation(actionDetail) {
@@ -1077,7 +1078,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1077
1078
  riskType: "file_access",
1078
1079
  confidence: 95,
1079
1080
  source: "logic_rule",
1080
- violatedLayers: ["2"],
1081
+ violatedLayers: ["L2.1"],
1081
1082
  isCredential: true
1082
1083
  };
1083
1084
  }
@@ -1093,7 +1094,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1093
1094
  riskType: "file_access",
1094
1095
  confidence: 95,
1095
1096
  source: "logic_rule",
1096
- violatedLayers: ["2"]
1097
+ violatedLayers: ["L2.1"]
1097
1098
  };
1098
1099
  }
1099
1100
  }
@@ -1107,7 +1108,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd) {
1107
1108
  riskType: "file_access",
1108
1109
  confidence: 70,
1109
1110
  source: "logic_rule",
1110
- violatedLayers: ["2"]
1111
+ violatedLayers: ["L2.1"]
1111
1112
  };
1112
1113
  }
1113
1114
  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 +1120,7 @@ var init_logic_rules = __esm({
1119
1120
  init_rules_loader();
1120
1121
  ROOT_SENSITIVE_PATHS = _loadSensitivePathScope("root");
1121
1122
  HOME_SENSITIVE_PATHS = _loadSensitivePathScope("home");
1122
- CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc }) => ({ path: path6, riskLevel, desc, credential: true }));
1123
+ CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc, dirDelOnly }) => ({ path: path6, riskLevel, desc, credential: true, dirDelOnly }));
1123
1124
  NON_WRITE_TOOLS = /* @__PURE__ */ new Set([
1124
1125
  "ask_user_question",
1125
1126
  "glob",
@@ -1470,7 +1471,7 @@ function emptyRuntimeConfig() {
1470
1471
  L1_rules: {},
1471
1472
  L2_rules: {},
1472
1473
  L3_skills: {},
1473
- L2_4: {},
1474
+ L2_2: {},
1474
1475
  agent_rules: {},
1475
1476
  scenario_rules: {}
1476
1477
  };
@@ -1498,7 +1499,7 @@ function loadRuntimeConfig() {
1498
1499
  L1_rules: merged.L1_rules || {},
1499
1500
  L2_rules: merged.L2_rules || {},
1500
1501
  L3_skills: merged.L3_skills || {},
1501
- L2_4: merged.L2_4 || {},
1502
+ L2_2: merged.L2_2 || {},
1502
1503
  agent_rules: merged.agent_rules || {},
1503
1504
  scenario_rules: merged.scenario_rules || {}
1504
1505
  };
@@ -1551,6 +1552,7 @@ function getEnabledL2SensitivePaths(runtime) {
1551
1552
  const out = { path: r.path || "", risk_level: r.risk_level || "", desc: r.desc || "", deny_mode: r.deny_mode || "deny_write" };
1552
1553
  if (r.credential) out.credential = true;
1553
1554
  if (r.read_only) out.read_only = true;
1555
+ if (r.dir_del_only) out.dir_del_only = true;
1554
1556
  return out;
1555
1557
  });
1556
1558
  }
@@ -1659,6 +1661,7 @@ function _l2SensitivePathsToRules() {
1659
1661
  builtin: true
1660
1662
  };
1661
1663
  _syncDenyModeFields(rule);
1664
+ if (sp.dirDelOnly) rule.dir_del_only = true;
1662
1665
  return rule;
1663
1666
  });
1664
1667
  }
@@ -1875,7 +1878,7 @@ function generateSourceDefaults() {
1875
1878
  user_custom: { category_enabled: true, category_desc: "\u7528\u6237\u81EA\u5B9A\u4E49 L2 \u903B\u8F91\u89C4\u5219", rules: [] }
1876
1879
  },
1877
1880
  L3_skills: _l3SkillsToRules(),
1878
- L2_4: _ltlRulesToRuntime(),
1881
+ L2_2: _ltlRulesToRuntime(),
1879
1882
  agent_rules: _agentRulesToDefaults(),
1880
1883
  scenario_rules: _scenarioRulesToDefaults()
1881
1884
  };
@@ -2063,7 +2066,11 @@ function mergeConfig(userCfg, srcDefaults) {
2063
2066
  );
2064
2067
  _mergeRuleCategories(cfg, srcDefaults, "L1_rules");
2065
2068
  _mergeRuleCategories(cfg, srcDefaults, "L2_rules");
2066
- _mergeRuleCategories(cfg, srcDefaults, "L2_4");
2069
+ if (!cfg.L2_2 && cfg.L2_4) {
2070
+ cfg.L2_2 = cfg.L2_4;
2071
+ delete cfg.L2_4;
2072
+ }
2073
+ _mergeRuleCategories(cfg, srcDefaults, "L2_2");
2067
2074
  _mergeSkillCategories(cfg, srcDefaults, "L3_skills");
2068
2075
  _mergeAgentRules(cfg, srcDefaults);
2069
2076
  _mergeAgentRules(cfg, srcDefaults, "scenario_rules");
@@ -2683,7 +2690,7 @@ function runHeuristicCheck(input) {
2683
2690
  riskType: "sensitive_action",
2684
2691
  confidence: 90,
2685
2692
  source: "heuristic",
2686
- violatedLayers: ["1.1"]
2693
+ violatedLayers: ["L1.1"]
2687
2694
  };
2688
2695
  }
2689
2696
  }
@@ -2693,8 +2700,10 @@ function runHeuristicCheck(input) {
2693
2700
  const scanDetail = normalizePathSegments(stripLiteralTextSegments(input.aNext.actionDetail));
2694
2701
  const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
2695
2702
  let topRiskType = "";
2703
+ const exemptKeygen = isSshKeygenGenerate(input.aNext.actionDetail);
2696
2704
  for (const rule of DANGEROUS_PATTERNS) {
2697
2705
  if (rule.pattern.test(scanCommand)) {
2706
+ if (exemptKeygen && rule.riskType === "file_access") continue;
2698
2707
  matched.push(rule.description);
2699
2708
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
2700
2709
  highestSeverity = rule.severity;
@@ -2736,10 +2745,26 @@ function runHeuristicCheck(input) {
2736
2745
  const detailL = input.aNext.actionDetail.toLowerCase();
2737
2746
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2738
2747
  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;
2748
+ const pathResult = isExcludedType ? null : checkSensitivePath(detailL, input.cwd || "/", agentExt);
2749
+ const homeResult = checkHomeDeletion(actionTypeL, detailL);
2750
+ const mergeResults = [dangerousResult, pathResult, homeResult].filter(Boolean);
2751
+ if (mergeResults.length > 0) {
2752
+ let best = mergeResults[0];
2753
+ for (const r of mergeResults) {
2754
+ if (severityRank(r.riskLevel) > severityRank(best.riskLevel)) best = r;
2755
+ }
2756
+ if (severityRank(highestSeverity) > severityRank(best.riskLevel)) {
2757
+ return {
2758
+ allowed: false,
2759
+ reason: `\u68C0\u6D4B\u5230\u9AD8\u5371\u64CD\u4F5C: ${matched.join(", ")}`,
2760
+ riskLevel: highestSeverity,
2761
+ riskType: highestRiskType,
2762
+ confidence: 95,
2763
+ source: "heuristic",
2764
+ violatedLayers: ["L1.1"]
2765
+ };
2766
+ }
2767
+ return best;
2743
2768
  }
2744
2769
  if (matched.length > 0) {
2745
2770
  return {
@@ -2749,8 +2774,7 @@ function runHeuristicCheck(input) {
2749
2774
  riskType: highestRiskType,
2750
2775
  confidence: 95,
2751
2776
  source: "heuristic",
2752
- violatedLayers: ["1.1"]
2753
- // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2777
+ violatedLayers: ["L1.1"]
2754
2778
  };
2755
2779
  }
2756
2780
  const lateralResult = checkLateralMovement(detailL);
@@ -2878,6 +2902,7 @@ function isSearchActionType(actionType) {
2878
2902
  function checkSensitivePath(command, cwd, agentExt) {
2879
2903
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2880
2904
  const isWriteOpFlag = isWriteOperation("", command);
2905
+ const keygenGenerate = isSshKeygenGenerate(command);
2881
2906
  const pathScanText = normalizePathSegments(stripLiteralTextSegments(command));
2882
2907
  const pathMatches = (text, path6) => {
2883
2908
  const parts = path6.split(/(\(\?!.*?\))/);
@@ -2890,6 +2915,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2890
2915
  };
2891
2916
  for (const sp of ROOT_SENSITIVE_PATHS) {
2892
2917
  if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2918
+ if (keygenGenerate) continue;
2893
2919
  if (sp.path === "/dev/random") {
2894
2920
  if (isWriteOpFlag) {
2895
2921
  continue;
@@ -2901,7 +2927,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2901
2927
  riskType: "file_access",
2902
2928
  confidence: 90,
2903
2929
  source: "heuristic",
2904
- violatedLayers: ["1.1"]
2930
+ violatedLayers: ["L1.1"]
2905
2931
  };
2906
2932
  }
2907
2933
  if (!isWriteOpFlag && !sp.credential) {
@@ -2914,16 +2940,51 @@ function checkSensitivePath(command, cwd, agentExt) {
2914
2940
  riskType: "file_access",
2915
2941
  confidence: 90,
2916
2942
  source: "heuristic",
2917
- violatedLayers: ["1.1"],
2943
+ violatedLayers: ["L1.1"],
2918
2944
  isCredential: !!sp.credential
2919
2945
  };
2920
2946
  }
2921
2947
  }
2922
2948
  for (const sp of HOME_SENSITIVE_PATHS) {
2923
2949
  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)) {
2950
+ let pPath = sp.path;
2951
+ if (!sp.path.endsWith(".pub")) pPath = `${sp.path}(?!\\.pub)`;
2952
+ let dirPrefixMatch = null;
2953
+ if (sp.dirDelOnly) {
2954
+ const dirRe = (t) => new RegExp(`(?:^|[\\s/\\\\'"])${t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:/|[\\s'"\\\\]|$)`, "i");
2955
+ dirPrefixMatch = dirRe(sp.path).test(pathScanText) || dirRe(expanded.toLowerCase()).test(pathScanText);
2956
+ }
2957
+ const pathReMatch = sp.dirDelOnly ? !!dirPrefixMatch : pathMatches(pathScanText, pPath);
2958
+ const expandedReMatch = sp.dirDelOnly ? false : pathMatches(pathScanText, expanded + (sp.path.endsWith(".pub") ? "" : "(?!\\.pub)"));
2959
+ if (pathReMatch || expandedReMatch) {
2960
+ if (keygenGenerate) continue;
2961
+ if (sp.dirDelOnly) {
2962
+ if (isDeleteActionForDir(command, sp.path)) {
2963
+ return {
2964
+ allowed: false,
2965
+ reason: `\u5220\u9664\u654F\u611F\u76EE\u5F55: ${sp.desc} (${sp.path})`,
2966
+ riskLevel: sp.riskLevel,
2967
+ riskType: "file_access",
2968
+ confidence: 90,
2969
+ source: "heuristic",
2970
+ violatedLayers: ["L1.1"],
2971
+ isCredential: !!sp.credential
2972
+ };
2973
+ }
2974
+ if (isArchiveReadOfDir(command, sp.path)) {
2975
+ return {
2976
+ allowed: false,
2977
+ reason: `\u5F52\u6863/\u590D\u5236\u8BFB\u53D6\u654F\u611F\u76EE\u5F55: ${sp.desc} (${sp.path})`,
2978
+ riskLevel: sp.riskLevel,
2979
+ riskType: "file_access",
2980
+ confidence: 90,
2981
+ source: "heuristic",
2982
+ violatedLayers: ["L1.1"],
2983
+ isCredential: !!sp.credential
2984
+ };
2985
+ }
2986
+ continue;
2987
+ }
2927
2988
  if (!isWriteOpFlag && !sp.credential) {
2928
2989
  continue;
2929
2990
  }
@@ -2934,7 +2995,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2934
2995
  riskType: "file_access",
2935
2996
  confidence: 90,
2936
2997
  source: "heuristic",
2937
- violatedLayers: ["1.1"],
2998
+ violatedLayers: ["L1.1"],
2938
2999
  isCredential: !!sp.credential
2939
3000
  };
2940
3001
  }
@@ -2957,7 +3018,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2957
3018
  riskType: "file_access",
2958
3019
  confidence: 90,
2959
3020
  source: "heuristic",
2960
- violatedLayers: ["1.1"],
3021
+ violatedLayers: ["L1.1"],
2961
3022
  isCredential: !!sp.credential
2962
3023
  };
2963
3024
  }
@@ -2974,7 +3035,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2974
3035
  riskType: "file_access",
2975
3036
  confidence: 90,
2976
3037
  source: "heuristic",
2977
- violatedLayers: ["1.1"],
3038
+ violatedLayers: ["L1.1"],
2978
3039
  isCredential: true
2979
3040
  };
2980
3041
  }
@@ -2987,18 +3048,142 @@ function checkSensitivePath(command, cwd, agentExt) {
2987
3048
  riskType: "file_access",
2988
3049
  confidence: 90,
2989
3050
  source: "heuristic",
2990
- violatedLayers: ["1.1"]
3051
+ violatedLayers: ["L1.1"]
2991
3052
  };
2992
3053
  }
2993
3054
  return null;
2994
3055
  }
3056
+ function isHomeValue(path6) {
3057
+ let p = path6.trim().toLowerCase();
3058
+ if (p.startsWith("$(") && p.endsWith(")")) {
3059
+ const inner = p.slice(2, -1).trim();
3060
+ return HOME_TARGET_RE.test(" " + inner);
3061
+ }
3062
+ if (/^\/home\/[^/\s]+\/?$/.test(p)) return true;
3063
+ return HOME_TARGET_RE.test(" " + p);
3064
+ }
3065
+ function isDeleteCommand(seg) {
3066
+ const s = seg.trimStart();
3067
+ if (/^(?:rm|rmdir|unlink)\b/i.test(s)) return true;
3068
+ return /删除|移除/.test(s);
3069
+ }
3070
+ function homeEscapeTarget(seg) {
3071
+ const low = seg.toLowerCase();
3072
+ const re = /(?:~|(?:\$home|\$\{home\})|\/home\/[a-z0-9_.-]+)/g;
3073
+ let m;
3074
+ while ((m = re.exec(low)) !== null) {
3075
+ const endIdx = m.index + m[0].length;
3076
+ const tail = low.slice(endIdx);
3077
+ if (!tail) continue;
3078
+ let token = tail.trimStart().split(/\s+/)[0] || "";
3079
+ token = token.split("&&")[0].split("||")[0].split(";")[0].replace(/\/+$/, "");
3080
+ token = token.replace(/^['"]|['"]$/g, "");
3081
+ const segments = token.split("/").filter((s) => s && s !== ".");
3082
+ if (!segments.some((s) => s === "..")) continue;
3083
+ let depth = 0;
3084
+ let minDepth = 0;
3085
+ for (const s of segments) {
3086
+ depth += s === ".." ? -1 : 1;
3087
+ minDepth = Math.min(minDepth, depth);
3088
+ }
3089
+ if (minDepth < 0) return true;
3090
+ if (depth <= 0) return true;
3091
+ }
3092
+ return false;
3093
+ }
3094
+ function checkHomeDeletion(actionType, actionDetail) {
3095
+ if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") return null;
3096
+ const segs = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
3097
+ for (const seg of segs) {
3098
+ if (!isDeleteCommand(seg)) continue;
3099
+ if (HOME_TARGET_RE.test(seg) || homeEscapeTarget(seg)) {
3100
+ return {
3101
+ allowed: false,
3102
+ reason: "\u68C0\u6D4B\u5230\u5220\u9664\u4E3B\u76EE\u5F55 (~/$HOME) \u64CD\u4F5C",
3103
+ riskLevel: "high",
3104
+ riskType: "script_execution",
3105
+ confidence: 90,
3106
+ source: "heuristic",
3107
+ violatedLayers: ["L1.1"]
3108
+ };
3109
+ }
3110
+ }
3111
+ const assigned = {};
3112
+ for (const m of actionDetail.matchAll(HOME_ASSIGN_RE)) assigned[m[1]] = m[2];
3113
+ for (const [varName, path6] of Object.entries(assigned)) {
3114
+ if (!isHomeValue(path6)) continue;
3115
+ const varRefRe = new RegExp(`(?:${escapeRegExp(varName)}|\\$\\{${escapeRegExp(varName)}\\})`);
3116
+ for (const seg of segs) {
3117
+ if (!isDeleteCommand(seg)) continue;
3118
+ if (varRefRe.test(seg)) {
3119
+ return {
3120
+ allowed: false,
3121
+ reason: `\u68C0\u6D4B\u5230\u901A\u8FC7\u53D8\u91CF ${varName}\uFF08=${path6}\uFF0C\u4E3B\u76EE\u5F55\uFF09\u6267\u884C\u5220\u9664`,
3122
+ riskLevel: "high",
3123
+ riskType: "script_execution",
3124
+ confidence: 90,
3125
+ source: "heuristic",
3126
+ violatedLayers: ["L1.1"]
3127
+ };
3128
+ }
3129
+ }
3130
+ }
3131
+ return null;
3132
+ }
3133
+ function escapeRegExp(s) {
3134
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3135
+ }
3136
+ function isDeleteActionForDir(command, dirPath) {
3137
+ const low = command.toLowerCase();
3138
+ const dpath = dirPath.toLowerCase();
3139
+ if (new RegExp(`\\b(?:rm|unlink|rmdir)\\b[^&|;]*?${escapeRegExp(dpath)}`, "i").test(low)) {
3140
+ return true;
3141
+ }
3142
+ if (new RegExp(`(?:\u5220\u9664|\u79FB\u9664)[^\\n;|&]*?${escapeRegExp(dpath)}`).test(low)) {
3143
+ return true;
3144
+ }
3145
+ for (const seg of low.split(/&&|\|\||;|\n/)) {
3146
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
3147
+ if ((fw === "rm" || fw === "unlink" || fw === "rmdir") && low.includes(dpath)) {
3148
+ return true;
3149
+ }
3150
+ }
3151
+ return false;
3152
+ }
3153
+ function isArchiveReadOfDir(command, dirPath) {
3154
+ const low = command.toLowerCase();
3155
+ const dpath = dirPath.toLowerCase();
3156
+ for (const seg of low.split(/&&|\|\||;|\n/)) {
3157
+ const s = seg.trim();
3158
+ if (!s || !s.includes(dpath)) continue;
3159
+ const parts = s.split(/\s+/);
3160
+ const fw = parts[0] || "";
3161
+ if (fw === "zip") return true;
3162
+ if (fw === "tar") {
3163
+ if (/(?:^|\s)-[a-zA-Z]*x|--extract/.test(s)) continue;
3164
+ return true;
3165
+ }
3166
+ if (fw === "cp" || fw === "rsync") {
3167
+ const segPaths = s.match(/[/~][^\s;|&><'"]*/g) || [];
3168
+ if (segPaths.length && segPaths[0].includes(dpath)) return true;
3169
+ }
3170
+ }
3171
+ return false;
3172
+ }
3173
+ function isSshKeygenGenerate(command) {
3174
+ const low = command.toLowerCase();
3175
+ if (!low.includes("ssh-keygen")) return false;
3176
+ if (!/(?:^|\s)-t\s+\S+/.test(low)) return false;
3177
+ if (/(?:^|\s)-\S*[ylpB]\S*/.test(low)) return false;
3178
+ return true;
3179
+ }
2995
3180
  function checkDangerousPatterns(actionType, actionDetail) {
2996
3181
  if (actionType === "file_write" || actionType === "file_edit") return null;
2997
3182
  const scan = stripHeredocContent(actionDetail);
2998
3183
  const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
2999
3184
  const isTempPath = TEMP_DIRS.some(
3000
3185
  (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
3001
- );
3186
+ ) || /(?:^|[\s;]|&&\s*|\|\|\s*)var\/tmp(?:[/\s;]|$)/.test(actionDetail);
3002
3187
  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
3188
  if (!isTempPath && wildcardDeleteRe.test(scan)) {
3004
3189
  return {
@@ -3008,7 +3193,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3008
3193
  riskType: "script_execution",
3009
3194
  confidence: 90,
3010
3195
  source: "heuristic",
3011
- violatedLayers: ["1.1"]
3196
+ violatedLayers: ["L1.1"]
3012
3197
  };
3013
3198
  }
3014
3199
  const findWriteMatch = actionDetail.match(
@@ -3023,10 +3208,10 @@ function checkDangerousPatterns(actionType, actionDetail) {
3023
3208
  riskType: "script_execution",
3024
3209
  confidence: 90,
3025
3210
  source: "heuristic",
3026
- violatedLayers: ["1.1"]
3211
+ violatedLayers: ["L1.1"]
3027
3212
  };
3028
3213
  }
3029
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
3214
+ if (/>\s*['"]?\/etc\//.test(actionDetail) || />\s*['"]?\/boot\//.test(actionDetail) || />\s*['"]?\/proc\//.test(actionDetail)) {
3030
3215
  return {
3031
3216
  allowed: false,
3032
3217
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -3034,7 +3219,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3034
3219
  riskType: "file_access",
3035
3220
  confidence: 95,
3036
3221
  source: "heuristic",
3037
- violatedLayers: ["1.1"]
3222
+ violatedLayers: ["L1.1"]
3038
3223
  };
3039
3224
  }
3040
3225
  if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
@@ -3045,7 +3230,7 @@ function checkDangerousPatterns(actionType, actionDetail) {
3045
3230
  riskType: "file_access",
3046
3231
  confidence: 90,
3047
3232
  source: "heuristic",
3048
- violatedLayers: ["1.1"]
3233
+ violatedLayers: ["L1.1"]
3049
3234
  };
3050
3235
  }
3051
3236
  return null;
@@ -3136,13 +3321,13 @@ function checkLateralMovement(actionDetail) {
3136
3321
  riskType: "lateral_movement",
3137
3322
  confidence: 80,
3138
3323
  source: "heuristic",
3139
- violatedLayers: ["1.1"]
3324
+ violatedLayers: ["L1.1"]
3140
3325
  };
3141
3326
  }
3142
3327
  }
3143
3328
  return null;
3144
3329
  }
3145
- var INJECTION_KEYWORDS, DANGEROUS_PATTERNS, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES;
3330
+ var INJECTION_KEYWORDS, DANGEROUS_PATTERNS, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES, HOME_TARGET_RE, HOME_ASSIGN_RE;
3146
3331
  var init_heuristic = __esm({
3147
3332
  "src/engine/heuristic.ts"() {
3148
3333
  "use strict";
@@ -3172,6 +3357,8 @@ var init_heuristic = __esm({
3172
3357
  })();
3173
3358
  SENSITIVE_EXCLUDED_PREFIXES = ["memory-", "skill-", "skill-manager-", "mailbox-", "app-"];
3174
3359
  SEARCH_ACTION_PREFIXES = ["search-", "web-", "browser-", "query-", "websearch", "web_search"];
3360
+ HOME_TARGET_RE = /(?:^|[\s="'($])(?:~|(?:\$home|\$\{home\}))(?:\/(?:\.\*|\*|\.)?)?(?=[\s"';}|&)]|$)/i;
3361
+ 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
3362
  }
3176
3363
  });
3177
3364
 
@@ -3350,112 +3537,6 @@ var init_token_stats = __esm({
3350
3537
  }
3351
3538
  });
3352
3539
 
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
3540
  // node_modules/better-sqlite3/lib/util.js
3460
3541
  var require_util = __commonJS({
3461
3542
  "node_modules/better-sqlite3/lib/util.js"(exports) {
@@ -4283,7 +4364,7 @@ var AnalyzeRequestSchema = z.object({
4283
4364
  session_history: z.array(z.object({
4284
4365
  session_id: z.string(),
4285
4366
  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)."),
4367
+ })).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
4368
  a_next: ActionItemSchema.describe("Next action to be executed"),
4288
4369
  reason: z.string().default("").describe("Reason for executing this action"),
4289
4370
  os_type: z.string().default("").describe("OS type: 'linux', 'windows', or empty for auto-detect"),
@@ -4300,7 +4381,9 @@ var AnalyzeResponseSchema = z.object({
4300
4381
  policy: z.string().default(""),
4301
4382
  violated_policy: z.string().default(""),
4302
4383
  confidence: z.number().int().min(0).max(100).default(100),
4303
- analysis_duration_ms: z.number().default(0)
4384
+ analysis_duration_ms: z.number().default(0),
4385
+ // 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
4386
+ violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
4304
4387
  });
4305
4388
  var HealthResponseSchema = z.object({
4306
4389
  status: z.string().default("healthy"),
@@ -4563,6 +4646,7 @@ ${result.scriptContent}
4563
4646
  init_esm_shims();
4564
4647
  init_heuristic();
4565
4648
  init_logic_rules();
4649
+ init_inline_analyzer();
4566
4650
  import { statSync as statSync2, existsSync as existsSync9 } from "fs";
4567
4651
  import { resolve as resolve8, join as join6, isAbsolute as isAbsolute2 } from "path";
4568
4652
  import { homedir as homedir4 } from "os";
@@ -6177,6 +6261,13 @@ function extractScriptPath2(command) {
6177
6261
  return null;
6178
6262
  }
6179
6263
  function extractScriptPathWithArgs(command) {
6264
+ const inline = command.match(/(?:^|\s|&&|\|\|)\s*(?:bash|\/bin\/bash|sh|zsh)\s+-c\s*(['"])([\s\S]*?)\1/i);
6265
+ if (inline) {
6266
+ const inner = inline[2];
6267
+ const realPath = extractScriptPath2(inner);
6268
+ if (realPath) return { path: realPath, args: [] };
6269
+ return { path: null, args: [] };
6270
+ }
6180
6271
  const scriptPath = extractScriptPath2(command);
6181
6272
  if (!scriptPath) return { path: null, args: [] };
6182
6273
  const idx = command.indexOf(scriptPath);
@@ -6190,11 +6281,59 @@ function extractScriptPathWithArgs(command) {
6190
6281
  }
6191
6282
  return { path: scriptPath, args };
6192
6283
  }
6284
+ function looksLikeScriptPath(tok) {
6285
+ const t = tok.trim().replace(/^['"]|['"]$/g, "");
6286
+ if (!t) return false;
6287
+ const low = t.toLowerCase();
6288
+ if (["bash", "sh", "zsh", "dash", "python", "python3", "perl", "ruby", "node", "php", "lua"].includes(low)) return false;
6289
+ if (/\.(sh|bash|py|pl|rb|js|php|lua)$/.test(low)) return true;
6290
+ if (t.includes("/") && !t.startsWith("-")) return true;
6291
+ return false;
6292
+ }
6193
6293
  function extractInlineScriptPath(actionDetail) {
6294
+ const code = extractInlineCode(actionDetail);
6295
+ if (code) {
6296
+ const candidates = [];
6297
+ const osRe = /os\.(?:system|popen)\s*\(\s*(['"])(.*?)\1\s*\)/gi;
6298
+ let m;
6299
+ while ((m = osRe.exec(code)) !== null) candidates.push(m[2]);
6300
+ const subListRe = /subprocess\.(?:run|call|Popen|check_output|check_call)\s*\(\s*\[(.*?)\]/gi;
6301
+ while ((m = subListRe.exec(code)) !== null) candidates.push(m[1]);
6302
+ const subShellRe = /subprocess\.(?:run|call|Popen|check_output|check_call)\s*\(\s*(['"])(.*?)\1[^)]*?shell\s*=\s*true/gi;
6303
+ while ((m = subShellRe.exec(code)) !== null) candidates.push(m[2]);
6304
+ for (const cand of candidates) {
6305
+ const tokens = cand.includes(",") ? cand.split(/\s*,\s*/).map((t) => t.trim().replace(/^['"]|['"]$/g, "")) : cand.split(/\s+/);
6306
+ for (const tok of tokens) {
6307
+ if (looksLikeScriptPath(tok)) {
6308
+ try {
6309
+ const resolved = resolvePath2(tok.replace(/^['"]|['"]$/g, ""));
6310
+ if (existsSync9(resolved)) {
6311
+ const stat = statSync2(resolved);
6312
+ if (stat.isFile()) return resolved;
6313
+ }
6314
+ } catch {
6315
+ }
6316
+ }
6317
+ }
6318
+ }
6319
+ const openRe = /open\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
6320
+ while ((m = openRe.exec(code)) !== null) {
6321
+ const p = m[1];
6322
+ try {
6323
+ const resolved = resolvePath2(p);
6324
+ if (existsSync9(resolved)) {
6325
+ const stat = statSync2(resolved);
6326
+ if (stat.isFile()) return resolved;
6327
+ }
6328
+ } catch {
6329
+ }
6330
+ }
6331
+ return null;
6332
+ }
6194
6333
  const re = /open\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
6195
- let m;
6196
- while ((m = re.exec(actionDetail)) !== null) {
6197
- const p = m[1];
6334
+ let m2;
6335
+ while ((m2 = re.exec(actionDetail)) !== null) {
6336
+ const p = m2[1];
6198
6337
  let resolved;
6199
6338
  try {
6200
6339
  resolved = resolvePath2(p);
@@ -6579,7 +6718,7 @@ async function analyzeWithLLM(prompt, config) {
6579
6718
  riskType: riskType || "",
6580
6719
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
6581
6720
  source: "llm",
6582
- violatedLayers: ["3"]
6721
+ violatedLayers: ["L3.1"]
6583
6722
  };
6584
6723
  } catch (err) {
6585
6724
  if (abortController.signal.aborted) {
@@ -7140,7 +7279,111 @@ init_esm_shims();
7140
7279
  // src/engine/formal-engine/builder.ts
7141
7280
  init_esm_shims();
7142
7281
  init_rules_loader();
7143
- init_decode();
7282
+
7283
+ // src/engine/formal-engine/decode.ts
7284
+ init_esm_shims();
7285
+ var B64_TOKEN_RE = /[A-Za-z0-9+/]+={0,2}/g;
7286
+ function isValidBase64Token(tok) {
7287
+ if (tok.length < 8) return false;
7288
+ if (!(tok.length % 4 === 0 || tok.endsWith("==") || tok.endsWith("="))) return false;
7289
+ return true;
7290
+ }
7291
+ function isPrintable(s) {
7292
+ for (let i = 0; i < s.length; i++) {
7293
+ const c = s.charCodeAt(i);
7294
+ if (c < 32 || c > 126) return false;
7295
+ }
7296
+ return true;
7297
+ }
7298
+ function decodeBase64Literals(text) {
7299
+ const out = [];
7300
+ const seen = /* @__PURE__ */ new Set();
7301
+ for (const m of text.matchAll(B64_TOKEN_RE)) {
7302
+ const tok = m[0];
7303
+ if (!isValidBase64Token(tok)) continue;
7304
+ if (!/[A-Z]/.test(tok) || !/[a-z]/.test(tok)) continue;
7305
+ try {
7306
+ const padded = tok + "=".repeat((4 - tok.length % 4) % 4);
7307
+ const bin = Buffer.from(padded, "base64");
7308
+ const s = bin.toString("utf8");
7309
+ if (s && isPrintable(s) && !seen.has(s)) {
7310
+ seen.add(s);
7311
+ out.push(s);
7312
+ }
7313
+ } catch {
7314
+ }
7315
+ }
7316
+ return out;
7317
+ }
7318
+ var QUOTED_STR_RE = /('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`)/g;
7319
+ var OUTPUT_CMDS = /* @__PURE__ */ new Set(["echo", "printf", "print"]);
7320
+ function stripStringLiterals(text) {
7321
+ const m = text.match(/\s*([A-Za-z_][\w-]*|\/?[\w./-]+)\b/);
7322
+ if (!m) return text;
7323
+ const cmd = m[1].split("/").pop().toLowerCase();
7324
+ if (!OUTPUT_CMDS.has(cmd)) return text;
7325
+ return text.replace(QUOTED_STR_RE, "");
7326
+ }
7327
+ function decodeUrlAndNumeric(text) {
7328
+ const out = [];
7329
+ let s = text;
7330
+ for (let i = 0; i < 3; i++) {
7331
+ let nxt = s;
7332
+ try {
7333
+ nxt = decodeURIComponent(s);
7334
+ } catch {
7335
+ break;
7336
+ }
7337
+ if (nxt === s) break;
7338
+ s = nxt;
7339
+ }
7340
+ if (s !== text) out.push(s);
7341
+ if (/\\[0-7]{3}/.test(text)) {
7342
+ let expanded = "";
7343
+ try {
7344
+ expanded = text.replace(/\\[0-7]{3}/g, (m) => String.fromCharCode(parseInt(m.slice(1), 8)));
7345
+ } catch {
7346
+ }
7347
+ if (expanded && expanded !== text) out.push(expanded);
7348
+ }
7349
+ if (/\\x[0-9a-fA-F]{2}/.test(text)) {
7350
+ let expanded = "";
7351
+ try {
7352
+ expanded = text.replace(/\\x[0-9a-fA-F]{2}/g, (m) => String.fromCharCode(parseInt(m.slice(2), 16)));
7353
+ } catch {
7354
+ }
7355
+ if (expanded && expanded !== text) out.push(expanded);
7356
+ }
7357
+ return out;
7358
+ }
7359
+ function allCandidates(detail, max = 8) {
7360
+ const shell = stripStringLiterals(detail);
7361
+ const cands = [shell];
7362
+ cands.push(...decodeBase64Literals(shell));
7363
+ cands.push(...decodeUrlAndNumeric(shell));
7364
+ cands.push(...decodeBase64Literals(detail));
7365
+ cands.push(...decodeUrlAndNumeric(detail));
7366
+ const seen = /* @__PURE__ */ new Set();
7367
+ const out = [];
7368
+ for (const c of cands) {
7369
+ if (seen.has(c) || c.length > 4096) continue;
7370
+ seen.add(c);
7371
+ out.push(c);
7372
+ if (out.length >= max) break;
7373
+ }
7374
+ return out;
7375
+ }
7376
+ function matchStageAny(detail, stages) {
7377
+ for (const cand of allCandidates(detail)) {
7378
+ for (const [name, pats] of Object.entries(stages)) {
7379
+ for (const p of pats) {
7380
+ p.lastIndex = 0;
7381
+ if (p.test(cand)) return name;
7382
+ }
7383
+ }
7384
+ }
7385
+ return "";
7386
+ }
7144
7387
 
7145
7388
  // src/engine/formal-engine/ltl.ts
7146
7389
  init_esm_shims();
@@ -7174,11 +7417,7 @@ function compileAps(apPatterns) {
7174
7417
  }
7175
7418
  function labelEvent(actionDetail) {
7176
7419
  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
- }
7420
+ const candidates = allCandidates(actionDetail);
7182
7421
  const hits = /* @__PURE__ */ new Set();
7183
7422
  for (const [name, pats] of Object.entries(AP_RE || {})) {
7184
7423
  for (const c of candidates) {
@@ -7505,7 +7744,7 @@ function checkSensitiveDataFlow(graph) {
7505
7744
  const n = nodes[i];
7506
7745
  if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
7507
7746
  violations.push({
7508
- propertyId: "sensitive-data-exfil",
7747
+ propertyId: "sensitive-data-flow",
7509
7748
  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
7749
  riskLevel: "critical",
7511
7750
  path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
@@ -7556,7 +7795,7 @@ function checkNeverExfilAfterPrivesc(graph) {
7556
7795
  if (pre) {
7557
7796
  const post = nodes[firstEv];
7558
7797
  return [{
7559
- propertyId: "no-privesc-then-exfil",
7798
+ propertyId: "no-exfil-after-privesc",
7560
7799
  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
7800
  riskLevel: "critical",
7562
7801
  path: [
@@ -7572,7 +7811,7 @@ function checkNeverExfilAfterPrivesc(graph) {
7572
7811
  for (let j = i + 1; j < nodes.length; j++) {
7573
7812
  if (isExfilNode(nodes[j])) {
7574
7813
  violations.push({
7575
- propertyId: "no-privesc-then-exfil",
7814
+ propertyId: "no-exfil-after-privesc",
7576
7815
  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
7816
  riskLevel: "critical",
7578
7817
  path: [
@@ -7605,7 +7844,7 @@ function checkNeverExfilAfterCred(graph) {
7605
7844
  if (pre) {
7606
7845
  const post = nodes[firstEv];
7607
7846
  return [{
7608
- propertyId: "no-cred-then-exfil",
7847
+ propertyId: "no-exfil-after-credential",
7609
7848
  reason: "\u8BFB\u53D6\u51ED\u636E\u540E\u51FA\u73B0\u5916\u4F20\uFF0C\u8FDD\u53CD \u25A1(CRED\u2192\xAC\u25C7EXFIL)",
7610
7849
  riskLevel: "critical",
7611
7850
  path: [
@@ -7621,7 +7860,7 @@ function checkNeverExfilAfterCred(graph) {
7621
7860
  for (let j = i + 1; j < nodes.length; j++) {
7622
7861
  if (isExfilNode(nodes[j])) {
7623
7862
  violations.push({
7624
- propertyId: "no-cred-then-exfil",
7863
+ propertyId: "no-exfil-after-credential",
7625
7864
  reason: "\u8BFB\u53D6\u51ED\u636E\u540E\u51FA\u73B0\u5916\u4F20\uFF0C\u8FDD\u53CD \u25A1(CRED\u2192\xAC\u25C7EXFIL)",
7626
7865
  riskLevel: "critical",
7627
7866
  path: [
@@ -7691,7 +7930,7 @@ function checkLowIntegrityCommandExec(graph) {
7691
7930
  const n = nodes[i];
7692
7931
  if (lowAt.has(i) && isComputeNode(n)) {
7693
7932
  violations.push({
7694
- propertyId: "low-integrity-controls-command-exec",
7933
+ propertyId: "low-integrity-command-exec",
7695
7934
  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
7935
  riskLevel: "critical",
7697
7936
  path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
@@ -7738,7 +7977,7 @@ function checkCrossSessionReachability(graph) {
7738
7977
  const srcIdx = Math.min(...sources);
7739
7978
  const srcDesc = nodes[srcIdx].actionDetail.slice(0, 40);
7740
7979
  violations.push({
7741
- propertyId: "cross-session-reachability",
7980
+ propertyId: "low-integrity-reachability",
7742
7981
  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
7982
  riskLevel: "high",
7744
7983
  path: nodes.slice(-3).map((m) => ({ sessionId: m.sessionId, seq: m.seq, detail: m.actionDetail.slice(0, 60) }))
@@ -7790,17 +8029,17 @@ function checkSensitiveDataAggregation(graph) {
7790
8029
  return violations;
7791
8030
  }
7792
8031
  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 },
8032
+ { id: "cross-session-chain", check: checkCrossSessionChain, shortName: "P1" },
8033
+ { id: "sensitive-data-flow", check: checkSensitiveDataFlow, shortName: "P2" },
8034
+ { id: "low-integrity-reachability", check: checkCrossSessionReachability, shortName: "P3" },
8035
+ { id: "no-exfil-after-privesc", check: checkNeverExfilAfterPrivesc, shortName: "P4" },
8036
+ { id: "no-exfil-after-credential", check: checkNeverExfilAfterCred, shortName: "P5" },
7798
8037
  // 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 }
8038
+ { id: "mc-ltl-rules", check: checkMCRules, shortName: "P6" },
8039
+ // P7 低完整性控制命令执行(integrity 维度,站点 L2.1 最有区分度的检测之一)
8040
+ { id: "low-integrity-command-exec", check: checkLowIntegrityCommandExec, shortName: "P7" },
8041
+ // P8 敏感数据聚合外传(egress 聚合 ≥2 事件源)
8042
+ { id: "sensitive-data-aggregation", check: checkSensitiveDataAggregation, shortName: "P8" }
7804
8043
  ];
7805
8044
 
7806
8045
  // src/engine/formal-engine/engine.ts
@@ -7863,11 +8102,41 @@ var FormalEngine = class {
7863
8102
  // src/engine/formal-engine/stateful.ts
7864
8103
  init_esm_shims();
7865
8104
  init_rules_loader();
7866
- init_decode();
7867
8105
  var DESTROY_RE = /\b(?:pkill|kill)\b|systemctl\s+stop|service\s+stop|crontab\s+-r|rm\s+-[rf]?\s+/i;
7868
8106
  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
8107
  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;
8108
+ var EXFIL_RE2 = /https?:\/\/(?!127\.\d+\.\d+\.\d+|0\.0\.0\.0|localhost|\[?::1\]?)|scp\s+.*@|rsync\s+.*@/i;
8109
+ var PORT_RE = /(?:-p\s+(\d{1,5})|-p(\d{1,5})|:(\d{1,5})\b)/i;
8110
+ var BARE_PORT_RE = /(?<=^|\s)(\d{1,5})(?=\s|$)/g;
8111
+ var SOCK_RE = /([\w./-]*(?:\/tmp\/|\/var\/run\/|\.sock\b)[\w./-]*)/;
8112
+ var UNIT_RE = /systemctl\s+(?:start|enable|restart)\s+([\w.-]+)/i;
8113
+ var PORT_STAGE_RE = /LISTEN|PERSIST|SCAN/;
8114
+ function extractInstances(actionDetail, stage) {
8115
+ const out = [];
8116
+ if (PORT_STAGE_RE.test(stage)) {
8117
+ const d = actionDetail;
8118
+ let m;
8119
+ const re = new RegExp(PORT_RE.source, "gi");
8120
+ while ((m = re.exec(d)) !== null) {
8121
+ const p = m[1] || m[2] || m[3];
8122
+ if (p && Number(p) > 0 && Number(p) <= 65535) out.push(`port:${p}`);
8123
+ }
8124
+ if (/SCAN/.test(stage)) {
8125
+ const bareRe = new RegExp(BARE_PORT_RE.source, "g");
8126
+ while ((m = bareRe.exec(d)) !== null) {
8127
+ if (m[1] && Number(m[1]) > 0 && Number(m[1]) <= 65535) out.push(`port:${m[1]}`);
8128
+ }
8129
+ }
8130
+ m = SOCK_RE.exec(d);
8131
+ if (m && m[1]) out.push(`sock:${m[1]}`);
8132
+ m = UNIT_RE.exec(d);
8133
+ if (m && m[1]) out.push(`unit:${m[1]}`);
8134
+ }
8135
+ return out;
8136
+ }
8137
+ function buckKey(chainId, inst) {
8138
+ return inst ? `${chainId}@@${inst}` : chainId;
8139
+ }
7871
8140
  var STAGES_PATTERNS = null;
7872
8141
  var CHAINS = null;
7873
8142
  function loadRules() {
@@ -7910,7 +8179,11 @@ function lifecycleFor(detail, stage, isError) {
7910
8179
  return "";
7911
8180
  }
7912
8181
  var MAX_NODES = 1e5;
7913
- var StatefulChainTracker = class {
8182
+ var StatefulChainTracker = class _StatefulChainTracker {
8183
+ /** 本地接收时刻(实时路径无 called_at 时的兜底时间戳,ISO)。 */
8184
+ static now() {
8185
+ return (/* @__PURE__ */ new Date()).toISOString();
8186
+ }
7914
8187
  agentId;
7915
8188
  nodes_ = [];
7916
8189
  ptr = {};
@@ -7932,10 +8205,14 @@ var StatefulChainTracker = class {
7932
8205
  for (const chain of chains) {
7933
8206
  const stages = chain.stages || [];
7934
8207
  if (stages.length === 0) continue;
7935
- const matched = this.ptr[String(chain.id || "")] || 0;
8208
+ const cid = String(chain.id || "");
8209
+ let matched = this.ptr[cid] || 0;
8210
+ for (const [k, v] of Object.entries(this.ptr)) {
8211
+ if (k.startsWith(`${cid}@@`) && v > matched && v < stages.length) matched = v;
8212
+ }
7936
8213
  if (matched > 0 && matched < stages.length) {
7937
8214
  const pf = {
7938
- chainId: String(chain.id || ""),
8215
+ chainId: cid,
7939
8216
  stages,
7940
8217
  matched,
7941
8218
  riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
@@ -8011,8 +8288,10 @@ var StatefulChainTracker = class {
8011
8288
  actionDetail: ad,
8012
8289
  stage,
8013
8290
  resources: extractResources2(ad),
8291
+ instances: extractInstances(ad, stage),
8014
8292
  isError,
8015
- lifecycle: lifecycleFor(ad, stage, isError)
8293
+ lifecycle: lifecycleFor(ad, stage, isError),
8294
+ timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
8016
8295
  };
8017
8296
  this.nodes_.push(node);
8018
8297
  this.seqNext += 1;
@@ -8020,11 +8299,19 @@ var StatefulChainTracker = class {
8020
8299
  }
8021
8300
  recordLifecycle(node) {
8022
8301
  if (node.lifecycle === "dead") {
8023
- let hit = false;
8302
+ const clearedInsts = [];
8303
+ for (const inst of node.instances) {
8304
+ const key = `inst:${inst}`;
8305
+ if (key in this.aliveResources) {
8306
+ delete this.aliveResources[key];
8307
+ clearedInsts.push(inst);
8308
+ }
8309
+ }
8310
+ let pathHit = false;
8024
8311
  for (const r of node.resources) {
8025
8312
  if (r in this.aliveResources) {
8026
8313
  delete this.aliveResources[r];
8027
- hit = true;
8314
+ pathHit = true;
8028
8315
  }
8029
8316
  }
8030
8317
  const detail = node.actionDetail;
@@ -8032,16 +8319,30 @@ var StatefulChainTracker = class {
8032
8319
  const base = r.split("/").pop() || "";
8033
8320
  if (base && detail.includes(base)) {
8034
8321
  delete this.aliveResources[r];
8035
- hit = true;
8322
+ pathHit = true;
8036
8323
  }
8037
8324
  }
8038
- if (hit) this.resetChainsOnDestroy();
8325
+ if (clearedInsts.length > 0 && !pathHit) {
8326
+ for (const inst of clearedInsts) this.resetChainsForInstance(inst);
8327
+ } else {
8328
+ this.resetChainsOnDestroy();
8329
+ }
8039
8330
  return;
8040
8331
  }
8041
8332
  if (node.lifecycle === "alive") {
8042
8333
  for (const r of node.resources) {
8043
8334
  this.aliveResources[r] = node.seq;
8044
8335
  }
8336
+ for (const inst of node.instances) {
8337
+ this.aliveResources[`inst:${inst}`] = node.seq;
8338
+ }
8339
+ }
8340
+ }
8341
+ /** 只重置指定实例在全部链上的进度(不影响其他端口/资源)。 */
8342
+ resetChainsForInstance(inst) {
8343
+ const key = buckKey("", inst).slice(2);
8344
+ for (const k of Object.keys(this.ptr)) {
8345
+ if (k.endsWith(`@@${inst}`)) this.ptr[k] = 0;
8045
8346
  }
8046
8347
  }
8047
8348
  resetChainsOnDestroy() {
@@ -8049,6 +8350,29 @@ var StatefulChainTracker = class {
8049
8350
  this.ptr[cid] = 0;
8050
8351
  }
8051
8352
  }
8353
+ /**
8354
+ * 当前节点应推进的链桶集合。
8355
+ * - 有实例节点(LISTEN/PERSIST 带端口/socket/unit)→ 只推自己的实例桶;
8356
+ * 同时为所有链的该实例建立桶(若首次出现)。
8357
+ * - 无实例节点(EXEC/SCAN 等中间段)→ 推全局桶 + 所有当前活跃实例桶
8358
+ * (信息不足时保守推进一切可能,避免漏判)。
8359
+ */
8360
+ targetsForNode(node, chainId) {
8361
+ const insts = node.instances;
8362
+ if (insts.length > 0) {
8363
+ const active = insts.filter((i) => this.aliveResources[`inst:${i}`] !== void 0);
8364
+ if (active.length > 0) return active.map((i) => buckKey(chainId, i));
8365
+ return [chainId];
8366
+ }
8367
+ const targets = [chainId];
8368
+ for (const k of Object.keys(this.aliveResources)) {
8369
+ if (k.startsWith("inst:")) {
8370
+ const inst = k.slice("inst:".length);
8371
+ targets.push(`${chainId}@@${inst}`);
8372
+ }
8373
+ }
8374
+ return targets;
8375
+ }
8052
8376
  advanceChains(node) {
8053
8377
  if (node.lifecycle === "dead") return null;
8054
8378
  if (node.lifecycle === "never_activated") {
@@ -8062,23 +8386,32 @@ var StatefulChainTracker = class {
8062
8386
  const stages = chain.stages || [];
8063
8387
  if (stages.length === 0) continue;
8064
8388
  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
- };
8389
+ const targets = this.targetsForNode(node, cid);
8390
+ for (const key of targets) {
8391
+ const cur = this.ptr[key] || 0;
8392
+ if (cur < stages.length && stages[cur] === stage) {
8393
+ this.ptr[key] = cur + 1;
8394
+ if (cur + 1 >= stages.length) {
8395
+ return {
8396
+ chainId: cid,
8397
+ stages,
8398
+ riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
8399
+ reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
8400
+ path: this.chainPath(stages)
8401
+ };
8402
+ }
8076
8403
  }
8077
8404
  }
8078
8405
  }
8079
8406
  return null;
8080
8407
  }
8081
8408
  invalidateChainOnFailure(node) {
8409
+ if (node.instances.length > 0) {
8410
+ for (const inst of node.instances) {
8411
+ this.resetChainsForInstance(inst);
8412
+ }
8413
+ return;
8414
+ }
8082
8415
  const failed = new Set(node.resources);
8083
8416
  if (failed.size === 0) return;
8084
8417
  const { chains } = loadRules();
@@ -8094,7 +8427,7 @@ var StatefulChainTracker = class {
8094
8427
  let want = stages[0];
8095
8428
  for (const n of this.nodes_) {
8096
8429
  if (n.stage === want) {
8097
- path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage });
8430
+ path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage, timestamp: n.timestamp });
8098
8431
  const idx = stages.indexOf(n.stage);
8099
8432
  if (idx + 1 < stages.length) {
8100
8433
  want = stages[idx + 1];
@@ -8130,7 +8463,7 @@ var StatefulChainTracker = class {
8130
8463
  if (!this.tainted.has(node.seq)) return [];
8131
8464
  if (EXFIL_RE2.test(node.actionDetail)) {
8132
8465
  return [{
8133
- property_id: "sensitive-data-exfil",
8466
+ property_id: "sensitive-data-flow",
8134
8467
  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
8468
  risk_level: "critical",
8136
8469
  path: [[node.sessionId, node.seq, node.actionDetail.slice(0, 60)]]
@@ -8172,8 +8505,12 @@ var StatefulChainTracker = class {
8172
8505
  actionDetail: String(raw.actionDetail || ""),
8173
8506
  stage: String(raw.stage || ""),
8174
8507
  resources: [...raw.resources || []],
8508
+ instances: [...raw.instances || []],
8509
+ // 旧快照无此字段 → 空
8175
8510
  isError: Boolean(raw.isError),
8176
- lifecycle: String(raw.lifecycle || "")
8511
+ lifecycle: String(raw.lifecycle || ""),
8512
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : null
8513
+ // 旧快照无此字段 → null
8177
8514
  });
8178
8515
  }
8179
8516
  this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
@@ -8402,7 +8739,7 @@ async function runAnalysis(input) {
8402
8739
  riskType: node.l1RiskType,
8403
8740
  confidence: 90,
8404
8741
  source: "heuristic_script_scan",
8405
- violatedLayers: ["1.1-script"]
8742
+ violatedLayers: ["L1.2"]
8406
8743
  };
8407
8744
  }
8408
8745
  }
@@ -8520,7 +8857,7 @@ async function runAnalysis(input) {
8520
8857
  riskType: inlineJudgment.riskType,
8521
8858
  confidence: 90,
8522
8859
  source: "logic_rule",
8523
- violatedLayers: ["1.2"]
8860
+ violatedLayers: ["L2.1"]
8524
8861
  };
8525
8862
  }
8526
8863
  return {
@@ -8553,7 +8890,7 @@ async function runAnalysis(input) {
8553
8890
  console.warn(
8554
8891
  `[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
8892
  );
8556
- observedFormalLayers.push("2.4");
8893
+ observedFormalLayers.push("L2.2");
8557
8894
  } else {
8558
8895
  return {
8559
8896
  allowed: false,
@@ -8562,7 +8899,7 @@ async function runAnalysis(input) {
8562
8899
  riskType: "formal_verification",
8563
8900
  confidence: 90,
8564
8901
  source: "formal_engine",
8565
- violatedLayers: ["2.4"]
8902
+ violatedLayers: ["L2.2"]
8566
8903
  };
8567
8904
  }
8568
8905
  }
@@ -8579,7 +8916,7 @@ async function runAnalysis(input) {
8579
8916
  riskType: node.l2RiskType,
8580
8917
  confidence: 85,
8581
8918
  source: "logic_rule_script_scan",
8582
- violatedLayers: ["1.2-script"]
8919
+ violatedLayers: ["L2.3"]
8583
8920
  };
8584
8921
  }
8585
8922
  }
@@ -8666,7 +9003,7 @@ async function runAnalysis(input) {
8666
9003
  riskType: "",
8667
9004
  confidence: 60,
8668
9005
  source: "llm_failure_fallback",
8669
- violatedLayers: ["3"]
9006
+ violatedLayers: ["L3.1"]
8670
9007
  };
8671
9008
  }
8672
9009
  if (!llmResult.allowed && llmResult.confidence && llmResult.confidence >= 90 && _brainExtractor) {
@@ -9035,7 +9372,7 @@ async function analyzeRoute(c) {
9035
9372
  const judgment = incMatchedToDeny(inc);
9036
9373
  if (judgment) {
9037
9374
  const layers = [...result.violatedLayers];
9038
- if (!layers.includes("2.4-stateful")) layers.push("2.4-stateful");
9375
+ if (!layers.includes("L2.2-stateful")) layers.push("L2.2-stateful");
9039
9376
  console.error(
9040
9377
  `[AgentMoss] stateful \u589E\u91CF\u62E6\u622A: agent=${agentId || "-"} property=${judgment.property_id} risk=${judgment.risk_level}`
9041
9378
  );
@@ -9049,7 +9386,8 @@ async function analyzeRoute(c) {
9049
9386
  policy: "",
9050
9387
  violated_policy: "",
9051
9388
  confidence: 90,
9052
- analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10
9389
+ analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
9390
+ violation_chain: judgment.violation_chain
9053
9391
  })
9054
9392
  );
9055
9393
  }
@@ -9088,17 +9426,20 @@ function getChainStateStoreFor(_agentId) {
9088
9426
  }
9089
9427
  function incMatchedToDeny(inc) {
9090
9428
  if (inc.matched) {
9429
+ const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp]);
9091
9430
  return {
9092
9431
  property_id: "cross-" + inc.matched.chainId,
9093
9432
  reason: inc.matched.reason,
9094
- risk_level: inc.matched.riskLevel
9433
+ risk_level: inc.matched.riskLevel,
9434
+ violation_chain: chain
9095
9435
  };
9096
9436
  }
9097
9437
  if (inc.exfil && inc.exfil.length > 0) {
9098
9438
  return {
9099
- property_id: "sensitive-data-exfil",
9439
+ property_id: "sensitive-data-flow",
9100
9440
  reason: inc.exfil[0].reason,
9101
- risk_level: inc.exfil[0].risk_level
9441
+ risk_level: inc.exfil[0].risk_level,
9442
+ violation_chain: []
9102
9443
  };
9103
9444
  }
9104
9445
  return null;
@@ -9224,7 +9565,7 @@ function createConsoleRouter() {
9224
9565
  const rc = updateLayerEnabled(body.layer_key, body.enabled);
9225
9566
  return c.json({ layers: rc.layers, env_overrides: getEnvOverrides() });
9226
9567
  });
9227
- function getL24View() {
9568
+ function getL22View() {
9228
9569
  const ltlHuman = {
9229
9570
  "mc-ltl-001": "\u5148\u8BFB\u53D6\u4E86\u51ED\u636E/\u5BC6\u94A5\uFF0C\u4E4B\u540E\u51FA\u73B0\u7F51\u7EDC\u5916\u4F20",
9230
9571
  "mc-ltl-002": "\u5148\u4E0B\u8F7D/\u83B7\u53D6\u4E86\u6587\u4EF6\uFF0C\u4E4B\u540E\u6267\u884C\u4E86\u6587\u4EF6",
@@ -9255,9 +9596,9 @@ function createConsoleRouter() {
9255
9596
  }))
9256
9597
  };
9257
9598
  }
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;
9599
+ function withL22View(rc) {
9600
+ if (rc && rc.L2_2 && typeof rc.L2_2 === "object" && !Array.isArray(rc.L2_2)) {
9601
+ const cat = rc.L2_2.ltl_rules;
9261
9602
  const rls = cat && cat.rules || [];
9262
9603
  const ltlHumanLocal = {
9263
9604
  "mc-ltl-001": "\u5148\u8BFB\u53D6\u4E86\u51ED\u636E/\u5BC6\u94A5\uFF0C\u4E4B\u540E\u51FA\u73B0\u7F51\u7EDC\u5916\u4F20",
@@ -9272,7 +9613,7 @@ function createConsoleRouter() {
9272
9613
  "mc-ltl-010": "\u5B89\u88C5\u4E86\u6301\u4E45\u5316\u673A\u5236\uFF08crontab/systemd/.bashrc \u7B49\uFF09\uFF0C\u9700\u5BA1\u6279",
9273
9614
  "mc-ctl-001": "\u4EFB\u4F55\u60C5\u51B5\u4E0B\u4E0D\u5141\u8BB8\u6E05\u9664\u65E5\u5FD7\uFF08\u5B89\u5168\u5E95\u7EBF\uFF09"
9274
9615
  };
9275
- rc.L2_4 = {
9616
+ rc.L2_2 = {
9276
9617
  enabled_via: "L2_logic_rules",
9277
9618
  description: "\u65F6\u5E8F\u4E0E\u6570\u636E\u6D41\u653B\u51FB\u68C0\u6D4B\uFF08L2.2 \u6A21\u578B\u68C0\u6D4B\uFF09",
9278
9619
  ltl_rules: rls.map((r) => ({
@@ -9291,22 +9632,22 @@ function createConsoleRouter() {
9291
9632
  }
9292
9633
  console2.get("/api/rules", (c) => {
9293
9634
  const rc = loadRuntimeConfig();
9294
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules, L2_4: getL24View() });
9635
+ return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules, L2_2: getL22View() });
9295
9636
  });
9296
9637
  console2.put("/api/rules/enabled", async (c) => {
9297
9638
  const b = await c.req.json();
9298
9639
  const rc = updateRuleEnabled(b.layer, b.category, b.rule_id, b.enabled);
9299
- return c.json(withL24View(rc));
9640
+ return c.json(withL22View(rc));
9300
9641
  });
9301
9642
  console2.put("/api/rules/deny_mode", async (c) => {
9302
9643
  const b = await c.req.json();
9303
9644
  const rc = updateRuleDenyMode(b.layer, b.category, b.rule_id, b.deny_mode);
9304
- return c.json(withL24View(rc));
9645
+ return c.json(withL22View(rc));
9305
9646
  });
9306
9647
  console2.put("/api/rules/skip_l3", async (c) => {
9307
9648
  const b = await c.req.json();
9308
9649
  const rc = updateRuleSkipL3(b.layer, b.category, b.rule_id, b.skip_l3_on_disabled);
9309
- return c.json(withL24View(rc));
9650
+ return c.json(withL22View(rc));
9310
9651
  });
9311
9652
  console2.put("/api/categories/enabled", async (c) => {
9312
9653
  const b = await c.req.json();
@@ -9456,7 +9797,7 @@ function createConsoleRouter() {
9456
9797
  });
9457
9798
  console2.get("/api/config", (c) => {
9458
9799
  const rc = loadRuntimeConfig();
9459
- return c.json({ ...rc, L2_4: getL24View() });
9800
+ return c.json({ ...rc, L2_2: getL22View() });
9460
9801
  });
9461
9802
  console2.get("/api/env-overrides", (c) => {
9462
9803
  return c.json({ overrides: getEnvOverrides() });
@@ -9536,4 +9877,14 @@ export {
9536
9877
  //! 顺序即 fail-closed 短路优先级:verify 逐个执行,首个返回违规的性质成为
9537
9878
  //! first_violation,后续不再评估。调整顺序影响判定归属,须与 Python
9538
9879
  //! properties.py ALL_PROPERTIES 保持一致并双端测试确认。
9880
+ //!
9881
+ //! shortName(P1~P8 连续短名)与 id(语义化英文)对照(与 Python 一致):
9882
+ //! P1 cross-session-chain 跨会话组合链
9883
+ //! P2 sensitive-data-flow 敏感数据传播
9884
+ //! P3 low-integrity-reachability 低完整性数据流可达
9885
+ //! P4 no-exfil-after-privesc 提权后禁外传
9886
+ //! P5 no-exfil-after-credential 凭据后禁外传
9887
+ //! P6 mc-ltl-rules LTL 模型检测
9888
+ //! P7 low-integrity-command-exec 低完整性命令执行
9889
+ //! P8 sensitive-data-aggregation 敏感数据聚合外传
9539
9890
  //# sourceMappingURL=server.js.map