@kenhkl/agent-moss 0.17.7 → 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 +344 -19
- package/dist/engine/coordinator.d.ts +38 -0
- package/dist/engine/coordinator.js +111 -7
- package/dist/server.js +344 -19
- 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
|
}
|
|
@@ -1439,7 +1453,42 @@ var init_models = __esm({
|
|
|
1439
1453
|
confidence: z.number().int().min(0).max(100).default(100),
|
|
1440
1454
|
analysis_duration_ms: z.number().default(0),
|
|
1441
1455
|
// 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
|
|
1442
|
-
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)
|
|
1443
1492
|
});
|
|
1444
1493
|
HealthResponseSchema = z.object({
|
|
1445
1494
|
status: z.string().default("healthy"),
|
|
@@ -9511,6 +9560,33 @@ var init_builder = __esm({
|
|
|
9511
9560
|
});
|
|
9512
9561
|
|
|
9513
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
|
+
}
|
|
9514
9590
|
function parseTs(ts) {
|
|
9515
9591
|
if (!ts) return null;
|
|
9516
9592
|
const t = Date.parse(String(ts).trim());
|
|
@@ -9568,7 +9644,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9568
9644
|
propertyId: "cross-" + chain.id,
|
|
9569
9645
|
reason: chain.reason,
|
|
9570
9646
|
riskLevel: chain.riskLevel,
|
|
9571
|
-
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 })))
|
|
9572
9653
|
});
|
|
9573
9654
|
return violations;
|
|
9574
9655
|
} else {
|
|
@@ -9588,7 +9669,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9588
9669
|
propertyId: "cross-" + chain.id,
|
|
9589
9670
|
reason: chain.reason,
|
|
9590
9671
|
riskLevel: chain.riskLevel,
|
|
9591
|
-
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 })))
|
|
9592
9678
|
});
|
|
9593
9679
|
}
|
|
9594
9680
|
}
|
|
@@ -9603,10 +9689,15 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9603
9689
|
for (let i = 0; i < nodes.length; i++) {
|
|
9604
9690
|
const n = nodes[i];
|
|
9605
9691
|
if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
|
|
9606
|
-
tainted.set(i,
|
|
9692
|
+
tainted.set(i, {
|
|
9693
|
+
source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
|
|
9694
|
+
parent: null
|
|
9695
|
+
});
|
|
9607
9696
|
}
|
|
9608
9697
|
}
|
|
9609
9698
|
if (tainted.size === 0) return violations;
|
|
9699
|
+
const taintEdges = /* @__PURE__ */ new Map();
|
|
9700
|
+
const edgeKey = (p, c) => `${p}:${c}`;
|
|
9610
9701
|
const edges = allEdges(graph);
|
|
9611
9702
|
let changed = true;
|
|
9612
9703
|
while (changed) {
|
|
@@ -9617,7 +9708,8 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9617
9708
|
const dstRes = nodes[e.dst].resources;
|
|
9618
9709
|
for (const r of srcRes) {
|
|
9619
9710
|
if (dstRes.has(r)) {
|
|
9620
|
-
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);
|
|
9621
9713
|
changed = true;
|
|
9622
9714
|
break;
|
|
9623
9715
|
}
|
|
@@ -9625,16 +9717,75 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9625
9717
|
}
|
|
9626
9718
|
}
|
|
9627
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
|
+
};
|
|
9628
9773
|
const witness = graph.witnessIdx;
|
|
9629
9774
|
const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
|
|
9630
9775
|
for (const i of candidates) {
|
|
9631
9776
|
const n = nodes[i];
|
|
9632
9777
|
if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
|
|
9778
|
+
const chain = tracePath(i);
|
|
9633
9779
|
violations.push({
|
|
9634
9780
|
propertyId: "sensitive-data-flow",
|
|
9635
|
-
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`,
|
|
9636
9782
|
riskLevel: "critical",
|
|
9637
|
-
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)
|
|
9638
9789
|
});
|
|
9639
9790
|
if (witness !== void 0) return violations;
|
|
9640
9791
|
}
|
|
@@ -10176,6 +10327,12 @@ var init_stateful = __esm({
|
|
|
10176
10327
|
nodes_ = [];
|
|
10177
10328
|
ptr = {};
|
|
10178
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();
|
|
10179
10336
|
/** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
|
|
10180
10337
|
taintedResources = /* @__PURE__ */ new Set();
|
|
10181
10338
|
aliveResources = {};
|
|
@@ -10386,7 +10543,8 @@ var init_stateful = __esm({
|
|
|
10386
10543
|
stages,
|
|
10387
10544
|
riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
|
|
10388
10545
|
reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
|
|
10389
|
-
path: this.chainPath(stages)
|
|
10546
|
+
path: this.chainPath(stages),
|
|
10547
|
+
graph: this.buildChainGraph(stages, cid)
|
|
10390
10548
|
};
|
|
10391
10549
|
}
|
|
10392
10550
|
}
|
|
@@ -10433,21 +10591,69 @@ var init_stateful = __esm({
|
|
|
10433
10591
|
}
|
|
10434
10592
|
return path6;
|
|
10435
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
|
+
}
|
|
10436
10622
|
// ---------------- 敏感数据 taint(P2) ----------------
|
|
10437
10623
|
advanceTaint(node) {
|
|
10438
10624
|
if (node.lifecycle === "dead") return;
|
|
10439
10625
|
const idx = node.seq;
|
|
10440
10626
|
const taintedRes = /* @__PURE__ */ new Set();
|
|
10627
|
+
let parent = null;
|
|
10628
|
+
let label = "";
|
|
10629
|
+
let foundParent = false;
|
|
10441
10630
|
for (const r of node.resources) {
|
|
10442
|
-
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
|
+
}
|
|
10443
10642
|
}
|
|
10444
10643
|
if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
|
|
10445
10644
|
this.tainted.add(idx);
|
|
10645
|
+
parent = null;
|
|
10646
|
+
label = "";
|
|
10446
10647
|
for (const r of node.resources) taintedRes.add(r);
|
|
10447
10648
|
}
|
|
10448
10649
|
if (taintedRes.size > 0) {
|
|
10449
10650
|
this.tainted.add(idx);
|
|
10450
|
-
|
|
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
|
+
}
|
|
10451
10657
|
}
|
|
10452
10658
|
}
|
|
10453
10659
|
resourceTainted(resource) {
|
|
@@ -10457,16 +10663,84 @@ var init_stateful = __esm({
|
|
|
10457
10663
|
checkExfilAfterTaint(node) {
|
|
10458
10664
|
if (!this.tainted.has(node.seq)) return [];
|
|
10459
10665
|
if (EXFIL_RE2.test(node.actionDetail)) {
|
|
10666
|
+
const chain = this.traceTaintChain(node.seq);
|
|
10460
10667
|
return [{
|
|
10461
10668
|
property_id: "sensitive-data-flow",
|
|
10462
|
-
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`,
|
|
10463
10670
|
risk_level: "critical",
|
|
10464
|
-
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)
|
|
10465
10676
|
}];
|
|
10466
10677
|
}
|
|
10467
10678
|
return [];
|
|
10468
10679
|
}
|
|
10469
|
-
|
|
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
|
+
}
|
|
10470
10744
|
const sorted = [...this.tainted].sort((a, b) => a - b);
|
|
10471
10745
|
for (const i of sorted) {
|
|
10472
10746
|
if (i === node.seq) continue;
|
|
@@ -10485,6 +10759,9 @@ var init_stateful = __esm({
|
|
|
10485
10759
|
nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
|
|
10486
10760
|
ptrs: { ...this.ptr },
|
|
10487
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()]),
|
|
10488
10765
|
alive: { ...this.aliveResources },
|
|
10489
10766
|
seq_next: this.seqNext
|
|
10490
10767
|
};
|
|
@@ -10512,15 +10789,53 @@ var init_stateful = __esm({
|
|
|
10512
10789
|
this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
|
|
10513
10790
|
this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
|
|
10514
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();
|
|
10515
10803
|
this.taintedResources = /* @__PURE__ */ new Set();
|
|
10516
10804
|
for (const i of this.tainted) {
|
|
10517
10805
|
const n = this.nodes_[i];
|
|
10518
10806
|
if (n) for (const r of n.resources) this.taintedResources.add(r);
|
|
10519
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
|
+
}
|
|
10520
10818
|
this.aliveResources = Object.fromEntries(
|
|
10521
10819
|
Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
|
|
10522
10820
|
);
|
|
10523
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
|
+
}
|
|
10524
10839
|
};
|
|
10525
10840
|
}
|
|
10526
10841
|
});
|
|
@@ -10944,7 +11259,9 @@ async function runAnalysis(input) {
|
|
|
10944
11259
|
riskType: "formal_verification",
|
|
10945
11260
|
confidence: 90,
|
|
10946
11261
|
source: "formal_engine",
|
|
10947
|
-
violatedLayers: ["L2.2"]
|
|
11262
|
+
violatedLayers: ["L2.2"],
|
|
11263
|
+
violationGraph: v?.graph ?? null,
|
|
11264
|
+
violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
|
|
10948
11265
|
};
|
|
10949
11266
|
}
|
|
10950
11267
|
}
|
|
@@ -11489,7 +11806,9 @@ async function analyzeInner(input, mossLog) {
|
|
|
11489
11806
|
violated_layers: judgment.violatedLayers || [],
|
|
11490
11807
|
policy: "",
|
|
11491
11808
|
violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
|
|
11492
|
-
confidence: judgment.confidence
|
|
11809
|
+
confidence: judgment.confidence,
|
|
11810
|
+
violation_graph: judgment.violationGraph ?? null,
|
|
11811
|
+
violation_chain: judgment.violationChain ?? []
|
|
11493
11812
|
};
|
|
11494
11813
|
mossLog.logResult("Deny", denied.reason);
|
|
11495
11814
|
return denied;
|
|
@@ -12110,7 +12429,8 @@ async function analyzeRoute(c) {
|
|
|
12110
12429
|
confidence: 90,
|
|
12111
12430
|
analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
|
|
12112
12431
|
// 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
|
|
12113
|
-
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)
|
|
12114
12434
|
})
|
|
12115
12435
|
);
|
|
12116
12436
|
}
|
|
@@ -12130,7 +12450,9 @@ async function analyzeRoute(c) {
|
|
|
12130
12450
|
policy: result.policy,
|
|
12131
12451
|
violated_policy: result.violated_policy,
|
|
12132
12452
|
confidence: result.confidence,
|
|
12133
|
-
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)
|
|
12134
12456
|
})
|
|
12135
12457
|
);
|
|
12136
12458
|
} catch (err) {
|
|
@@ -12160,7 +12482,8 @@ function incMatchedToDeny(inc) {
|
|
|
12160
12482
|
property_id: "cross-" + inc.matched.chainId,
|
|
12161
12483
|
reason: inc.matched.reason,
|
|
12162
12484
|
risk_level: inc.matched.riskLevel,
|
|
12163
|
-
violation_chain: chain
|
|
12485
|
+
violation_chain: chain,
|
|
12486
|
+
violation_graph: inc.matched.graph ?? null
|
|
12164
12487
|
};
|
|
12165
12488
|
}
|
|
12166
12489
|
if (inc.exfil && inc.exfil.length > 0) {
|
|
@@ -12168,7 +12491,9 @@ function incMatchedToDeny(inc) {
|
|
|
12168
12491
|
property_id: "sensitive-data-flow",
|
|
12169
12492
|
reason: inc.exfil[0].reason,
|
|
12170
12493
|
risk_level: inc.exfil[0].risk_level,
|
|
12171
|
-
|
|
12494
|
+
// P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
|
|
12495
|
+
violation_chain: inc.exfil[0].path ?? [],
|
|
12496
|
+
violation_graph: inc.exfil[0].graph ?? null
|
|
12172
12497
|
};
|
|
12173
12498
|
}
|
|
12174
12499
|
return null;
|
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
interface AttackChainNode {
|
|
2
|
+
session_id: string;
|
|
3
|
+
seq: number;
|
|
4
|
+
stage: string;
|
|
5
|
+
action_detail: string;
|
|
6
|
+
resources: string[];
|
|
7
|
+
/** 节点在性质中的角色:source/carrier/sink(P2),或阶段名(P1) */
|
|
8
|
+
role?: string;
|
|
9
|
+
/** 是否为违例见证节点(当前动作补全链 / 触发外传) */
|
|
10
|
+
witness?: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface AttackChainEdge {
|
|
13
|
+
from: number;
|
|
14
|
+
to: number;
|
|
15
|
+
type: 'next' | 'data';
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
/** 性质表达式 token:token 文本 + 对应节点下标(可为多个)。 */
|
|
19
|
+
interface AttackChainExprToken {
|
|
20
|
+
token: string;
|
|
21
|
+
node_index: number | number[];
|
|
22
|
+
}
|
|
23
|
+
/** 结构化攻击链图(前端 SVG 渲染的数据契约,与 Python 端逐字段对齐)。 */
|
|
24
|
+
interface AttackChainGraph {
|
|
25
|
+
/** 性质元信息(前端展示"违反了哪条性质") */
|
|
26
|
+
property_id?: string;
|
|
27
|
+
property_short?: string;
|
|
28
|
+
property_name?: string;
|
|
29
|
+
/** 性质表达式与节点位置的对应关系 */
|
|
30
|
+
property_expression?: AttackChainExprToken[];
|
|
31
|
+
nodes: AttackChainNode[];
|
|
32
|
+
edges: AttackChainEdge[];
|
|
33
|
+
}
|
|
34
|
+
|
|
1
35
|
/** Security judgment result from a single analysis layer */
|
|
2
36
|
interface SecurityJudgment {
|
|
3
37
|
allowed: boolean;
|
|
@@ -8,6 +42,10 @@ interface SecurityJudgment {
|
|
|
8
42
|
source: string;
|
|
9
43
|
violatedLayers: string[];
|
|
10
44
|
isCredential?: boolean;
|
|
45
|
+
/** 组合式拦截的结构化攻击链(前端可视化用) */
|
|
46
|
+
violationGraph?: AttackChainGraph | null;
|
|
47
|
+
/** 兼容旧调用方:反例路径数组 */
|
|
48
|
+
violationChain?: unknown[];
|
|
11
49
|
}
|
|
12
50
|
|
|
13
51
|
/** 把命令按 shell 的连接符切成若干段,去掉空白段。
|
|
@@ -8483,6 +8483,33 @@ function allNodesFlat(mg) {
|
|
|
8483
8483
|
// src/engine/formal-engine/properties.ts
|
|
8484
8484
|
init_esm_shims();
|
|
8485
8485
|
init_rules_loader();
|
|
8486
|
+
function graphFromPath(nodes, path5, edgeType = "next", propertyMeta, roles, witnessLast = true, propertyExpression) {
|
|
8487
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
8488
|
+
for (const n of nodes) byKey.set(`${n.sessionId}#${n.seq}`, n);
|
|
8489
|
+
const gNodes = [];
|
|
8490
|
+
const gEdges = [];
|
|
8491
|
+
for (const item of path5) {
|
|
8492
|
+
const n = byKey.get(`${item.sessionId}#${item.seq}`);
|
|
8493
|
+
gNodes.push({
|
|
8494
|
+
session_id: item.sessionId,
|
|
8495
|
+
seq: item.seq,
|
|
8496
|
+
stage: n ? n.stage : "",
|
|
8497
|
+
action_detail: n ? n.actionDetail : item.detail,
|
|
8498
|
+
resources: n ? [...n.resources].sort() : []
|
|
8499
|
+
});
|
|
8500
|
+
if (gNodes.length > 1) {
|
|
8501
|
+
gEdges.push({ from: gNodes.length - 2, to: gNodes.length - 1, type: edgeType });
|
|
8502
|
+
}
|
|
8503
|
+
}
|
|
8504
|
+
(roles || []).forEach((role, i) => {
|
|
8505
|
+
if (i < gNodes.length && role) gNodes[i].role = role;
|
|
8506
|
+
});
|
|
8507
|
+
if (witnessLast && gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
8508
|
+
const out = { nodes: gNodes, edges: gEdges };
|
|
8509
|
+
if (propertyMeta) Object.assign(out, propertyMeta);
|
|
8510
|
+
if (propertyExpression) out.property_expression = propertyExpression;
|
|
8511
|
+
return out;
|
|
8512
|
+
}
|
|
8486
8513
|
function parseTs(ts) {
|
|
8487
8514
|
if (!ts) return null;
|
|
8488
8515
|
const t = Date.parse(String(ts).trim());
|
|
@@ -8543,7 +8570,12 @@ function checkCrossSessionChain(graph) {
|
|
|
8543
8570
|
propertyId: "cross-" + chain.id,
|
|
8544
8571
|
reason: chain.reason,
|
|
8545
8572
|
riskLevel: chain.riskLevel,
|
|
8546
|
-
path: path5
|
|
8573
|
+
path: path5,
|
|
8574
|
+
graph: graphFromPath(nodes, path5, "next", {
|
|
8575
|
+
property_id: "cross-" + chain.id,
|
|
8576
|
+
property_short: "P1",
|
|
8577
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
|
|
8578
|
+
}, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
|
|
8547
8579
|
});
|
|
8548
8580
|
return violations;
|
|
8549
8581
|
} else {
|
|
@@ -8563,7 +8595,12 @@ function checkCrossSessionChain(graph) {
|
|
|
8563
8595
|
propertyId: "cross-" + chain.id,
|
|
8564
8596
|
reason: chain.reason,
|
|
8565
8597
|
riskLevel: chain.riskLevel,
|
|
8566
|
-
path: path5
|
|
8598
|
+
path: path5,
|
|
8599
|
+
graph: graphFromPath(nodes, path5, "next", {
|
|
8600
|
+
property_id: "cross-" + chain.id,
|
|
8601
|
+
property_short: "P1",
|
|
8602
|
+
property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
|
|
8603
|
+
}, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
|
|
8567
8604
|
});
|
|
8568
8605
|
}
|
|
8569
8606
|
}
|
|
@@ -8578,10 +8615,15 @@ function checkSensitiveDataFlow(graph) {
|
|
|
8578
8615
|
for (let i = 0; i < nodes.length; i++) {
|
|
8579
8616
|
const n = nodes[i];
|
|
8580
8617
|
if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
|
|
8581
|
-
tainted.set(i,
|
|
8618
|
+
tainted.set(i, {
|
|
8619
|
+
source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
|
|
8620
|
+
parent: null
|
|
8621
|
+
});
|
|
8582
8622
|
}
|
|
8583
8623
|
}
|
|
8584
8624
|
if (tainted.size === 0) return violations;
|
|
8625
|
+
const taintEdges = /* @__PURE__ */ new Map();
|
|
8626
|
+
const edgeKey = (p, c) => `${p}:${c}`;
|
|
8585
8627
|
const edges = allEdges(graph);
|
|
8586
8628
|
let changed = true;
|
|
8587
8629
|
while (changed) {
|
|
@@ -8592,7 +8634,8 @@ function checkSensitiveDataFlow(graph) {
|
|
|
8592
8634
|
const dstRes = nodes[e.dst].resources;
|
|
8593
8635
|
for (const r of srcRes) {
|
|
8594
8636
|
if (dstRes.has(r)) {
|
|
8595
|
-
tainted.set(e.dst, tainted.get(e.src));
|
|
8637
|
+
tainted.set(e.dst, { source: tainted.get(e.src).source, parent: e.src });
|
|
8638
|
+
taintEdges.set(edgeKey(e.src, e.dst), r);
|
|
8596
8639
|
changed = true;
|
|
8597
8640
|
break;
|
|
8598
8641
|
}
|
|
@@ -8600,16 +8643,75 @@ function checkSensitiveDataFlow(graph) {
|
|
|
8600
8643
|
}
|
|
8601
8644
|
}
|
|
8602
8645
|
}
|
|
8646
|
+
const tracePath = (idx) => {
|
|
8647
|
+
const chain = [idx];
|
|
8648
|
+
const seen = /* @__PURE__ */ new Set([idx]);
|
|
8649
|
+
while (true) {
|
|
8650
|
+
const parent = tainted.get(chain[chain.length - 1])?.parent;
|
|
8651
|
+
if (parent === void 0 || parent === null || seen.has(parent)) break;
|
|
8652
|
+
chain.push(parent);
|
|
8653
|
+
seen.add(parent);
|
|
8654
|
+
}
|
|
8655
|
+
return chain.reverse();
|
|
8656
|
+
};
|
|
8657
|
+
const taintGraph = (chain) => {
|
|
8658
|
+
const gNodes = [];
|
|
8659
|
+
const gEdges = [];
|
|
8660
|
+
chain.forEach((ni, k) => {
|
|
8661
|
+
const n = nodes[ni];
|
|
8662
|
+
const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
|
|
8663
|
+
gNodes.push({
|
|
8664
|
+
session_id: n.sessionId,
|
|
8665
|
+
seq: n.seq,
|
|
8666
|
+
stage: n.stage,
|
|
8667
|
+
action_detail: n.actionDetail,
|
|
8668
|
+
resources: [...n.resources].sort(),
|
|
8669
|
+
role
|
|
8670
|
+
});
|
|
8671
|
+
if (k > 0) {
|
|
8672
|
+
const prev = chain[k - 1];
|
|
8673
|
+
gEdges.push({
|
|
8674
|
+
from: k - 1,
|
|
8675
|
+
to: k,
|
|
8676
|
+
type: "data",
|
|
8677
|
+
label: taintEdges.get(edgeKey(prev, ni)) || ""
|
|
8678
|
+
});
|
|
8679
|
+
}
|
|
8680
|
+
});
|
|
8681
|
+
if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
|
|
8682
|
+
const expr = [{ token: "CRED", node_index: 0 }];
|
|
8683
|
+
if (chain.length > 2) {
|
|
8684
|
+
expr.push({
|
|
8685
|
+
token: "\u5171\u4EAB\u8F7D\u4F53",
|
|
8686
|
+
node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
|
|
8687
|
+
});
|
|
8688
|
+
}
|
|
8689
|
+
expr.push({ token: "EXFIL", node_index: chain.length - 1 });
|
|
8690
|
+
return {
|
|
8691
|
+
property_id: "sensitive-data-flow",
|
|
8692
|
+
property_short: "P2",
|
|
8693
|
+
property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
|
|
8694
|
+
property_expression: expr,
|
|
8695
|
+
nodes: gNodes,
|
|
8696
|
+
edges: gEdges
|
|
8697
|
+
};
|
|
8698
|
+
};
|
|
8603
8699
|
const witness = graph.witnessIdx;
|
|
8604
8700
|
const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
|
|
8605
8701
|
for (const i of candidates) {
|
|
8606
8702
|
const n = nodes[i];
|
|
8607
8703
|
if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
|
|
8704
|
+
const chain = tracePath(i);
|
|
8608
8705
|
violations.push({
|
|
8609
8706
|
propertyId: "sensitive-data-flow",
|
|
8610
|
-
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`,
|
|
8707
|
+
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`,
|
|
8611
8708
|
riskLevel: "critical",
|
|
8612
|
-
path:
|
|
8709
|
+
path: chain.map((j) => ({
|
|
8710
|
+
sessionId: nodes[j].sessionId,
|
|
8711
|
+
seq: nodes[j].seq,
|
|
8712
|
+
detail: nodes[j].actionDetail.slice(0, 60)
|
|
8713
|
+
})),
|
|
8714
|
+
graph: taintGraph(chain)
|
|
8613
8715
|
});
|
|
8614
8716
|
if (witness !== void 0) return violations;
|
|
8615
8717
|
}
|
|
@@ -9353,7 +9455,9 @@ async function runAnalysis(input) {
|
|
|
9353
9455
|
riskType: "formal_verification",
|
|
9354
9456
|
confidence: 90,
|
|
9355
9457
|
source: "formal_engine",
|
|
9356
|
-
violatedLayers: ["L2.2"]
|
|
9458
|
+
violatedLayers: ["L2.2"],
|
|
9459
|
+
violationGraph: v?.graph ?? null,
|
|
9460
|
+
violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
|
|
9357
9461
|
};
|
|
9358
9462
|
}
|
|
9359
9463
|
}
|
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
|
}
|
|
@@ -8994,6 +9008,33 @@ var init_builder = __esm({
|
|
|
8994
9008
|
});
|
|
8995
9009
|
|
|
8996
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
|
+
}
|
|
8997
9038
|
function parseTs(ts) {
|
|
8998
9039
|
if (!ts) return null;
|
|
8999
9040
|
const t = Date.parse(String(ts).trim());
|
|
@@ -9051,7 +9092,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9051
9092
|
propertyId: "cross-" + chain.id,
|
|
9052
9093
|
reason: chain.reason,
|
|
9053
9094
|
riskLevel: chain.riskLevel,
|
|
9054
|
-
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 })))
|
|
9055
9101
|
});
|
|
9056
9102
|
return violations;
|
|
9057
9103
|
} else {
|
|
@@ -9071,7 +9117,12 @@ function checkCrossSessionChain(graph) {
|
|
|
9071
9117
|
propertyId: "cross-" + chain.id,
|
|
9072
9118
|
reason: chain.reason,
|
|
9073
9119
|
riskLevel: chain.riskLevel,
|
|
9074
|
-
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 })))
|
|
9075
9126
|
});
|
|
9076
9127
|
}
|
|
9077
9128
|
}
|
|
@@ -9086,10 +9137,15 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9086
9137
|
for (let i = 0; i < nodes.length; i++) {
|
|
9087
9138
|
const n = nodes[i];
|
|
9088
9139
|
if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
|
|
9089
|
-
tainted.set(i,
|
|
9140
|
+
tainted.set(i, {
|
|
9141
|
+
source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
|
|
9142
|
+
parent: null
|
|
9143
|
+
});
|
|
9090
9144
|
}
|
|
9091
9145
|
}
|
|
9092
9146
|
if (tainted.size === 0) return violations;
|
|
9147
|
+
const taintEdges = /* @__PURE__ */ new Map();
|
|
9148
|
+
const edgeKey = (p, c) => `${p}:${c}`;
|
|
9093
9149
|
const edges = allEdges(graph);
|
|
9094
9150
|
let changed = true;
|
|
9095
9151
|
while (changed) {
|
|
@@ -9100,7 +9156,8 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9100
9156
|
const dstRes = nodes[e.dst].resources;
|
|
9101
9157
|
for (const r of srcRes) {
|
|
9102
9158
|
if (dstRes.has(r)) {
|
|
9103
|
-
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);
|
|
9104
9161
|
changed = true;
|
|
9105
9162
|
break;
|
|
9106
9163
|
}
|
|
@@ -9108,16 +9165,75 @@ function checkSensitiveDataFlow(graph) {
|
|
|
9108
9165
|
}
|
|
9109
9166
|
}
|
|
9110
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
|
+
};
|
|
9111
9221
|
const witness = graph.witnessIdx;
|
|
9112
9222
|
const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
|
|
9113
9223
|
for (const i of candidates) {
|
|
9114
9224
|
const n = nodes[i];
|
|
9115
9225
|
if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
|
|
9226
|
+
const chain = tracePath(i);
|
|
9116
9227
|
violations.push({
|
|
9117
9228
|
propertyId: "sensitive-data-flow",
|
|
9118
|
-
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`,
|
|
9119
9230
|
riskLevel: "critical",
|
|
9120
|
-
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)
|
|
9121
9237
|
});
|
|
9122
9238
|
if (witness !== void 0) return violations;
|
|
9123
9239
|
}
|
|
@@ -9659,6 +9775,12 @@ var init_stateful = __esm({
|
|
|
9659
9775
|
nodes_ = [];
|
|
9660
9776
|
ptr = {};
|
|
9661
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();
|
|
9662
9784
|
/** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
|
|
9663
9785
|
taintedResources = /* @__PURE__ */ new Set();
|
|
9664
9786
|
aliveResources = {};
|
|
@@ -9869,7 +9991,8 @@ var init_stateful = __esm({
|
|
|
9869
9991
|
stages,
|
|
9870
9992
|
riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
|
|
9871
9993
|
reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
|
|
9872
|
-
path: this.chainPath(stages)
|
|
9994
|
+
path: this.chainPath(stages),
|
|
9995
|
+
graph: this.buildChainGraph(stages, cid)
|
|
9873
9996
|
};
|
|
9874
9997
|
}
|
|
9875
9998
|
}
|
|
@@ -9916,21 +10039,69 @@ var init_stateful = __esm({
|
|
|
9916
10039
|
}
|
|
9917
10040
|
return path6;
|
|
9918
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
|
+
}
|
|
9919
10070
|
// ---------------- 敏感数据 taint(P2) ----------------
|
|
9920
10071
|
advanceTaint(node) {
|
|
9921
10072
|
if (node.lifecycle === "dead") return;
|
|
9922
10073
|
const idx = node.seq;
|
|
9923
10074
|
const taintedRes = /* @__PURE__ */ new Set();
|
|
10075
|
+
let parent = null;
|
|
10076
|
+
let label = "";
|
|
10077
|
+
let foundParent = false;
|
|
9924
10078
|
for (const r of node.resources) {
|
|
9925
|
-
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
|
+
}
|
|
9926
10090
|
}
|
|
9927
10091
|
if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
|
|
9928
10092
|
this.tainted.add(idx);
|
|
10093
|
+
parent = null;
|
|
10094
|
+
label = "";
|
|
9929
10095
|
for (const r of node.resources) taintedRes.add(r);
|
|
9930
10096
|
}
|
|
9931
10097
|
if (taintedRes.size > 0) {
|
|
9932
10098
|
this.tainted.add(idx);
|
|
9933
|
-
|
|
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
|
+
}
|
|
9934
10105
|
}
|
|
9935
10106
|
}
|
|
9936
10107
|
resourceTainted(resource) {
|
|
@@ -9940,16 +10111,84 @@ var init_stateful = __esm({
|
|
|
9940
10111
|
checkExfilAfterTaint(node) {
|
|
9941
10112
|
if (!this.tainted.has(node.seq)) return [];
|
|
9942
10113
|
if (EXFIL_RE2.test(node.actionDetail)) {
|
|
10114
|
+
const chain = this.traceTaintChain(node.seq);
|
|
9943
10115
|
return [{
|
|
9944
10116
|
property_id: "sensitive-data-flow",
|
|
9945
|
-
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`,
|
|
9946
10118
|
risk_level: "critical",
|
|
9947
|
-
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)
|
|
9948
10124
|
}];
|
|
9949
10125
|
}
|
|
9950
10126
|
return [];
|
|
9951
10127
|
}
|
|
9952
|
-
|
|
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
|
+
}
|
|
9953
10192
|
const sorted = [...this.tainted].sort((a, b) => a - b);
|
|
9954
10193
|
for (const i of sorted) {
|
|
9955
10194
|
if (i === node.seq) continue;
|
|
@@ -9968,6 +10207,9 @@ var init_stateful = __esm({
|
|
|
9968
10207
|
nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
|
|
9969
10208
|
ptrs: { ...this.ptr },
|
|
9970
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()]),
|
|
9971
10213
|
alive: { ...this.aliveResources },
|
|
9972
10214
|
seq_next: this.seqNext
|
|
9973
10215
|
};
|
|
@@ -9995,15 +10237,53 @@ var init_stateful = __esm({
|
|
|
9995
10237
|
this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
|
|
9996
10238
|
this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
|
|
9997
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();
|
|
9998
10251
|
this.taintedResources = /* @__PURE__ */ new Set();
|
|
9999
10252
|
for (const i of this.tainted) {
|
|
10000
10253
|
const n = this.nodes_[i];
|
|
10001
10254
|
if (n) for (const r of n.resources) this.taintedResources.add(r);
|
|
10002
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
|
+
}
|
|
10003
10266
|
this.aliveResources = Object.fromEntries(
|
|
10004
10267
|
Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
|
|
10005
10268
|
);
|
|
10006
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
|
+
}
|
|
10007
10287
|
};
|
|
10008
10288
|
}
|
|
10009
10289
|
});
|
|
@@ -10427,7 +10707,9 @@ async function runAnalysis(input) {
|
|
|
10427
10707
|
riskType: "formal_verification",
|
|
10428
10708
|
confidence: 90,
|
|
10429
10709
|
source: "formal_engine",
|
|
10430
|
-
violatedLayers: ["L2.2"]
|
|
10710
|
+
violatedLayers: ["L2.2"],
|
|
10711
|
+
violationGraph: v?.graph ?? null,
|
|
10712
|
+
violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
|
|
10431
10713
|
};
|
|
10432
10714
|
}
|
|
10433
10715
|
}
|
|
@@ -10800,7 +11082,42 @@ var AnalyzeResponseSchema = z.object({
|
|
|
10800
11082
|
confidence: z.number().int().min(0).max(100).default(100),
|
|
10801
11083
|
analysis_duration_ms: z.number().default(0),
|
|
10802
11084
|
// 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
|
|
10803
|
-
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)
|
|
10804
11121
|
});
|
|
10805
11122
|
var HealthResponseSchema = z.object({
|
|
10806
11123
|
status: z.string().default("healthy"),
|
|
@@ -11224,7 +11541,9 @@ async function analyzeInner(input, mossLog) {
|
|
|
11224
11541
|
violated_layers: judgment.violatedLayers || [],
|
|
11225
11542
|
policy: "",
|
|
11226
11543
|
violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
|
|
11227
|
-
confidence: judgment.confidence
|
|
11544
|
+
confidence: judgment.confidence,
|
|
11545
|
+
violation_graph: judgment.violationGraph ?? null,
|
|
11546
|
+
violation_chain: judgment.violationChain ?? []
|
|
11228
11547
|
};
|
|
11229
11548
|
mossLog.logResult("Deny", denied.reason);
|
|
11230
11549
|
return denied;
|
|
@@ -11781,7 +12100,8 @@ async function analyzeRoute(c) {
|
|
|
11781
12100
|
confidence: 90,
|
|
11782
12101
|
analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
|
|
11783
12102
|
// 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
|
|
11784
|
-
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)
|
|
11785
12105
|
})
|
|
11786
12106
|
);
|
|
11787
12107
|
}
|
|
@@ -11801,7 +12121,9 @@ async function analyzeRoute(c) {
|
|
|
11801
12121
|
policy: result.policy,
|
|
11802
12122
|
violated_policy: result.violated_policy,
|
|
11803
12123
|
confidence: result.confidence,
|
|
11804
|
-
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)
|
|
11805
12127
|
})
|
|
11806
12128
|
);
|
|
11807
12129
|
} catch (err) {
|
|
@@ -11832,7 +12154,8 @@ function incMatchedToDeny(inc) {
|
|
|
11832
12154
|
property_id: "cross-" + inc.matched.chainId,
|
|
11833
12155
|
reason: inc.matched.reason,
|
|
11834
12156
|
risk_level: inc.matched.riskLevel,
|
|
11835
|
-
violation_chain: chain
|
|
12157
|
+
violation_chain: chain,
|
|
12158
|
+
violation_graph: inc.matched.graph ?? null
|
|
11836
12159
|
};
|
|
11837
12160
|
}
|
|
11838
12161
|
if (inc.exfil && inc.exfil.length > 0) {
|
|
@@ -11840,7 +12163,9 @@ function incMatchedToDeny(inc) {
|
|
|
11840
12163
|
property_id: "sensitive-data-flow",
|
|
11841
12164
|
reason: inc.exfil[0].reason,
|
|
11842
12165
|
risk_level: inc.exfil[0].risk_level,
|
|
11843
|
-
|
|
12166
|
+
// P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
|
|
12167
|
+
violation_chain: inc.exfil[0].path ?? [],
|
|
12168
|
+
violation_graph: inc.exfil[0].graph ?? null
|
|
11844
12169
|
};
|
|
11845
12170
|
}
|
|
11846
12171
|
return null;
|