@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/cli.js
CHANGED
|
@@ -1258,6 +1258,20 @@ function redactChain(chain, currentSessionId) {
|
|
|
1258
1258
|
return step;
|
|
1259
1259
|
});
|
|
1260
1260
|
}
|
|
1261
|
+
function redactGraph(graph, currentSessionId) {
|
|
1262
|
+
if (!graph) return null;
|
|
1263
|
+
if (chainDetailMode() === "full") return graph;
|
|
1264
|
+
const nodes = (graph.nodes || []).map((n) => {
|
|
1265
|
+
const m = { ...n };
|
|
1266
|
+
if (String(m.session_id ?? "") !== String(currentSessionId)) {
|
|
1267
|
+
m.session_id = "<other-session>";
|
|
1268
|
+
m.action_detail = "<\u5DF2\u9690\u85CF>";
|
|
1269
|
+
m.resources = [];
|
|
1270
|
+
}
|
|
1271
|
+
return m;
|
|
1272
|
+
});
|
|
1273
|
+
return { nodes, edges: graph.edges || [] };
|
|
1274
|
+
}
|
|
1261
1275
|
function sessionSweepThreshold() {
|
|
1262
1276
|
return envInt("AGENT_MOSS_SESSION_SWEEP_THRESHOLD", 50);
|
|
1263
1277
|
}
|
|
@@ -1340,6 +1354,12 @@ function agentInflight() {
|
|
|
1340
1354
|
function boundAgentOf(principal) {
|
|
1341
1355
|
return principal.startsWith("agent:") ? principal.slice("agent:".length) : "";
|
|
1342
1356
|
}
|
|
1357
|
+
function chainScope() {
|
|
1358
|
+
return (env("AGENT_MOSS_CHAIN_SCOPE", "system") || "system").toLowerCase();
|
|
1359
|
+
}
|
|
1360
|
+
function chainBucket(agentId = "") {
|
|
1361
|
+
return chainScope() === "agent" ? agentId || "default" : "__system__";
|
|
1362
|
+
}
|
|
1343
1363
|
function rateLimitQps() {
|
|
1344
1364
|
return envInt("AGENT_MOSS_RATE_LIMIT_QPS", 100);
|
|
1345
1365
|
}
|
|
@@ -1411,6 +1431,8 @@ var init_models = __esm({
|
|
|
1411
1431
|
action_history: z.array(ActionItemSchema).default([]).describe("Historical action sequence"),
|
|
1412
1432
|
session_history: z.array(z.object({
|
|
1413
1433
|
session_id: z.string(),
|
|
1434
|
+
/** 来源 agent(AgentGate 逐会话带上):记录在链路径里供告警/取证,不参与链匹配。 */
|
|
1435
|
+
agent_id: z.string().optional(),
|
|
1414
1436
|
action_history: z.array(ActionItemSchema).default([])
|
|
1415
1437
|
})).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)."),
|
|
1416
1438
|
a_next: ActionItemSchema.describe("Next action to be executed"),
|
|
@@ -1431,7 +1453,42 @@ var init_models = __esm({
|
|
|
1431
1453
|
confidence: z.number().int().min(0).max(100).default(100),
|
|
1432
1454
|
analysis_duration_ms: z.number().default(0),
|
|
1433
1455
|
// 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
|
|
1434
|
-
violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
|
|
1456
|
+
violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([]),
|
|
1457
|
+
// 结构化攻击链图(前端可视化):节点 + 边(next 时序 / data 数据流)
|
|
1458
|
+
violation_graph: z.object({
|
|
1459
|
+
// 性质元信息(前端展示"违反了哪条性质")
|
|
1460
|
+
property_id: z.string().optional(),
|
|
1461
|
+
property_short: z.string().optional(),
|
|
1462
|
+
property_name: z.string().optional(),
|
|
1463
|
+
// 性质表达式 token → 节点位置(前端展示"表达式对应哪个节点")
|
|
1464
|
+
property_expression: z.array(
|
|
1465
|
+
z.object({
|
|
1466
|
+
token: z.string(),
|
|
1467
|
+
node_index: z.union([z.number(), z.array(z.number())])
|
|
1468
|
+
})
|
|
1469
|
+
).optional(),
|
|
1470
|
+
nodes: z.array(
|
|
1471
|
+
z.object({
|
|
1472
|
+
session_id: z.string().default(""),
|
|
1473
|
+
seq: z.number().default(0),
|
|
1474
|
+
stage: z.string().default(""),
|
|
1475
|
+
action_detail: z.string().default(""),
|
|
1476
|
+
resources: z.array(z.string()).default([]),
|
|
1477
|
+
// 节点在性质中的角色:source/carrier/sink(P2)或阶段名(P1)
|
|
1478
|
+
role: z.string().optional(),
|
|
1479
|
+
// 是否为违例见证节点(当前动作补全链 / 触发外传)
|
|
1480
|
+
witness: z.boolean().optional()
|
|
1481
|
+
})
|
|
1482
|
+
),
|
|
1483
|
+
edges: z.array(
|
|
1484
|
+
z.object({
|
|
1485
|
+
from: z.number(),
|
|
1486
|
+
to: z.number(),
|
|
1487
|
+
type: z.enum(["next", "data"]).default("next"),
|
|
1488
|
+
label: z.string().optional()
|
|
1489
|
+
})
|
|
1490
|
+
)
|
|
1491
|
+
}).nullable().default(null)
|
|
1435
1492
|
});
|
|
1436
1493
|
HealthResponseSchema = z.object({
|
|
1437
1494
|
status: z.string().default("healthy"),
|
|
@@ -2999,7 +3056,7 @@ function _scenarioRulesToDefaults() {
|
|
|
2999
3056
|
if (!existsSync7(scenDir)) return out;
|
|
3000
3057
|
let entries;
|
|
3001
3058
|
try {
|
|
3002
|
-
entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3059
|
+
entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("__")).map((d) => d.name);
|
|
3003
3060
|
} catch {
|
|
3004
3061
|
return out;
|
|
3005
3062
|
}
|
|
@@ -9379,7 +9436,7 @@ function markIntegrity(actionDetail) {
|
|
|
9379
9436
|
}
|
|
9380
9437
|
return { integrity: "medium", isLowSource: false };
|
|
9381
9438
|
}
|
|
9382
|
-
function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
|
|
9439
|
+
function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "") {
|
|
9383
9440
|
const { integrity, isLowSource } = markIntegrity(actionDetail);
|
|
9384
9441
|
return {
|
|
9385
9442
|
sessionId,
|
|
@@ -9387,13 +9444,14 @@ function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
|
|
|
9387
9444
|
actionType,
|
|
9388
9445
|
actionDetail,
|
|
9389
9446
|
stage: markStage(actionDetail),
|
|
9447
|
+
agentId: agentId || void 0,
|
|
9390
9448
|
resources: extractResources(actionDetail),
|
|
9391
9449
|
integrity,
|
|
9392
9450
|
isLowSource,
|
|
9393
9451
|
timestamp: timestamp || void 0
|
|
9394
9452
|
};
|
|
9395
9453
|
}
|
|
9396
|
-
function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
|
|
9454
|
+
function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "") {
|
|
9397
9455
|
const nodes = [];
|
|
9398
9456
|
const edges = [];
|
|
9399
9457
|
const stageFlags = /* @__PURE__ */ new Set();
|
|
@@ -9403,18 +9461,20 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
|
|
|
9403
9461
|
if (ad) details.push({
|
|
9404
9462
|
actionType: String(a.actionType || ""),
|
|
9405
9463
|
actionDetail: ad,
|
|
9406
|
-
calledAt: a.calledAt || void 0
|
|
9464
|
+
calledAt: a.calledAt || void 0,
|
|
9465
|
+
agentId: a.agentId || agentId || void 0
|
|
9407
9466
|
});
|
|
9408
9467
|
}
|
|
9409
9468
|
if (aNext) {
|
|
9410
9469
|
details.push({
|
|
9411
9470
|
actionType: String(aNext.actionType || ""),
|
|
9412
9471
|
actionDetail: String(aNext.actionDetail || ""),
|
|
9413
|
-
calledAt: aNext.calledAt || void 0
|
|
9472
|
+
calledAt: aNext.calledAt || void 0,
|
|
9473
|
+
agentId: aNext.agentId || agentId || void 0
|
|
9414
9474
|
});
|
|
9415
9475
|
}
|
|
9416
9476
|
details.forEach((d, seq) => {
|
|
9417
|
-
const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt);
|
|
9477
|
+
const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "");
|
|
9418
9478
|
nodes.push(node);
|
|
9419
9479
|
if (node.stage) stageFlags.add(node.stage);
|
|
9420
9480
|
});
|
|
@@ -9431,7 +9491,7 @@ function buildMultiSessionGraph(sessions, aNext) {
|
|
|
9431
9491
|
}
|
|
9432
9492
|
for (const s of sessions) {
|
|
9433
9493
|
const sid = String(s.sessionId || "s");
|
|
9434
|
-
mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid));
|
|
9494
|
+
mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid, s.agentId || ""));
|
|
9435
9495
|
}
|
|
9436
9496
|
if (aNext) {
|
|
9437
9497
|
const last = mg.sessions[mg.sessions.length - 1];
|
|
@@ -9441,7 +9501,8 @@ function buildMultiSessionGraph(sessions, aNext) {
|
|
|
9441
9501
|
last.nodes.length,
|
|
9442
9502
|
String(aNext.actionType || ""),
|
|
9443
9503
|
String(aNext.actionDetail || ""),
|
|
9444
|
-
aNext.calledAt || void 0
|
|
9504
|
+
aNext.calledAt || void 0,
|
|
9505
|
+
aNext.agentId || ""
|
|
9445
9506
|
));
|
|
9446
9507
|
if (last.nodes.length > 1) {
|
|
9447
9508
|
last.edges.push({ src: last.nodes.length - 2, dst: last.nodes.length - 1, kind: "next" });
|
|
@@ -9499,6 +9560,33 @@ var init_builder = __esm({
|
|
|
9499
9560
|
});
|
|
9500
9561
|
|
|
9501
9562
|
// src/engine/formal-engine/properties.ts
|
|
9563
|
+
function graphFromPath(nodes, path6, edgeType = "next", propertyMeta, roles, witnessLast = true, propertyExpression) {
|
|
9564
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
9565
|
+
for (const n of nodes) byKey.set(`${n.sessionId}#${n.seq}`, n);
|
|
9566
|
+
const gNodes = [];
|
|
9567
|
+
const gEdges = [];
|
|
9568
|
+
for (const item of path6) {
|
|
9569
|
+
const n = byKey.get(`${item.sessionId}#${item.seq}`);
|
|
9570
|
+
gNodes.push({
|
|
9571
|
+
session_id: item.sessionId,
|
|
9572
|
+
seq: item.seq,
|
|
9573
|
+
stage: n ? n.stage : "",
|
|
9574
|
+
action_detail: n ? n.actionDetail : item.detail,
|
|
9575
|
+
resources: n ? [...n.resources].sort() : []
|
|
9576
|
+
});
|
|
9577
|
+
if (gNodes.length > 1) {
|
|
9578
|
+
gEdges.push({ from: gNodes.length - 2, to: gNodes.length - 1, type: edgeType });
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
(roles || []).forEach((role, i) => {
|
|
9582
|
+
if (i < gNodes.length && role) gNodes[i].role = role;
|
|
9583
|
+
});
|
|
9584
|
+
if (witnessLast && gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
9585
|
+
const out = { nodes: gNodes, edges: gEdges };
|
|
9586
|
+
if (propertyMeta) Object.assign(out, propertyMeta);
|
|
9587
|
+
if (propertyExpression) out.property_expression = propertyExpression;
|
|
9588
|
+
return out;
|
|
9589
|
+
}
|
|
9502
9590
|
function parseTs(ts) {
|
|
9503
9591
|
if (!ts) return null;
|
|
9504
9592
|
const t = Date.parse(String(ts).trim());
|
|
@@ -9556,7 +9644,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9556
9644
|
propertyId: "cross-" + chain.id,
|
|
9557
9645
|
reason: chain.reason,
|
|
9558
9646
|
riskLevel: chain.riskLevel,
|
|
9559
|
-
path: path6
|
|
9647
|
+
path: path6,
|
|
9648
|
+
graph: graphFromPath(nodes, path6, "next", {
|
|
9649
|
+
property_id: "cross-" + chain.id,
|
|
9650
|
+
property_short: "P1",
|
|
9651
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
|
|
9652
|
+
}, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
|
|
9560
9653
|
});
|
|
9561
9654
|
return violations;
|
|
9562
9655
|
} else {
|
|
@@ -9576,7 +9669,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9576
9669
|
propertyId: "cross-" + chain.id,
|
|
9577
9670
|
reason: chain.reason,
|
|
9578
9671
|
riskLevel: chain.riskLevel,
|
|
9579
|
-
path: path6
|
|
9672
|
+
path: path6,
|
|
9673
|
+
graph: graphFromPath(nodes, path6, "next", {
|
|
9674
|
+
property_id: "cross-" + chain.id,
|
|
9675
|
+
property_short: "P1",
|
|
9676
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
|
|
9677
|
+
}, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
|
|
9580
9678
|
});
|
|
9581
9679
|
}
|
|
9582
9680
|
}
|
|
@@ -9591,10 +9689,15 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9591
9689
|
for (let i = 0; i < nodes.length; i++) {
|
|
9592
9690
|
const n = nodes[i];
|
|
9593
9691
|
if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
|
|
9594
|
-
tainted.set(i,
|
|
9692
|
+
tainted.set(i, {
|
|
9693
|
+
source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
|
|
9694
|
+
parent: null
|
|
9695
|
+
});
|
|
9595
9696
|
}
|
|
9596
9697
|
}
|
|
9597
9698
|
if (tainted.size === 0) return violations;
|
|
9699
|
+
const taintEdges = /* @__PURE__ */ new Map();
|
|
9700
|
+
const edgeKey = (p, c) => `${p}:${c}`;
|
|
9598
9701
|
const edges = allEdges(graph);
|
|
9599
9702
|
let changed = true;
|
|
9600
9703
|
while (changed) {
|
|
@@ -9605,7 +9708,8 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9605
9708
|
const dstRes = nodes[e.dst].resources;
|
|
9606
9709
|
for (const r of srcRes) {
|
|
9607
9710
|
if (dstRes.has(r)) {
|
|
9608
|
-
tainted.set(e.dst, tainted.get(e.src));
|
|
9711
|
+
tainted.set(e.dst, { source: tainted.get(e.src).source, parent: e.src });
|
|
9712
|
+
taintEdges.set(edgeKey(e.src, e.dst), r);
|
|
9609
9713
|
changed = true;
|
|
9610
9714
|
break;
|
|
9611
9715
|
}
|
|
@@ -9613,16 +9717,75 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9613
9717
|
}
|
|
9614
9718
|
}
|
|
9615
9719
|
}
|
|
9720
|
+
const tracePath = (idx) => {
|
|
9721
|
+
const chain = [idx];
|
|
9722
|
+
const seen = /* @__PURE__ */ new Set([idx]);
|
|
9723
|
+
while (true) {
|
|
9724
|
+
const parent = tainted.get(chain[chain.length - 1])?.parent;
|
|
9725
|
+
if (parent === void 0 || parent === null || seen.has(parent)) break;
|
|
9726
|
+
chain.push(parent);
|
|
9727
|
+
seen.add(parent);
|
|
9728
|
+
}
|
|
9729
|
+
return chain.reverse();
|
|
9730
|
+
};
|
|
9731
|
+
const taintGraph = (chain) => {
|
|
9732
|
+
const gNodes = [];
|
|
9733
|
+
const gEdges = [];
|
|
9734
|
+
chain.forEach((ni, k) => {
|
|
9735
|
+
const n = nodes[ni];
|
|
9736
|
+
const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
|
|
9737
|
+
gNodes.push({
|
|
9738
|
+
session_id: n.sessionId,
|
|
9739
|
+
seq: n.seq,
|
|
9740
|
+
stage: n.stage,
|
|
9741
|
+
action_detail: n.actionDetail,
|
|
9742
|
+
resources: [...n.resources].sort(),
|
|
9743
|
+
role
|
|
9744
|
+
});
|
|
9745
|
+
if (k > 0) {
|
|
9746
|
+
const prev = chain[k - 1];
|
|
9747
|
+
gEdges.push({
|
|
9748
|
+
from: k - 1,
|
|
9749
|
+
to: k,
|
|
9750
|
+
type: "data",
|
|
9751
|
+
label: taintEdges.get(edgeKey(prev, ni)) || ""
|
|
9752
|
+
});
|
|
9753
|
+
}
|
|
9754
|
+
});
|
|
9755
|
+
if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
9756
|
+
const expr = [{ token: "CRED", node_index: 0 }];
|
|
9757
|
+
if (chain.length > 2) {
|
|
9758
|
+
expr.push({
|
|
9759
|
+
token: "\u5171\u4EAB\u8F7D\u4F53",
|
|
9760
|
+
node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
|
|
9761
|
+
});
|
|
9762
|
+
}
|
|
9763
|
+
expr.push({ token: "EXFIL", node_index: chain.length - 1 });
|
|
9764
|
+
return {
|
|
9765
|
+
property_id: "sensitive-data-flow",
|
|
9766
|
+
property_short: "P2",
|
|
9767
|
+
property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
|
|
9768
|
+
property_expression: expr,
|
|
9769
|
+
nodes: gNodes,
|
|
9770
|
+
edges: gEdges
|
|
9771
|
+
};
|
|
9772
|
+
};
|
|
9616
9773
|
const witness = graph.witnessIdx;
|
|
9617
9774
|
const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
|
|
9618
9775
|
for (const i of candidates) {
|
|
9619
9776
|
const n = nodes[i];
|
|
9620
9777
|
if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
|
|
9778
|
+
const chain = tracePath(i);
|
|
9621
9779
|
violations.push({
|
|
9622
9780
|
propertyId: "sensitive-data-flow",
|
|
9623
|
-
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`,
|
|
9781
|
+
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`,
|
|
9624
9782
|
riskLevel: "critical",
|
|
9625
|
-
path:
|
|
9783
|
+
path: chain.map((j) => ({
|
|
9784
|
+
sessionId: nodes[j].sessionId,
|
|
9785
|
+
seq: nodes[j].seq,
|
|
9786
|
+
detail: nodes[j].actionDetail.slice(0, 60)
|
|
9787
|
+
})),
|
|
9788
|
+
graph: taintGraph(chain)
|
|
9626
9789
|
});
|
|
9627
9790
|
if (witness !== void 0) return violations;
|
|
9628
9791
|
}
|
|
@@ -10164,6 +10327,12 @@ var init_stateful = __esm({
|
|
|
10164
10327
|
nodes_ = [];
|
|
10165
10328
|
ptr = {};
|
|
10166
10329
|
tainted = /* @__PURE__ */ new Set();
|
|
10330
|
+
/** taint 传播路径:nodeIdx -> 使该节点染毒的前驱 nodeIdx(CRED 源为 null) */
|
|
10331
|
+
taintParent = /* @__PURE__ */ new Map();
|
|
10332
|
+
/** "parent:child" -> 触发传播的共享资源(图上的 data 边标签) */
|
|
10333
|
+
taintEdgeLabel = /* @__PURE__ */ new Map();
|
|
10334
|
+
/** 资源 -> 首次染毒该资源的节点 seq */
|
|
10335
|
+
taintOrigin = /* @__PURE__ */ new Map();
|
|
10167
10336
|
/** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
|
|
10168
10337
|
taintedResources = /* @__PURE__ */ new Set();
|
|
10169
10338
|
aliveResources = {};
|
|
@@ -10228,8 +10397,8 @@ var init_stateful = __esm({
|
|
|
10228
10397
|
}
|
|
10229
10398
|
/** 增量推进:把当前动作追加到时间线 → 链指针推进 → 判定。 */
|
|
10230
10399
|
advance(args) {
|
|
10231
|
-
const { aNext, sessionId = "current", isError = false } = args;
|
|
10232
|
-
const node = this.appendNode(aNext, sessionId, isError);
|
|
10400
|
+
const { aNext, sessionId = "current", isError = false, agentId = "" } = args;
|
|
10401
|
+
const node = this.appendNode(aNext, sessionId, isError, agentId);
|
|
10233
10402
|
if (!node) {
|
|
10234
10403
|
return { seq: this.seqNext, node: null, matched: null, pendingPrefixes: this.pendingPrefixes, exfil: [], shouldBlock: false };
|
|
10235
10404
|
}
|
|
@@ -10249,7 +10418,7 @@ var init_stateful = __esm({
|
|
|
10249
10418
|
}
|
|
10250
10419
|
};
|
|
10251
10420
|
}
|
|
10252
|
-
appendNode(action, sessionId, isError = false) {
|
|
10421
|
+
appendNode(action, sessionId, isError = false, agentId = "") {
|
|
10253
10422
|
const ad = String(action.actionDetail || "");
|
|
10254
10423
|
const at = String(action.actionType || "");
|
|
10255
10424
|
if (!ad) return null;
|
|
@@ -10266,6 +10435,7 @@ var init_stateful = __esm({
|
|
|
10266
10435
|
resources: extractResources2(ad),
|
|
10267
10436
|
instances: extractInstances(ad, stage),
|
|
10268
10437
|
isError,
|
|
10438
|
+
agentId: String(action.agentId || agentId || ""),
|
|
10269
10439
|
lifecycle: lifecycleFor(ad, stage, isError),
|
|
10270
10440
|
timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
|
|
10271
10441
|
};
|
|
@@ -10373,7 +10543,8 @@ var init_stateful = __esm({
|
|
|
10373
10543
|
stages,
|
|
10374
10544
|
riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
|
|
10375
10545
|
reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
|
|
10376
|
-
path: this.chainPath(stages)
|
|
10546
|
+
path: this.chainPath(stages),
|
|
10547
|
+
graph: this.buildChainGraph(stages, cid)
|
|
10377
10548
|
};
|
|
10378
10549
|
}
|
|
10379
10550
|
}
|
|
@@ -10403,7 +10574,13 @@ var init_stateful = __esm({
|
|
|
10403
10574
|
let want = stages[0];
|
|
10404
10575
|
for (const n of this.nodes_) {
|
|
10405
10576
|
if (n.stage === want) {
|
|
10406
|
-
path6.push({
|
|
10577
|
+
path6.push({
|
|
10578
|
+
sessionId: n.sessionId,
|
|
10579
|
+
seq: n.seq,
|
|
10580
|
+
stage: n.stage,
|
|
10581
|
+
timestamp: n.timestamp,
|
|
10582
|
+
agentId: n.agentId
|
|
10583
|
+
});
|
|
10407
10584
|
const idx = stages.indexOf(n.stage);
|
|
10408
10585
|
if (idx + 1 < stages.length) {
|
|
10409
10586
|
want = stages[idx + 1];
|
|
@@ -10414,21 +10591,69 @@ var init_stateful = __esm({
|
|
|
10414
10591
|
}
|
|
10415
10592
|
return path6;
|
|
10416
10593
|
}
|
|
10594
|
+
/** 把链路径转成前端可画的结构化图(相邻节点 next 边)。 */
|
|
10595
|
+
buildChainGraph(stages, chainId) {
|
|
10596
|
+
const path6 = this.chainPath(stages);
|
|
10597
|
+
const gNodes = path6.map((p, i) => {
|
|
10598
|
+
const n = this.nodes_.find((x) => x.sessionId === p.sessionId && x.seq === p.seq);
|
|
10599
|
+
return {
|
|
10600
|
+
session_id: p.sessionId,
|
|
10601
|
+
seq: p.seq,
|
|
10602
|
+
stage: p.stage,
|
|
10603
|
+
action_detail: n ? n.actionDetail : "",
|
|
10604
|
+
resources: n ? [...n.resources].sort() : [],
|
|
10605
|
+
role: stages[i] || ""
|
|
10606
|
+
};
|
|
10607
|
+
});
|
|
10608
|
+
const gEdges = [];
|
|
10609
|
+
for (let i = 1; i < gNodes.length; i++) {
|
|
10610
|
+
gEdges.push({ from: i - 1, to: i, type: "next" });
|
|
10611
|
+
}
|
|
10612
|
+
if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
10613
|
+
return {
|
|
10614
|
+
property_id: "cross-" + chainId,
|
|
10615
|
+
property_short: "P1",
|
|
10616
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE",
|
|
10617
|
+
property_expression: stages.map((s, i) => ({ token: s, node_index: i })),
|
|
10618
|
+
nodes: gNodes,
|
|
10619
|
+
edges: gEdges
|
|
10620
|
+
};
|
|
10621
|
+
}
|
|
10417
10622
|
// ---------------- 敏感数据 taint(P2) ----------------
|
|
10418
10623
|
advanceTaint(node) {
|
|
10419
10624
|
if (node.lifecycle === "dead") return;
|
|
10420
10625
|
const idx = node.seq;
|
|
10421
10626
|
const taintedRes = /* @__PURE__ */ new Set();
|
|
10627
|
+
let parent = null;
|
|
10628
|
+
let label = "";
|
|
10629
|
+
let foundParent = false;
|
|
10422
10630
|
for (const r of node.resources) {
|
|
10423
|
-
if (this.resourceTainted(r))
|
|
10631
|
+
if (this.resourceTainted(r)) {
|
|
10632
|
+
taintedRes.add(r);
|
|
10633
|
+
if (!foundParent) {
|
|
10634
|
+
const origin = this.taintOrigin.get(r);
|
|
10635
|
+
if (origin !== void 0) {
|
|
10636
|
+
parent = origin;
|
|
10637
|
+
label = r;
|
|
10638
|
+
foundParent = true;
|
|
10639
|
+
}
|
|
10640
|
+
}
|
|
10641
|
+
}
|
|
10424
10642
|
}
|
|
10425
10643
|
if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
|
|
10426
10644
|
this.tainted.add(idx);
|
|
10645
|
+
parent = null;
|
|
10646
|
+
label = "";
|
|
10427
10647
|
for (const r of node.resources) taintedRes.add(r);
|
|
10428
10648
|
}
|
|
10429
10649
|
if (taintedRes.size > 0) {
|
|
10430
10650
|
this.tainted.add(idx);
|
|
10431
|
-
|
|
10651
|
+
this.taintParent.set(idx, parent);
|
|
10652
|
+
if (parent !== null) this.taintEdgeLabel.set(`${parent}:${idx}`, label);
|
|
10653
|
+
for (const r of node.resources) {
|
|
10654
|
+
this.taintedResources.add(r);
|
|
10655
|
+
if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, idx);
|
|
10656
|
+
}
|
|
10432
10657
|
}
|
|
10433
10658
|
}
|
|
10434
10659
|
resourceTainted(resource) {
|
|
@@ -10438,16 +10663,84 @@ var init_stateful = __esm({
|
|
|
10438
10663
|
checkExfilAfterTaint(node) {
|
|
10439
10664
|
if (!this.tainted.has(node.seq)) return [];
|
|
10440
10665
|
if (EXFIL_RE2.test(node.actionDetail)) {
|
|
10666
|
+
const chain = this.traceTaintChain(node.seq);
|
|
10441
10667
|
return [{
|
|
10442
10668
|
property_id: "sensitive-data-flow",
|
|
10443
|
-
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`,
|
|
10669
|
+
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`,
|
|
10444
10670
|
risk_level: "critical",
|
|
10445
|
-
path:
|
|
10671
|
+
path: chain.map((j) => {
|
|
10672
|
+
const n = this.nodes_[j];
|
|
10673
|
+
return [n.sessionId, n.seq, n.actionDetail.slice(0, 60), n.timestamp];
|
|
10674
|
+
}),
|
|
10675
|
+
graph: this.taintGraph(chain)
|
|
10446
10676
|
}];
|
|
10447
10677
|
}
|
|
10448
10678
|
return [];
|
|
10449
10679
|
}
|
|
10450
|
-
|
|
10680
|
+
/** 沿 taint parent 回溯出 CRED 源 → 当前节点的完整路径。 */
|
|
10681
|
+
traceTaintChain(idx) {
|
|
10682
|
+
const chain = [idx];
|
|
10683
|
+
const seen = /* @__PURE__ */ new Set([idx]);
|
|
10684
|
+
while (true) {
|
|
10685
|
+
const parent = this.taintParent.get(chain[chain.length - 1]);
|
|
10686
|
+
if (parent === void 0 || parent === null || seen.has(parent)) break;
|
|
10687
|
+
chain.push(parent);
|
|
10688
|
+
seen.add(parent);
|
|
10689
|
+
}
|
|
10690
|
+
return chain.reverse();
|
|
10691
|
+
}
|
|
10692
|
+
/** 把 taint 路径转成前端可画的结构化图(data 边带共享资源标签)。 */
|
|
10693
|
+
taintGraph(chain) {
|
|
10694
|
+
const gNodes = [];
|
|
10695
|
+
const gEdges = [];
|
|
10696
|
+
chain.forEach((ni, k) => {
|
|
10697
|
+
const n = this.nodes_[ni];
|
|
10698
|
+
const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
|
|
10699
|
+
gNodes.push({
|
|
10700
|
+
session_id: n.sessionId,
|
|
10701
|
+
seq: n.seq,
|
|
10702
|
+
stage: n.stage,
|
|
10703
|
+
action_detail: n.actionDetail,
|
|
10704
|
+
resources: [...n.resources].sort(),
|
|
10705
|
+
role
|
|
10706
|
+
});
|
|
10707
|
+
if (k > 0) {
|
|
10708
|
+
const prev = chain[k - 1];
|
|
10709
|
+
gEdges.push({
|
|
10710
|
+
from: k - 1,
|
|
10711
|
+
to: k,
|
|
10712
|
+
type: "data",
|
|
10713
|
+
label: this.taintEdgeLabel.get(`${prev}:${ni}`) || ""
|
|
10714
|
+
});
|
|
10715
|
+
}
|
|
10716
|
+
});
|
|
10717
|
+
if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
10718
|
+
const expr = [{ token: "CRED", node_index: 0 }];
|
|
10719
|
+
if (chain.length > 2) {
|
|
10720
|
+
expr.push({
|
|
10721
|
+
token: "\u5171\u4EAB\u8F7D\u4F53",
|
|
10722
|
+
node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
|
|
10723
|
+
});
|
|
10724
|
+
}
|
|
10725
|
+
expr.push({ token: "EXFIL", node_index: chain.length - 1 });
|
|
10726
|
+
return {
|
|
10727
|
+
property_id: "sensitive-data-flow",
|
|
10728
|
+
property_short: "P2",
|
|
10729
|
+
property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
|
|
10730
|
+
property_expression: expr,
|
|
10731
|
+
nodes: gNodes,
|
|
10732
|
+
edges: gEdges
|
|
10733
|
+
};
|
|
10734
|
+
}
|
|
10735
|
+
taintSource(node, chain = []) {
|
|
10736
|
+
if (chain.length > 0) {
|
|
10737
|
+
const src = this.nodes_[chain[0]];
|
|
10738
|
+
if (src) {
|
|
10739
|
+
for (const r of node.resources) {
|
|
10740
|
+
if (src.resources.includes(r)) return `${src.sessionId}#${src.seq} \u8BBF\u95EE ${r}`;
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
}
|
|
10451
10744
|
const sorted = [...this.tainted].sort((a, b) => a - b);
|
|
10452
10745
|
for (const i of sorted) {
|
|
10453
10746
|
if (i === node.seq) continue;
|
|
@@ -10466,6 +10759,9 @@ var init_stateful = __esm({
|
|
|
10466
10759
|
nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
|
|
10467
10760
|
ptrs: { ...this.ptr },
|
|
10468
10761
|
tainted: [...this.tainted].sort((a, b) => a - b),
|
|
10762
|
+
// taint 传播路径(前端攻击链图);边标签不入快照,重启后可从资源交集重建
|
|
10763
|
+
taint_parent: Object.fromEntries([...this.taintParent.entries()].map(([k, v]) => [String(k), v])),
|
|
10764
|
+
taint_origin: Object.fromEntries([...this.taintOrigin.entries()]),
|
|
10469
10765
|
alive: { ...this.aliveResources },
|
|
10470
10766
|
seq_next: this.seqNext
|
|
10471
10767
|
};
|
|
@@ -10477,6 +10773,7 @@ var init_stateful = __esm({
|
|
|
10477
10773
|
this.nodes_.push({
|
|
10478
10774
|
seq: Number(raw.seq || 0),
|
|
10479
10775
|
sessionId: String(raw.sessionId || ""),
|
|
10776
|
+
agentId: String(raw.agentId || ""),
|
|
10480
10777
|
actionType: String(raw.actionType || ""),
|
|
10481
10778
|
actionDetail: String(raw.actionDetail || ""),
|
|
10482
10779
|
stage: String(raw.stage || ""),
|
|
@@ -10492,15 +10789,53 @@ var init_stateful = __esm({
|
|
|
10492
10789
|
this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
|
|
10493
10790
|
this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
|
|
10494
10791
|
this.tainted = new Set((snapshot.tainted || []).map(Number));
|
|
10792
|
+
this.taintParent = new Map(
|
|
10793
|
+
Object.entries(snapshot.taint_parent || {}).map(
|
|
10794
|
+
([k, v]) => [Number(k), v === null || v === void 0 ? null : Number(v)]
|
|
10795
|
+
)
|
|
10796
|
+
);
|
|
10797
|
+
this.taintOrigin = new Map(
|
|
10798
|
+
Object.entries(snapshot.taint_origin || {}).map(
|
|
10799
|
+
([k, v]) => [String(k), Number(v)]
|
|
10800
|
+
)
|
|
10801
|
+
);
|
|
10802
|
+
this.taintEdgeLabel = /* @__PURE__ */ new Map();
|
|
10495
10803
|
this.taintedResources = /* @__PURE__ */ new Set();
|
|
10496
10804
|
for (const i of this.tainted) {
|
|
10497
10805
|
const n = this.nodes_[i];
|
|
10498
10806
|
if (n) for (const r of n.resources) this.taintedResources.add(r);
|
|
10499
10807
|
}
|
|
10808
|
+
if (this.tainted.size > 0 && this.taintParent.size === 0) this.rebuildTaintParent();
|
|
10809
|
+
if (this.taintOrigin.size === 0) {
|
|
10810
|
+
for (const i of [...this.tainted].sort((a, b) => a - b)) {
|
|
10811
|
+
const n = this.nodes_[i];
|
|
10812
|
+
if (!n) continue;
|
|
10813
|
+
for (const r of n.resources) {
|
|
10814
|
+
if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, i);
|
|
10815
|
+
}
|
|
10816
|
+
}
|
|
10817
|
+
}
|
|
10500
10818
|
this.aliveResources = Object.fromEntries(
|
|
10501
10819
|
Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
|
|
10502
10820
|
);
|
|
10503
10821
|
}
|
|
10822
|
+
/** 旧快照无 parent 信息时,按 seq 顺序找最近的资源相交染毒前驱。 */
|
|
10823
|
+
rebuildTaintParent() {
|
|
10824
|
+
for (const i of [...this.tainted].sort((a, b) => a - b)) {
|
|
10825
|
+
const n = this.nodes_[i];
|
|
10826
|
+
if (!n) continue;
|
|
10827
|
+
let parent = null;
|
|
10828
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
10829
|
+
if (!this.tainted.has(j)) continue;
|
|
10830
|
+
const m = this.nodes_[j];
|
|
10831
|
+
if (m && m.resources.some((r) => n.resources.includes(r))) {
|
|
10832
|
+
parent = j;
|
|
10833
|
+
break;
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10836
|
+
this.taintParent.set(i, parent);
|
|
10837
|
+
}
|
|
10838
|
+
}
|
|
10504
10839
|
};
|
|
10505
10840
|
}
|
|
10506
10841
|
});
|
|
@@ -10583,9 +10918,9 @@ var init_chain_state = __esm({
|
|
|
10583
10918
|
delete this.data[agentId];
|
|
10584
10919
|
this._scheduleWrite();
|
|
10585
10920
|
}
|
|
10586
|
-
applyAction(agentId, aNext, sessionId, isError = false) {
|
|
10921
|
+
applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "") {
|
|
10587
10922
|
const t = this.load(agentId) || new StatefulChainTracker(agentId);
|
|
10588
|
-
const r = t.advance({ aNext, sessionId, isError });
|
|
10923
|
+
const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent });
|
|
10589
10924
|
this.save(t);
|
|
10590
10925
|
return r;
|
|
10591
10926
|
}
|
|
@@ -10900,10 +11235,14 @@ async function runAnalysis(input) {
|
|
|
10900
11235
|
const formalSessions = [];
|
|
10901
11236
|
if (input.sessionHistory) {
|
|
10902
11237
|
for (const s of input.sessionHistory) {
|
|
10903
|
-
formalSessions.push({
|
|
11238
|
+
formalSessions.push({
|
|
11239
|
+
sessionId: s.sessionId || "s",
|
|
11240
|
+
...s.agentId ? { agentId: s.agentId } : {},
|
|
11241
|
+
actionHistory: s.actionHistory || []
|
|
11242
|
+
});
|
|
10904
11243
|
}
|
|
10905
11244
|
}
|
|
10906
|
-
formalSessions.push({ sessionId: "current", actionHistory: input.actionHistory });
|
|
11245
|
+
formalSessions.push({ sessionId: "current", agentId: input.agentId, actionHistory: input.actionHistory });
|
|
10907
11246
|
const formalResult = formalEngine.verify(formalSessions, input.aNext);
|
|
10908
11247
|
if (!formalResult.verified) {
|
|
10909
11248
|
const v = formalResult.violations[0];
|
|
@@ -10920,7 +11259,9 @@ async function runAnalysis(input) {
|
|
|
10920
11259
|
riskType: "formal_verification",
|
|
10921
11260
|
confidence: 90,
|
|
10922
11261
|
source: "formal_engine",
|
|
10923
|
-
violatedLayers: ["L2.2"]
|
|
11262
|
+
violatedLayers: ["L2.2"],
|
|
11263
|
+
violationGraph: v?.graph ?? null,
|
|
11264
|
+
violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
|
|
10924
11265
|
};
|
|
10925
11266
|
}
|
|
10926
11267
|
}
|
|
@@ -11465,7 +11806,9 @@ async function analyzeInner(input, mossLog) {
|
|
|
11465
11806
|
violated_layers: judgment.violatedLayers || [],
|
|
11466
11807
|
policy: "",
|
|
11467
11808
|
violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
|
|
11468
|
-
confidence: judgment.confidence
|
|
11809
|
+
confidence: judgment.confidence,
|
|
11810
|
+
violation_graph: judgment.violationGraph ?? null,
|
|
11811
|
+
violation_chain: judgment.violationChain ?? []
|
|
11469
11812
|
};
|
|
11470
11813
|
mossLog.logResult("Deny", denied.reason);
|
|
11471
11814
|
return denied;
|
|
@@ -11999,6 +12342,8 @@ async function analyzeRoute(c) {
|
|
|
11999
12342
|
})),
|
|
12000
12343
|
sessionHistory: (req.session_history || []).map((s) => ({
|
|
12001
12344
|
sessionId: s.session_id,
|
|
12345
|
+
// 来源 agent(AgentGate 逐会话带上,用于链路径取证展示;不参与链匹配)
|
|
12346
|
+
...s.agent_id ? { agentId: s.agent_id } : {},
|
|
12002
12347
|
actionHistory: (s.action_history || []).map((h) => ({
|
|
12003
12348
|
actionType: h.action_type,
|
|
12004
12349
|
actionDetail: h.action_detail,
|
|
@@ -12057,10 +12402,14 @@ async function analyzeRoute(c) {
|
|
|
12057
12402
|
if (incEnabled === "1" || incEnabled === "true" || incEnabled === "yes" || incEnabled === "on") {
|
|
12058
12403
|
try {
|
|
12059
12404
|
const store = getChainStateStoreFor(agentId);
|
|
12060
|
-
const inc = store.applyAction(
|
|
12061
|
-
|
|
12062
|
-
actionDetail: req.a_next.action_detail
|
|
12063
|
-
|
|
12405
|
+
const inc = store.applyAction(
|
|
12406
|
+
chainBucket(agentId),
|
|
12407
|
+
{ actionType: req.a_next.action_type, actionDetail: req.a_next.action_detail },
|
|
12408
|
+
req.session_id,
|
|
12409
|
+
Boolean(meta.is_error),
|
|
12410
|
+
agentId
|
|
12411
|
+
// 来源:真实 agent(桶 key 可能是 __system__)
|
|
12412
|
+
);
|
|
12064
12413
|
const judgment = incMatchedToDeny(inc);
|
|
12065
12414
|
if (judgment) {
|
|
12066
12415
|
const layers = [...result.violated_layers];
|
|
@@ -12080,7 +12429,8 @@ async function analyzeRoute(c) {
|
|
|
12080
12429
|
confidence: 90,
|
|
12081
12430
|
analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
|
|
12082
12431
|
// 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
|
|
12083
|
-
violation_chain: redactChain(judgment.violation_chain, req.session_id)
|
|
12432
|
+
violation_chain: redactChain(judgment.violation_chain, req.session_id),
|
|
12433
|
+
violation_graph: redactGraph(judgment.violation_graph, req.session_id)
|
|
12084
12434
|
})
|
|
12085
12435
|
);
|
|
12086
12436
|
}
|
|
@@ -12100,7 +12450,9 @@ async function analyzeRoute(c) {
|
|
|
12100
12450
|
policy: result.policy,
|
|
12101
12451
|
violated_policy: result.violated_policy,
|
|
12102
12452
|
confidence: result.confidence,
|
|
12103
|
-
analysis_duration_ms: Math.round(durationMs * 10) / 10
|
|
12453
|
+
analysis_duration_ms: Math.round(durationMs * 10) / 10,
|
|
12454
|
+
violation_chain: result.violation_chain ?? [],
|
|
12455
|
+
violation_graph: redactGraph(result.violation_graph, req.session_id)
|
|
12104
12456
|
})
|
|
12105
12457
|
);
|
|
12106
12458
|
} catch (err) {
|
|
@@ -12125,12 +12477,13 @@ function getChainStateStoreFor(_agentId) {
|
|
|
12125
12477
|
}
|
|
12126
12478
|
function incMatchedToDeny(inc) {
|
|
12127
12479
|
if (inc.matched) {
|
|
12128
|
-
const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp]);
|
|
12480
|
+
const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp, p.agentId ?? null]);
|
|
12129
12481
|
return {
|
|
12130
12482
|
property_id: "cross-" + inc.matched.chainId,
|
|
12131
12483
|
reason: inc.matched.reason,
|
|
12132
12484
|
risk_level: inc.matched.riskLevel,
|
|
12133
|
-
violation_chain: chain
|
|
12485
|
+
violation_chain: chain,
|
|
12486
|
+
violation_graph: inc.matched.graph ?? null
|
|
12134
12487
|
};
|
|
12135
12488
|
}
|
|
12136
12489
|
if (inc.exfil && inc.exfil.length > 0) {
|
|
@@ -12138,7 +12491,9 @@ function incMatchedToDeny(inc) {
|
|
|
12138
12491
|
property_id: "sensitive-data-flow",
|
|
12139
12492
|
reason: inc.exfil[0].reason,
|
|
12140
12493
|
risk_level: inc.exfil[0].risk_level,
|
|
12141
|
-
|
|
12494
|
+
// P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
|
|
12495
|
+
violation_chain: inc.exfil[0].path ?? [],
|
|
12496
|
+
violation_graph: inc.exfil[0].graph ?? null
|
|
12142
12497
|
};
|
|
12143
12498
|
}
|
|
12144
12499
|
return null;
|
|
@@ -12150,9 +12505,10 @@ async function reviewRoute(c) {
|
|
|
12150
12505
|
const req = ReviewRequestSchema.parse(body);
|
|
12151
12506
|
const agentId = req.agent_id || "default";
|
|
12152
12507
|
const store = getChainStateStoreFor(agentId);
|
|
12153
|
-
|
|
12508
|
+
const bucket = chainBucket(agentId);
|
|
12509
|
+
let tracker = store.load(bucket);
|
|
12154
12510
|
if (!tracker) {
|
|
12155
|
-
tracker = new StatefulChainTracker(
|
|
12511
|
+
tracker = new StatefulChainTracker(bucket);
|
|
12156
12512
|
}
|
|
12157
12513
|
if (req.db_path && !pathWithin(req.db_path, reviewRoots())) {
|
|
12158
12514
|
return c.json({
|