@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/api-bin.js +1 -1
- package/dist/{chunk-5D5ZZEZM.js → chunk-ASCA3UFM.js} +148 -2
- package/dist/chunk-ASCA3UFM.js.map +1 -0
- package/dist/{chunk-TBPGFEMQ.js → chunk-W4Q3TXHR.js} +162 -2
- package/dist/chunk-W4Q3TXHR.js.map +1 -0
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +316 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +312 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +1 -1
- package/llms-full.txt +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/chunk-5D5ZZEZM.js.map +0 -1
- package/dist/chunk-TBPGFEMQ.js.map +0 -1
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.
|
|
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=>({'&':'&','<':'<','>':'>','"':'"'}[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,
|