@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.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(src_exports, {
|
|
|
36
36
|
AuthorizationError: () => AuthorizationError,
|
|
37
37
|
CLIENTS: () => CLIENTS,
|
|
38
38
|
CONFIDENCE: () => CONFIDENCE,
|
|
39
|
+
CORRELATION_CONFIDENCE: () => CORRELATION_CONFIDENCE,
|
|
39
40
|
CartographyDB: () => CartographyDB,
|
|
40
41
|
ComplianceReportSchema: () => ComplianceReportSchema,
|
|
41
42
|
ComplianceRuleSchema: () => ComplianceRuleSchema,
|
|
@@ -122,6 +123,7 @@ __export(src_exports, {
|
|
|
122
123
|
computeIdentity: () => computeIdentity,
|
|
123
124
|
connectionsScanner: () => connectionsScanner,
|
|
124
125
|
contentHash: () => contentHash,
|
|
126
|
+
correlateTopology: () => correlateTopology,
|
|
125
127
|
createBashTool: () => createBashTool,
|
|
126
128
|
createCartographyTools: () => createCartographyTools,
|
|
127
129
|
createClaudeProvider: () => createClaudeProvider,
|
|
@@ -137,6 +139,7 @@ __export(src_exports, {
|
|
|
137
139
|
createSqliteQueryBackend: () => createSqliteQueryBackend,
|
|
138
140
|
currentOs: () => currentOs,
|
|
139
141
|
cursorDeeplink: () => cursorDeeplink,
|
|
142
|
+
dashboardHtml: () => dashboardHtml,
|
|
140
143
|
databasesScanner: () => databasesScanner,
|
|
141
144
|
deepMerge: () => deepMerge,
|
|
142
145
|
defaultAllowedHosts: () => defaultAllowedHosts,
|
|
@@ -168,6 +171,7 @@ __export(src_exports, {
|
|
|
168
171
|
exportJGF: () => exportJGF,
|
|
169
172
|
exportJSON: () => exportJSON,
|
|
170
173
|
extractListeningPorts: () => extractListeningPorts,
|
|
174
|
+
extractSignals: () => extractSignals,
|
|
171
175
|
filterBySeverity: () => filterBySeverity,
|
|
172
176
|
findAnonViolations: () => findAnonViolations,
|
|
173
177
|
formatComplianceText: () => formatComplianceText,
|
|
@@ -6133,9 +6137,146 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
|
|
|
6133
6137
|
return executeNlQuery(db, sessionId, search, parseNlQuery(raw), opts);
|
|
6134
6138
|
}
|
|
6135
6139
|
|
|
6140
|
+
// src/correlation/signals.ts
|
|
6141
|
+
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;
|
|
6142
|
+
var DNS = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.){1,8}[a-z]{2,24}\b/gi;
|
|
6143
|
+
var ENDPOINT = /\b((?:[a-z0-9][a-z0-9.-]{0,253})):(\d{1,5})\b/gi;
|
|
6144
|
+
function isPrivateIp(ip) {
|
|
6145
|
+
return /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(ip);
|
|
6146
|
+
}
|
|
6147
|
+
function sources(node) {
|
|
6148
|
+
const out = [node.id, node.name];
|
|
6149
|
+
const meta = node.metadata;
|
|
6150
|
+
if (meta) {
|
|
6151
|
+
for (const k of ["host", "hostname", "ip", "address", "dns", "dnsName", "endpoint", "fqdn", "publicIp", "privateIp", "url"]) {
|
|
6152
|
+
const v = meta[k];
|
|
6153
|
+
if (typeof v === "string") out.push(v);
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
return out;
|
|
6157
|
+
}
|
|
6158
|
+
var KNOWN_PROVIDERS = /* @__PURE__ */ new Set(["aws", "gcp", "azure", "k8s", "kubernetes", "localhost", "docker"]);
|
|
6159
|
+
function parseProvider(id) {
|
|
6160
|
+
const seg = id.split(":");
|
|
6161
|
+
if (seg.length >= 3 && KNOWN_PROVIDERS.has(seg[1])) return seg[1];
|
|
6162
|
+
return void 0;
|
|
6163
|
+
}
|
|
6164
|
+
function extractSignals(node) {
|
|
6165
|
+
const text = sources(node).join(" \n ");
|
|
6166
|
+
const publicIps = /* @__PURE__ */ new Set();
|
|
6167
|
+
const privateIps = /* @__PURE__ */ new Set();
|
|
6168
|
+
const dnsNames = /* @__PURE__ */ new Set();
|
|
6169
|
+
const endpoints = /* @__PURE__ */ new Set();
|
|
6170
|
+
for (const m of text.matchAll(IPV4)) (isPrivateIp(m[0]) ? privateIps : publicIps).add(m[0]);
|
|
6171
|
+
for (const m of text.matchAll(DNS)) dnsNames.add(m[0].toLowerCase());
|
|
6172
|
+
for (const m of text.matchAll(ENDPOINT)) endpoints.add(`${m[1].toLowerCase()}:${m[2]}`);
|
|
6173
|
+
const provider = parseProvider(node.id);
|
|
6174
|
+
const sortUniq = (s) => [...s].sort();
|
|
6175
|
+
return {
|
|
6176
|
+
...provider ? { provider } : {},
|
|
6177
|
+
endpoints: sortUniq(endpoints),
|
|
6178
|
+
publicIps: sortUniq(publicIps),
|
|
6179
|
+
privateIps: sortUniq(privateIps),
|
|
6180
|
+
// The DNS regex requires an alphabetic TLD, so pure IPv4s never land here.
|
|
6181
|
+
dnsNames: sortUniq(dnsNames)
|
|
6182
|
+
};
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6185
|
+
// src/correlation/correlate.ts
|
|
6186
|
+
var CORRELATION_CONFIDENCE = {
|
|
6187
|
+
"global-identity": 1,
|
|
6188
|
+
"dns-name": 0.95,
|
|
6189
|
+
"public-ip": 0.9,
|
|
6190
|
+
"endpoint": 0.85,
|
|
6191
|
+
"private-ip": 0.5
|
|
6192
|
+
};
|
|
6193
|
+
var MERGE_THRESHOLD = 0.85;
|
|
6194
|
+
var DSU = class {
|
|
6195
|
+
parent;
|
|
6196
|
+
constructor(n) {
|
|
6197
|
+
this.parent = Array.from({ length: n }, (_, i) => i);
|
|
6198
|
+
}
|
|
6199
|
+
find(x) {
|
|
6200
|
+
while (this.parent[x] !== x) {
|
|
6201
|
+
this.parent[x] = this.parent[this.parent[x]];
|
|
6202
|
+
x = this.parent[x];
|
|
6203
|
+
}
|
|
6204
|
+
return x;
|
|
6205
|
+
}
|
|
6206
|
+
union(a, b) {
|
|
6207
|
+
const ra = this.find(a), rb = this.find(b);
|
|
6208
|
+
if (ra !== rb) this.parent[Math.max(ra, rb)] = Math.min(ra, rb);
|
|
6209
|
+
}
|
|
6210
|
+
};
|
|
6211
|
+
function correlateTopology(nodes, _edges = []) {
|
|
6212
|
+
const n = nodes.length;
|
|
6213
|
+
const signals = nodes.map(extractSignals);
|
|
6214
|
+
const dsu = new DSU(n);
|
|
6215
|
+
const correlations = [];
|
|
6216
|
+
const merged = /* @__PURE__ */ new Set();
|
|
6217
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6218
|
+
const add = (sig, val, i) => {
|
|
6219
|
+
const k = `${sig}|${val}`;
|
|
6220
|
+
const arr = buckets.get(k);
|
|
6221
|
+
if (arr) arr.push(i);
|
|
6222
|
+
else buckets.set(k, [i]);
|
|
6223
|
+
};
|
|
6224
|
+
nodes.forEach((node, i) => {
|
|
6225
|
+
if (node.globalId) add("global-identity", node.globalId, i);
|
|
6226
|
+
for (const d of signals[i].dnsNames) add("dns-name", d, i);
|
|
6227
|
+
for (const ip of signals[i].publicIps) add("public-ip", ip, i);
|
|
6228
|
+
for (const e of signals[i].endpoints) add("endpoint", e, i);
|
|
6229
|
+
});
|
|
6230
|
+
const order = ["global-identity", "dns-name", "public-ip", "endpoint"];
|
|
6231
|
+
for (const sig of order) {
|
|
6232
|
+
const conf = CORRELATION_CONFIDENCE[sig];
|
|
6233
|
+
const keys = [...buckets.keys()].filter((k) => k.startsWith(`${sig}|`)).sort();
|
|
6234
|
+
for (const key of keys) {
|
|
6235
|
+
const members = buckets.get(key).slice().sort((x, y) => nodes[x].id.localeCompare(nodes[y].id));
|
|
6236
|
+
const value = key.slice(sig.length + 1);
|
|
6237
|
+
for (let j = 1; j < members.length; j++) {
|
|
6238
|
+
const a = members[0], b = members[j];
|
|
6239
|
+
if (conf < MERGE_THRESHOLD) continue;
|
|
6240
|
+
dsu.union(a, b);
|
|
6241
|
+
merged.add(a);
|
|
6242
|
+
merged.add(b);
|
|
6243
|
+
const [s, t] = [nodes[a].id, nodes[b].id].sort();
|
|
6244
|
+
correlations.push({ sourceId: s, targetId: t, relationship: "same_as", signal: sig, confidence: conf, evidence: `[${sig}] shared ${value}` });
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
6249
|
+
for (let i = 0; i < n; i++) {
|
|
6250
|
+
const r = dsu.find(i);
|
|
6251
|
+
const arr = clusters.get(r);
|
|
6252
|
+
if (arr) arr.push(i);
|
|
6253
|
+
else clusters.set(r, [i]);
|
|
6254
|
+
}
|
|
6255
|
+
const canonical = [];
|
|
6256
|
+
let crossCloud = 0;
|
|
6257
|
+
for (const idxs of clusters.values()) {
|
|
6258
|
+
const memberNodes = idxs.map((i) => nodes[i]);
|
|
6259
|
+
const members = memberNodes.map((m) => m.id).sort();
|
|
6260
|
+
const providers = [...new Set(idxs.map((i) => signals[i].provider).filter((p) => !!p))].sort();
|
|
6261
|
+
const rep = memberNodes.reduce((a, b) => a.id < b.id ? a : b);
|
|
6262
|
+
const memberIds = new Set(members);
|
|
6263
|
+
const internal = correlations.filter((c) => memberIds.has(c.sourceId) && memberIds.has(c.targetId));
|
|
6264
|
+
const confidence = internal.length ? Math.min(...internal.map((c) => c.confidence)) : 1;
|
|
6265
|
+
if (providers.length > 1) crossCloud += 1;
|
|
6266
|
+
canonical.push({ id: rep.id, type: rep.type, name: rep.name, members, providers, confidence });
|
|
6267
|
+
}
|
|
6268
|
+
canonical.sort((a, b) => a.id.localeCompare(b.id));
|
|
6269
|
+
correlations.sort((a, b) => b.confidence - a.confidence || a.sourceId.localeCompare(b.sourceId) || a.targetId.localeCompare(b.targetId));
|
|
6270
|
+
return {
|
|
6271
|
+
canonical,
|
|
6272
|
+
correlations,
|
|
6273
|
+
summary: { rawNodes: n, canonicalNodes: canonical.length, collapsed: n - canonical.length, crossCloud }
|
|
6274
|
+
};
|
|
6275
|
+
}
|
|
6276
|
+
|
|
6136
6277
|
// src/mcp/server.ts
|
|
6137
6278
|
var SERVER_NAME = "cartography";
|
|
6138
|
-
var SERVER_VERSION = "2.
|
|
6279
|
+
var SERVER_VERSION = "2.10.0";
|
|
6139
6280
|
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
6140
6281
|
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
6141
6282
|
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
@@ -6295,6 +6436,15 @@ function createMcpServer(opts = {}) {
|
|
|
6295
6436
|
return json(db.getGraphSummary(sid));
|
|
6296
6437
|
}
|
|
6297
6438
|
);
|
|
6439
|
+
server.registerTool(
|
|
6440
|
+
"correlate_topology",
|
|
6441
|
+
{ 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 },
|
|
6442
|
+
() => {
|
|
6443
|
+
const sid = resolveSession();
|
|
6444
|
+
if (!sid) return json({ error: "No discovery session found." });
|
|
6445
|
+
return json(correlateTopology(db.getNodes(sid), db.getEdges(sid)));
|
|
6446
|
+
}
|
|
6447
|
+
);
|
|
6298
6448
|
server.registerTool(
|
|
6299
6449
|
"get_cost_summary",
|
|
6300
6450
|
{ title: "Get cost summary", description: "FinOps rollup: cost by domain and owner, currency/period-bucketed (3.3).", inputSchema: {}, annotations: readOnly },
|
|
@@ -8520,6 +8670,156 @@ function handleGraphqlGet() {
|
|
|
8520
8670
|
return { status: 200, body: SDL };
|
|
8521
8671
|
}
|
|
8522
8672
|
|
|
8673
|
+
// src/web/dashboard.ts
|
|
8674
|
+
var STYLE = `
|
|
8675
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
8676
|
+
:root{--bg:#0f1419;--panel:#161b22;--line:#2d333b;--fg:#e6edf3;--dim:#8b949e;--accent:#3b82f6;--ok:#3fb950;--warn:#d29922;--crit:#f85149}
|
|
8677
|
+
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}
|
|
8678
|
+
header{display:flex;align-items:center;gap:12px;padding:8px 14px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
8679
|
+
header h1{font-size:15px;font-weight:600;letter-spacing:.3px}
|
|
8680
|
+
header .ver{color:var(--dim);font-size:11px}
|
|
8681
|
+
header .spacer{flex:1}
|
|
8682
|
+
header input{background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:5px 8px;font-size:12px;width:200px}
|
|
8683
|
+
header input:focus{outline:none;border-color:var(--accent)}
|
|
8684
|
+
header button{background:var(--accent);border:none;color:#fff;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer}
|
|
8685
|
+
main{flex:1;display:grid;grid-template-columns:300px 1fr 320px;overflow:hidden}
|
|
8686
|
+
.col{overflow:auto;padding:12px;border-right:1px solid var(--line)}
|
|
8687
|
+
.col:last-child{border-right:none;border-left:1px solid var(--line)}
|
|
8688
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:10px}
|
|
8689
|
+
.card h2{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim);margin-bottom:8px}
|
|
8690
|
+
.stat{display:flex;justify-content:space-between;padding:2px 0}
|
|
8691
|
+
.stat b{font-variant-numeric:tabular-nums}
|
|
8692
|
+
.bar{height:4px;border-radius:2px;background:var(--accent);margin-top:2px}
|
|
8693
|
+
#search{width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:6px 8px;margin-bottom:8px}
|
|
8694
|
+
.node-item{padding:6px 8px;border-radius:6px;cursor:pointer;border:1px solid transparent}
|
|
8695
|
+
.node-item:hover{background:var(--panel)}
|
|
8696
|
+
.node-item.sel{background:var(--panel);border-color:var(--accent)}
|
|
8697
|
+
.node-item .t{color:var(--dim);font-size:11px}
|
|
8698
|
+
#center{position:relative;padding:0}
|
|
8699
|
+
#graph{display:block;width:100%;height:100%;background:radial-gradient(circle at 50% 40%,#11161d,#0d1117)}
|
|
8700
|
+
#empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--dim);text-align:center;padding:20px}
|
|
8701
|
+
.kv{display:flex;justify-content:space-between;gap:8px;padding:3px 0;border-bottom:1px solid var(--line)}
|
|
8702
|
+
.kv span:first-child{color:var(--dim)}
|
|
8703
|
+
.kv span:last-child{text-align:right;word-break:break-all}
|
|
8704
|
+
.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}
|
|
8705
|
+
.sev-high,.sev-critical{color:var(--crit)} .sev-medium,.sev-warning{color:var(--warn)} .sev-low,.sev-info{color:var(--dim)}
|
|
8706
|
+
#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}
|
|
8707
|
+
#toast.show{opacity:1}
|
|
8708
|
+
`;
|
|
8709
|
+
var SCRIPT = String.raw`
|
|
8710
|
+
const $=(s)=>document.querySelector(s), api=(p)=>{
|
|
8711
|
+
const h={accept:'application/json'};
|
|
8712
|
+
const t=sessionStorage.getItem('cartograph_token'); if(t) h.authorization='Bearer '+t;
|
|
8713
|
+
const tn=sessionStorage.getItem('cartograph_tenant'); if(tn) h['x-cartograph-tenant']=tn;
|
|
8714
|
+
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(); });
|
|
8715
|
+
};
|
|
8716
|
+
function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2600); }
|
|
8717
|
+
let NODES=[], SELECTED=null;
|
|
8718
|
+
|
|
8719
|
+
async function boot(){
|
|
8720
|
+
try{
|
|
8721
|
+
const s=await api('/v1/summary'); renderSummary(s);
|
|
8722
|
+
const n=await api('/v1/nodes?limit=1000'); NODES=n.nodes; renderList(NODES);
|
|
8723
|
+
}catch(e){
|
|
8724
|
+
if(e.status===401){ toast('Unauthorized — enter a bearer token and Reload.'); }
|
|
8725
|
+
else if(e.status===404){ const em=$('#empty'); em.textContent='No discovery session yet. Run a scan, then Reload.'; em.style.display='flex'; }
|
|
8726
|
+
else toast('Failed to load: '+e.message);
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
function renderSummary(s){
|
|
8730
|
+
const max=Math.max(1,...Object.values(s.nodesByType));
|
|
8731
|
+
const types=Object.entries(s.nodesByType).sort((a,b)=>b[1]-a[1]).slice(0,12)
|
|
8732
|
+
.map(([k,v])=>'<div class="stat"><span>'+esc(k)+'</span><b>'+v+'</b></div><div class="bar" style="width:'+(v/max*100)+'%"></div>').join('');
|
|
8733
|
+
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>';
|
|
8734
|
+
$('#summary').innerHTML=
|
|
8735
|
+
'<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>'+
|
|
8736
|
+
'<div class="card"><h2>Nodes by type</h2>'+types+'</div>'+
|
|
8737
|
+
'<div class="card"><h2>Anomalies</h2>'+anom+'</div>';
|
|
8738
|
+
}
|
|
8739
|
+
function renderList(nodes){
|
|
8740
|
+
$('#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>';
|
|
8741
|
+
$('#list').querySelectorAll('.node-item').forEach(el=>el.onclick=()=>select(el.dataset.id));
|
|
8742
|
+
}
|
|
8743
|
+
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
8744
|
+
|
|
8745
|
+
async function select(id){
|
|
8746
|
+
SELECTED=id;
|
|
8747
|
+
$('#list').querySelectorAll('.node-item').forEach(el=>el.classList.toggle('sel',el.dataset.id===id));
|
|
8748
|
+
const node=NODES.find(n=>n.id===id);
|
|
8749
|
+
try{
|
|
8750
|
+
const dep=await api('/v1/nodes/'+encodeURIComponent(id)+'/dependencies?direction=both&maxDepth=2');
|
|
8751
|
+
renderDetail(node,dep); drawGraph(id,dep);
|
|
8752
|
+
}catch(e){ toast('drill-down failed: '+e.message); }
|
|
8753
|
+
}
|
|
8754
|
+
function renderDetail(node,dep){
|
|
8755
|
+
if(!node){ $('#detail').innerHTML='<div class="t">node not in current page</div>'; return; }
|
|
8756
|
+
const fields=[['id',node.id],['type',node.type],['name',node.name],['confidence',node.confidence],['domain',node.domain],['owner',node.owner]]
|
|
8757
|
+
.filter(([,v])=>v!=null).map(([k,v])=>'<div class="kv"><span>'+k+'</span><span>'+esc(v)+'</span></div>').join('');
|
|
8758
|
+
const tags=(node.tags||[]).map(t=>'<span class="chip">'+esc(t)+'</span>').join('');
|
|
8759
|
+
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>';
|
|
8760
|
+
$('#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>';
|
|
8761
|
+
}
|
|
8762
|
+
|
|
8763
|
+
const cv=()=>$('#graph'), ctx=()=>cv().getContext('2d');
|
|
8764
|
+
function drawGraph(rootId,dep){
|
|
8765
|
+
$('#empty').style.display='none';
|
|
8766
|
+
const c=cv(); const dpr=window.devicePixelRatio||1; const w=c.clientWidth,h=c.clientHeight;
|
|
8767
|
+
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);
|
|
8768
|
+
const nodes=dep.nodes||[]; const cx=w/2,cy=h/2;
|
|
8769
|
+
// root at center; others on a circle, radius by depth.
|
|
8770
|
+
const pos={}; pos[rootId]={x:cx,y:cy};
|
|
8771
|
+
const others=nodes.filter(n=>n.id!==rootId);
|
|
8772
|
+
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}; });
|
|
8773
|
+
// edges
|
|
8774
|
+
g.strokeStyle='#30363d'; g.lineWidth=1.2;
|
|
8775
|
+
(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(); } });
|
|
8776
|
+
// nodes
|
|
8777
|
+
const byId={}; nodes.forEach(n=>byId[n.id]=n);
|
|
8778
|
+
Object.entries(pos).forEach(([id,p])=>{ const n=byId[id]; const root=id===rootId;
|
|
8779
|
+
g.beginPath(); g.arc(p.x,p.y,root?13:8,0,Math.PI*2);
|
|
8780
|
+
g.fillStyle=root?'#3b82f6':'#21262d'; g.fill(); g.lineWidth=root?2:1; g.strokeStyle=root?'#60a5fa':'#484f58'; g.stroke();
|
|
8781
|
+
g.fillStyle='#c9d1d9'; g.font=(root?'600 12px':'11px')+' ui-sans-serif'; g.textAlign='center';
|
|
8782
|
+
g.fillText((n&&n.name?n.name:id).slice(0,22),p.x,p.y-(root?20:14));
|
|
8783
|
+
});
|
|
8784
|
+
}
|
|
8785
|
+
|
|
8786
|
+
document.addEventListener('DOMContentLoaded',()=>{
|
|
8787
|
+
const t=sessionStorage.getItem('cartograph_token'); if(t)$('#token').value=t;
|
|
8788
|
+
const tn=sessionStorage.getItem('cartograph_tenant'); if(tn)$('#tenant').value=tn;
|
|
8789
|
+
$('#reload').onclick=()=>{ sessionStorage.setItem('cartograph_token',$('#token').value.trim()); sessionStorage.setItem('cartograph_tenant',$('#tenant').value.trim()); boot(); };
|
|
8790
|
+
$('#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))); };
|
|
8791
|
+
boot();
|
|
8792
|
+
});
|
|
8793
|
+
`;
|
|
8794
|
+
function dashboardHtml(opts = {}) {
|
|
8795
|
+
const version = opts.version ?? "";
|
|
8796
|
+
return `<!DOCTYPE html>
|
|
8797
|
+
<html lang="en"><head>
|
|
8798
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8799
|
+
<title>Cartograph dashboard</title>
|
|
8800
|
+
<style>${STYLE}</style>
|
|
8801
|
+
</head><body>
|
|
8802
|
+
<header>
|
|
8803
|
+
<h1>Cartograph</h1><span class="ver">${version ? `v${version}` : ""}</span>
|
|
8804
|
+
<span class="spacer"></span>
|
|
8805
|
+
<input id="tenant" placeholder="tenant (optional)" autocomplete="off">
|
|
8806
|
+
<input id="token" type="password" placeholder="bearer token" autocomplete="off">
|
|
8807
|
+
<button id="reload">Reload</button>
|
|
8808
|
+
</header>
|
|
8809
|
+
<main>
|
|
8810
|
+
<div class="col"><div id="summary"></div></div>
|
|
8811
|
+
<div class="col" id="center"><canvas id="graph"></canvas><div id="empty">Select a node to explore its dependencies.</div></div>
|
|
8812
|
+
<div class="col">
|
|
8813
|
+
<input id="search" placeholder="Search nodes\u2026" autocomplete="off">
|
|
8814
|
+
<div id="list"></div>
|
|
8815
|
+
<div id="detail"></div>
|
|
8816
|
+
</div>
|
|
8817
|
+
</main>
|
|
8818
|
+
<div id="toast"></div>
|
|
8819
|
+
<script>${SCRIPT}</script>
|
|
8820
|
+
</body></html>`;
|
|
8821
|
+
}
|
|
8822
|
+
|
|
8523
8823
|
// src/api/server.ts
|
|
8524
8824
|
var DEPENDENCIES_RE = /^\/v1\/nodes\/(.+)\/dependencies$/;
|
|
8525
8825
|
var MAX_GRAPHQL_BYTES = 1024 * 1024;
|
|
@@ -8561,6 +8861,8 @@ async function runApi(opts) {
|
|
|
8561
8861
|
});
|
|
8562
8862
|
const restDeps = { backend: opts.backend, version: opts.version };
|
|
8563
8863
|
const openApiDoc = buildOpenApiDocument({ version: opts.version });
|
|
8864
|
+
const dashboardEnabled = opts.dashboard !== false;
|
|
8865
|
+
const dashboardPage = dashboardEnabled ? dashboardHtml({ version: opts.version }) : "";
|
|
8564
8866
|
const allowedOrigins = opts.allowedOrigins ?? [];
|
|
8565
8867
|
assertSafeBind({ host: host2, port: requestedPort, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...token ? { token } : {} });
|
|
8566
8868
|
let allowedHosts = opts.allowedHosts ?? [];
|
|
@@ -8603,6 +8905,11 @@ async function runApi(opts) {
|
|
|
8603
8905
|
finish(200);
|
|
8604
8906
|
return;
|
|
8605
8907
|
}
|
|
8908
|
+
if (dashboardEnabled && (path === "/" || path === "/app") && req.method === "GET") {
|
|
8909
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", ...cors }).end(dashboardPage);
|
|
8910
|
+
finish(200);
|
|
8911
|
+
return;
|
|
8912
|
+
}
|
|
8606
8913
|
if (path === "/v1/health") {
|
|
8607
8914
|
if (req.method !== "GET") {
|
|
8608
8915
|
send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
|
|
@@ -8767,6 +9074,7 @@ function parseApiArgs(argv) {
|
|
|
8767
9074
|
const a = argv[i];
|
|
8768
9075
|
if (a === "--http") continue;
|
|
8769
9076
|
else if (a === "--no-graphql") opts.graphql = false;
|
|
9077
|
+
else if (a === "--no-dashboard") opts.dashboard = false;
|
|
8770
9078
|
else if (a === "--port") opts.port = Number(argv[++i]);
|
|
8771
9079
|
else if (a === "--host") opts.host = argv[++i];
|
|
8772
9080
|
else if (a === "--allowed-hosts") opts.allowedHosts = splitList(argv[++i]);
|
|
@@ -8802,12 +9110,14 @@ async function startApi(opts = {}) {
|
|
|
8802
9110
|
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
8803
9111
|
...token ? { token } : {},
|
|
8804
9112
|
...opts.graphql === false ? { graphql: false } : {},
|
|
9113
|
+
...opts.dashboard === false ? { dashboard: false } : {},
|
|
8805
9114
|
...opts.tenant ? { tenant: { defaultTenant: normalizeTenant(opts.tenant) } } : {},
|
|
8806
9115
|
log: log2
|
|
8807
9116
|
});
|
|
8808
9117
|
const graphqlNote = opts.graphql === false ? " [REST only]" : " + /graphql";
|
|
9118
|
+
const dashNote = opts.dashboard === false ? "" : ` \xB7 dashboard http://${host2}:${port}/`;
|
|
8809
9119
|
log2(
|
|
8810
|
-
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})`
|
|
9120
|
+
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})${dashNote}`
|
|
8811
9121
|
);
|
|
8812
9122
|
return server;
|
|
8813
9123
|
}
|
|
@@ -12059,6 +12369,7 @@ function checkClaudePrerequisites() {
|
|
|
12059
12369
|
AuthorizationError,
|
|
12060
12370
|
CLIENTS,
|
|
12061
12371
|
CONFIDENCE,
|
|
12372
|
+
CORRELATION_CONFIDENCE,
|
|
12062
12373
|
CartographyDB,
|
|
12063
12374
|
ComplianceReportSchema,
|
|
12064
12375
|
ComplianceRuleSchema,
|
|
@@ -12145,6 +12456,7 @@ function checkClaudePrerequisites() {
|
|
|
12145
12456
|
computeIdentity,
|
|
12146
12457
|
connectionsScanner,
|
|
12147
12458
|
contentHash,
|
|
12459
|
+
correlateTopology,
|
|
12148
12460
|
createBashTool,
|
|
12149
12461
|
createCartographyTools,
|
|
12150
12462
|
createClaudeProvider,
|
|
@@ -12160,6 +12472,7 @@ function checkClaudePrerequisites() {
|
|
|
12160
12472
|
createSqliteQueryBackend,
|
|
12161
12473
|
currentOs,
|
|
12162
12474
|
cursorDeeplink,
|
|
12475
|
+
dashboardHtml,
|
|
12163
12476
|
databasesScanner,
|
|
12164
12477
|
deepMerge,
|
|
12165
12478
|
defaultAllowedHosts,
|
|
@@ -12191,6 +12504,7 @@ function checkClaudePrerequisites() {
|
|
|
12191
12504
|
exportJGF,
|
|
12192
12505
|
exportJSON,
|
|
12193
12506
|
extractListeningPorts,
|
|
12507
|
+
extractSignals,
|
|
12194
12508
|
filterBySeverity,
|
|
12195
12509
|
findAnonViolations,
|
|
12196
12510
|
formatComplianceText,
|