@datasynx/agentic-ai-cartography 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5840,9 +5840,146 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5840
5840
  return executeNlQuery(db, sessionId, search, parseNlQuery(raw), opts);
5841
5841
  }
5842
5842
 
5843
+ // src/correlation/signals.ts
5844
+ var IPV4 = /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g;
5845
+ var DNS = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.){1,8}[a-z]{2,24}\b/gi;
5846
+ var ENDPOINT = /\b((?:[a-z0-9][a-z0-9.-]{0,253})):(\d{1,5})\b/gi;
5847
+ function isPrivateIp(ip) {
5848
+ return /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(ip);
5849
+ }
5850
+ function sources(node) {
5851
+ const out = [node.id, node.name];
5852
+ const meta = node.metadata;
5853
+ if (meta) {
5854
+ for (const k of ["host", "hostname", "ip", "address", "dns", "dnsName", "endpoint", "fqdn", "publicIp", "privateIp", "url"]) {
5855
+ const v = meta[k];
5856
+ if (typeof v === "string") out.push(v);
5857
+ }
5858
+ }
5859
+ return out;
5860
+ }
5861
+ var KNOWN_PROVIDERS = /* @__PURE__ */ new Set(["aws", "gcp", "azure", "k8s", "kubernetes", "localhost", "docker"]);
5862
+ function parseProvider(id) {
5863
+ const seg = id.split(":");
5864
+ if (seg.length >= 3 && KNOWN_PROVIDERS.has(seg[1])) return seg[1];
5865
+ return void 0;
5866
+ }
5867
+ function extractSignals(node) {
5868
+ const text = sources(node).join(" \n ");
5869
+ const publicIps = /* @__PURE__ */ new Set();
5870
+ const privateIps = /* @__PURE__ */ new Set();
5871
+ const dnsNames = /* @__PURE__ */ new Set();
5872
+ const endpoints = /* @__PURE__ */ new Set();
5873
+ for (const m of text.matchAll(IPV4)) (isPrivateIp(m[0]) ? privateIps : publicIps).add(m[0]);
5874
+ for (const m of text.matchAll(DNS)) dnsNames.add(m[0].toLowerCase());
5875
+ for (const m of text.matchAll(ENDPOINT)) endpoints.add(`${m[1].toLowerCase()}:${m[2]}`);
5876
+ const provider = parseProvider(node.id);
5877
+ const sortUniq = (s) => [...s].sort();
5878
+ return {
5879
+ ...provider ? { provider } : {},
5880
+ endpoints: sortUniq(endpoints),
5881
+ publicIps: sortUniq(publicIps),
5882
+ privateIps: sortUniq(privateIps),
5883
+ // The DNS regex requires an alphabetic TLD, so pure IPv4s never land here.
5884
+ dnsNames: sortUniq(dnsNames)
5885
+ };
5886
+ }
5887
+
5888
+ // src/correlation/correlate.ts
5889
+ var CORRELATION_CONFIDENCE = {
5890
+ "global-identity": 1,
5891
+ "dns-name": 0.95,
5892
+ "public-ip": 0.9,
5893
+ "endpoint": 0.85,
5894
+ "private-ip": 0.5
5895
+ };
5896
+ var MERGE_THRESHOLD = 0.85;
5897
+ var DSU = class {
5898
+ parent;
5899
+ constructor(n) {
5900
+ this.parent = Array.from({ length: n }, (_, i) => i);
5901
+ }
5902
+ find(x) {
5903
+ while (this.parent[x] !== x) {
5904
+ this.parent[x] = this.parent[this.parent[x]];
5905
+ x = this.parent[x];
5906
+ }
5907
+ return x;
5908
+ }
5909
+ union(a, b) {
5910
+ const ra = this.find(a), rb = this.find(b);
5911
+ if (ra !== rb) this.parent[Math.max(ra, rb)] = Math.min(ra, rb);
5912
+ }
5913
+ };
5914
+ function correlateTopology(nodes, _edges = []) {
5915
+ const n = nodes.length;
5916
+ const signals = nodes.map(extractSignals);
5917
+ const dsu = new DSU(n);
5918
+ const correlations = [];
5919
+ const merged = /* @__PURE__ */ new Set();
5920
+ const buckets = /* @__PURE__ */ new Map();
5921
+ const add = (sig, val, i) => {
5922
+ const k = `${sig}|${val}`;
5923
+ const arr = buckets.get(k);
5924
+ if (arr) arr.push(i);
5925
+ else buckets.set(k, [i]);
5926
+ };
5927
+ nodes.forEach((node, i) => {
5928
+ if (node.globalId) add("global-identity", node.globalId, i);
5929
+ for (const d of signals[i].dnsNames) add("dns-name", d, i);
5930
+ for (const ip of signals[i].publicIps) add("public-ip", ip, i);
5931
+ for (const e of signals[i].endpoints) add("endpoint", e, i);
5932
+ });
5933
+ const order = ["global-identity", "dns-name", "public-ip", "endpoint"];
5934
+ for (const sig of order) {
5935
+ const conf = CORRELATION_CONFIDENCE[sig];
5936
+ const keys = [...buckets.keys()].filter((k) => k.startsWith(`${sig}|`)).sort();
5937
+ for (const key of keys) {
5938
+ const members = buckets.get(key).slice().sort((x, y) => nodes[x].id.localeCompare(nodes[y].id));
5939
+ const value = key.slice(sig.length + 1);
5940
+ for (let j = 1; j < members.length; j++) {
5941
+ const a = members[0], b = members[j];
5942
+ if (conf < MERGE_THRESHOLD) continue;
5943
+ dsu.union(a, b);
5944
+ merged.add(a);
5945
+ merged.add(b);
5946
+ const [s, t] = [nodes[a].id, nodes[b].id].sort();
5947
+ correlations.push({ sourceId: s, targetId: t, relationship: "same_as", signal: sig, confidence: conf, evidence: `[${sig}] shared ${value}` });
5948
+ }
5949
+ }
5950
+ }
5951
+ const clusters = /* @__PURE__ */ new Map();
5952
+ for (let i = 0; i < n; i++) {
5953
+ const r = dsu.find(i);
5954
+ const arr = clusters.get(r);
5955
+ if (arr) arr.push(i);
5956
+ else clusters.set(r, [i]);
5957
+ }
5958
+ const canonical = [];
5959
+ let crossCloud = 0;
5960
+ for (const idxs of clusters.values()) {
5961
+ const memberNodes = idxs.map((i) => nodes[i]);
5962
+ const members = memberNodes.map((m) => m.id).sort();
5963
+ const providers = [...new Set(idxs.map((i) => signals[i].provider).filter((p) => !!p))].sort();
5964
+ const rep = memberNodes.reduce((a, b) => a.id < b.id ? a : b);
5965
+ const memberIds = new Set(members);
5966
+ const internal = correlations.filter((c) => memberIds.has(c.sourceId) && memberIds.has(c.targetId));
5967
+ const confidence = internal.length ? Math.min(...internal.map((c) => c.confidence)) : 1;
5968
+ if (providers.length > 1) crossCloud += 1;
5969
+ canonical.push({ id: rep.id, type: rep.type, name: rep.name, members, providers, confidence });
5970
+ }
5971
+ canonical.sort((a, b) => a.id.localeCompare(b.id));
5972
+ correlations.sort((a, b) => b.confidence - a.confidence || a.sourceId.localeCompare(b.sourceId) || a.targetId.localeCompare(b.targetId));
5973
+ return {
5974
+ canonical,
5975
+ correlations,
5976
+ summary: { rawNodes: n, canonicalNodes: canonical.length, collapsed: n - canonical.length, crossCloud }
5977
+ };
5978
+ }
5979
+
5843
5980
  // src/mcp/server.ts
5844
5981
  var SERVER_NAME = "cartography";
5845
- var SERVER_VERSION = "2.8.0";
5982
+ var SERVER_VERSION = "2.10.0";
5846
5983
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5847
5984
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5848
5985
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -6002,6 +6139,15 @@ function createMcpServer(opts = {}) {
6002
6139
  return json(db.getGraphSummary(sid));
6003
6140
  }
6004
6141
  );
6142
+ server.registerTool(
6143
+ "correlate_topology",
6144
+ { title: "Correlate multi-cloud topology", description: "Collapse the same logical resource discovered across clouds/on-prem (by global identity or a shared DNS name / public IP / endpoint) into canonical entities with confidence-scored same_as links. Read-only, non-destructive (5.1).", inputSchema: {}, annotations: readOnly },
6145
+ () => {
6146
+ const sid = resolveSession();
6147
+ if (!sid) return json({ error: "No discovery session found." });
6148
+ return json(correlateTopology(db.getNodes(sid), db.getEdges(sid)));
6149
+ }
6150
+ );
6005
6151
  server.registerTool(
6006
6152
  "get_cost_summary",
6007
6153
  { title: "Get cost summary", description: "FinOps rollup: cost by domain and owner, currency/period-bucketed (3.3).", inputSchema: {}, annotations: readOnly },
@@ -8227,6 +8373,156 @@ function handleGraphqlGet() {
8227
8373
  return { status: 200, body: SDL };
8228
8374
  }
8229
8375
 
8376
+ // src/web/dashboard.ts
8377
+ var STYLE = `
8378
+ *{box-sizing:border-box;margin:0;padding:0}
8379
+ :root{--bg:#0f1419;--panel:#161b22;--line:#2d333b;--fg:#e6edf3;--dim:#8b949e;--accent:#3b82f6;--ok:#3fb950;--warn:#d29922;--crit:#f85149}
8380
+ body{font:13px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--fg);height:100vh;display:flex;flex-direction:column;overflow:hidden}
8381
+ header{display:flex;align-items:center;gap:12px;padding:8px 14px;border-bottom:1px solid var(--line);background:var(--panel)}
8382
+ header h1{font-size:15px;font-weight:600;letter-spacing:.3px}
8383
+ header .ver{color:var(--dim);font-size:11px}
8384
+ header .spacer{flex:1}
8385
+ header input{background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:5px 8px;font-size:12px;width:200px}
8386
+ header input:focus{outline:none;border-color:var(--accent)}
8387
+ header button{background:var(--accent);border:none;color:#fff;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer}
8388
+ main{flex:1;display:grid;grid-template-columns:300px 1fr 320px;overflow:hidden}
8389
+ .col{overflow:auto;padding:12px;border-right:1px solid var(--line)}
8390
+ .col:last-child{border-right:none;border-left:1px solid var(--line)}
8391
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:10px}
8392
+ .card h2{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim);margin-bottom:8px}
8393
+ .stat{display:flex;justify-content:space-between;padding:2px 0}
8394
+ .stat b{font-variant-numeric:tabular-nums}
8395
+ .bar{height:4px;border-radius:2px;background:var(--accent);margin-top:2px}
8396
+ #search{width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:6px 8px;margin-bottom:8px}
8397
+ .node-item{padding:6px 8px;border-radius:6px;cursor:pointer;border:1px solid transparent}
8398
+ .node-item:hover{background:var(--panel)}
8399
+ .node-item.sel{background:var(--panel);border-color:var(--accent)}
8400
+ .node-item .t{color:var(--dim);font-size:11px}
8401
+ #center{position:relative;padding:0}
8402
+ #graph{display:block;width:100%;height:100%;background:radial-gradient(circle at 50% 40%,#11161d,#0d1117)}
8403
+ #empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--dim);text-align:center;padding:20px}
8404
+ .kv{display:flex;justify-content:space-between;gap:8px;padding:3px 0;border-bottom:1px solid var(--line)}
8405
+ .kv span:first-child{color:var(--dim)}
8406
+ .kv span:last-child{text-align:right;word-break:break-all}
8407
+ .chip{display:inline-block;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:1px 8px;font-size:11px;margin:2px 2px 0 0}
8408
+ .sev-high,.sev-critical{color:var(--crit)} .sev-medium,.sev-warning{color:var(--warn)} .sev-low,.sev-info{color:var(--dim)}
8409
+ #toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--crit);color:#fff;padding:8px 14px;border-radius:6px;font-size:12px;opacity:0;transition:opacity .2s;pointer-events:none}
8410
+ #toast.show{opacity:1}
8411
+ `;
8412
+ var SCRIPT = String.raw`
8413
+ const $=(s)=>document.querySelector(s), api=(p)=>{
8414
+ const h={accept:'application/json'};
8415
+ const t=sessionStorage.getItem('cartograph_token'); if(t) h.authorization='Bearer '+t;
8416
+ const tn=sessionStorage.getItem('cartograph_tenant'); if(tn) h['x-cartograph-tenant']=tn;
8417
+ return fetch(p,{headers:h}).then(async r=>{ if(!r.ok){ const e=new Error('http '+r.status); e.status=r.status; throw e; } return r.json(); });
8418
+ };
8419
+ function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2600); }
8420
+ let NODES=[], SELECTED=null;
8421
+
8422
+ async function boot(){
8423
+ try{
8424
+ const s=await api('/v1/summary'); renderSummary(s);
8425
+ const n=await api('/v1/nodes?limit=1000'); NODES=n.nodes; renderList(NODES);
8426
+ }catch(e){
8427
+ if(e.status===401){ toast('Unauthorized — enter a bearer token and Reload.'); }
8428
+ else if(e.status===404){ const em=$('#empty'); em.textContent='No discovery session yet. Run a scan, then Reload.'; em.style.display='flex'; }
8429
+ else toast('Failed to load: '+e.message);
8430
+ }
8431
+ }
8432
+ function renderSummary(s){
8433
+ const max=Math.max(1,...Object.values(s.nodesByType));
8434
+ const types=Object.entries(s.nodesByType).sort((a,b)=>b[1]-a[1]).slice(0,12)
8435
+ .map(([k,v])=>'<div class="stat"><span>'+esc(k)+'</span><b>'+v+'</b></div><div class="bar" style="width:'+(v/max*100)+'%"></div>').join('');
8436
+ const anom=(s.anomalies||[]).slice(0,12).map(a=>'<div class="stat"><span class="sev-'+a.severity+'">'+esc(a.kind)+'</span><span class="t">'+esc(a.nodeId)+'</span></div>').join('') || '<div class="t">none</div>';
8437
+ $('#summary').innerHTML=
8438
+ '<div class="card"><h2>Totals</h2><div class="stat"><span>Nodes</span><b>'+s.totals.nodes+'</b></div><div class="stat"><span>Edges</span><b>'+s.totals.edges+'</b></div>'+(s.contributors!=null?'<div class="stat"><span>Contributors</span><b>'+s.contributors+'</b></div>':'')+'</div>'+
8439
+ '<div class="card"><h2>Nodes by type</h2>'+types+'</div>'+
8440
+ '<div class="card"><h2>Anomalies</h2>'+anom+'</div>';
8441
+ }
8442
+ function renderList(nodes){
8443
+ $('#list').innerHTML=nodes.map(n=>'<div class="node-item" data-id="'+esc(n.id)+'"><div>'+esc(n.name)+'</div><div class="t">'+esc(n.type)+'</div></div>').join('')||'<div class="t">no nodes</div>';
8444
+ $('#list').querySelectorAll('.node-item').forEach(el=>el.onclick=()=>select(el.dataset.id));
8445
+ }
8446
+ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
8447
+
8448
+ async function select(id){
8449
+ SELECTED=id;
8450
+ $('#list').querySelectorAll('.node-item').forEach(el=>el.classList.toggle('sel',el.dataset.id===id));
8451
+ const node=NODES.find(n=>n.id===id);
8452
+ try{
8453
+ const dep=await api('/v1/nodes/'+encodeURIComponent(id)+'/dependencies?direction=both&maxDepth=2');
8454
+ renderDetail(node,dep); drawGraph(id,dep);
8455
+ }catch(e){ toast('drill-down failed: '+e.message); }
8456
+ }
8457
+ function renderDetail(node,dep){
8458
+ if(!node){ $('#detail').innerHTML='<div class="t">node not in current page</div>'; return; }
8459
+ const fields=[['id',node.id],['type',node.type],['name',node.name],['confidence',node.confidence],['domain',node.domain],['owner',node.owner]]
8460
+ .filter(([,v])=>v!=null).map(([k,v])=>'<div class="kv"><span>'+k+'</span><span>'+esc(v)+'</span></div>').join('');
8461
+ const tags=(node.tags||[]).map(t=>'<span class="chip">'+esc(t)+'</span>').join('');
8462
+ const edges=(dep.edges||[]).map(e=>'<div class="kv"><span>'+esc(e.relationship)+'</span><span>'+esc(e.sourceId===node.id?('→ '+e.targetId):('← '+e.sourceId))+'</span></div>').join('')||'<div class="t">no dependencies</div>';
8463
+ $('#detail').innerHTML='<div class="card"><h2>Node</h2>'+fields+(tags?'<div style="margin-top:6px">'+tags+'</div>':'')+'</div><div class="card"><h2>Dependencies ('+(dep.nodes?dep.nodes.length:0)+')</h2>'+edges+'</div>';
8464
+ }
8465
+
8466
+ const cv=()=>$('#graph'), ctx=()=>cv().getContext('2d');
8467
+ function drawGraph(rootId,dep){
8468
+ $('#empty').style.display='none';
8469
+ const c=cv(); const dpr=window.devicePixelRatio||1; const w=c.clientWidth,h=c.clientHeight;
8470
+ c.width=w*dpr; c.height=h*dpr; const g=ctx(); g.setTransform(dpr,0,0,dpr,0,0); g.clearRect(0,0,w,h);
8471
+ const nodes=dep.nodes||[]; const cx=w/2,cy=h/2;
8472
+ // root at center; others on a circle, radius by depth.
8473
+ const pos={}; pos[rootId]={x:cx,y:cy};
8474
+ const others=nodes.filter(n=>n.id!==rootId);
8475
+ others.forEach((n,i)=>{ const a=(i/Math.max(1,others.length))*Math.PI*2; const r=90+(n.depth||1)*70; pos[n.id]={x:cx+Math.cos(a)*r,y:cy+Math.sin(a)*r}; });
8476
+ // edges
8477
+ g.strokeStyle='#30363d'; g.lineWidth=1.2;
8478
+ (dep.edges||[]).forEach(e=>{ const a=pos[e.sourceId],b=pos[e.targetId]; if(a&&b){ g.beginPath(); g.moveTo(a.x,a.y); g.lineTo(b.x,b.y); g.stroke(); } });
8479
+ // nodes
8480
+ const byId={}; nodes.forEach(n=>byId[n.id]=n);
8481
+ Object.entries(pos).forEach(([id,p])=>{ const n=byId[id]; const root=id===rootId;
8482
+ g.beginPath(); g.arc(p.x,p.y,root?13:8,0,Math.PI*2);
8483
+ g.fillStyle=root?'#3b82f6':'#21262d'; g.fill(); g.lineWidth=root?2:1; g.strokeStyle=root?'#60a5fa':'#484f58'; g.stroke();
8484
+ g.fillStyle='#c9d1d9'; g.font=(root?'600 12px':'11px')+' ui-sans-serif'; g.textAlign='center';
8485
+ g.fillText((n&&n.name?n.name:id).slice(0,22),p.x,p.y-(root?20:14));
8486
+ });
8487
+ }
8488
+
8489
+ document.addEventListener('DOMContentLoaded',()=>{
8490
+ const t=sessionStorage.getItem('cartograph_token'); if(t)$('#token').value=t;
8491
+ const tn=sessionStorage.getItem('cartograph_tenant'); if(tn)$('#tenant').value=tn;
8492
+ $('#reload').onclick=()=>{ sessionStorage.setItem('cartograph_token',$('#token').value.trim()); sessionStorage.setItem('cartograph_tenant',$('#tenant').value.trim()); boot(); };
8493
+ $('#search').oninput=(e)=>{ const q=e.target.value.toLowerCase(); renderList(NODES.filter(n=>n.name.toLowerCase().includes(q)||n.id.toLowerCase().includes(q)||n.type.toLowerCase().includes(q))); };
8494
+ boot();
8495
+ });
8496
+ `;
8497
+ function dashboardHtml(opts = {}) {
8498
+ const version = opts.version ?? "";
8499
+ return `<!DOCTYPE html>
8500
+ <html lang="en"><head>
8501
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
8502
+ <title>Cartograph dashboard</title>
8503
+ <style>${STYLE}</style>
8504
+ </head><body>
8505
+ <header>
8506
+ <h1>Cartograph</h1><span class="ver">${version ? `v${version}` : ""}</span>
8507
+ <span class="spacer"></span>
8508
+ <input id="tenant" placeholder="tenant (optional)" autocomplete="off">
8509
+ <input id="token" type="password" placeholder="bearer token" autocomplete="off">
8510
+ <button id="reload">Reload</button>
8511
+ </header>
8512
+ <main>
8513
+ <div class="col"><div id="summary"></div></div>
8514
+ <div class="col" id="center"><canvas id="graph"></canvas><div id="empty">Select a node to explore its dependencies.</div></div>
8515
+ <div class="col">
8516
+ <input id="search" placeholder="Search nodes\u2026" autocomplete="off">
8517
+ <div id="list"></div>
8518
+ <div id="detail"></div>
8519
+ </div>
8520
+ </main>
8521
+ <div id="toast"></div>
8522
+ <script>${SCRIPT}</script>
8523
+ </body></html>`;
8524
+ }
8525
+
8230
8526
  // src/api/server.ts
8231
8527
  var DEPENDENCIES_RE = /^\/v1\/nodes\/(.+)\/dependencies$/;
8232
8528
  var MAX_GRAPHQL_BYTES = 1024 * 1024;
@@ -8268,6 +8564,8 @@ async function runApi(opts) {
8268
8564
  });
8269
8565
  const restDeps = { backend: opts.backend, version: opts.version };
8270
8566
  const openApiDoc = buildOpenApiDocument({ version: opts.version });
8567
+ const dashboardEnabled = opts.dashboard !== false;
8568
+ const dashboardPage = dashboardEnabled ? dashboardHtml({ version: opts.version }) : "";
8271
8569
  const allowedOrigins = opts.allowedOrigins ?? [];
8272
8570
  assertSafeBind({ host: host2, port: requestedPort, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...token ? { token } : {} });
8273
8571
  let allowedHosts = opts.allowedHosts ?? [];
@@ -8310,6 +8608,11 @@ async function runApi(opts) {
8310
8608
  finish(200);
8311
8609
  return;
8312
8610
  }
8611
+ if (dashboardEnabled && (path === "/" || path === "/app") && req.method === "GET") {
8612
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", ...cors }).end(dashboardPage);
8613
+ finish(200);
8614
+ return;
8615
+ }
8313
8616
  if (path === "/v1/health") {
8314
8617
  if (req.method !== "GET") {
8315
8618
  send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
@@ -8473,6 +8776,7 @@ function parseApiArgs(argv) {
8473
8776
  const a = argv[i];
8474
8777
  if (a === "--http") continue;
8475
8778
  else if (a === "--no-graphql") opts.graphql = false;
8779
+ else if (a === "--no-dashboard") opts.dashboard = false;
8476
8780
  else if (a === "--port") opts.port = Number(argv[++i]);
8477
8781
  else if (a === "--host") opts.host = argv[++i];
8478
8782
  else if (a === "--allowed-hosts") opts.allowedHosts = splitList(argv[++i]);
@@ -8508,12 +8812,14 @@ async function startApi(opts = {}) {
8508
8812
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
8509
8813
  ...token ? { token } : {},
8510
8814
  ...opts.graphql === false ? { graphql: false } : {},
8815
+ ...opts.dashboard === false ? { dashboard: false } : {},
8511
8816
  ...opts.tenant ? { tenant: { defaultTenant: normalizeTenant(opts.tenant) } } : {},
8512
8817
  log: log2
8513
8818
  });
8514
8819
  const graphqlNote = opts.graphql === false ? " [REST only]" : " + /graphql";
8820
+ const dashNote = opts.dashboard === false ? "" : ` \xB7 dashboard http://${host2}:${port}/`;
8515
8821
  log2(
8516
- `Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})`
8822
+ `Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})${dashNote}`
8517
8823
  );
8518
8824
  return server;
8519
8825
  }
@@ -11764,6 +12070,7 @@ export {
11764
12070
  AuthorizationError,
11765
12071
  CLIENTS,
11766
12072
  CONFIDENCE,
12073
+ CORRELATION_CONFIDENCE,
11767
12074
  CartographyDB,
11768
12075
  ComplianceReportSchema,
11769
12076
  ComplianceRuleSchema,
@@ -11850,6 +12157,7 @@ export {
11850
12157
  computeIdentity,
11851
12158
  connectionsScanner,
11852
12159
  contentHash,
12160
+ correlateTopology,
11853
12161
  createBashTool,
11854
12162
  createCartographyTools,
11855
12163
  createClaudeProvider,
@@ -11865,6 +12173,7 @@ export {
11865
12173
  createSqliteQueryBackend,
11866
12174
  currentOs,
11867
12175
  cursorDeeplink,
12176
+ dashboardHtml,
11868
12177
  databasesScanner,
11869
12178
  deepMerge,
11870
12179
  defaultAllowedHosts,
@@ -11896,6 +12205,7 @@ export {
11896
12205
  exportJGF,
11897
12206
  exportJSON,
11898
12207
  extractListeningPorts,
12208
+ extractSignals,
11899
12209
  filterBySeverity,
11900
12210
  findAnonViolations,
11901
12211
  formatComplianceText,