@kenhkl/agent-moss 0.17.6 → 0.17.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +398 -42
- package/dist/engine/coordinator.d.ts +38 -0
- package/dist/engine/coordinator.js +129 -17
- package/dist/server.js +398 -42
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1104,6 +1104,20 @@ function redactChain(chain, currentSessionId) {
|
|
|
1104
1104
|
return step;
|
|
1105
1105
|
});
|
|
1106
1106
|
}
|
|
1107
|
+
function redactGraph(graph, currentSessionId) {
|
|
1108
|
+
if (!graph) return null;
|
|
1109
|
+
if (chainDetailMode() === "full") return graph;
|
|
1110
|
+
const nodes = (graph.nodes || []).map((n) => {
|
|
1111
|
+
const m = { ...n };
|
|
1112
|
+
if (String(m.session_id ?? "") !== String(currentSessionId)) {
|
|
1113
|
+
m.session_id = "<other-session>";
|
|
1114
|
+
m.action_detail = "<\u5DF2\u9690\u85CF>";
|
|
1115
|
+
m.resources = [];
|
|
1116
|
+
}
|
|
1117
|
+
return m;
|
|
1118
|
+
});
|
|
1119
|
+
return { nodes, edges: graph.edges || [] };
|
|
1120
|
+
}
|
|
1107
1121
|
function sessionSweepThreshold() {
|
|
1108
1122
|
return envInt("AGENT_MOSS_SESSION_SWEEP_THRESHOLD", 50);
|
|
1109
1123
|
}
|
|
@@ -1186,6 +1200,12 @@ function agentInflight() {
|
|
|
1186
1200
|
function boundAgentOf(principal) {
|
|
1187
1201
|
return principal.startsWith("agent:") ? principal.slice("agent:".length) : "";
|
|
1188
1202
|
}
|
|
1203
|
+
function chainScope() {
|
|
1204
|
+
return (env("AGENT_MOSS_CHAIN_SCOPE", "system") || "system").toLowerCase();
|
|
1205
|
+
}
|
|
1206
|
+
function chainBucket(agentId = "") {
|
|
1207
|
+
return chainScope() === "agent" ? agentId || "default" : "__system__";
|
|
1208
|
+
}
|
|
1189
1209
|
function rateLimitQps() {
|
|
1190
1210
|
return envInt("AGENT_MOSS_RATE_LIMIT_QPS", 100);
|
|
1191
1211
|
}
|
|
@@ -2749,7 +2769,7 @@ function _scenarioRulesToDefaults() {
|
|
|
2749
2769
|
if (!existsSync7(scenDir)) return out;
|
|
2750
2770
|
let entries;
|
|
2751
2771
|
try {
|
|
2752
|
-
entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2772
|
+
entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("__")).map((d) => d.name);
|
|
2753
2773
|
} catch {
|
|
2754
2774
|
return out;
|
|
2755
2775
|
}
|
|
@@ -8864,7 +8884,7 @@ function markIntegrity(actionDetail) {
|
|
|
8864
8884
|
}
|
|
8865
8885
|
return { integrity: "medium", isLowSource: false };
|
|
8866
8886
|
}
|
|
8867
|
-
function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
|
|
8887
|
+
function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "") {
|
|
8868
8888
|
const { integrity, isLowSource } = markIntegrity(actionDetail);
|
|
8869
8889
|
return {
|
|
8870
8890
|
sessionId,
|
|
@@ -8872,13 +8892,14 @@ function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
|
|
|
8872
8892
|
actionType,
|
|
8873
8893
|
actionDetail,
|
|
8874
8894
|
stage: markStage(actionDetail),
|
|
8895
|
+
agentId: agentId || void 0,
|
|
8875
8896
|
resources: extractResources(actionDetail),
|
|
8876
8897
|
integrity,
|
|
8877
8898
|
isLowSource,
|
|
8878
8899
|
timestamp: timestamp || void 0
|
|
8879
8900
|
};
|
|
8880
8901
|
}
|
|
8881
|
-
function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
|
|
8902
|
+
function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "") {
|
|
8882
8903
|
const nodes = [];
|
|
8883
8904
|
const edges = [];
|
|
8884
8905
|
const stageFlags = /* @__PURE__ */ new Set();
|
|
@@ -8888,18 +8909,20 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
|
|
|
8888
8909
|
if (ad) details.push({
|
|
8889
8910
|
actionType: String(a.actionType || ""),
|
|
8890
8911
|
actionDetail: ad,
|
|
8891
|
-
calledAt: a.calledAt || void 0
|
|
8912
|
+
calledAt: a.calledAt || void 0,
|
|
8913
|
+
agentId: a.agentId || agentId || void 0
|
|
8892
8914
|
});
|
|
8893
8915
|
}
|
|
8894
8916
|
if (aNext) {
|
|
8895
8917
|
details.push({
|
|
8896
8918
|
actionType: String(aNext.actionType || ""),
|
|
8897
8919
|
actionDetail: String(aNext.actionDetail || ""),
|
|
8898
|
-
calledAt: aNext.calledAt || void 0
|
|
8920
|
+
calledAt: aNext.calledAt || void 0,
|
|
8921
|
+
agentId: aNext.agentId || agentId || void 0
|
|
8899
8922
|
});
|
|
8900
8923
|
}
|
|
8901
8924
|
details.forEach((d, seq) => {
|
|
8902
|
-
const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt);
|
|
8925
|
+
const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "");
|
|
8903
8926
|
nodes.push(node);
|
|
8904
8927
|
if (node.stage) stageFlags.add(node.stage);
|
|
8905
8928
|
});
|
|
@@ -8916,7 +8939,7 @@ function buildMultiSessionGraph(sessions, aNext) {
|
|
|
8916
8939
|
}
|
|
8917
8940
|
for (const s of sessions) {
|
|
8918
8941
|
const sid = String(s.sessionId || "s");
|
|
8919
|
-
mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid));
|
|
8942
|
+
mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid, s.agentId || ""));
|
|
8920
8943
|
}
|
|
8921
8944
|
if (aNext) {
|
|
8922
8945
|
const last = mg.sessions[mg.sessions.length - 1];
|
|
@@ -8926,7 +8949,8 @@ function buildMultiSessionGraph(sessions, aNext) {
|
|
|
8926
8949
|
last.nodes.length,
|
|
8927
8950
|
String(aNext.actionType || ""),
|
|
8928
8951
|
String(aNext.actionDetail || ""),
|
|
8929
|
-
aNext.calledAt || void 0
|
|
8952
|
+
aNext.calledAt || void 0,
|
|
8953
|
+
aNext.agentId || ""
|
|
8930
8954
|
));
|
|
8931
8955
|
if (last.nodes.length > 1) {
|
|
8932
8956
|
last.edges.push({ src: last.nodes.length - 2, dst: last.nodes.length - 1, kind: "next" });
|
|
@@ -8984,6 +9008,33 @@ var init_builder = __esm({
|
|
|
8984
9008
|
});
|
|
8985
9009
|
|
|
8986
9010
|
// src/engine/formal-engine/properties.ts
|
|
9011
|
+
function graphFromPath(nodes, path6, edgeType = "next", propertyMeta, roles, witnessLast = true, propertyExpression) {
|
|
9012
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
9013
|
+
for (const n of nodes) byKey.set(`${n.sessionId}#${n.seq}`, n);
|
|
9014
|
+
const gNodes = [];
|
|
9015
|
+
const gEdges = [];
|
|
9016
|
+
for (const item of path6) {
|
|
9017
|
+
const n = byKey.get(`${item.sessionId}#${item.seq}`);
|
|
9018
|
+
gNodes.push({
|
|
9019
|
+
session_id: item.sessionId,
|
|
9020
|
+
seq: item.seq,
|
|
9021
|
+
stage: n ? n.stage : "",
|
|
9022
|
+
action_detail: n ? n.actionDetail : item.detail,
|
|
9023
|
+
resources: n ? [...n.resources].sort() : []
|
|
9024
|
+
});
|
|
9025
|
+
if (gNodes.length > 1) {
|
|
9026
|
+
gEdges.push({ from: gNodes.length - 2, to: gNodes.length - 1, type: edgeType });
|
|
9027
|
+
}
|
|
9028
|
+
}
|
|
9029
|
+
(roles || []).forEach((role, i) => {
|
|
9030
|
+
if (i < gNodes.length && role) gNodes[i].role = role;
|
|
9031
|
+
});
|
|
9032
|
+
if (witnessLast && gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
9033
|
+
const out = { nodes: gNodes, edges: gEdges };
|
|
9034
|
+
if (propertyMeta) Object.assign(out, propertyMeta);
|
|
9035
|
+
if (propertyExpression) out.property_expression = propertyExpression;
|
|
9036
|
+
return out;
|
|
9037
|
+
}
|
|
8987
9038
|
function parseTs(ts) {
|
|
8988
9039
|
if (!ts) return null;
|
|
8989
9040
|
const t = Date.parse(String(ts).trim());
|
|
@@ -9041,7 +9092,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9041
9092
|
propertyId: "cross-" + chain.id,
|
|
9042
9093
|
reason: chain.reason,
|
|
9043
9094
|
riskLevel: chain.riskLevel,
|
|
9044
|
-
path: path6
|
|
9095
|
+
path: path6,
|
|
9096
|
+
graph: graphFromPath(nodes, path6, "next", {
|
|
9097
|
+
property_id: "cross-" + chain.id,
|
|
9098
|
+
property_short: "P1",
|
|
9099
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
|
|
9100
|
+
}, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
|
|
9045
9101
|
});
|
|
9046
9102
|
return violations;
|
|
9047
9103
|
} else {
|
|
@@ -9061,7 +9117,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9061
9117
|
propertyId: "cross-" + chain.id,
|
|
9062
9118
|
reason: chain.reason,
|
|
9063
9119
|
riskLevel: chain.riskLevel,
|
|
9064
|
-
path: path6
|
|
9120
|
+
path: path6,
|
|
9121
|
+
graph: graphFromPath(nodes, path6, "next", {
|
|
9122
|
+
property_id: "cross-" + chain.id,
|
|
9123
|
+
property_short: "P1",
|
|
9124
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
|
|
9125
|
+
}, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
|
|
9065
9126
|
});
|
|
9066
9127
|
}
|
|
9067
9128
|
}
|
|
@@ -9076,10 +9137,15 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9076
9137
|
for (let i = 0; i < nodes.length; i++) {
|
|
9077
9138
|
const n = nodes[i];
|
|
9078
9139
|
if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
|
|
9079
|
-
tainted.set(i,
|
|
9140
|
+
tainted.set(i, {
|
|
9141
|
+
source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
|
|
9142
|
+
parent: null
|
|
9143
|
+
});
|
|
9080
9144
|
}
|
|
9081
9145
|
}
|
|
9082
9146
|
if (tainted.size === 0) return violations;
|
|
9147
|
+
const taintEdges = /* @__PURE__ */ new Map();
|
|
9148
|
+
const edgeKey = (p, c) => `${p}:${c}`;
|
|
9083
9149
|
const edges = allEdges(graph);
|
|
9084
9150
|
let changed = true;
|
|
9085
9151
|
while (changed) {
|
|
@@ -9090,7 +9156,8 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9090
9156
|
const dstRes = nodes[e.dst].resources;
|
|
9091
9157
|
for (const r of srcRes) {
|
|
9092
9158
|
if (dstRes.has(r)) {
|
|
9093
|
-
tainted.set(e.dst, tainted.get(e.src));
|
|
9159
|
+
tainted.set(e.dst, { source: tainted.get(e.src).source, parent: e.src });
|
|
9160
|
+
taintEdges.set(edgeKey(e.src, e.dst), r);
|
|
9094
9161
|
changed = true;
|
|
9095
9162
|
break;
|
|
9096
9163
|
}
|
|
@@ -9098,16 +9165,75 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9098
9165
|
}
|
|
9099
9166
|
}
|
|
9100
9167
|
}
|
|
9168
|
+
const tracePath = (idx) => {
|
|
9169
|
+
const chain = [idx];
|
|
9170
|
+
const seen = /* @__PURE__ */ new Set([idx]);
|
|
9171
|
+
while (true) {
|
|
9172
|
+
const parent = tainted.get(chain[chain.length - 1])?.parent;
|
|
9173
|
+
if (parent === void 0 || parent === null || seen.has(parent)) break;
|
|
9174
|
+
chain.push(parent);
|
|
9175
|
+
seen.add(parent);
|
|
9176
|
+
}
|
|
9177
|
+
return chain.reverse();
|
|
9178
|
+
};
|
|
9179
|
+
const taintGraph = (chain) => {
|
|
9180
|
+
const gNodes = [];
|
|
9181
|
+
const gEdges = [];
|
|
9182
|
+
chain.forEach((ni, k) => {
|
|
9183
|
+
const n = nodes[ni];
|
|
9184
|
+
const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
|
|
9185
|
+
gNodes.push({
|
|
9186
|
+
session_id: n.sessionId,
|
|
9187
|
+
seq: n.seq,
|
|
9188
|
+
stage: n.stage,
|
|
9189
|
+
action_detail: n.actionDetail,
|
|
9190
|
+
resources: [...n.resources].sort(),
|
|
9191
|
+
role
|
|
9192
|
+
});
|
|
9193
|
+
if (k > 0) {
|
|
9194
|
+
const prev = chain[k - 1];
|
|
9195
|
+
gEdges.push({
|
|
9196
|
+
from: k - 1,
|
|
9197
|
+
to: k,
|
|
9198
|
+
type: "data",
|
|
9199
|
+
label: taintEdges.get(edgeKey(prev, ni)) || ""
|
|
9200
|
+
});
|
|
9201
|
+
}
|
|
9202
|
+
});
|
|
9203
|
+
if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
9204
|
+
const expr = [{ token: "CRED", node_index: 0 }];
|
|
9205
|
+
if (chain.length > 2) {
|
|
9206
|
+
expr.push({
|
|
9207
|
+
token: "\u5171\u4EAB\u8F7D\u4F53",
|
|
9208
|
+
node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
|
|
9209
|
+
});
|
|
9210
|
+
}
|
|
9211
|
+
expr.push({ token: "EXFIL", node_index: chain.length - 1 });
|
|
9212
|
+
return {
|
|
9213
|
+
property_id: "sensitive-data-flow",
|
|
9214
|
+
property_short: "P2",
|
|
9215
|
+
property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
|
|
9216
|
+
property_expression: expr,
|
|
9217
|
+
nodes: gNodes,
|
|
9218
|
+
edges: gEdges
|
|
9219
|
+
};
|
|
9220
|
+
};
|
|
9101
9221
|
const witness = graph.witnessIdx;
|
|
9102
9222
|
const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
|
|
9103
9223
|
for (const i of candidates) {
|
|
9104
9224
|
const n = nodes[i];
|
|
9105
9225
|
if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
|
|
9226
|
+
const chain = tracePath(i);
|
|
9106
9227
|
violations.push({
|
|
9107
9228
|
propertyId: "sensitive-data-flow",
|
|
9108
|
-
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`,
|
|
9229
|
+
reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${tainted.get(i).source} \u2192 ${n.sessionId}#${n.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
|
|
9109
9230
|
riskLevel: "critical",
|
|
9110
|
-
path:
|
|
9231
|
+
path: chain.map((j) => ({
|
|
9232
|
+
sessionId: nodes[j].sessionId,
|
|
9233
|
+
seq: nodes[j].seq,
|
|
9234
|
+
detail: nodes[j].actionDetail.slice(0, 60)
|
|
9235
|
+
})),
|
|
9236
|
+
graph: taintGraph(chain)
|
|
9111
9237
|
});
|
|
9112
9238
|
if (witness !== void 0) return violations;
|
|
9113
9239
|
}
|
|
@@ -9649,6 +9775,12 @@ var init_stateful = __esm({
|
|
|
9649
9775
|
nodes_ = [];
|
|
9650
9776
|
ptr = {};
|
|
9651
9777
|
tainted = /* @__PURE__ */ new Set();
|
|
9778
|
+
/** taint 传播路径:nodeIdx -> 使该节点染毒的前驱 nodeIdx(CRED 源为 null) */
|
|
9779
|
+
taintParent = /* @__PURE__ */ new Map();
|
|
9780
|
+
/** "parent:child" -> 触发传播的共享资源(图上的 data 边标签) */
|
|
9781
|
+
taintEdgeLabel = /* @__PURE__ */ new Map();
|
|
9782
|
+
/** 资源 -> 首次染毒该资源的节点 seq */
|
|
9783
|
+
taintOrigin = /* @__PURE__ */ new Map();
|
|
9652
9784
|
/** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
|
|
9653
9785
|
taintedResources = /* @__PURE__ */ new Set();
|
|
9654
9786
|
aliveResources = {};
|
|
@@ -9713,8 +9845,8 @@ var init_stateful = __esm({
|
|
|
9713
9845
|
}
|
|
9714
9846
|
/** 增量推进:把当前动作追加到时间线 → 链指针推进 → 判定。 */
|
|
9715
9847
|
advance(args) {
|
|
9716
|
-
const { aNext, sessionId = "current", isError = false } = args;
|
|
9717
|
-
const node = this.appendNode(aNext, sessionId, isError);
|
|
9848
|
+
const { aNext, sessionId = "current", isError = false, agentId = "" } = args;
|
|
9849
|
+
const node = this.appendNode(aNext, sessionId, isError, agentId);
|
|
9718
9850
|
if (!node) {
|
|
9719
9851
|
return { seq: this.seqNext, node: null, matched: null, pendingPrefixes: this.pendingPrefixes, exfil: [], shouldBlock: false };
|
|
9720
9852
|
}
|
|
@@ -9734,7 +9866,7 @@ var init_stateful = __esm({
|
|
|
9734
9866
|
}
|
|
9735
9867
|
};
|
|
9736
9868
|
}
|
|
9737
|
-
appendNode(action, sessionId, isError = false) {
|
|
9869
|
+
appendNode(action, sessionId, isError = false, agentId = "") {
|
|
9738
9870
|
const ad = String(action.actionDetail || "");
|
|
9739
9871
|
const at = String(action.actionType || "");
|
|
9740
9872
|
if (!ad) return null;
|
|
@@ -9751,6 +9883,7 @@ var init_stateful = __esm({
|
|
|
9751
9883
|
resources: extractResources2(ad),
|
|
9752
9884
|
instances: extractInstances(ad, stage),
|
|
9753
9885
|
isError,
|
|
9886
|
+
agentId: String(action.agentId || agentId || ""),
|
|
9754
9887
|
lifecycle: lifecycleFor(ad, stage, isError),
|
|
9755
9888
|
timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
|
|
9756
9889
|
};
|
|
@@ -9858,7 +9991,8 @@ var init_stateful = __esm({
|
|
|
9858
9991
|
stages,
|
|
9859
9992
|
riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
|
|
9860
9993
|
reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
|
|
9861
|
-
path: this.chainPath(stages)
|
|
9994
|
+
path: this.chainPath(stages),
|
|
9995
|
+
graph: this.buildChainGraph(stages, cid)
|
|
9862
9996
|
};
|
|
9863
9997
|
}
|
|
9864
9998
|
}
|
|
@@ -9888,7 +10022,13 @@ var init_stateful = __esm({
|
|
|
9888
10022
|
let want = stages[0];
|
|
9889
10023
|
for (const n of this.nodes_) {
|
|
9890
10024
|
if (n.stage === want) {
|
|
9891
|
-
path6.push({
|
|
10025
|
+
path6.push({
|
|
10026
|
+
sessionId: n.sessionId,
|
|
10027
|
+
seq: n.seq,
|
|
10028
|
+
stage: n.stage,
|
|
10029
|
+
timestamp: n.timestamp,
|
|
10030
|
+
agentId: n.agentId
|
|
10031
|
+
});
|
|
9892
10032
|
const idx = stages.indexOf(n.stage);
|
|
9893
10033
|
if (idx + 1 < stages.length) {
|
|
9894
10034
|
want = stages[idx + 1];
|
|
@@ -9899,21 +10039,69 @@ var init_stateful = __esm({
|
|
|
9899
10039
|
}
|
|
9900
10040
|
return path6;
|
|
9901
10041
|
}
|
|
10042
|
+
/** 把链路径转成前端可画的结构化图(相邻节点 next 边)。 */
|
|
10043
|
+
buildChainGraph(stages, chainId) {
|
|
10044
|
+
const path6 = this.chainPath(stages);
|
|
10045
|
+
const gNodes = path6.map((p, i) => {
|
|
10046
|
+
const n = this.nodes_.find((x) => x.sessionId === p.sessionId && x.seq === p.seq);
|
|
10047
|
+
return {
|
|
10048
|
+
session_id: p.sessionId,
|
|
10049
|
+
seq: p.seq,
|
|
10050
|
+
stage: p.stage,
|
|
10051
|
+
action_detail: n ? n.actionDetail : "",
|
|
10052
|
+
resources: n ? [...n.resources].sort() : [],
|
|
10053
|
+
role: stages[i] || ""
|
|
10054
|
+
};
|
|
10055
|
+
});
|
|
10056
|
+
const gEdges = [];
|
|
10057
|
+
for (let i = 1; i < gNodes.length; i++) {
|
|
10058
|
+
gEdges.push({ from: i - 1, to: i, type: "next" });
|
|
10059
|
+
}
|
|
10060
|
+
if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
10061
|
+
return {
|
|
10062
|
+
property_id: "cross-" + chainId,
|
|
10063
|
+
property_short: "P1",
|
|
10064
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE",
|
|
10065
|
+
property_expression: stages.map((s, i) => ({ token: s, node_index: i })),
|
|
10066
|
+
nodes: gNodes,
|
|
10067
|
+
edges: gEdges
|
|
10068
|
+
};
|
|
10069
|
+
}
|
|
9902
10070
|
// ---------------- 敏感数据 taint(P2) ----------------
|
|
9903
10071
|
advanceTaint(node) {
|
|
9904
10072
|
if (node.lifecycle === "dead") return;
|
|
9905
10073
|
const idx = node.seq;
|
|
9906
10074
|
const taintedRes = /* @__PURE__ */ new Set();
|
|
10075
|
+
let parent = null;
|
|
10076
|
+
let label = "";
|
|
10077
|
+
let foundParent = false;
|
|
9907
10078
|
for (const r of node.resources) {
|
|
9908
|
-
if (this.resourceTainted(r))
|
|
10079
|
+
if (this.resourceTainted(r)) {
|
|
10080
|
+
taintedRes.add(r);
|
|
10081
|
+
if (!foundParent) {
|
|
10082
|
+
const origin = this.taintOrigin.get(r);
|
|
10083
|
+
if (origin !== void 0) {
|
|
10084
|
+
parent = origin;
|
|
10085
|
+
label = r;
|
|
10086
|
+
foundParent = true;
|
|
10087
|
+
}
|
|
10088
|
+
}
|
|
10089
|
+
}
|
|
9909
10090
|
}
|
|
9910
10091
|
if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
|
|
9911
10092
|
this.tainted.add(idx);
|
|
10093
|
+
parent = null;
|
|
10094
|
+
label = "";
|
|
9912
10095
|
for (const r of node.resources) taintedRes.add(r);
|
|
9913
10096
|
}
|
|
9914
10097
|
if (taintedRes.size > 0) {
|
|
9915
10098
|
this.tainted.add(idx);
|
|
9916
|
-
|
|
10099
|
+
this.taintParent.set(idx, parent);
|
|
10100
|
+
if (parent !== null) this.taintEdgeLabel.set(`${parent}:${idx}`, label);
|
|
10101
|
+
for (const r of node.resources) {
|
|
10102
|
+
this.taintedResources.add(r);
|
|
10103
|
+
if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, idx);
|
|
10104
|
+
}
|
|
9917
10105
|
}
|
|
9918
10106
|
}
|
|
9919
10107
|
resourceTainted(resource) {
|
|
@@ -9923,16 +10111,84 @@ var init_stateful = __esm({
|
|
|
9923
10111
|
checkExfilAfterTaint(node) {
|
|
9924
10112
|
if (!this.tainted.has(node.seq)) return [];
|
|
9925
10113
|
if (EXFIL_RE2.test(node.actionDetail)) {
|
|
10114
|
+
const chain = this.traceTaintChain(node.seq);
|
|
9926
10115
|
return [{
|
|
9927
10116
|
property_id: "sensitive-data-flow",
|
|
9928
|
-
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`,
|
|
10117
|
+
reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${this.taintSource(node, chain)} \u2192 ${node.sessionId}#${node.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
|
|
9929
10118
|
risk_level: "critical",
|
|
9930
|
-
path:
|
|
10119
|
+
path: chain.map((j) => {
|
|
10120
|
+
const n = this.nodes_[j];
|
|
10121
|
+
return [n.sessionId, n.seq, n.actionDetail.slice(0, 60), n.timestamp];
|
|
10122
|
+
}),
|
|
10123
|
+
graph: this.taintGraph(chain)
|
|
9931
10124
|
}];
|
|
9932
10125
|
}
|
|
9933
10126
|
return [];
|
|
9934
10127
|
}
|
|
9935
|
-
|
|
10128
|
+
/** 沿 taint parent 回溯出 CRED 源 → 当前节点的完整路径。 */
|
|
10129
|
+
traceTaintChain(idx) {
|
|
10130
|
+
const chain = [idx];
|
|
10131
|
+
const seen = /* @__PURE__ */ new Set([idx]);
|
|
10132
|
+
while (true) {
|
|
10133
|
+
const parent = this.taintParent.get(chain[chain.length - 1]);
|
|
10134
|
+
if (parent === void 0 || parent === null || seen.has(parent)) break;
|
|
10135
|
+
chain.push(parent);
|
|
10136
|
+
seen.add(parent);
|
|
10137
|
+
}
|
|
10138
|
+
return chain.reverse();
|
|
10139
|
+
}
|
|
10140
|
+
/** 把 taint 路径转成前端可画的结构化图(data 边带共享资源标签)。 */
|
|
10141
|
+
taintGraph(chain) {
|
|
10142
|
+
const gNodes = [];
|
|
10143
|
+
const gEdges = [];
|
|
10144
|
+
chain.forEach((ni, k) => {
|
|
10145
|
+
const n = this.nodes_[ni];
|
|
10146
|
+
const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
|
|
10147
|
+
gNodes.push({
|
|
10148
|
+
session_id: n.sessionId,
|
|
10149
|
+
seq: n.seq,
|
|
10150
|
+
stage: n.stage,
|
|
10151
|
+
action_detail: n.actionDetail,
|
|
10152
|
+
resources: [...n.resources].sort(),
|
|
10153
|
+
role
|
|
10154
|
+
});
|
|
10155
|
+
if (k > 0) {
|
|
10156
|
+
const prev = chain[k - 1];
|
|
10157
|
+
gEdges.push({
|
|
10158
|
+
from: k - 1,
|
|
10159
|
+
to: k,
|
|
10160
|
+
type: "data",
|
|
10161
|
+
label: this.taintEdgeLabel.get(`${prev}:${ni}`) || ""
|
|
10162
|
+
});
|
|
10163
|
+
}
|
|
10164
|
+
});
|
|
10165
|
+
if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
10166
|
+
const expr = [{ token: "CRED", node_index: 0 }];
|
|
10167
|
+
if (chain.length > 2) {
|
|
10168
|
+
expr.push({
|
|
10169
|
+
token: "\u5171\u4EAB\u8F7D\u4F53",
|
|
10170
|
+
node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
|
|
10171
|
+
});
|
|
10172
|
+
}
|
|
10173
|
+
expr.push({ token: "EXFIL", node_index: chain.length - 1 });
|
|
10174
|
+
return {
|
|
10175
|
+
property_id: "sensitive-data-flow",
|
|
10176
|
+
property_short: "P2",
|
|
10177
|
+
property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
|
|
10178
|
+
property_expression: expr,
|
|
10179
|
+
nodes: gNodes,
|
|
10180
|
+
edges: gEdges
|
|
10181
|
+
};
|
|
10182
|
+
}
|
|
10183
|
+
taintSource(node, chain = []) {
|
|
10184
|
+
if (chain.length > 0) {
|
|
10185
|
+
const src = this.nodes_[chain[0]];
|
|
10186
|
+
if (src) {
|
|
10187
|
+
for (const r of node.resources) {
|
|
10188
|
+
if (src.resources.includes(r)) return `${src.sessionId}#${src.seq} \u8BBF\u95EE ${r}`;
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
9936
10192
|
const sorted = [...this.tainted].sort((a, b) => a - b);
|
|
9937
10193
|
for (const i of sorted) {
|
|
9938
10194
|
if (i === node.seq) continue;
|
|
@@ -9951,6 +10207,9 @@ var init_stateful = __esm({
|
|
|
9951
10207
|
nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
|
|
9952
10208
|
ptrs: { ...this.ptr },
|
|
9953
10209
|
tainted: [...this.tainted].sort((a, b) => a - b),
|
|
10210
|
+
// taint 传播路径(前端攻击链图);边标签不入快照,重启后可从资源交集重建
|
|
10211
|
+
taint_parent: Object.fromEntries([...this.taintParent.entries()].map(([k, v]) => [String(k), v])),
|
|
10212
|
+
taint_origin: Object.fromEntries([...this.taintOrigin.entries()]),
|
|
9954
10213
|
alive: { ...this.aliveResources },
|
|
9955
10214
|
seq_next: this.seqNext
|
|
9956
10215
|
};
|
|
@@ -9962,6 +10221,7 @@ var init_stateful = __esm({
|
|
|
9962
10221
|
this.nodes_.push({
|
|
9963
10222
|
seq: Number(raw.seq || 0),
|
|
9964
10223
|
sessionId: String(raw.sessionId || ""),
|
|
10224
|
+
agentId: String(raw.agentId || ""),
|
|
9965
10225
|
actionType: String(raw.actionType || ""),
|
|
9966
10226
|
actionDetail: String(raw.actionDetail || ""),
|
|
9967
10227
|
stage: String(raw.stage || ""),
|
|
@@ -9977,15 +10237,53 @@ var init_stateful = __esm({
|
|
|
9977
10237
|
this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
|
|
9978
10238
|
this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
|
|
9979
10239
|
this.tainted = new Set((snapshot.tainted || []).map(Number));
|
|
10240
|
+
this.taintParent = new Map(
|
|
10241
|
+
Object.entries(snapshot.taint_parent || {}).map(
|
|
10242
|
+
([k, v]) => [Number(k), v === null || v === void 0 ? null : Number(v)]
|
|
10243
|
+
)
|
|
10244
|
+
);
|
|
10245
|
+
this.taintOrigin = new Map(
|
|
10246
|
+
Object.entries(snapshot.taint_origin || {}).map(
|
|
10247
|
+
([k, v]) => [String(k), Number(v)]
|
|
10248
|
+
)
|
|
10249
|
+
);
|
|
10250
|
+
this.taintEdgeLabel = /* @__PURE__ */ new Map();
|
|
9980
10251
|
this.taintedResources = /* @__PURE__ */ new Set();
|
|
9981
10252
|
for (const i of this.tainted) {
|
|
9982
10253
|
const n = this.nodes_[i];
|
|
9983
10254
|
if (n) for (const r of n.resources) this.taintedResources.add(r);
|
|
9984
10255
|
}
|
|
10256
|
+
if (this.tainted.size > 0 && this.taintParent.size === 0) this.rebuildTaintParent();
|
|
10257
|
+
if (this.taintOrigin.size === 0) {
|
|
10258
|
+
for (const i of [...this.tainted].sort((a, b) => a - b)) {
|
|
10259
|
+
const n = this.nodes_[i];
|
|
10260
|
+
if (!n) continue;
|
|
10261
|
+
for (const r of n.resources) {
|
|
10262
|
+
if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, i);
|
|
10263
|
+
}
|
|
10264
|
+
}
|
|
10265
|
+
}
|
|
9985
10266
|
this.aliveResources = Object.fromEntries(
|
|
9986
10267
|
Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
|
|
9987
10268
|
);
|
|
9988
10269
|
}
|
|
10270
|
+
/** 旧快照无 parent 信息时,按 seq 顺序找最近的资源相交染毒前驱。 */
|
|
10271
|
+
rebuildTaintParent() {
|
|
10272
|
+
for (const i of [...this.tainted].sort((a, b) => a - b)) {
|
|
10273
|
+
const n = this.nodes_[i];
|
|
10274
|
+
if (!n) continue;
|
|
10275
|
+
let parent = null;
|
|
10276
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
10277
|
+
if (!this.tainted.has(j)) continue;
|
|
10278
|
+
const m = this.nodes_[j];
|
|
10279
|
+
if (m && m.resources.some((r) => n.resources.includes(r))) {
|
|
10280
|
+
parent = j;
|
|
10281
|
+
break;
|
|
10282
|
+
}
|
|
10283
|
+
}
|
|
10284
|
+
this.taintParent.set(i, parent);
|
|
10285
|
+
}
|
|
10286
|
+
}
|
|
9989
10287
|
};
|
|
9990
10288
|
}
|
|
9991
10289
|
});
|
|
@@ -10068,9 +10366,9 @@ var init_chain_state = __esm({
|
|
|
10068
10366
|
delete this.data[agentId];
|
|
10069
10367
|
this._scheduleWrite();
|
|
10070
10368
|
}
|
|
10071
|
-
applyAction(agentId, aNext, sessionId, isError = false) {
|
|
10369
|
+
applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "") {
|
|
10072
10370
|
const t = this.load(agentId) || new StatefulChainTracker(agentId);
|
|
10073
|
-
const r = t.advance({ aNext, sessionId, isError });
|
|
10371
|
+
const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent });
|
|
10074
10372
|
this.save(t);
|
|
10075
10373
|
return r;
|
|
10076
10374
|
}
|
|
@@ -10385,10 +10683,14 @@ async function runAnalysis(input) {
|
|
|
10385
10683
|
const formalSessions = [];
|
|
10386
10684
|
if (input.sessionHistory) {
|
|
10387
10685
|
for (const s of input.sessionHistory) {
|
|
10388
|
-
formalSessions.push({
|
|
10686
|
+
formalSessions.push({
|
|
10687
|
+
sessionId: s.sessionId || "s",
|
|
10688
|
+
...s.agentId ? { agentId: s.agentId } : {},
|
|
10689
|
+
actionHistory: s.actionHistory || []
|
|
10690
|
+
});
|
|
10389
10691
|
}
|
|
10390
10692
|
}
|
|
10391
|
-
formalSessions.push({ sessionId: "current", actionHistory: input.actionHistory });
|
|
10693
|
+
formalSessions.push({ sessionId: "current", agentId: input.agentId, actionHistory: input.actionHistory });
|
|
10392
10694
|
const formalResult = formalEngine.verify(formalSessions, input.aNext);
|
|
10393
10695
|
if (!formalResult.verified) {
|
|
10394
10696
|
const v = formalResult.violations[0];
|
|
@@ -10405,7 +10707,9 @@ async function runAnalysis(input) {
|
|
|
10405
10707
|
riskType: "formal_verification",
|
|
10406
10708
|
confidence: 90,
|
|
10407
10709
|
source: "formal_engine",
|
|
10408
|
-
violatedLayers: ["L2.2"]
|
|
10710
|
+
violatedLayers: ["L2.2"],
|
|
10711
|
+
violationGraph: v?.graph ?? null,
|
|
10712
|
+
violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
|
|
10409
10713
|
};
|
|
10410
10714
|
}
|
|
10411
10715
|
}
|
|
@@ -10756,6 +11060,8 @@ var AnalyzeRequestSchema = z.object({
|
|
|
10756
11060
|
action_history: z.array(ActionItemSchema).default([]).describe("Historical action sequence"),
|
|
10757
11061
|
session_history: z.array(z.object({
|
|
10758
11062
|
session_id: z.string(),
|
|
11063
|
+
/** 来源 agent(AgentGate 逐会话带上):记录在链路径里供告警/取证,不参与链匹配。 */
|
|
11064
|
+
agent_id: z.string().optional(),
|
|
10759
11065
|
action_history: z.array(ActionItemSchema).default([])
|
|
10760
11066
|
})).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)."),
|
|
10761
11067
|
a_next: ActionItemSchema.describe("Next action to be executed"),
|
|
@@ -10776,7 +11082,42 @@ var AnalyzeResponseSchema = z.object({
|
|
|
10776
11082
|
confidence: z.number().int().min(0).max(100).default(100),
|
|
10777
11083
|
analysis_duration_ms: z.number().default(0),
|
|
10778
11084
|
// 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
|
|
10779
|
-
violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
|
|
11085
|
+
violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([]),
|
|
11086
|
+
// 结构化攻击链图(前端可视化):节点 + 边(next 时序 / data 数据流)
|
|
11087
|
+
violation_graph: z.object({
|
|
11088
|
+
// 性质元信息(前端展示"违反了哪条性质")
|
|
11089
|
+
property_id: z.string().optional(),
|
|
11090
|
+
property_short: z.string().optional(),
|
|
11091
|
+
property_name: z.string().optional(),
|
|
11092
|
+
// 性质表达式 token → 节点位置(前端展示"表达式对应哪个节点")
|
|
11093
|
+
property_expression: z.array(
|
|
11094
|
+
z.object({
|
|
11095
|
+
token: z.string(),
|
|
11096
|
+
node_index: z.union([z.number(), z.array(z.number())])
|
|
11097
|
+
})
|
|
11098
|
+
).optional(),
|
|
11099
|
+
nodes: z.array(
|
|
11100
|
+
z.object({
|
|
11101
|
+
session_id: z.string().default(""),
|
|
11102
|
+
seq: z.number().default(0),
|
|
11103
|
+
stage: z.string().default(""),
|
|
11104
|
+
action_detail: z.string().default(""),
|
|
11105
|
+
resources: z.array(z.string()).default([]),
|
|
11106
|
+
// 节点在性质中的角色:source/carrier/sink(P2)或阶段名(P1)
|
|
11107
|
+
role: z.string().optional(),
|
|
11108
|
+
// 是否为违例见证节点(当前动作补全链 / 触发外传)
|
|
11109
|
+
witness: z.boolean().optional()
|
|
11110
|
+
})
|
|
11111
|
+
),
|
|
11112
|
+
edges: z.array(
|
|
11113
|
+
z.object({
|
|
11114
|
+
from: z.number(),
|
|
11115
|
+
to: z.number(),
|
|
11116
|
+
type: z.enum(["next", "data"]).default("next"),
|
|
11117
|
+
label: z.string().optional()
|
|
11118
|
+
})
|
|
11119
|
+
)
|
|
11120
|
+
}).nullable().default(null)
|
|
10780
11121
|
});
|
|
10781
11122
|
var HealthResponseSchema = z.object({
|
|
10782
11123
|
status: z.string().default("healthy"),
|
|
@@ -11200,7 +11541,9 @@ async function analyzeInner(input, mossLog) {
|
|
|
11200
11541
|
violated_layers: judgment.violatedLayers || [],
|
|
11201
11542
|
policy: "",
|
|
11202
11543
|
violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
|
|
11203
|
-
confidence: judgment.confidence
|
|
11544
|
+
confidence: judgment.confidence,
|
|
11545
|
+
violation_graph: judgment.violationGraph ?? null,
|
|
11546
|
+
violation_chain: judgment.violationChain ?? []
|
|
11204
11547
|
};
|
|
11205
11548
|
mossLog.logResult("Deny", denied.reason);
|
|
11206
11549
|
return denied;
|
|
@@ -11670,6 +12013,8 @@ async function analyzeRoute(c) {
|
|
|
11670
12013
|
})),
|
|
11671
12014
|
sessionHistory: (req.session_history || []).map((s) => ({
|
|
11672
12015
|
sessionId: s.session_id,
|
|
12016
|
+
// 来源 agent(AgentGate 逐会话带上,用于链路径取证展示;不参与链匹配)
|
|
12017
|
+
...s.agent_id ? { agentId: s.agent_id } : {},
|
|
11673
12018
|
actionHistory: (s.action_history || []).map((h) => ({
|
|
11674
12019
|
actionType: h.action_type,
|
|
11675
12020
|
actionDetail: h.action_detail,
|
|
@@ -11728,10 +12073,14 @@ async function analyzeRoute(c) {
|
|
|
11728
12073
|
if (incEnabled === "1" || incEnabled === "true" || incEnabled === "yes" || incEnabled === "on") {
|
|
11729
12074
|
try {
|
|
11730
12075
|
const store = getChainStateStoreFor(agentId);
|
|
11731
|
-
const inc = store.applyAction(
|
|
11732
|
-
|
|
11733
|
-
actionDetail: req.a_next.action_detail
|
|
11734
|
-
|
|
12076
|
+
const inc = store.applyAction(
|
|
12077
|
+
chainBucket(agentId),
|
|
12078
|
+
{ actionType: req.a_next.action_type, actionDetail: req.a_next.action_detail },
|
|
12079
|
+
req.session_id,
|
|
12080
|
+
Boolean(meta.is_error),
|
|
12081
|
+
agentId
|
|
12082
|
+
// 来源:真实 agent(桶 key 可能是 __system__)
|
|
12083
|
+
);
|
|
11735
12084
|
const judgment = incMatchedToDeny(inc);
|
|
11736
12085
|
if (judgment) {
|
|
11737
12086
|
const layers = [...result.violated_layers];
|
|
@@ -11751,7 +12100,8 @@ async function analyzeRoute(c) {
|
|
|
11751
12100
|
confidence: 90,
|
|
11752
12101
|
analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
|
|
11753
12102
|
// 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
|
|
11754
|
-
violation_chain: redactChain(judgment.violation_chain, req.session_id)
|
|
12103
|
+
violation_chain: redactChain(judgment.violation_chain, req.session_id),
|
|
12104
|
+
violation_graph: redactGraph(judgment.violation_graph, req.session_id)
|
|
11755
12105
|
})
|
|
11756
12106
|
);
|
|
11757
12107
|
}
|
|
@@ -11771,7 +12121,9 @@ async function analyzeRoute(c) {
|
|
|
11771
12121
|
policy: result.policy,
|
|
11772
12122
|
violated_policy: result.violated_policy,
|
|
11773
12123
|
confidence: result.confidence,
|
|
11774
|
-
analysis_duration_ms: Math.round(durationMs * 10) / 10
|
|
12124
|
+
analysis_duration_ms: Math.round(durationMs * 10) / 10,
|
|
12125
|
+
violation_chain: result.violation_chain ?? [],
|
|
12126
|
+
violation_graph: redactGraph(result.violation_graph, req.session_id)
|
|
11775
12127
|
})
|
|
11776
12128
|
);
|
|
11777
12129
|
} catch (err) {
|
|
@@ -11797,12 +12149,13 @@ function getChainStateStoreFor(_agentId) {
|
|
|
11797
12149
|
}
|
|
11798
12150
|
function incMatchedToDeny(inc) {
|
|
11799
12151
|
if (inc.matched) {
|
|
11800
|
-
const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp]);
|
|
12152
|
+
const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp, p.agentId ?? null]);
|
|
11801
12153
|
return {
|
|
11802
12154
|
property_id: "cross-" + inc.matched.chainId,
|
|
11803
12155
|
reason: inc.matched.reason,
|
|
11804
12156
|
risk_level: inc.matched.riskLevel,
|
|
11805
|
-
violation_chain: chain
|
|
12157
|
+
violation_chain: chain,
|
|
12158
|
+
violation_graph: inc.matched.graph ?? null
|
|
11806
12159
|
};
|
|
11807
12160
|
}
|
|
11808
12161
|
if (inc.exfil && inc.exfil.length > 0) {
|
|
@@ -11810,7 +12163,9 @@ function incMatchedToDeny(inc) {
|
|
|
11810
12163
|
property_id: "sensitive-data-flow",
|
|
11811
12164
|
reason: inc.exfil[0].reason,
|
|
11812
12165
|
risk_level: inc.exfil[0].risk_level,
|
|
11813
|
-
|
|
12166
|
+
// P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
|
|
12167
|
+
violation_chain: inc.exfil[0].path ?? [],
|
|
12168
|
+
violation_graph: inc.exfil[0].graph ?? null
|
|
11814
12169
|
};
|
|
11815
12170
|
}
|
|
11816
12171
|
return null;
|
|
@@ -11822,9 +12177,10 @@ async function reviewRoute(c) {
|
|
|
11822
12177
|
const req = ReviewRequestSchema.parse(body);
|
|
11823
12178
|
const agentId = req.agent_id || "default";
|
|
11824
12179
|
const store = getChainStateStoreFor(agentId);
|
|
11825
|
-
|
|
12180
|
+
const bucket = chainBucket(agentId);
|
|
12181
|
+
let tracker = store.load(bucket);
|
|
11826
12182
|
if (!tracker) {
|
|
11827
|
-
tracker = new StatefulChainTracker(
|
|
12183
|
+
tracker = new StatefulChainTracker(bucket);
|
|
11828
12184
|
}
|
|
11829
12185
|
if (req.db_path && !pathWithin(req.db_path, reviewRoots())) {
|
|
11830
12186
|
return c.json({
|