pentesting 0.73.7 → 0.73.9

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.
@@ -13,13 +13,14 @@ import {
13
13
  getAllProcesses,
14
14
  getBackgroundProcessesMap,
15
15
  getLimits,
16
+ getOptionalRuntimeSection,
16
17
  getProcess,
17
18
  getProcessEventLog,
18
19
  getRequiredRuntimeSection,
19
20
  getRuntimeSectionOr,
20
21
  logEvent,
21
22
  setProcess
22
- } from "./chunk-KAUE3MSR.js";
23
+ } from "./chunk-S5ZMXFHR.js";
23
24
 
24
25
  // src/shared/constants/time/conversions.ts
25
26
  var MS_PER_MINUTE = 6e4;
@@ -347,7 +348,7 @@ var INPUT_PROMPT_PATTERNS = [
347
348
  ];
348
349
 
349
350
  // src/shared/constants/agent.ts
350
- var APP_VERSION = "0.73.7";
351
+ var APP_VERSION = "0.73.9";
351
352
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
352
353
  var LLM_ROLES = {
353
354
  SYSTEM: "system",
@@ -841,6 +842,8 @@ var UI_COMMANDS = {
841
842
  START_SHORT: "s",
842
843
  FINDINGS: "findings",
843
844
  FINDINGS_SHORT: "f",
845
+ REPORT: "report",
846
+ REPORT_SHORT: "r",
844
847
  ASSETS: "assets",
845
848
  ASSETS_SHORT: "a",
846
849
  LOGS: "logs",
@@ -2798,7 +2801,7 @@ async function cleanupAllProcesses() {
2798
2801
  cleanupDone = false;
2799
2802
  return;
2800
2803
  }
2801
- const { getBackgroundProcessesMap: getBackgroundProcessesMap2 } = await import("./process-registry-7XV46TDC.js");
2804
+ const { getBackgroundProcessesMap: getBackgroundProcessesMap2 } = await import("./process-registry-4Y3HB4YQ.js");
2802
2805
  const backgroundProcesses = getBackgroundProcessesMap2();
2803
2806
  terminateAllNatively(backgroundProcesses, "SIGTERM");
2804
2807
  await new Promise((r) => setTimeout(r, SYSTEM_LIMITS.CLEANUP_BATCH_WAIT_MS));
@@ -3383,6 +3386,28 @@ function matchesServiceProfile(serviceProfile, services) {
3383
3386
  // src/shared/utils/agent-memory/session-snapshot.ts
3384
3387
  import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
3385
3388
  import { join as join3 } from "path";
3389
+
3390
+ // src/agents/memory-rollout-config.ts
3391
+ var DEFAULT_BRANCH_MEMORY_ROLLOUT = {
3392
+ main_prompt: true,
3393
+ strategist: true,
3394
+ delegated_prompt: true,
3395
+ resume_hints: true
3396
+ };
3397
+ function getMemoryRolloutConfig() {
3398
+ return getOptionalRuntimeSection("memory_rollout") ?? {};
3399
+ }
3400
+ function getBranchMemoryRollout() {
3401
+ return {
3402
+ ...DEFAULT_BRANCH_MEMORY_ROLLOUT,
3403
+ ...getMemoryRolloutConfig().branch_memory ?? {}
3404
+ };
3405
+ }
3406
+ function isBranchMemoryEnabled(target) {
3407
+ return getBranchMemoryRollout()[target];
3408
+ }
3409
+
3410
+ // src/shared/utils/agent-memory/session-snapshot.ts
3386
3411
  var SNAPSHOT_FILE = join3(WORKSPACE.MEMORY, SPECIAL_FILES.SESSION_SNAPSHOT);
3387
3412
  function saveSessionSnapshot(snapshot) {
3388
3413
  try {
@@ -3413,6 +3438,10 @@ function snapshotToPrompt() {
3413
3438
  "NEXT PRIORITIES (resume here):",
3414
3439
  ...snap.next.map((n, i) => ` ${i + 1}. ${n}`)
3415
3440
  ];
3441
+ if (isBranchMemoryEnabled("resume_hints") && snap.resumeHints && snap.resumeHints.length > 0) {
3442
+ lines.push("", "RESUME HINTS:");
3443
+ snap.resumeHints.forEach((hint) => lines.push(` \u21AA ${hint}`));
3444
+ }
3416
3445
  if (snap.credentials.length > 0) {
3417
3446
  lines.push("", "USABLE CREDENTIALS:");
3418
3447
  snap.credentials.forEach((c) => lines.push(` \u{1F511} ${c}`));
@@ -3595,6 +3624,1491 @@ var PersistentMemory = class {
3595
3624
  }
3596
3625
  };
3597
3626
 
3627
+ // src/shared/utils/attack-graph/types.ts
3628
+ var GRAPH_STATUS = {
3629
+ NEEDS_SEARCH: "needs_search",
3630
+ POTENTIAL: "potential"
3631
+ };
3632
+ var GRAPH_LIMITS = {
3633
+ /** Maximum recommended attack chains to return */
3634
+ MAX_CHAINS: 10,
3635
+ /** Maximum chains to show in prompt */
3636
+ PROMPT_CHAINS: 5,
3637
+ /** Maximum unexploited vulns to show in prompt */
3638
+ PROMPT_VULNS: 5,
3639
+ /** Maximum failed paths to show in prompt */
3640
+ PROMPT_FAILED: 5,
3641
+ /** Maximum DFS depth for chain discovery */
3642
+ MAX_CHAIN_DEPTH: 6,
3643
+ /** Maximum nodes to display in ASCII graph */
3644
+ ASCII_MAX_NODES: 30
3645
+ };
3646
+ var NODE_TYPE = {
3647
+ HOST: "host",
3648
+ SERVICE: "service",
3649
+ CREDENTIAL: "credential",
3650
+ VULNERABILITY: "vulnerability",
3651
+ ACCESS: "access",
3652
+ LOOT: "loot",
3653
+ OSINT: "osint",
3654
+ // ─── Active Directory Node Types (2차 — Hard/Insane support) ───
3655
+ /** AD domain object (e.g. DOMAIN.LOCAL) */
3656
+ DOMAIN: "domain",
3657
+ /** AD user or computer account */
3658
+ USER_ACCOUNT: "user-account",
3659
+ /** Group Policy Object — may grant code exec if writable */
3660
+ GPO: "gpo",
3661
+ /** ACL entry — WriteDACL/GenericAll/etc. on another object */
3662
+ ACL: "acl",
3663
+ /** ADCS certificate template — ESC1-13 attack surface */
3664
+ CERTIFICATE_TEMPLATE: "certificate-template"
3665
+ };
3666
+ var NODE_STATUS = {
3667
+ DISCOVERED: "discovered",
3668
+ ATTEMPTED: "attempted",
3669
+ SUCCEEDED: "succeeded",
3670
+ FAILED: "failed"
3671
+ };
3672
+ var EDGE_STATUS = {
3673
+ UNTESTED: "untested",
3674
+ TESTING: "testing",
3675
+ SUCCEEDED: "succeeded",
3676
+ FAILED: "failed"
3677
+ };
3678
+ var IMPACT_WEIGHT = {
3679
+ critical: 4,
3680
+ high: 3,
3681
+ medium: 2,
3682
+ low: 1
3683
+ };
3684
+
3685
+ // src/shared/utils/attack-graph/node.ts
3686
+ function generateNodeId(type, label) {
3687
+ return `${type}:${label}`.toLowerCase().replace(/\s+/g, "_");
3688
+ }
3689
+ function createNode(type, label, data = {}, status = NODE_STATUS.DISCOVERED) {
3690
+ const id = generateNodeId(type, label);
3691
+ return {
3692
+ id,
3693
+ type,
3694
+ label,
3695
+ data,
3696
+ status,
3697
+ discoveredAt: Date.now()
3698
+ };
3699
+ }
3700
+ function canMarkAttempted(node) {
3701
+ return node.status === NODE_STATUS.DISCOVERED;
3702
+ }
3703
+
3704
+ // src/shared/utils/attack-graph/edge.ts
3705
+ function createEdge(from, to, relation, confidence = 0.5, status = EDGE_STATUS.UNTESTED) {
3706
+ return {
3707
+ from,
3708
+ to,
3709
+ relation,
3710
+ confidence,
3711
+ status
3712
+ };
3713
+ }
3714
+ function edgeExists(edges, fromId, toId, relation) {
3715
+ return edges.some(
3716
+ (e) => e.from === fromId && e.to === toId && e.relation === relation
3717
+ );
3718
+ }
3719
+ function getIncomingEdges(edges, nodeId) {
3720
+ return edges.filter((e) => e.to === nodeId);
3721
+ }
3722
+ function markEdgeSucceeded(edge) {
3723
+ return {
3724
+ ...edge,
3725
+ status: EDGE_STATUS.SUCCEEDED
3726
+ };
3727
+ }
3728
+ function markEdgeFailed(edge, reason) {
3729
+ return {
3730
+ ...edge,
3731
+ status: EDGE_STATUS.FAILED,
3732
+ failReason: reason
3733
+ };
3734
+ }
3735
+ function isTestableEdge(edge) {
3736
+ return edge.status === EDGE_STATUS.UNTESTED || edge.status === EDGE_STATUS.TESTING;
3737
+ }
3738
+
3739
+ // src/shared/utils/attack-graph/factories/host-service.ts
3740
+ function createHostNode(ip, hostname) {
3741
+ return createNode(NODE_TYPE.HOST, ip, { ip, hostname });
3742
+ }
3743
+ function createServiceNode(host, port, service, version) {
3744
+ const hostId = generateNodeId(NODE_TYPE.HOST, host);
3745
+ const node = createNode(
3746
+ NODE_TYPE.SERVICE,
3747
+ `${host}:${port}`,
3748
+ { host, port, service, version }
3749
+ );
3750
+ const hostEdge = createEdge(hostId, node.id, "has_service", 0.95);
3751
+ return { node, hostEdge };
3752
+ }
3753
+ function findTargetServices(nodes, target) {
3754
+ const results = [];
3755
+ for (const node of nodes.values()) {
3756
+ if (node.type === NODE_TYPE.SERVICE && node.label.includes(target)) {
3757
+ results.push(node);
3758
+ }
3759
+ }
3760
+ return results;
3761
+ }
3762
+
3763
+ // src/shared/utils/attack-graph/factories/credential-exploit.ts
3764
+ var AUTH_SERVICES = [
3765
+ "ssh",
3766
+ "ftp",
3767
+ "rdp",
3768
+ "smb",
3769
+ "http",
3770
+ "mysql",
3771
+ "postgresql",
3772
+ "mssql",
3773
+ "winrm",
3774
+ "vnc",
3775
+ "telnet"
3776
+ ];
3777
+ var HIGH_PRIVILEGE_LEVELS = ["root", "admin", "SYSTEM", "Administrator"];
3778
+ function createCredentialNode(username, password, source) {
3779
+ return createNode(
3780
+ NODE_TYPE.CREDENTIAL,
3781
+ `${username}:***`,
3782
+ { username, password, source }
3783
+ );
3784
+ }
3785
+ function createCredentialSprayEdges(credId, nodes) {
3786
+ const edges = [];
3787
+ for (const [id, node] of nodes) {
3788
+ if (node.type === NODE_TYPE.SERVICE) {
3789
+ const svc = String(node.data.service || "");
3790
+ if (AUTH_SERVICES.some((s) => svc.includes(s))) {
3791
+ edges.push(createEdge(credId, id, "can_try_on", 0.6));
3792
+ }
3793
+ }
3794
+ }
3795
+ return edges;
3796
+ }
3797
+ function createVulnerabilityNode(title, target, severity, hasExploit = false) {
3798
+ return createNode(
3799
+ NODE_TYPE.VULNERABILITY,
3800
+ title,
3801
+ { target, severity, hasExploit }
3802
+ );
3803
+ }
3804
+ function createVersionSearchNode(service, version) {
3805
+ return createNode(
3806
+ NODE_TYPE.VULNERABILITY,
3807
+ `CVE search: ${service} ${version}`,
3808
+ { service, version, status: GRAPH_STATUS.NEEDS_SEARCH }
3809
+ );
3810
+ }
3811
+ function createAccessNode(host, level, via) {
3812
+ return createNode(
3813
+ NODE_TYPE.ACCESS,
3814
+ `${level}@${host}`,
3815
+ { host, level, via }
3816
+ );
3817
+ }
3818
+ function createPotentialAccessNode(exploitTitle) {
3819
+ return createNode(
3820
+ NODE_TYPE.ACCESS,
3821
+ `shell via ${exploitTitle}`,
3822
+ { via: exploitTitle, status: GRAPH_STATUS.POTENTIAL }
3823
+ );
3824
+ }
3825
+ function isHighPrivilege(level) {
3826
+ return HIGH_PRIVILEGE_LEVELS.includes(level);
3827
+ }
3828
+
3829
+ // src/shared/utils/attack-graph/factories/osint-loot.ts
3830
+ function createLootNode(host) {
3831
+ return createNode(
3832
+ NODE_TYPE.LOOT,
3833
+ `flags on ${host}`,
3834
+ { host, status: GRAPH_STATUS.NEEDS_SEARCH }
3835
+ );
3836
+ }
3837
+ function createOSINTNode(category, detail, data = {}) {
3838
+ return createNode(
3839
+ NODE_TYPE.OSINT,
3840
+ `${category}: ${detail}`,
3841
+ { category, detail, ...data }
3842
+ );
3843
+ }
3844
+ function findOSINTRelatedNodes(nodes, detail) {
3845
+ const results = [];
3846
+ for (const [id, node] of nodes) {
3847
+ if (node.type === NODE_TYPE.HOST || node.type === NODE_TYPE.SERVICE) {
3848
+ const hostIp = String(node.data.ip || node.data.host || "");
3849
+ const hostname = String(node.data.hostname || "");
3850
+ if (hostIp && detail.includes(hostIp) || hostname && detail.includes(hostname)) {
3851
+ results.push({ id, node });
3852
+ }
3853
+ }
3854
+ }
3855
+ return results;
3856
+ }
3857
+ function formatFailedPath(from, to, reason) {
3858
+ return `${from} \u2192 ${to}${reason ? ` (${reason})` : ""}`;
3859
+ }
3860
+ function formatFailedNode(label, reason) {
3861
+ return `${label}${reason ? ` (${reason})` : ""}`;
3862
+ }
3863
+
3864
+ // src/shared/utils/attack-graph/chain-discovery.ts
3865
+ var IMPACT_WEIGHT_MAP = {
3866
+ [NODE_TYPE.LOOT]: SEVERITIES.CRITICAL,
3867
+ [NODE_TYPE.DOMAIN]: SEVERITIES.CRITICAL,
3868
+ [NODE_TYPE.CERTIFICATE_TEMPLATE]: SEVERITIES.HIGH
3869
+ };
3870
+ function recommendChains(nodes, edges) {
3871
+ const chains = [];
3872
+ const visited = /* @__PURE__ */ new Set();
3873
+ const goalTypes = [
3874
+ NODE_TYPE.ACCESS,
3875
+ NODE_TYPE.LOOT,
3876
+ NODE_TYPE.DOMAIN,
3877
+ NODE_TYPE.CERTIFICATE_TEMPLATE
3878
+ ];
3879
+ const entryNodeTypes = [
3880
+ NODE_TYPE.HOST,
3881
+ NODE_TYPE.SERVICE,
3882
+ NODE_TYPE.CREDENTIAL,
3883
+ NODE_TYPE.OSINT
3884
+ ];
3885
+ const entryNodes = Array.from(nodes.values()).filter(
3886
+ (n) => entryNodeTypes.includes(n.type)
3887
+ );
3888
+ const ctx = { nodes, edges, goalTypes, results: chains };
3889
+ for (const entry of entryNodes) {
3890
+ visited.clear();
3891
+ dfsChains(entry.id, [], [], visited, ctx);
3892
+ }
3893
+ return prioritizeChains(chains);
3894
+ }
3895
+ function prioritizeChains(chains) {
3896
+ const seen = /* @__PURE__ */ new Set();
3897
+ const unique = chains.filter((c) => {
3898
+ if (seen.has(c.description)) return false;
3899
+ seen.add(c.description);
3900
+ return true;
3901
+ });
3902
+ unique.sort(
3903
+ (a, b) => b.probability * (IMPACT_WEIGHT[b.impact] || 1) - a.probability * (IMPACT_WEIGHT[a.impact] || 1)
3904
+ );
3905
+ return unique.slice(0, GRAPH_LIMITS.MAX_CHAINS);
3906
+ }
3907
+ function dfsChains(nodeId, path4, pathEdges, visited, ctx) {
3908
+ const { nodes, edges, goalTypes, results } = ctx;
3909
+ const node = nodes.get(nodeId);
3910
+ if (!node || visited.has(nodeId) || path4.length >= GRAPH_LIMITS.MAX_CHAIN_DEPTH || node.status === NODE_STATUS.FAILED) {
3911
+ return;
3912
+ }
3913
+ visited.add(nodeId);
3914
+ path4.push(node);
3915
+ if (isGoalReached(node, goalTypes, path4)) {
3916
+ results.push(buildChain(path4, pathEdges, node));
3917
+ }
3918
+ const outEdges = edges.filter((e) => e.from === nodeId && e.status !== EDGE_STATUS.FAILED);
3919
+ for (const edge of outEdges) {
3920
+ dfsChains(edge.to, path4, [...pathEdges, edge], visited, ctx);
3921
+ }
3922
+ path4.pop();
3923
+ visited.delete(nodeId);
3924
+ }
3925
+ function isGoalReached(node, goalTypes, path4) {
3926
+ return goalTypes.includes(node.type) && node.status !== NODE_STATUS.SUCCEEDED && path4.length > 1;
3927
+ }
3928
+ function buildChain(path4, edges, targetNode) {
3929
+ return {
3930
+ steps: [...path4],
3931
+ edges: [...edges],
3932
+ description: path4.map((n) => n.label).join(" \u2192 "),
3933
+ probability: edges.reduce((acc, e) => acc * e.confidence, 1),
3934
+ impact: estimateImpact(targetNode),
3935
+ length: path4.length
3936
+ };
3937
+ }
3938
+ function estimateImpact(node) {
3939
+ if (IMPACT_WEIGHT_MAP[node.type]) return IMPACT_WEIGHT_MAP[node.type];
3940
+ if (node.type === NODE_TYPE.ACCESS) {
3941
+ const level = String(node.data.level || "");
3942
+ return ["root", "SYSTEM", "Administrator", "admin"].includes(level) ? SEVERITIES.CRITICAL : SEVERITIES.HIGH;
3943
+ }
3944
+ if (node.type === NODE_TYPE.VULNERABILITY) {
3945
+ const sev = String(node.data.severity || "").toLowerCase();
3946
+ if (sev === "critical") return SEVERITIES.CRITICAL;
3947
+ if (sev === "high") return SEVERITIES.HIGH;
3948
+ if (sev === "medium") return SEVERITIES.MEDIUM;
3949
+ }
3950
+ return SEVERITIES.LOW;
3951
+ }
3952
+ function extractSuccessfulChain(nodes, edges) {
3953
+ const succeededNodes = Array.from(nodes.values()).filter((n) => n.status === NODE_STATUS.SUCCEEDED);
3954
+ if (succeededNodes.length < 2) return null;
3955
+ const succeededEdges = edges.filter((e) => e.status === EDGE_STATUS.SUCCEEDED);
3956
+ const { chainLabels, toolsUsed } = traceSucceededPath(succeededNodes, succeededEdges, nodes);
3957
+ if (chainLabels.length < 2) return null;
3958
+ const serviceProfile = succeededNodes.length > 0 ? succeededNodes.map((n) => abstractifyLabel(n.label)).filter(Boolean).join(" + ") : "unknown";
3959
+ return {
3960
+ serviceProfile,
3961
+ chainSummary: chainLabels.join(" \u2192 "),
3962
+ toolsUsed: [...new Set(toolsUsed.filter(Boolean))]
3963
+ };
3964
+ }
3965
+ function traceSucceededPath(succeededNodes, succeededEdges, nodes) {
3966
+ const labels = [];
3967
+ const tools = [];
3968
+ const visited = /* @__PURE__ */ new Set();
3969
+ const incomingTargets = new Set(succeededEdges.map((e) => e.to));
3970
+ const entryNodes = succeededNodes.filter((n) => !incomingTargets.has(n.id));
3971
+ const queue = entryNodes.length > 0 ? [entryNodes[0]] : [succeededNodes[0]];
3972
+ while (queue.length > 0) {
3973
+ const current = queue.shift();
3974
+ if (visited.has(current.id)) continue;
3975
+ visited.add(current.id);
3976
+ labels.push(abstractifyLabel(current.label));
3977
+ if (current.technique) tools.push(current.technique.split(" ")[0]);
3978
+ const nextEdges = succeededEdges.filter((e) => e.from === current.id);
3979
+ for (const edge of nextEdges) {
3980
+ const nextNode = nodes.get(edge.to);
3981
+ if (nextNode?.status === NODE_STATUS.SUCCEEDED) {
3982
+ if (edge.action) tools.push(edge.action.split(" ")[0]);
3983
+ queue.push(nextNode);
3984
+ }
3985
+ }
3986
+ }
3987
+ return { chainLabels: labels, toolsUsed: tools };
3988
+ }
3989
+ function abstractifyLabel(label) {
3990
+ let result = label.replace(/^(host|service|vulnerability|access|loot|osint|credential|cve_search):/i, "").replace(/^https?:\/\/|^ftp:\/\//gi, "").replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?\b/g, "").replace(/:\d{2,5}\b/g, "").replace(/\b[0-9a-f]{16,}\b/gi, "").replace(/(\d+\.\d+)\.\d+/g, "$1.x").trim().replace(/\s+/g, " ");
3991
+ return result || label;
3992
+ }
3993
+
3994
+ // src/shared/utils/attack-graph/prompt-formatter.ts
3995
+ function formatGraphForPrompt(nodes, edges, failedPaths) {
3996
+ if (nodes.size === 0) return "";
3997
+ const lines = ["<attack-graph>"];
3998
+ const nodesByType = {};
3999
+ const nodesByStatus = {};
4000
+ for (const node of nodes.values()) {
4001
+ nodesByType[node.type] = (nodesByType[node.type] || 0) + 1;
4002
+ nodesByStatus[node.status] = (nodesByStatus[node.status] || 0) + 1;
4003
+ }
4004
+ lines.push(`Nodes: ${Object.entries(nodesByType).map(([t, c]) => `${c} ${t}s`).join(", ")}`);
4005
+ lines.push(`Status: ${Object.entries(nodesByStatus).map(([s, c]) => `${c} ${s}`).join(", ")}`);
4006
+ lines.push(`Edges: ${edges.length} (${edges.filter((e) => e.status === EDGE_STATUS.FAILED).length} failed)`);
4007
+ const chains = recommendChains(nodes, edges);
4008
+ if (chains.length > 0) {
4009
+ lines.push("");
4010
+ lines.push("RECOMMENDED ATTACK PATHS (try these):");
4011
+ for (let i = 0; i < Math.min(GRAPH_LIMITS.PROMPT_CHAINS, chains.length); i++) {
4012
+ const c = chains[i];
4013
+ const prob = (c.probability * 100).toFixed(0);
4014
+ lines.push(` PATH ${i + 1} [${c.impact.toUpperCase()} | ${prob}%]: ${c.description}`);
4015
+ }
4016
+ }
4017
+ if (failedPaths.length > 0) {
4018
+ lines.push("");
4019
+ lines.push("FAILED PATHS (DO NOT RETRY):");
4020
+ for (const fp of failedPaths.slice(-GRAPH_LIMITS.PROMPT_FAILED)) {
4021
+ lines.push(` \u2717 ${fp}`);
4022
+ }
4023
+ }
4024
+ const unexploitedVulns = Array.from(nodes.values()).filter((n) => n.type === NODE_TYPE.VULNERABILITY && n.status !== NODE_STATUS.SUCCEEDED && n.status !== NODE_STATUS.FAILED);
4025
+ if (unexploitedVulns.length > 0) {
4026
+ lines.push("");
4027
+ lines.push(`UNEXPLOITED VULNERABILITIES (${unexploitedVulns.length}):`);
4028
+ for (const v of unexploitedVulns.slice(0, GRAPH_LIMITS.PROMPT_VULNS)) {
4029
+ const sev = v.data.severity || "unknown";
4030
+ const status = v.status === NODE_STATUS.ATTEMPTED ? " \u23F3 testing" : "";
4031
+ lines.push(` - ${v.label} (${sev})${status}`);
4032
+ }
4033
+ }
4034
+ const accessNodes = Array.from(nodes.values()).filter((n) => n.type === NODE_TYPE.ACCESS && n.status === NODE_STATUS.SUCCEEDED);
4035
+ if (accessNodes.length > 0) {
4036
+ lines.push("");
4037
+ lines.push("ACHIEVED ACCESS:");
4038
+ for (const a of accessNodes) {
4039
+ lines.push(` \u2713 ${a.label}`);
4040
+ }
4041
+ }
4042
+ const credNodes = Array.from(nodes.values()).filter((n) => n.type === NODE_TYPE.CREDENTIAL);
4043
+ if (credNodes.length > 0) {
4044
+ lines.push("");
4045
+ lines.push(`CREDENTIALS (${credNodes.length} \u2014 try on all services):`);
4046
+ for (const c of credNodes) {
4047
+ const source = c.data.source || "unknown";
4048
+ const sprayEdges = edges.filter((e) => e.from === c.id && e.relation === "can_try_on");
4049
+ const testedCount = sprayEdges.filter((e) => e.status !== EDGE_STATUS.UNTESTED).length;
4050
+ lines.push(` \u{1F511} ${c.label} (from: ${source}) [sprayed: ${testedCount}/${sprayEdges.length}]`);
4051
+ }
4052
+ }
4053
+ lines.push("</attack-graph>");
4054
+ return lines.join("\n");
4055
+ }
4056
+
4057
+ // src/shared/utils/attack-graph/formatters/icons.ts
4058
+ var TYPE_ICONS = {
4059
+ [NODE_TYPE.HOST]: "\u{1F5A5}",
4060
+ [NODE_TYPE.SERVICE]: "\u2699",
4061
+ [NODE_TYPE.VULNERABILITY]: "\u26A0",
4062
+ [NODE_TYPE.CREDENTIAL]: "\u{1F511}",
4063
+ [NODE_TYPE.ACCESS]: "\u{1F513}",
4064
+ [NODE_TYPE.LOOT]: "\u{1F3F4}",
4065
+ [NODE_TYPE.OSINT]: "\u{1F50D}",
4066
+ // AD node icons (2차 — Hard/Insane support)
4067
+ [NODE_TYPE.DOMAIN]: "\u{1F3F0}",
4068
+ [NODE_TYPE.USER_ACCOUNT]: "\u{1F464}",
4069
+ [NODE_TYPE.GPO]: "\u{1F4CB}",
4070
+ [NODE_TYPE.ACL]: "\u{1F510}",
4071
+ [NODE_TYPE.CERTIFICATE_TEMPLATE]: "\u{1F4DC}"
4072
+ };
4073
+ var STATUS_ICONS = {
4074
+ [NODE_STATUS.DISCOVERED]: "\u25CB",
4075
+ [NODE_STATUS.ATTEMPTED]: "\u25D0",
4076
+ [NODE_STATUS.SUCCEEDED]: "\u25CF",
4077
+ [NODE_STATUS.FAILED]: "\u2717"
4078
+ };
4079
+
4080
+ // src/shared/utils/attack-graph/formatters/stats.ts
4081
+ function getGraphStats(nodes, edges) {
4082
+ const succeeded = Array.from(nodes.values()).filter((n) => n.status === NODE_STATUS.SUCCEEDED).length;
4083
+ const failed = Array.from(nodes.values()).filter((n) => n.status === NODE_STATUS.FAILED).length;
4084
+ return {
4085
+ nodes: nodes.size,
4086
+ edges: edges.length,
4087
+ succeeded,
4088
+ failed,
4089
+ chains: recommendChains(nodes, edges).length
4090
+ };
4091
+ }
4092
+
4093
+ // src/shared/utils/attack-graph/formatters/graph-ascii.ts
4094
+ function formatGraphAsASCII(nodes, edges) {
4095
+ if (nodes.size === 0) return "(Empty attack graph \u2014 no discoveries yet)";
4096
+ const lines = [];
4097
+ lines.push("\u250C\u2500\u2500\u2500 Attack Graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
4098
+ const groups = {};
4099
+ for (const node of nodes.values()) {
4100
+ if (!groups[node.type]) groups[node.type] = [];
4101
+ groups[node.type].push(node);
4102
+ }
4103
+ const typeSummary = Object.entries(groups).map(([t, ns]) => `${TYPE_ICONS[t] || "\xB7"} ${ns.length}`).join(" ");
4104
+ lines.push(`\u2502 ${typeSummary}`);
4105
+ for (const [type, typeNodes] of Object.entries(groups)) {
4106
+ const icon = TYPE_ICONS[type] || "\xB7";
4107
+ lines.push(`\u2502`);
4108
+ lines.push(`\u2502 ${icon} ${type.toUpperCase()} (${typeNodes.length})`);
4109
+ for (const node of typeNodes) {
4110
+ const sIcon = STATUS_ICONS[node.status] || "?";
4111
+ const fail = node.status === NODE_STATUS.FAILED && node.failReason ? ` \u2014 ${node.failReason}` : "";
4112
+ let detail = "";
4113
+ if (type === NODE_TYPE.CREDENTIAL) {
4114
+ const source = node.data.source ? ` (from: ${node.data.source})` : "";
4115
+ detail = source;
4116
+ } else if (type === NODE_TYPE.VULNERABILITY) {
4117
+ const sev = node.data.severity ? ` [${String(node.data.severity).toUpperCase()}]` : "";
4118
+ const exploit = node.data.hasExploit ? " [EXPLOIT]" : "";
4119
+ const target = node.data.target ? ` \u2192 ${node.data.target}` : "";
4120
+ detail = `${sev}${exploit}${target}`;
4121
+ } else if (type === NODE_TYPE.ACCESS) {
4122
+ const via = node.data.via ? ` via ${node.data.via}` : "";
4123
+ detail = via;
4124
+ } else if (type === NODE_TYPE.SERVICE) {
4125
+ const ver = node.data.version ? ` (${node.data.version})` : "";
4126
+ detail = ver;
4127
+ }
4128
+ const outEdges = edges.filter((e) => e.from === node.id);
4129
+ const edgeStr = outEdges.length > 0 ? ` \u2192 ${outEdges.map((e) => {
4130
+ const target = nodes.get(e.to);
4131
+ const eName = target ? target.label : e.to;
4132
+ const eStatus = e.status === EDGE_STATUS.FAILED ? " \u2717" : e.status === EDGE_STATUS.SUCCEEDED ? " \u2713" : "";
4133
+ return `${eName}${eStatus}`;
4134
+ }).join(", ")}` : "";
4135
+ lines.push(`\u2502 ${sIcon} ${node.label}${detail}${fail}${edgeStr}`);
4136
+ }
4137
+ }
4138
+ const succeededNodes = Array.from(nodes.values()).filter((n) => n.status === NODE_STATUS.SUCCEEDED);
4139
+ if (succeededNodes.length > 0) {
4140
+ lines.push(`\u2502`);
4141
+ lines.push(`\u251C\u2500\u2500\u2500 Exploited \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524`);
4142
+ for (const n of succeededNodes) {
4143
+ const via = n.data.via ? ` via ${n.data.via}` : "";
4144
+ lines.push(`\u2502 \u25CF ${n.label}${via}`);
4145
+ }
4146
+ }
4147
+ const stats = getGraphStats(nodes, edges);
4148
+ lines.push(`\u2502`);
4149
+ lines.push(`\u251C\u2500\u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524`);
4150
+ lines.push(`\u2502 Nodes: ${stats.nodes} | Edges: ${stats.edges} | Succeeded: ${stats.succeeded} | Failed: ${stats.failed} | Chains: ${stats.chains}`);
4151
+ lines.push(`\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`);
4152
+ return lines.join("\n");
4153
+ }
4154
+
4155
+ // src/shared/utils/attack-graph/formatters/paths-list.ts
4156
+ function formatPathsList(nodes, edges, failedPaths) {
4157
+ const chains = recommendChains(nodes, edges);
4158
+ if (chains.length === 0) {
4159
+ if (nodes.size === 0) return "No attack graph data yet. Start reconnaissance first.";
4160
+ return "No viable attack paths found. All paths may be exhausted or failed.";
4161
+ }
4162
+ const lines = [];
4163
+ lines.push(`\u2500\u2500\u2500 ${chains.length} Attack Paths (sorted by priority) \u2500\u2500\u2500`);
4164
+ lines.push("");
4165
+ for (let i = 0; i < chains.length; i++) {
4166
+ const c = chains[i];
4167
+ const prob = (c.probability * 100).toFixed(0);
4168
+ const impactColors = {
4169
+ [SEVERITIES.CRITICAL]: "\u{1F534}",
4170
+ [SEVERITIES.HIGH]: "\u{1F7E0}",
4171
+ [SEVERITIES.MEDIUM]: "\u{1F7E1}",
4172
+ [SEVERITIES.LOW]: "\u{1F7E2}"
4173
+ };
4174
+ const impactIcon = impactColors[c.impact] || "\u26AA";
4175
+ lines.push(`${impactIcon} PATH ${i + 1} [${c.impact.toUpperCase()} | prob: ${prob}% | steps: ${c.length}]`);
4176
+ lines.push(` ${c.description}`);
4177
+ for (const edge of c.edges) {
4178
+ const statusTag = edge.status === EDGE_STATUS.UNTESTED ? " ?" : edge.status === EDGE_STATUS.TESTING ? " \u23F3" : edge.status === EDGE_STATUS.SUCCEEDED ? " \u2713" : " \u2717";
4179
+ lines.push(` ${statusTag} ${edge.from.split(":").pop()} \u2014[${edge.relation}]\u2192 ${edge.to.split(":").pop()}`);
4180
+ }
4181
+ lines.push("");
4182
+ }
4183
+ if (failedPaths.length > 0) {
4184
+ lines.push(`\u2500\u2500\u2500 Failed Paths (${failedPaths.length}) \u2500\u2500\u2500`);
4185
+ for (const fp of failedPaths) {
4186
+ lines.push(` \u2717 ${fp}`);
4187
+ }
4188
+ }
4189
+ return lines.join("\n");
4190
+ }
4191
+
4192
+ // src/shared/utils/attack-graph/operations/infrastructure.ts
4193
+ function registerHost(graph, ip, hostname) {
4194
+ const node = createHostNode(ip, hostname);
4195
+ if (!graph.nodes.has(node.id)) {
4196
+ graph.nodes.set(node.id, node);
4197
+ }
4198
+ return node.id;
4199
+ }
4200
+ function registerService(graph, host, port, service, version) {
4201
+ const hostId = registerHost(graph, host);
4202
+ const { node, hostEdge } = createServiceNode(host, port, service, version);
4203
+ if (!graph.nodes.has(node.id)) {
4204
+ graph.nodes.set(node.id, node);
4205
+ }
4206
+ if (hostEdge && !edgeExists(graph.edges, hostEdge.from, hostEdge.to, hostEdge.relation)) {
4207
+ graph.edges.push(hostEdge);
4208
+ }
4209
+ if (version) {
4210
+ const vulnNode = createVersionSearchNode(service, version);
4211
+ if (!graph.nodes.has(vulnNode.id)) {
4212
+ graph.nodes.set(vulnNode.id, vulnNode);
4213
+ }
4214
+ graph.addEdge(node.id, vulnNode.id, "may_have", 0.3);
4215
+ }
4216
+ return node.id;
4217
+ }
4218
+
4219
+ // src/shared/utils/attack-graph/operations/security.ts
4220
+ function registerCredential(graph, username, password, source) {
4221
+ const node = createCredentialNode(username, password, source);
4222
+ if (!graph.nodes.has(node.id)) {
4223
+ graph.nodes.set(node.id, node);
4224
+ }
4225
+ const sprayEdges = createCredentialSprayEdges(node.id, graph.nodes);
4226
+ for (const edge of sprayEdges) {
4227
+ if (!edgeExists(graph.edges, edge.from, edge.to, edge.relation)) {
4228
+ graph.edges.push(edge);
4229
+ }
4230
+ }
4231
+ return node.id;
4232
+ }
4233
+ function registerVulnerability(graph, title, target, severity, hasExploit = false) {
4234
+ const vulnNode = createVulnerabilityNode(title, target, severity, hasExploit);
4235
+ if (!graph.nodes.has(vulnNode.id)) {
4236
+ graph.nodes.set(vulnNode.id, vulnNode);
4237
+ }
4238
+ const targetServices = findTargetServices(graph.nodes, target);
4239
+ for (const svc of targetServices) {
4240
+ graph.addEdge(svc.id, vulnNode.id, "has_vulnerability", 0.8);
4241
+ }
4242
+ if (hasExploit) {
4243
+ const accessNode = createPotentialAccessNode(title);
4244
+ if (!graph.nodes.has(accessNode.id)) {
4245
+ graph.nodes.set(accessNode.id, accessNode);
4246
+ }
4247
+ graph.addEdge(vulnNode.id, accessNode.id, "leads_to", 0.7);
4248
+ }
4249
+ return vulnNode.id;
4250
+ }
4251
+ function registerAccess(graph, host, level, via) {
4252
+ const accessNode = createAccessNode(host, level, via);
4253
+ if (!graph.nodes.has(accessNode.id)) {
4254
+ graph.nodes.set(accessNode.id, accessNode);
4255
+ }
4256
+ const node = graph.nodes.get(accessNode.id);
4257
+ if (node) {
4258
+ node.status = NODE_STATUS.SUCCEEDED;
4259
+ node.resolvedAt = Date.now();
4260
+ const incomingEdges = getIncomingEdges(graph.edges, accessNode.id);
4261
+ for (const edge of incomingEdges) {
4262
+ Object.assign(edge, markEdgeSucceeded(edge));
4263
+ }
4264
+ }
4265
+ if (isHighPrivilege(level)) {
4266
+ const lootNode = createLootNode(host);
4267
+ if (!graph.nodes.has(lootNode.id)) {
4268
+ graph.nodes.set(lootNode.id, lootNode);
4269
+ }
4270
+ graph.addEdge(accessNode.id, lootNode.id, "can_access", 0.9);
4271
+ }
4272
+ return accessNode.id;
4273
+ }
4274
+
4275
+ // src/shared/utils/attack-graph/operations/discovery.ts
4276
+ function registerOSINT(graph, category, detail, data = {}) {
4277
+ const osintNode = createOSINTNode(category, detail, data);
4278
+ if (!graph.nodes.has(osintNode.id)) {
4279
+ graph.nodes.set(osintNode.id, osintNode);
4280
+ }
4281
+ const relatedNodes = findOSINTRelatedNodes(graph.nodes, detail);
4282
+ for (const { id } of relatedNodes) {
4283
+ graph.addEdge(osintNode.id, id, "relates_to", 0.5);
4284
+ }
4285
+ return osintNode.id;
4286
+ }
4287
+
4288
+ // src/shared/utils/attack-graph/status-management.ts
4289
+ function markNodeAttemptedInGraph(nodes, nodeId) {
4290
+ const node = nodes.get(nodeId);
4291
+ if (node && canMarkAttempted(node)) {
4292
+ node.status = NODE_STATUS.ATTEMPTED;
4293
+ node.attemptedAt = Date.now();
4294
+ }
4295
+ }
4296
+ function markNodeSucceededInGraph(nodes, edges, nodeId) {
4297
+ const node = nodes.get(nodeId);
4298
+ if (node) {
4299
+ node.status = NODE_STATUS.SUCCEEDED;
4300
+ node.resolvedAt = Date.now();
4301
+ const incomingEdges = getIncomingEdges(edges, nodeId);
4302
+ for (const edge of incomingEdges) {
4303
+ if (edge.status === EDGE_STATUS.TESTING) {
4304
+ Object.assign(edge, markEdgeSucceeded(edge));
4305
+ }
4306
+ }
4307
+ }
4308
+ }
4309
+ function markNodeFailedInGraph(nodes, edges, nodeId, reason, callbacks) {
4310
+ const node = nodes.get(nodeId);
4311
+ if (node) {
4312
+ node.status = NODE_STATUS.FAILED;
4313
+ node.resolvedAt = Date.now();
4314
+ node.failReason = reason;
4315
+ callbacks.onFailedPath(formatFailedNode(node.label, reason));
4316
+ const incomingEdges = getIncomingEdges(edges, nodeId);
4317
+ for (const edge of incomingEdges) {
4318
+ if (isTestableEdge(edge)) {
4319
+ Object.assign(edge, markEdgeFailed(edge, reason));
4320
+ }
4321
+ }
4322
+ }
4323
+ }
4324
+ function markEdgeTestingInList(edges, fromId, toId) {
4325
+ for (const edge of edges) {
4326
+ if (edge.from === fromId && edge.to === toId) {
4327
+ edge.status = EDGE_STATUS.TESTING;
4328
+ }
4329
+ }
4330
+ }
4331
+ function markEdgeFailedInList(edges, fromId, toId, reason, callbacks) {
4332
+ for (const edge of edges) {
4333
+ if (edge.from === fromId && edge.to === toId) {
4334
+ edge.status = EDGE_STATUS.FAILED;
4335
+ edge.failReason = reason;
4336
+ }
4337
+ }
4338
+ callbacks.onFailedPath(formatFailedPath(fromId, toId, reason));
4339
+ }
4340
+
4341
+ // src/shared/utils/attack-graph/store.ts
4342
+ var AttackGraphStore = class {
4343
+ nodes = /* @__PURE__ */ new Map();
4344
+ edges = [];
4345
+ failedPaths = [];
4346
+ /** Reset the store to an empty state. */
4347
+ reset() {
4348
+ this.nodes.clear();
4349
+ this.edges = [];
4350
+ this.failedPaths = [];
4351
+ }
4352
+ /**
4353
+ * Set/update a node with IMP-1 cap.
4354
+ * Evicts oldest 'discovered' node when limit reached.
4355
+ */
4356
+ setNode(id, node) {
4357
+ if (!this.nodes.has(id) && this.nodes.size >= AGENT_LIMITS.MAX_ATTACK_NODES) {
4358
+ const oldestDiscovered = Array.from(this.nodes.entries()).find(([, n]) => n.status === "discovered");
4359
+ if (oldestDiscovered) {
4360
+ this.nodes.delete(oldestDiscovered[0]);
4361
+ }
4362
+ }
4363
+ this.nodes.set(id, node);
4364
+ }
4365
+ /** Get node by ID. */
4366
+ getNode(id) {
4367
+ return this.nodes.get(id);
4368
+ }
4369
+ /** Check if node exists. */
4370
+ hasNode(id) {
4371
+ return this.nodes.has(id);
4372
+ }
4373
+ /**
4374
+ * Add an edge with IMP-15 deduplication and IMP-1 cap.
4375
+ * Deduplication key: from + to + relation.
4376
+ */
4377
+ addEdge(edge) {
4378
+ const isDuplicate = this.edges.some(
4379
+ (e) => e.from === edge.from && e.to === edge.to && e.relation === edge.relation
4380
+ );
4381
+ if (isDuplicate) return;
4382
+ this.edges.push(edge);
4383
+ if (this.edges.length > AGENT_LIMITS.MAX_ATTACK_EDGES) {
4384
+ const prunableIdx = this.edges.findIndex(
4385
+ (e) => e.status === "failed" || e.status === "untested"
4386
+ );
4387
+ if (prunableIdx >= 0) {
4388
+ this.edges.splice(prunableIdx, 1);
4389
+ }
4390
+ }
4391
+ }
4392
+ /**
4393
+ * Record a failed path with IMP-1 cap.
4394
+ * Keeps only the most recent MAX_FAILED_PATHS entries.
4395
+ */
4396
+ recordFailedPath(path4) {
4397
+ this.failedPaths.push(path4);
4398
+ if (this.failedPaths.length > AGENT_LIMITS.MAX_FAILED_PATHS) {
4399
+ this.failedPaths = this.failedPaths.slice(-AGENT_LIMITS.MAX_FAILED_PATHS);
4400
+ }
4401
+ }
4402
+ /** Get internal nodes map reference (for operations). */
4403
+ getNodesMap() {
4404
+ return this.nodes;
4405
+ }
4406
+ /** Get internal edges list reference (for operations). */
4407
+ getEdgesList() {
4408
+ return this.edges;
4409
+ }
4410
+ /** Get failed paths list. */
4411
+ getFailedPaths() {
4412
+ return this.failedPaths;
4413
+ }
4414
+ /** Get all nodes as array. */
4415
+ getAllNodes() {
4416
+ return Array.from(this.nodes.values());
4417
+ }
4418
+ /** Get all edges as array. */
4419
+ getAllEdges() {
4420
+ return [...this.edges];
4421
+ }
4422
+ };
4423
+
4424
+ // src/shared/utils/attack-graph/core.ts
4425
+ var AttackGraph = class {
4426
+ store = new AttackGraphStore();
4427
+ // ─── Core Graph Operations ──────────────────────────────────
4428
+ /** Reset the graph to an empty state. */
4429
+ reset() {
4430
+ this.store.reset();
4431
+ }
4432
+ /**
4433
+ * Add a node to the attack graph.
4434
+ */
4435
+ addNode(type, label, data = {}) {
4436
+ const node = createNode(type, label, data);
4437
+ if (!this.store.hasNode(node.id)) {
4438
+ this.store.setNode(node.id, node);
4439
+ }
4440
+ return node.id;
4441
+ }
4442
+ /**
4443
+ * Add an edge (relationship) between two nodes.
4444
+ */
4445
+ addEdge(fromId, toId, relation, confidence = 0.5) {
4446
+ if (!edgeExists(this.store.getEdgesList(), fromId, toId, relation)) {
4447
+ this.store.addEdge(createEdge(fromId, toId, relation, confidence));
4448
+ }
4449
+ }
4450
+ /**
4451
+ * Get node by ID.
4452
+ */
4453
+ getNode(nodeId) {
4454
+ return this.store.getNode(nodeId);
4455
+ }
4456
+ // ─── Status Management ──────────────────────────────────────
4457
+ /**
4458
+ * Mark a node as being attempted (in-progress attack).
4459
+ */
4460
+ markAttempted(nodeId) {
4461
+ markNodeAttemptedInGraph(this.store.getNodesMap(), nodeId);
4462
+ }
4463
+ /**
4464
+ * Mark a node as successfully exploited/achieved.
4465
+ */
4466
+ markSucceeded(nodeId) {
4467
+ markNodeSucceededInGraph(this.store.getNodesMap(), this.store.getEdgesList(), nodeId);
4468
+ }
4469
+ /**
4470
+ * Mark a node as failed (attack didn't work).
4471
+ */
4472
+ markFailed(nodeId, reason) {
4473
+ markNodeFailedInGraph(this.store.getNodesMap(), this.store.getEdgesList(), nodeId, reason, {
4474
+ onFailedPath: (path4) => this.store.recordFailedPath(path4)
4475
+ });
4476
+ }
4477
+ /**
4478
+ * Mark an edge as being tested.
4479
+ */
4480
+ markEdgeTesting(fromId, toId) {
4481
+ markEdgeTestingInList(this.store.getEdgesList(), fromId, toId);
4482
+ }
4483
+ /**
4484
+ * Mark an edge as failed.
4485
+ */
4486
+ markEdgeFailed(fromId, toId, reason) {
4487
+ markEdgeFailedInList(this.store.getEdgesList(), fromId, toId, reason, {
4488
+ onFailedPath: (path4) => this.store.recordFailedPath(path4)
4489
+ });
4490
+ }
4491
+ // ─── Domain-Specific Registration ───────────────────────────
4492
+ /** Internal graph container for domain operations */
4493
+ get graphContainer() {
4494
+ return {
4495
+ nodes: this.store.getNodesMap(),
4496
+ edges: this.store.getEdgesList(),
4497
+ addEdge: this.addEdge.bind(this),
4498
+ markSucceeded: this.markSucceeded.bind(this)
4499
+ };
4500
+ }
4501
+ /**
4502
+ * Record a host discovery.
4503
+ */
4504
+ addHost(ip, hostname) {
4505
+ return registerHost(this.graphContainer, ip, hostname);
4506
+ }
4507
+ /**
4508
+ * Record a service discovery and auto-create edges.
4509
+ */
4510
+ addService(host, port, service, version) {
4511
+ return registerService(this.graphContainer, host, port, service, version);
4512
+ }
4513
+ /**
4514
+ * Record a credential discovery and create spray edges.
4515
+ */
4516
+ addCredential(username, password, source) {
4517
+ return registerCredential(this.graphContainer, username, password, source);
4518
+ }
4519
+ /**
4520
+ * Record a vulnerability finding.
4521
+ */
4522
+ addVulnerability(title, target, severity, hasExploit = false) {
4523
+ return registerVulnerability(this.graphContainer, title, target, severity, hasExploit);
4524
+ }
4525
+ /**
4526
+ * Record gained access.
4527
+ */
4528
+ addAccess(host, level, via) {
4529
+ return registerAccess(this.graphContainer, host, level, via);
4530
+ }
4531
+ /**
4532
+ * Record OSINT discovery (Docker image, GitHub repo, company info, etc.)
4533
+ */
4534
+ addOSINT(category, detail, data = {}) {
4535
+ return registerOSINT(this.graphContainer, category, detail, data);
4536
+ }
4537
+ /**
4538
+ * Record an Active Directory object (domain, user-account, GPO, ACL, certificate-template).
4539
+ *
4540
+ * WHY: AD attacks chain through complex object relationships (ACL → user → cert → DA).
4541
+ * Tracking these as graph nodes lets the Strategist see multi-hop AD paths.
4542
+ *
4543
+ * type: 'domain' | 'user-account' | 'gpo' | 'acl' | 'certificate-template'
4544
+ * label: human-readable name (e.g. "CORP.LOCAL", "svc_sql", "ESC1-vulnerable")
4545
+ * data: freeform metadata (members, rights, target, confidence, etc.)
4546
+ */
4547
+ addDomainObject(type, label, data = {}) {
4548
+ return this.addNode(type, label, { adObject: true, ...data });
4549
+ }
4550
+ // ─── Chain Discovery (DFS) ──────────────────────────────────
4551
+ /**
4552
+ * Find all viable attack paths using DFS.
4553
+ */
4554
+ recommendChains() {
4555
+ return recommendChains(this.store.getNodesMap(), this.store.getEdgesList());
4556
+ }
4557
+ // ─── Prompt Generation ──────────────────────────────────────
4558
+ /**
4559
+ * Format attack graph status for prompt injection.
4560
+ */
4561
+ toPrompt() {
4562
+ return formatGraphForPrompt(this.store.getNodesMap(), this.store.getEdgesList(), this.store.getFailedPaths());
4563
+ }
4564
+ // ─── TUI Visualization ──────────────────────────────────────
4565
+ /**
4566
+ * Generate ASCII visualization for TUI /graph command.
4567
+ */
4568
+ toASCII() {
4569
+ return formatGraphAsASCII(this.store.getNodesMap(), this.store.getEdgesList());
4570
+ }
4571
+ /**
4572
+ * Generate path listing for TUI /paths command.
4573
+ */
4574
+ toPathsList() {
4575
+ return formatPathsList(this.store.getNodesMap(), this.store.getEdgesList(), this.store.getFailedPaths());
4576
+ }
4577
+ // ─── Stats ──────────────────────────────────────────────────
4578
+ /**
4579
+ * Get graph stats for metrics display.
4580
+ */
4581
+ getStats() {
4582
+ return getGraphStats(this.store.getNodesMap(), this.store.getEdgesList());
4583
+ }
4584
+ /**
4585
+ * Get all nodes.
4586
+ */
4587
+ getAllNodes() {
4588
+ return this.store.getAllNodes();
4589
+ }
4590
+ /**
4591
+ * Get all edges.
4592
+ */
4593
+ getAllEdges() {
4594
+ return this.store.getAllEdges();
4595
+ }
4596
+ /**
4597
+ * Get failed paths list.
4598
+ */
4599
+ getFailedPaths() {
4600
+ return this.store.getFailedPaths();
4601
+ }
4602
+ /**
4603
+ * Check if the graph has any data.
4604
+ */
4605
+ isEmpty() {
4606
+ return this.store.getNodesMap().size === 0;
4607
+ }
4608
+ };
4609
+
4610
+ // src/shared/utils/agent-memory/branch-index.ts
4611
+ var IPV4_HOST_PATTERN = /\b(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?\b/;
4612
+ var DEFAULT_BRANCH_CONFIDENCE = 25;
4613
+ var MEMORY_FAILURE_CONFIDENCE = 40;
4614
+ var GRAPH_RECOMMENDATION_CONFIDENCE_MULTIPLIER = 100;
4615
+ var ACCESS_CONFIDENCE = 100;
4616
+ function buildMemoryBranchIndex(state3) {
4617
+ const builder = new MemoryBranchIndexBuilder(state3);
4618
+ return builder.build();
4619
+ }
4620
+ var MemoryBranchIndexBuilder = class {
4621
+ constructor(state3) {
4622
+ this.state = state3;
4623
+ this.buildServiceLookup();
4624
+ }
4625
+ branches = /* @__PURE__ */ new Map();
4626
+ serviceLookup = /* @__PURE__ */ new Map();
4627
+ build() {
4628
+ this.addTargetBranches();
4629
+ this.addFindingBranches();
4630
+ this.addWorkingMemoryBranches();
4631
+ this.addDelegatedBranches();
4632
+ this.addAttackGraphBranches();
4633
+ this.addAttackGraphRecommendations();
4634
+ return {
4635
+ branches: Array.from(this.branches.values()).map((branch) => this.finalizeBranch(branch)).sort((a, b) => b.touchedAt - a.touchedAt)
4636
+ };
4637
+ }
4638
+ buildServiceLookup() {
4639
+ for (const target of this.state.getAllTargets()) {
4640
+ for (const port of target.ports) {
4641
+ this.serviceLookup.set(this.serviceLookupKey(target.ip, port.port), port.service);
4642
+ }
4643
+ }
4644
+ }
4645
+ addTargetBranches() {
4646
+ for (const target of this.state.getAllTargets()) {
4647
+ if (target.ports.length === 0) {
4648
+ this.touchBranch({ host: target.ip }, "canonical_state", DEFAULT_BRANCH_CONFIDENCE);
4649
+ continue;
4650
+ }
4651
+ for (const port of target.ports) {
4652
+ this.touchBranch({
4653
+ host: target.ip,
4654
+ service: port.service,
4655
+ port: port.port
4656
+ }, "canonical_state", DEFAULT_BRANCH_CONFIDENCE);
4657
+ }
4658
+ }
4659
+ }
4660
+ addFindingBranches() {
4661
+ for (const finding of this.state.getFindings()) {
4662
+ const affectedTargets = finding.affected.map((affected) => this.parseBranchTarget(affected)).filter((target) => target !== null);
4663
+ if (affectedTargets.length === 0) {
4664
+ continue;
4665
+ }
4666
+ for (const target of affectedTargets) {
4667
+ const branch = this.touchBranch({
4668
+ host: target.host,
4669
+ service: target.service ?? this.lookupService(target.host, target.port),
4670
+ port: target.port,
4671
+ vectorType: "vuln",
4672
+ vectorId: finding.title
4673
+ }, "canonical_state", finding.confidence, finding.foundAt);
4674
+ this.pushUnique(branch.findings, finding.title);
4675
+ }
4676
+ }
4677
+ }
4678
+ addWorkingMemoryBranches() {
4679
+ for (const entry of this.state.workingMemory.getEntries()) {
4680
+ if (entry.fingerprint) {
4681
+ this.addFailureEntry(entry, entry.fingerprint);
4682
+ }
4683
+ }
4684
+ }
4685
+ addFailureEntry(entry, fingerprint) {
4686
+ const rawCommand = typeof entry.context.command === "string" ? entry.context.command : "";
4687
+ const target = this.parseBranchTarget(rawCommand) ?? this.parseBranchTarget(fingerprint.target);
4688
+ if (!target) {
4689
+ return;
4690
+ }
4691
+ const resolvedPort = target.port ?? this.lookupPort(target.host, target.service);
4692
+ const resolvedService = target.service ?? this.lookupService(target.host, resolvedPort);
4693
+ const branch = this.touchBranch({
4694
+ host: target.host,
4695
+ service: resolvedService,
4696
+ port: resolvedPort
4697
+ }, "memory", MEMORY_FAILURE_CONFIDENCE, entry.timestamp);
4698
+ this.pushUnique(branch.recentAttempts, entry.content);
4699
+ this.pushUnique(branch.recentFailures, entry.content);
4700
+ if (fingerprint.hypothesizedReason) {
4701
+ this.pushUnique(branch.blockers, fingerprint.hypothesizedReason);
4702
+ }
4703
+ }
4704
+ addDelegatedBranches() {
4705
+ const activeExecution = this.state.getActiveDelegatedExecution();
4706
+ if (activeExecution?.target) {
4707
+ this.addDelegatedBranch(activeExecution.target, activeExecution.task, activeExecution.updatedAt);
4708
+ }
4709
+ for (const task of this.state.getDelegatedTasks()) {
4710
+ if (!task.target) {
4711
+ continue;
4712
+ }
4713
+ const branch = this.addDelegatedBranch(task.target, task.summary || task.task, task.updatedAt);
4714
+ this.pushUnique(branch.nextCandidates, task.suggestedNext);
4715
+ if (task.resumeHint) {
4716
+ this.pushUnique(branch.blockers, task.resumeHint);
4717
+ }
4718
+ }
4719
+ }
4720
+ addDelegatedBranch(targetValue, detail, touchedAt) {
4721
+ const target = this.parseBranchTarget(targetValue) ?? { host: targetValue };
4722
+ const branch = this.touchBranch({
4723
+ host: target.host,
4724
+ service: target.service ?? this.lookupService(target.host, target.port),
4725
+ port: target.port
4726
+ }, "delegated", DEFAULT_BRANCH_CONFIDENCE, touchedAt);
4727
+ this.pushUnique(branch.nextCandidates, detail);
4728
+ return branch;
4729
+ }
4730
+ addAttackGraphBranches() {
4731
+ const nodes = this.state.attackGraph.getAllNodes();
4732
+ const edges = this.state.attackGraph.getAllEdges();
4733
+ const nodesById = new Map(nodes.map((node) => [node.id, node]));
4734
+ for (const node of nodes) {
4735
+ this.addAttackNode(node);
4736
+ }
4737
+ for (const edge of edges) {
4738
+ this.addCredentialSprayEdge(edge, nodesById);
4739
+ }
4740
+ for (const failedPath of this.state.attackGraph.getFailedPaths()) {
4741
+ const target = this.parseBranchTarget(failedPath);
4742
+ if (!target) {
4743
+ continue;
4744
+ }
4745
+ const branch = this.touchBranch({
4746
+ host: target.host,
4747
+ service: target.service ?? this.lookupService(target.host, target.port),
4748
+ port: target.port
4749
+ }, "graph", MEMORY_FAILURE_CONFIDENCE);
4750
+ this.pushUnique(branch.blockers, failedPath);
4751
+ }
4752
+ }
4753
+ addAttackNode(node) {
4754
+ if (node.type === NODE_TYPE.SERVICE) {
4755
+ const branch = this.touchBranch({
4756
+ host: this.readString(node.data.host) || node.label,
4757
+ service: this.readString(node.data.service),
4758
+ port: this.readNumber(node.data.port)
4759
+ }, "graph", DEFAULT_BRANCH_CONFIDENCE, node.discoveredAt);
4760
+ this.pushUnique(branch.nextCandidates, `Service discovered: ${node.label}`);
4761
+ return;
4762
+ }
4763
+ if (node.type === NODE_TYPE.VULNERABILITY) {
4764
+ const target = this.parseBranchTarget(this.readString(node.data.target) || "");
4765
+ if (!target) {
4766
+ return;
4767
+ }
4768
+ const branch = this.touchBranch({
4769
+ host: target.host,
4770
+ service: target.service ?? this.lookupService(target.host, target.port),
4771
+ port: target.port,
4772
+ vectorType: "vuln",
4773
+ vectorId: node.label
4774
+ }, "graph", DEFAULT_BRANCH_CONFIDENCE, node.discoveredAt);
4775
+ this.pushUnique(branch.findings, node.label);
4776
+ return;
4777
+ }
4778
+ if (node.type === NODE_TYPE.ACCESS) {
4779
+ const host = this.readString(node.data.host);
4780
+ const level = this.readString(node.data.level) || node.label;
4781
+ if (!host) {
4782
+ return;
4783
+ }
4784
+ const branch = this.touchBranch({
4785
+ host,
4786
+ vectorType: "shell",
4787
+ vectorId: level
4788
+ }, "graph", ACCESS_CONFIDENCE, node.resolvedAt ?? node.discoveredAt);
4789
+ this.pushUnique(branch.accessState, node.label);
4790
+ return;
4791
+ }
4792
+ }
4793
+ addCredentialSprayEdge(edge, nodesById) {
4794
+ if (edge.relation !== "can_try_on") {
4795
+ return;
4796
+ }
4797
+ const credentialNode = nodesById.get(edge.from);
4798
+ const serviceNode = nodesById.get(edge.to);
4799
+ if (!credentialNode || !serviceNode || serviceNode.type !== NODE_TYPE.SERVICE) {
4800
+ return;
4801
+ }
4802
+ const host = this.readString(serviceNode.data.host);
4803
+ if (!host) {
4804
+ return;
4805
+ }
4806
+ const branch = this.touchBranch({
4807
+ host,
4808
+ service: this.readString(serviceNode.data.service),
4809
+ port: this.readNumber(serviceNode.data.port)
4810
+ }, "graph", DEFAULT_BRANCH_CONFIDENCE, serviceNode.discoveredAt);
4811
+ this.pushUnique(branch.credentials, credentialNode.label);
4812
+ }
4813
+ addAttackGraphRecommendations() {
4814
+ const chains = this.state.attackGraph.recommendChains();
4815
+ for (const chain of chains.slice(0, 3)) {
4816
+ const branchKey = this.getBranchKeyFromChain(chain);
4817
+ if (!branchKey) {
4818
+ continue;
4819
+ }
4820
+ const branch = this.touchBranch(
4821
+ branchKey,
4822
+ "graph",
4823
+ Math.round(chain.probability * GRAPH_RECOMMENDATION_CONFIDENCE_MULTIPLIER)
4824
+ );
4825
+ this.pushUnique(branch.nextCandidates, `Recommended path: ${chain.description}`);
4826
+ }
4827
+ }
4828
+ getBranchKeyFromChain(chain) {
4829
+ for (const node of chain.steps) {
4830
+ if (node.type !== NODE_TYPE.SERVICE) {
4831
+ continue;
4832
+ }
4833
+ const host = this.readString(node.data.host);
4834
+ if (!host) {
4835
+ continue;
4836
+ }
4837
+ return {
4838
+ host,
4839
+ service: this.readString(node.data.service),
4840
+ port: this.readNumber(node.data.port)
4841
+ };
4842
+ }
4843
+ return null;
4844
+ }
4845
+ touchBranch(key, source, confidence, touchedAt = Date.now()) {
4846
+ const branchKey = this.serializeBranchKey(key);
4847
+ const existing = this.branches.get(branchKey);
4848
+ if (existing) {
4849
+ existing.confidence = Math.max(existing.confidence, confidence);
4850
+ existing.touchedAt = Math.max(existing.touchedAt, touchedAt);
4851
+ this.pushUniqueSource(existing.sources, source);
4852
+ return existing;
4853
+ }
4854
+ const created = {
4855
+ key,
4856
+ status: "new",
4857
+ findings: [],
4858
+ credentials: [],
4859
+ recentAttempts: [],
4860
+ recentFailures: [],
4861
+ successfulActions: [],
4862
+ blockers: [],
4863
+ accessState: [],
4864
+ nextCandidates: [],
4865
+ confidence,
4866
+ touchedAt,
4867
+ sources: [source]
4868
+ };
4869
+ this.branches.set(branchKey, created);
4870
+ return created;
4871
+ }
4872
+ finalizeBranch(branch) {
4873
+ return {
4874
+ ...branch,
4875
+ status: this.resolveStatus(branch),
4876
+ findings: [...branch.findings],
4877
+ credentials: [...branch.credentials],
4878
+ recentAttempts: [...branch.recentAttempts],
4879
+ recentFailures: [...branch.recentFailures],
4880
+ successfulActions: [...branch.successfulActions],
4881
+ blockers: [...branch.blockers],
4882
+ accessState: [...branch.accessState],
4883
+ nextCandidates: [...branch.nextCandidates],
4884
+ sources: [...branch.sources]
4885
+ };
4886
+ }
4887
+ resolveStatus(branch) {
4888
+ if (branch.accessState.length > 0) {
4889
+ return "succeeded";
4890
+ }
4891
+ if (branch.blockers.length > 0 && branch.nextCandidates.length === 0) {
4892
+ return "blocked";
4893
+ }
4894
+ if (branch.findings.length > 0 || branch.nextCandidates.length > 0 || branch.credentials.length > 0) {
4895
+ return "promising";
4896
+ }
4897
+ if (branch.recentAttempts.length > 0 || branch.recentFailures.length > 0) {
4898
+ return "enumerating";
4899
+ }
4900
+ return "new";
4901
+ }
4902
+ lookupService(host, port) {
4903
+ if (!port) {
4904
+ return void 0;
4905
+ }
4906
+ return this.serviceLookup.get(this.serviceLookupKey(host, port));
4907
+ }
4908
+ lookupPort(host, service) {
4909
+ if (!service) {
4910
+ return void 0;
4911
+ }
4912
+ const target = this.state.getTarget(host);
4913
+ const match = target?.ports.find((port) => port.service === service);
4914
+ return match?.port;
4915
+ }
4916
+ parseBranchTarget(raw) {
4917
+ if (!raw) {
4918
+ return null;
4919
+ }
4920
+ const fromUrl = this.parseUrlTarget(raw);
4921
+ if (fromUrl) {
4922
+ return fromUrl;
4923
+ }
4924
+ const match = raw.match(IPV4_HOST_PATTERN);
4925
+ if (!match) {
4926
+ return null;
4927
+ }
4928
+ return {
4929
+ host: match[1],
4930
+ port: match[2] ? Number(match[2]) : void 0
4931
+ };
4932
+ }
4933
+ parseUrlTarget(raw) {
4934
+ if (!raw.includes("://")) {
4935
+ return null;
4936
+ }
4937
+ try {
4938
+ const match = raw.match(/\b[a-z][a-z0-9+.-]*:\/\/\S+/i);
4939
+ if (!match) {
4940
+ return null;
4941
+ }
4942
+ const parsed = new URL(match[0]);
4943
+ return {
4944
+ host: parsed.hostname,
4945
+ port: parsed.port ? Number(parsed.port) : void 0,
4946
+ service: parsed.protocol.replace(":", "")
4947
+ };
4948
+ } catch {
4949
+ return null;
4950
+ }
4951
+ }
4952
+ readNumber(value) {
4953
+ return typeof value === "number" ? value : void 0;
4954
+ }
4955
+ readString(value) {
4956
+ return typeof value === "string" && value.trim() ? value : void 0;
4957
+ }
4958
+ serializeBranchKey(key) {
4959
+ return [
4960
+ key.host,
4961
+ key.service ?? "",
4962
+ key.port ?? "",
4963
+ key.vectorType ?? "",
4964
+ key.vectorId ?? ""
4965
+ ].join("|");
4966
+ }
4967
+ serviceLookupKey(host, port) {
4968
+ return `${host}:${port}`;
4969
+ }
4970
+ pushUnique(target, value) {
4971
+ if (!value || target.includes(value)) {
4972
+ return;
4973
+ }
4974
+ target.push(value);
4975
+ }
4976
+ pushUniqueSource(target, value) {
4977
+ if (!value || target.includes(value)) {
4978
+ return;
4979
+ }
4980
+ target.push(value);
4981
+ }
4982
+ };
4983
+
4984
+ // src/shared/utils/agent-memory/branch-summary.ts
4985
+ var DEFAULT_RANKED_BRANCH_LIMIT = 5;
4986
+ var DEFAULT_BLOCKED_BRANCH_LIMIT = 3;
4987
+ var DEFAULT_RESUME_HINT_LIMIT = 3;
4988
+ var MAX_BRANCH_DETAILS = 2;
4989
+ function rankMemoryBranches(index, limit = DEFAULT_RANKED_BRANCH_LIMIT) {
4990
+ return [...index.branches].sort(compareMemoryBranch).slice(0, Math.max(0, Math.floor(limit)));
4991
+ }
4992
+ function buildCampaignMemorySummary(index, rankedLimit = DEFAULT_RANKED_BRANCH_LIMIT, blockedLimit = DEFAULT_BLOCKED_BRANCH_LIMIT) {
4993
+ if (index.branches.length === 0) {
4994
+ return "";
4995
+ }
4996
+ const ranked = rankMemoryBranches(index, rankedLimit);
4997
+ const blocked = index.branches.filter((branch) => branch.status === "blocked").sort(compareMemoryBranch).slice(0, Math.max(0, Math.floor(blockedLimit)));
4998
+ const sections = [
4999
+ "<campaign-memory>",
5000
+ "<ranked-branches>",
5001
+ ...ranked.map((branch, indexValue) => formatRankedBranch(indexValue + 1, branch)),
5002
+ "</ranked-branches>"
5003
+ ];
5004
+ if (blocked.length > 0) {
5005
+ sections.push("<blocked-branches>");
5006
+ sections.push(...blocked.map((branch) => formatBlockedBranch(branch)));
5007
+ sections.push("</blocked-branches>");
5008
+ }
5009
+ sections.push("</campaign-memory>");
5010
+ return sections.join("\n");
5011
+ }
5012
+ function buildMicroBranchSummary(branch, activationReason, signals = []) {
5013
+ const lines = [
5014
+ "<delegated-branch-memory>",
5015
+ `target: ${formatBranchTarget(branch)}`,
5016
+ `status: ${branch.status}`,
5017
+ `confidence: ${branch.confidence}`,
5018
+ `activation_reason: ${activationReason}`,
5019
+ signals.length > 0 ? `signals: ${signals.join(", ")}` : "",
5020
+ formatList("findings", branch.findings),
5021
+ formatList("blockers", branch.blockers),
5022
+ formatList("next", branch.nextCandidates),
5023
+ formatList("access", branch.accessState),
5024
+ formatList("recent_failures", branch.recentFailures),
5025
+ "</delegated-branch-memory>"
5026
+ ].filter(Boolean);
5027
+ return lines.join("\n");
5028
+ }
5029
+ function buildSnapshotResumeHints(index, limit = DEFAULT_RESUME_HINT_LIMIT) {
5030
+ return rankMemoryBranches(index, limit).map(buildResumeHintLine).filter(Boolean);
5031
+ }
5032
+ function formatRankedBranch(rank, branch) {
5033
+ const details = [
5034
+ `#${rank} ${formatBranchTarget(branch)} | status=${branch.status} | confidence=${branch.confidence}`,
5035
+ formatList("findings", branch.findings),
5036
+ formatList("next", branch.nextCandidates),
5037
+ formatList("credentials", branch.credentials),
5038
+ formatList("access", branch.accessState),
5039
+ formatList("recent_failures", branch.recentFailures)
5040
+ ].filter(Boolean);
5041
+ return details.join("\n");
5042
+ }
5043
+ function formatBlockedBranch(branch) {
5044
+ const blockers = formatList("blockers", branch.blockers);
5045
+ const next = formatList("next", branch.nextCandidates);
5046
+ const failures = formatList("recent_failures", branch.recentFailures);
5047
+ return [
5048
+ `${formatBranchTarget(branch)} | confidence=${branch.confidence}`,
5049
+ blockers,
5050
+ next,
5051
+ failures
5052
+ ].filter(Boolean).join("\n");
5053
+ }
5054
+ function formatBranchTarget(branch) {
5055
+ const host = branch.key.host;
5056
+ const port = branch.key.port ? `:${branch.key.port}` : "";
5057
+ const service = branch.key.service ? ` (${branch.key.service})` : "";
5058
+ const vector = branch.key.vectorId ? ` [${branch.key.vectorId}]` : "";
5059
+ return `${host}${port}${service}${vector}`;
5060
+ }
5061
+ function formatList(label, values) {
5062
+ if (values.length === 0) {
5063
+ return "";
5064
+ }
5065
+ const visible = values.slice(0, MAX_BRANCH_DETAILS).join(" | ");
5066
+ const more = values.length > MAX_BRANCH_DETAILS ? ` (+${values.length - MAX_BRANCH_DETAILS} more)` : "";
5067
+ return `${label}: ${visible}${more}`;
5068
+ }
5069
+ function buildResumeHintLine(branch) {
5070
+ const target = formatBranchTarget(branch);
5071
+ const next = branch.nextCandidates[0];
5072
+ const blocker = branch.blockers[0];
5073
+ const finding = branch.findings[0];
5074
+ const hints = [
5075
+ finding ? `finding=${finding}` : "",
5076
+ next ? `next=${next}` : "",
5077
+ blocker ? `blocker=${blocker}` : "",
5078
+ branch.accessState[0] ? `access=${branch.accessState[0]}` : ""
5079
+ ].filter(Boolean);
5080
+ if (hints.length === 0) {
5081
+ return `${target} | status=${branch.status} | confidence=${branch.confidence}`;
5082
+ }
5083
+ return `${target} | ${hints.join(" | ")}`;
5084
+ }
5085
+ function compareMemoryBranch(left, right) {
5086
+ const statusDelta = getStatusRank(right.status) - getStatusRank(left.status);
5087
+ if (statusDelta !== 0) {
5088
+ return statusDelta;
5089
+ }
5090
+ if (right.confidence !== left.confidence) {
5091
+ return right.confidence - left.confidence;
5092
+ }
5093
+ return right.touchedAt - left.touchedAt;
5094
+ }
5095
+ function getStatusRank(status) {
5096
+ switch (status) {
5097
+ case "succeeded":
5098
+ return 5;
5099
+ case "promising":
5100
+ return 4;
5101
+ case "enumerating":
5102
+ return 3;
5103
+ case "new":
5104
+ return 2;
5105
+ case "blocked":
5106
+ return 1;
5107
+ case "abandoned":
5108
+ return 0;
5109
+ }
5110
+ }
5111
+
3598
5112
  // src/shared/utils/agent-memory/dynamic/extraction.ts
3599
5113
  var KNOWN_TECHS = [
3600
5114
  "apache",
@@ -4186,13 +5700,23 @@ export {
4186
5700
  EVENT_TYPES,
4187
5701
  COMMAND_EVENT_TYPES,
4188
5702
  UI_COMMANDS,
5703
+ NODE_TYPE,
5704
+ NODE_STATUS,
5705
+ EDGE_STATUS,
5706
+ extractSuccessfulChain,
5707
+ AttackGraph,
4189
5708
  generateId,
4190
5709
  generatePrefixedId,
4191
5710
  WorkingMemory,
4192
5711
  EpisodicMemory,
5712
+ isBranchMemoryEnabled,
4193
5713
  saveSessionSnapshot,
4194
5714
  snapshotToPrompt,
4195
5715
  PersistentMemory,
5716
+ buildMemoryBranchIndex,
5717
+ buildCampaignMemorySummary,
5718
+ buildMicroBranchSummary,
5719
+ buildSnapshotResumeHints,
4196
5720
  DynamicTechniqueLibrary,
4197
5721
  createSessionRuntime,
4198
5722
  setActiveSessionRuntime,