@datasynx/agentic-ai-cartography 2.3.0 → 2.5.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 +3 -3
- package/dist/{chunk-7QEBFMN4.js → chunk-GA4427LB.js} +147 -18
- package/dist/chunk-GA4427LB.js.map +1 -0
- package/dist/{chunk-7VZH5PFV.js → chunk-NQXZUWOI.js} +42 -12
- package/dist/chunk-NQXZUWOI.js.map +1 -0
- package/dist/{chunk-WCR47QA2.js → chunk-QQOQBE2A.js} +16 -5
- package/dist/chunk-QQOQBE2A.js.map +1 -0
- package/dist/{chunk-B2AKONVW.js → chunk-RYQ4KQCK.js} +253 -56
- package/dist/chunk-RYQ4KQCK.js.map +1 -0
- package/dist/cli.js +89 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +502 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +390 -11
- package/dist/index.d.ts +390 -11
- package/dist/index.js +475 -73
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +3 -3
- package/dist/{types-TJWXAQ2L.js → types-5L3AGZLG.js} +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/chunk-7QEBFMN4.js.map +0 -1
- package/dist/chunk-7VZH5PFV.js.map +0 -1
- package/dist/chunk-B2AKONVW.js.map +0 -1
- package/dist/chunk-WCR47QA2.js.map +0 -1
- /package/dist/{types-TJWXAQ2L.js.map → types-5L3AGZLG.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
ACTIONS: () => ACTIONS,
|
|
34
|
+
ActionSchema: () => ActionSchema,
|
|
35
|
+
AuthConfigSchema: () => AuthConfigSchema,
|
|
36
|
+
AuthorizationError: () => AuthorizationError,
|
|
33
37
|
CLIENTS: () => CLIENTS,
|
|
34
38
|
CONFIDENCE: () => CONFIDENCE,
|
|
35
39
|
CartographyDB: () => CartographyDB,
|
|
@@ -38,6 +42,7 @@ __export(src_exports, {
|
|
|
38
42
|
ConditionSchema: () => ConditionSchema,
|
|
39
43
|
ConfigError: () => ConfigError,
|
|
40
44
|
ControlResultSchema: () => ControlResultSchema,
|
|
45
|
+
CredentialConfigSchema: () => CredentialConfigSchema,
|
|
41
46
|
CsvCostSource: () => CsvCostSource,
|
|
42
47
|
DEFAULT_ANOMALY_THRESHOLDS: () => DEFAULT_ANOMALY_THRESHOLDS,
|
|
43
48
|
DEFAULT_SERVER_NAME: () => DEFAULT_SERVER_NAME,
|
|
@@ -46,16 +51,22 @@ __export(src_exports, {
|
|
|
46
51
|
INGEST_SCHEMA_VERSION: () => INGEST_SCHEMA_VERSION,
|
|
47
52
|
IngestEnvelopeSchema: () => IngestEnvelopeSchema,
|
|
48
53
|
InvalidTenantError: () => InvalidTenantError,
|
|
54
|
+
JiraSink: () => JiraSink,
|
|
49
55
|
LOOPBACK_HOSTS: () => LOOPBACK_HOSTS2,
|
|
50
56
|
MCP_BIN: () => MCP_BIN,
|
|
51
57
|
NotFoundError: () => NotFoundError,
|
|
52
58
|
PACKAGE_NAME: () => PACKAGE_NAME,
|
|
59
|
+
PAGERDUTY_ENQUEUE_URL: () => PAGERDUTY_ENQUEUE_URL,
|
|
53
60
|
PERSONAL: () => PERSONAL,
|
|
54
61
|
PORT_MAP: () => PORT_MAP,
|
|
55
62
|
PRIVATE_IP: () => PRIVATE_IP,
|
|
56
63
|
PUSH_SCHEMA_VERSION: () => PUSH_SCHEMA_VERSION,
|
|
64
|
+
PagerDutySink: () => PagerDutySink,
|
|
65
|
+
PrincipalSchema: () => PrincipalSchema,
|
|
57
66
|
ProviderRegistry: () => ProviderRegistry,
|
|
58
67
|
RELATION_TO_DIRECTION: () => RELATION_TO_DIRECTION,
|
|
68
|
+
ROLES: () => ROLES,
|
|
69
|
+
RoleSchema: () => RoleSchema,
|
|
59
70
|
RuleCheckSchema: () => RuleCheckSchema,
|
|
60
71
|
RulesetSchema: () => RulesetSchema,
|
|
61
72
|
SCAN_ARG_PATTERNS: () => SCAN_ARG_PATTERNS,
|
|
@@ -65,10 +76,13 @@ __export(src_exports, {
|
|
|
65
76
|
ScannerRegistry: () => ScannerRegistry,
|
|
66
77
|
ScannerShape: () => ScannerShape,
|
|
67
78
|
SharingLevelSchema: () => SharingLevelSchema,
|
|
79
|
+
SlackSink: () => SlackSink,
|
|
80
|
+
SqliteCredentialStore: () => SqliteCredentialStore,
|
|
68
81
|
SqliteQueryBackend: () => SqliteQueryBackend,
|
|
69
82
|
SqliteStoreBackend: () => SqliteStoreBackend,
|
|
70
83
|
StdoutSink: () => StdoutSink,
|
|
71
84
|
TENANT_HEADER: () => TENANT_HEADER,
|
|
85
|
+
TenantMismatchError: () => TenantMismatchError,
|
|
72
86
|
VectorStore: () => VectorStore,
|
|
73
87
|
WebhookSink: () => WebhookSink,
|
|
74
88
|
applyInstall: () => applyInstall,
|
|
@@ -76,7 +90,9 @@ __export(src_exports, {
|
|
|
76
90
|
assertReadOnly: () => assertReadOnly,
|
|
77
91
|
assertSafeBind: () => assertSafeBind,
|
|
78
92
|
assertSafeScanArg: () => assertSafeScanArg,
|
|
93
|
+
assertSameTenant: () => assertSameTenant,
|
|
79
94
|
assignColors: () => assignColors,
|
|
95
|
+
authorize: () => authorize,
|
|
80
96
|
bearerToken: () => bearerToken,
|
|
81
97
|
bookmarksScanner: () => bookmarksScanner,
|
|
82
98
|
buildCartographyToolHandlers: () => buildCartographyToolHandlers,
|
|
@@ -84,6 +100,7 @@ __export(src_exports, {
|
|
|
84
100
|
buildOpenApiDocument: () => buildOpenApiDocument,
|
|
85
101
|
buildReport: () => buildReport,
|
|
86
102
|
buildSinks: () => buildSinks,
|
|
103
|
+
can: () => can,
|
|
87
104
|
centralDbFromEnv: () => centralDbFromEnv,
|
|
88
105
|
checkBearer: () => checkBearer,
|
|
89
106
|
checkPrerequisites: () => checkPrerequisites,
|
|
@@ -149,6 +166,9 @@ __export(src_exports, {
|
|
|
149
166
|
filterBySeverity: () => filterBySeverity,
|
|
150
167
|
findAnonViolations: () => findAnonViolations,
|
|
151
168
|
formatComplianceText: () => formatComplianceText,
|
|
169
|
+
formatJira: () => formatJira,
|
|
170
|
+
formatPagerDuty: () => formatPagerDuty,
|
|
171
|
+
formatSlack: () => formatSlack,
|
|
152
172
|
generateDependencyMermaid: () => generateDependencyMermaid,
|
|
153
173
|
generateDiffMermaid: () => generateDiffMermaid,
|
|
154
174
|
generateTopologyMermaid: () => generateTopologyMermaid,
|
|
@@ -157,6 +177,7 @@ __export(src_exports, {
|
|
|
157
177
|
globalId: () => globalId,
|
|
158
178
|
groupByDomain: () => groupByDomain,
|
|
159
179
|
handleGraphqlGet: () => handleGraphqlGet,
|
|
180
|
+
hashToken: () => hashToken,
|
|
160
181
|
hexCorners: () => hexCorners,
|
|
161
182
|
hexDistance: () => hexDistance,
|
|
162
183
|
hexNeighbors: () => hexNeighbors,
|
|
@@ -171,6 +192,7 @@ __export(src_exports, {
|
|
|
171
192
|
isPersonalHost: () => isPersonalHost,
|
|
172
193
|
isReadOnlyCommand: () => isReadOnlyCommand,
|
|
173
194
|
isRemembered: () => isRemembered,
|
|
195
|
+
isSecureWebhookUrl: () => isSecureWebhookUrl,
|
|
174
196
|
k8sScanner: () => k8sScanner,
|
|
175
197
|
keyMetaOf: () => keyMetaOf,
|
|
176
198
|
layoutClusters: () => layoutClusters,
|
|
@@ -209,6 +231,7 @@ __export(src_exports, {
|
|
|
209
231
|
pixelToHex: () => pixelToHex,
|
|
210
232
|
planInstall: () => planInstall,
|
|
211
233
|
portsScanner: () => portsScanner,
|
|
234
|
+
postJson: () => postJson,
|
|
212
235
|
previewShare: () => previewShare,
|
|
213
236
|
pseudonymize: () => pseudonymize,
|
|
214
237
|
pseudonymizeFragment: () => pseudonymizeFragment,
|
|
@@ -221,6 +244,7 @@ __export(src_exports, {
|
|
|
221
244
|
renderDiff: () => renderDiff,
|
|
222
245
|
resolveEffectiveLevel: () => resolveEffectiveLevel,
|
|
223
246
|
resolveNlQuery: () => resolveNlQuery,
|
|
247
|
+
resolvePrincipal: () => resolvePrincipal,
|
|
224
248
|
resolveSharingLevel: () => resolveSharingLevel,
|
|
225
249
|
resolveTenant: () => resolveTenant,
|
|
226
250
|
revalidateAnonymized: () => revalidateAnonymized,
|
|
@@ -240,6 +264,7 @@ __export(src_exports, {
|
|
|
240
264
|
safetyHook: () => safetyHook,
|
|
241
265
|
sanitizeUntrusted: () => sanitizeUntrusted,
|
|
242
266
|
sanitizeValue: () => sanitizeValue,
|
|
267
|
+
scopeReads: () => scopeReads,
|
|
243
268
|
scoreTopology: () => scoreTopology,
|
|
244
269
|
securityRelevantChange: () => securityRelevantChange,
|
|
245
270
|
serializeConfig: () => serializeConfig,
|
|
@@ -417,15 +442,26 @@ var SECURITY_METADATA_KEYS = [
|
|
|
417
442
|
var DriftConfigSchema = import_zod.z.object({
|
|
418
443
|
minSeverity: import_zod.z.enum(SEVERITIES).default("info"),
|
|
419
444
|
sinks: import_zod.z.array(import_zod.z.object({
|
|
420
|
-
type: import_zod.z.enum(["stdout", "webhook"]),
|
|
445
|
+
type: import_zod.z.enum(["stdout", "webhook", "slack", "pagerduty", "jira"]),
|
|
421
446
|
url: import_zod.z.string().url().optional(),
|
|
422
447
|
token: import_zod.z.string().optional(),
|
|
423
|
-
timeoutMs: import_zod.z.number().int().positive().optional()
|
|
448
|
+
timeoutMs: import_zod.z.number().int().positive().optional(),
|
|
449
|
+
routingKey: import_zod.z.string().optional(),
|
|
450
|
+
email: import_zod.z.string().optional(),
|
|
451
|
+
project: import_zod.z.string().optional(),
|
|
452
|
+
issueType: import_zod.z.string().optional()
|
|
424
453
|
})).default([{ type: "stdout" }])
|
|
425
454
|
}).superRefine((cfg, ctx) => {
|
|
426
455
|
for (const [i, s] of cfg.sinks.entries()) {
|
|
427
|
-
|
|
428
|
-
ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message:
|
|
456
|
+
const requireUrl = (msg) => {
|
|
457
|
+
if (!s.url) ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message: msg });
|
|
458
|
+
};
|
|
459
|
+
if (s.type === "webhook") requireUrl("webhook sink requires a url");
|
|
460
|
+
if (s.type === "slack") requireUrl("slack sink requires a webhook url");
|
|
461
|
+
if (s.type === "jira") {
|
|
462
|
+
requireUrl("jira sink requires a base url");
|
|
463
|
+
if (!s.email) ctx.addIssue({ code: "custom", path: ["sinks", i, "email"], message: "jira sink requires an email" });
|
|
464
|
+
if (!s.project) ctx.addIssue({ code: "custom", path: ["sinks", i, "project"], message: "jira sink requires a project key" });
|
|
429
465
|
}
|
|
430
466
|
}
|
|
431
467
|
});
|
|
@@ -2039,8 +2075,17 @@ function stripSensitive(target) {
|
|
|
2039
2075
|
const stripped = `${url.hostname}${url.port ? ":" + url.port : ""}`;
|
|
2040
2076
|
return stripped || raw;
|
|
2041
2077
|
} catch {
|
|
2042
|
-
|
|
2043
|
-
|
|
2078
|
+
let s = raw;
|
|
2079
|
+
const slash = s.indexOf("/");
|
|
2080
|
+
if (slash >= 0) s = s.slice(0, slash);
|
|
2081
|
+
const q = s.indexOf("?");
|
|
2082
|
+
if (q >= 0) s = s.slice(0, q);
|
|
2083
|
+
const at = s.indexOf("@");
|
|
2084
|
+
if (at >= 0) {
|
|
2085
|
+
const colon = s.lastIndexOf(":");
|
|
2086
|
+
if (colon > at) s = s.slice(0, at) + ":" + s.slice(colon + 1);
|
|
2087
|
+
}
|
|
2088
|
+
return s || raw;
|
|
2044
2089
|
}
|
|
2045
2090
|
}
|
|
2046
2091
|
var SCAN_ARG_PATTERNS = {
|
|
@@ -2058,7 +2103,7 @@ function assertSafeScanArg(kind, value) {
|
|
|
2058
2103
|
return value;
|
|
2059
2104
|
}
|
|
2060
2105
|
function redactSecrets(value) {
|
|
2061
|
-
return value.replace(/([a-z][a-z0-9+.-]
|
|
2106
|
+
return value.replace(/([a-z][a-z0-9+.-]{0,63}:\/\/[^:@/\s]{1,256}):[^@/\s]{1,256}@/gi, "$1:***@");
|
|
2062
2107
|
}
|
|
2063
2108
|
function redactValue(value) {
|
|
2064
2109
|
if (typeof value === "string") return redactSecrets(value);
|
|
@@ -3056,7 +3101,10 @@ CREATE TABLE IF NOT EXISTS activity_events (
|
|
|
3056
3101
|
duration_ms INTEGER,
|
|
3057
3102
|
command TEXT,
|
|
3058
3103
|
result_bytes INTEGER,
|
|
3059
|
-
tenant TEXT NOT NULL DEFAULT 'local'
|
|
3104
|
+
tenant TEXT NOT NULL DEFAULT 'local',
|
|
3105
|
+
actor_subject TEXT,
|
|
3106
|
+
actor_role TEXT,
|
|
3107
|
+
actor_tenant TEXT
|
|
3060
3108
|
);
|
|
3061
3109
|
|
|
3062
3110
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
@@ -3165,6 +3213,16 @@ CREATE INDEX IF NOT EXISTS idx_nodes_tenant_content ON nodes(tenant, content_has
|
|
|
3165
3213
|
CREATE INDEX IF NOT EXISTS idx_contrib_org ON node_contributors(organization, global_id);
|
|
3166
3214
|
CREATE INDEX IF NOT EXISTS idx_nodes_owner ON nodes(session_id, owner);
|
|
3167
3215
|
`;
|
|
3216
|
+
var AUTH_SCHEMA = `
|
|
3217
|
+
CREATE TABLE IF NOT EXISTS auth_credentials (
|
|
3218
|
+
token_hash TEXT PRIMARY KEY,
|
|
3219
|
+
subject TEXT NOT NULL,
|
|
3220
|
+
tenant TEXT NOT NULL DEFAULT 'local',
|
|
3221
|
+
role TEXT NOT NULL,
|
|
3222
|
+
created_at TEXT NOT NULL
|
|
3223
|
+
);
|
|
3224
|
+
CREATE INDEX IF NOT EXISTS idx_auth_subject ON auth_credentials(subject);
|
|
3225
|
+
`;
|
|
3168
3226
|
var CartographyDB = class {
|
|
3169
3227
|
db;
|
|
3170
3228
|
/** 3.6 anomaly settings; defaults apply when no `anomaly` config is supplied. */
|
|
@@ -3184,7 +3242,8 @@ var CartographyDB = class {
|
|
|
3184
3242
|
const version = this.db.pragma("user_version", { simple: true });
|
|
3185
3243
|
if (version === 0) {
|
|
3186
3244
|
this.db.exec(SCHEMA);
|
|
3187
|
-
this.db.
|
|
3245
|
+
this.db.exec(AUTH_SCHEMA);
|
|
3246
|
+
this.db.pragma("user_version = 15");
|
|
3188
3247
|
return;
|
|
3189
3248
|
} else if (version === 1) {
|
|
3190
3249
|
const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
|
|
@@ -3370,6 +3429,18 @@ var CartographyDB = class {
|
|
|
3370
3429
|
}
|
|
3371
3430
|
this.db.pragma("user_version = 14");
|
|
3372
3431
|
}
|
|
3432
|
+
const v14 = this.db.pragma("user_version", { simple: true });
|
|
3433
|
+
if (v14 < 15) {
|
|
3434
|
+
this.db.exec(AUTH_SCHEMA);
|
|
3435
|
+
const ev = this.db.prepare("PRAGMA table_info(activity_events)").all();
|
|
3436
|
+
if (ev.length > 0) {
|
|
3437
|
+
const cols = ev.map((c) => c.name);
|
|
3438
|
+
if (!cols.includes("actor_subject")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_subject TEXT");
|
|
3439
|
+
if (!cols.includes("actor_role")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_role TEXT");
|
|
3440
|
+
if (!cols.includes("actor_tenant")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_tenant TEXT");
|
|
3441
|
+
}
|
|
3442
|
+
this.db.pragma("user_version = 15");
|
|
3443
|
+
}
|
|
3373
3444
|
}
|
|
3374
3445
|
close() {
|
|
3375
3446
|
this.db.pragma("optimize");
|
|
@@ -3800,13 +3871,13 @@ var CartographyDB = class {
|
|
|
3800
3871
|
});
|
|
3801
3872
|
}
|
|
3802
3873
|
// ── Events ──────────────────────────────
|
|
3803
|
-
insertEvent(sessionId, event, taskId) {
|
|
3874
|
+
insertEvent(sessionId, event, taskId, actor) {
|
|
3804
3875
|
const id = crypto.randomUUID();
|
|
3805
3876
|
const tenant = this.tenantOf(sessionId);
|
|
3806
3877
|
this.db.prepare(`
|
|
3807
3878
|
INSERT INTO activity_events
|
|
3808
|
-
(id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port, command, result_bytes, tenant)
|
|
3809
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3879
|
+
(id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port, command, result_bytes, tenant, actor_subject, actor_role, actor_tenant)
|
|
3880
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3810
3881
|
`).run(
|
|
3811
3882
|
id,
|
|
3812
3883
|
sessionId,
|
|
@@ -3820,9 +3891,52 @@ var CartographyDB = class {
|
|
|
3820
3891
|
event.port ?? null,
|
|
3821
3892
|
event.command ?? null,
|
|
3822
3893
|
event.resultBytes ?? null,
|
|
3823
|
-
tenant
|
|
3894
|
+
tenant,
|
|
3895
|
+
actor?.subject ?? null,
|
|
3896
|
+
actor?.role ?? null,
|
|
3897
|
+
actor?.tenant ?? null
|
|
3824
3898
|
);
|
|
3825
3899
|
}
|
|
3900
|
+
// ── RBAC credential store (4.5) ─────────────
|
|
3901
|
+
/** Number of stored credentials. `0` ⇒ no RBAC configured (fall back to shared/loopback). */
|
|
3902
|
+
countCredentials() {
|
|
3903
|
+
return this.db.prepare("SELECT COUNT(*) AS n FROM auth_credentials").get().n;
|
|
3904
|
+
}
|
|
3905
|
+
/** Look up a credential by its sha256 token hash. */
|
|
3906
|
+
findCredentialByHash(tokenHash) {
|
|
3907
|
+
const r = this.db.prepare("SELECT * FROM auth_credentials WHERE token_hash = ?").get(tokenHash);
|
|
3908
|
+
if (!r) return void 0;
|
|
3909
|
+
return {
|
|
3910
|
+
tokenHash: r["token_hash"],
|
|
3911
|
+
subject: r["subject"],
|
|
3912
|
+
tenant: r["tenant"],
|
|
3913
|
+
role: r["role"],
|
|
3914
|
+
createdAt: r["created_at"]
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
/** Upsert a credential (idempotent on the token hash). Stores only the hash, never the raw token. */
|
|
3918
|
+
addCredential(rec) {
|
|
3919
|
+
this.db.prepare(`
|
|
3920
|
+
INSERT INTO auth_credentials (token_hash, subject, tenant, role, created_at)
|
|
3921
|
+
VALUES (?, ?, ?, ?, ?)
|
|
3922
|
+
ON CONFLICT(token_hash) DO UPDATE SET subject = excluded.subject, tenant = excluded.tenant, role = excluded.role
|
|
3923
|
+
`).run(rec.tokenHash, rec.subject, rec.tenant, rec.role, (/* @__PURE__ */ new Date()).toISOString());
|
|
3924
|
+
}
|
|
3925
|
+
/** List all credentials (token hashes only — the raw token is unrecoverable). */
|
|
3926
|
+
listCredentials() {
|
|
3927
|
+
const rows = this.db.prepare("SELECT * FROM auth_credentials ORDER BY created_at").all();
|
|
3928
|
+
return rows.map((r) => ({
|
|
3929
|
+
tokenHash: r["token_hash"],
|
|
3930
|
+
subject: r["subject"],
|
|
3931
|
+
tenant: r["tenant"],
|
|
3932
|
+
role: r["role"],
|
|
3933
|
+
createdAt: r["created_at"]
|
|
3934
|
+
}));
|
|
3935
|
+
}
|
|
3936
|
+
/** Revoke every credential for a subject. Returns the number removed. */
|
|
3937
|
+
revokeCredentialsBySubject(subject) {
|
|
3938
|
+
return this.db.prepare("DELETE FROM auth_credentials WHERE subject = ?").run(subject).changes;
|
|
3939
|
+
}
|
|
3826
3940
|
getEvents(sessionId, since) {
|
|
3827
3941
|
const rows = since ? this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp").all(sessionId, since) : this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? ORDER BY timestamp").all(sessionId);
|
|
3828
3942
|
return rows.map((r) => {
|
|
@@ -5156,6 +5270,41 @@ var StdoutSink = class {
|
|
|
5156
5270
|
|
|
5157
5271
|
// src/sinks/webhook.ts
|
|
5158
5272
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
5273
|
+
async function postJson(opts) {
|
|
5274
|
+
const doFetch = opts.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0);
|
|
5275
|
+
if (!doFetch) {
|
|
5276
|
+
logWarn("sink unavailable: global fetch missing", { sink: opts.sinkName });
|
|
5277
|
+
return;
|
|
5278
|
+
}
|
|
5279
|
+
if (!opts.url) {
|
|
5280
|
+
logWarn("sink unavailable: no url configured", { sink: opts.sinkName });
|
|
5281
|
+
return;
|
|
5282
|
+
}
|
|
5283
|
+
if (!isSecureWebhookUrl(opts.url)) {
|
|
5284
|
+
logWarn("sink refused: insecure scheme (use https:// or a loopback host)", {
|
|
5285
|
+
sink: opts.sinkName,
|
|
5286
|
+
host: stripSensitive(opts.url)
|
|
5287
|
+
});
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
try {
|
|
5291
|
+
const res = await doFetch(opts.url, {
|
|
5292
|
+
method: "POST",
|
|
5293
|
+
headers: { "content-type": "application/json", ...opts.headers ?? {} },
|
|
5294
|
+
body: JSON.stringify(opts.body),
|
|
5295
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
|
|
5296
|
+
});
|
|
5297
|
+
if (!res.ok) {
|
|
5298
|
+
logError("sink delivery failed", { sink: opts.sinkName, host: stripSensitive(opts.url), status: res.status });
|
|
5299
|
+
}
|
|
5300
|
+
} catch (err) {
|
|
5301
|
+
logError("sink delivery failed", {
|
|
5302
|
+
sink: opts.sinkName,
|
|
5303
|
+
host: stripSensitive(opts.url),
|
|
5304
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
5305
|
+
});
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5159
5308
|
function isSecureWebhookUrl(url, env = process.env) {
|
|
5160
5309
|
if (env.CARTOGRAPHY_ALLOW_INSECURE_SYNC === "1") return true;
|
|
5161
5310
|
let parsed;
|
|
@@ -5174,59 +5323,177 @@ var WebhookSink = class {
|
|
|
5174
5323
|
}
|
|
5175
5324
|
name = "webhook";
|
|
5176
5325
|
async emit(alert) {
|
|
5177
|
-
if (typeof fetch !== "function") {
|
|
5178
|
-
logWarn("webhook sink unavailable: global fetch missing", { sink: this.name });
|
|
5179
|
-
return;
|
|
5180
|
-
}
|
|
5181
5326
|
const { url, token, timeoutMs } = this.opts;
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5327
|
+
await postJson({
|
|
5328
|
+
url,
|
|
5329
|
+
body: redactValue(alert),
|
|
5330
|
+
...token ? { headers: { authorization: `Bearer ${token}` } } : {},
|
|
5331
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
5332
|
+
sinkName: this.name
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
};
|
|
5336
|
+
|
|
5337
|
+
// src/sinks/providers.ts
|
|
5338
|
+
var MAX_ITEMS2 = 20;
|
|
5339
|
+
var SEVERITY_EMOJI = { info: "\u{1F7E2}", warning: "\u{1F7E1}", critical: "\u{1F534}" };
|
|
5340
|
+
function headline(alert) {
|
|
5341
|
+
const s = alert.summary;
|
|
5342
|
+
return `${s.nodesAdded}+ / ${s.nodesRemoved}- / ${s.nodesChanged}~ nodes, ${s.edgesAdded}+ / ${s.edgesRemoved}- edges`;
|
|
5343
|
+
}
|
|
5344
|
+
function itemLine(it) {
|
|
5345
|
+
const sec = it.securityFields?.length ? ` [security: ${it.securityFields.join(", ")}]` : "";
|
|
5346
|
+
const fields = it.changedFields?.length ? ` (${it.changedFields.join(", ")})` : "";
|
|
5347
|
+
return `${it.severity.toUpperCase()} \xB7 ${it.kind} \xB7 ${it.label}${fields}${sec}`;
|
|
5348
|
+
}
|
|
5349
|
+
function bodyText(alert) {
|
|
5350
|
+
const lines = alert.items.slice(0, MAX_ITEMS2).map(itemLine);
|
|
5351
|
+
const more = alert.items.length > MAX_ITEMS2 ? [`\u2026and ${alert.items.length - MAX_ITEMS2} more`] : [];
|
|
5352
|
+
return [headline(alert), "", ...lines, ...more].join("\n");
|
|
5353
|
+
}
|
|
5354
|
+
function formatSlack(alert) {
|
|
5355
|
+
const title = `${SEVERITY_EMOJI[alert.severity]} Topology drift \u2014 ${alert.severity}`;
|
|
5356
|
+
return {
|
|
5357
|
+
text: `${title}: ${headline(alert)}`,
|
|
5358
|
+
blocks: [
|
|
5359
|
+
{ type: "header", text: { type: "plain_text", text: title, emoji: true } },
|
|
5360
|
+
{ type: "section", text: { type: "mrkdwn", text: "```" + bodyText(alert) + "```" } },
|
|
5361
|
+
{ type: "context", elements: [{ type: "mrkdwn", text: `base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId} \xB7 ${alert.generatedAt}` }] }
|
|
5362
|
+
]
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
var PD_SEVERITY = {
|
|
5366
|
+
info: "info",
|
|
5367
|
+
warning: "warning",
|
|
5368
|
+
critical: "critical"
|
|
5369
|
+
};
|
|
5370
|
+
function formatPagerDuty(alert, routingKey) {
|
|
5371
|
+
return {
|
|
5372
|
+
routing_key: routingKey,
|
|
5373
|
+
event_action: "trigger",
|
|
5374
|
+
// Stable per base→current pair so repeated alerts for the same delta de-duplicate.
|
|
5375
|
+
dedup_key: `cartograph-drift:${alert.base.sessionId}:${alert.current.sessionId}`,
|
|
5376
|
+
payload: {
|
|
5377
|
+
summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
|
|
5378
|
+
source: "cartograph",
|
|
5379
|
+
severity: PD_SEVERITY[alert.severity],
|
|
5380
|
+
timestamp: alert.generatedAt,
|
|
5381
|
+
custom_details: {
|
|
5382
|
+
summary: alert.summary,
|
|
5383
|
+
items: alert.items.slice(0, MAX_ITEMS2).map((it) => ({
|
|
5384
|
+
kind: it.kind,
|
|
5385
|
+
ref: it.ref,
|
|
5386
|
+
severity: it.severity,
|
|
5387
|
+
...it.changedFields ? { changedFields: it.changedFields } : {},
|
|
5388
|
+
...it.securityFields ? { securityFields: it.securityFields } : {}
|
|
5389
|
+
}))
|
|
5205
5390
|
}
|
|
5206
|
-
} catch (err) {
|
|
5207
|
-
logError("webhook sink failed", {
|
|
5208
|
-
sink: this.name,
|
|
5209
|
-
host: stripSensitive(url),
|
|
5210
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
5211
|
-
});
|
|
5212
5391
|
}
|
|
5392
|
+
};
|
|
5393
|
+
}
|
|
5394
|
+
function formatJira(alert, opts) {
|
|
5395
|
+
return {
|
|
5396
|
+
fields: {
|
|
5397
|
+
project: { key: opts.project },
|
|
5398
|
+
issuetype: { name: opts.issueType ?? "Task" },
|
|
5399
|
+
summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
|
|
5400
|
+
description: bodyText(alert) + `
|
|
5401
|
+
|
|
5402
|
+
base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId}
|
|
5403
|
+
generated ${alert.generatedAt}`
|
|
5404
|
+
}
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5408
|
+
// src/sinks/provider-sink.ts
|
|
5409
|
+
var PAGERDUTY_ENQUEUE_URL = "https://events.pagerduty.com/v2/enqueue";
|
|
5410
|
+
function deliver(name, url, body, opts, headers) {
|
|
5411
|
+
return postJson({
|
|
5412
|
+
url,
|
|
5413
|
+
body,
|
|
5414
|
+
sinkName: name,
|
|
5415
|
+
...headers ? { headers } : {},
|
|
5416
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
5417
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5420
|
+
var SlackSink = class {
|
|
5421
|
+
constructor(opts) {
|
|
5422
|
+
this.opts = opts;
|
|
5423
|
+
}
|
|
5424
|
+
name = "slack";
|
|
5425
|
+
async emit(alert) {
|
|
5426
|
+
await deliver(this.name, this.opts.url, formatSlack(redactValue(alert)), this.opts);
|
|
5427
|
+
}
|
|
5428
|
+
};
|
|
5429
|
+
var PagerDutySink = class {
|
|
5430
|
+
constructor(opts) {
|
|
5431
|
+
this.opts = opts;
|
|
5432
|
+
}
|
|
5433
|
+
name = "pagerduty";
|
|
5434
|
+
async emit(alert) {
|
|
5435
|
+
const body = formatPagerDuty(redactValue(alert), this.opts.routingKey);
|
|
5436
|
+
await deliver(this.name, this.opts.url || PAGERDUTY_ENQUEUE_URL, body, this.opts);
|
|
5437
|
+
}
|
|
5438
|
+
};
|
|
5439
|
+
var JiraSink = class {
|
|
5440
|
+
constructor(opts) {
|
|
5441
|
+
this.opts = opts;
|
|
5442
|
+
}
|
|
5443
|
+
name = "jira";
|
|
5444
|
+
async emit(alert) {
|
|
5445
|
+
const body = formatJira(redactValue(alert), {
|
|
5446
|
+
project: this.opts.project,
|
|
5447
|
+
...this.opts.issueType ? { issueType: this.opts.issueType } : {}
|
|
5448
|
+
});
|
|
5449
|
+
const auth = Buffer.from(`${this.opts.email}:${this.opts.token}`).toString("base64");
|
|
5450
|
+
const base = this.opts.url.replace(/\/+$/, "");
|
|
5451
|
+
await deliver(this.name, `${base}/rest/api/2/issue`, body, this.opts, { authorization: `Basic ${auth}` });
|
|
5213
5452
|
}
|
|
5214
5453
|
};
|
|
5215
5454
|
|
|
5216
5455
|
// src/sinks/index.ts
|
|
5217
5456
|
function buildSinks(drift) {
|
|
5218
5457
|
const configs = drift?.sinks && drift.sinks.length > 0 ? drift.sinks : [{ type: "stdout" }];
|
|
5458
|
+
const envSecret = process.env.CARTOGRAPHY_DRIFT_TOKEN;
|
|
5219
5459
|
const sinks = [];
|
|
5220
5460
|
for (const s of configs) {
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5461
|
+
const timeoutMs = s.timeoutMs;
|
|
5462
|
+
switch (s.type) {
|
|
5463
|
+
case "webhook":
|
|
5464
|
+
if (!s.url) {
|
|
5465
|
+
logWarn("drift sink skipped: webhook requires a url", { sink: s.type });
|
|
5466
|
+
break;
|
|
5467
|
+
}
|
|
5468
|
+
sinks.push(new WebhookSink({ url: s.url, token: s.token ?? envSecret, timeoutMs }));
|
|
5469
|
+
break;
|
|
5470
|
+
case "slack":
|
|
5471
|
+
if (!s.url) {
|
|
5472
|
+
logWarn("drift sink skipped: slack requires a webhook url", { sink: s.type });
|
|
5473
|
+
break;
|
|
5474
|
+
}
|
|
5475
|
+
sinks.push(new SlackSink({ url: s.url, timeoutMs }));
|
|
5476
|
+
break;
|
|
5477
|
+
case "pagerduty": {
|
|
5478
|
+
const routingKey = s.routingKey ?? s.token ?? envSecret;
|
|
5479
|
+
if (!routingKey) {
|
|
5480
|
+
logWarn("drift sink skipped: pagerduty requires a routingKey (or CARTOGRAPHY_DRIFT_TOKEN)", { sink: s.type });
|
|
5481
|
+
break;
|
|
5482
|
+
}
|
|
5483
|
+
sinks.push(new PagerDutySink({ url: s.url ?? PAGERDUTY_ENQUEUE_URL, routingKey, timeoutMs }));
|
|
5484
|
+
break;
|
|
5485
|
+
}
|
|
5486
|
+
case "jira": {
|
|
5487
|
+
const token = s.token ?? envSecret;
|
|
5488
|
+
if (!s.url || !s.email || !s.project || !token) {
|
|
5489
|
+
logWarn("drift sink skipped: jira requires url, email, project and a token", { sink: s.type });
|
|
5490
|
+
break;
|
|
5491
|
+
}
|
|
5492
|
+
sinks.push(new JiraSink({ url: s.url, email: s.email, token, project: s.project, issueType: s.issueType, timeoutMs }));
|
|
5493
|
+
break;
|
|
5494
|
+
}
|
|
5495
|
+
default:
|
|
5496
|
+
sinks.push(new StdoutSink());
|
|
5230
5497
|
}
|
|
5231
5498
|
}
|
|
5232
5499
|
return sinks.length > 0 ? sinks : [new StdoutSink()];
|
|
@@ -5348,6 +5615,36 @@ async function runDrift(db, config, opts = {}) {
|
|
|
5348
5615
|
return alert;
|
|
5349
5616
|
}
|
|
5350
5617
|
|
|
5618
|
+
// src/auth/rbac.ts
|
|
5619
|
+
var ROLE_RANK = { viewer: 1, operator: 2, admin: 3 };
|
|
5620
|
+
var ACTION_MIN_ROLE = { read: "viewer", discovery: "operator", admin: "admin" };
|
|
5621
|
+
function can(role, action) {
|
|
5622
|
+
return ROLE_RANK[role] >= ROLE_RANK[ACTION_MIN_ROLE[action]];
|
|
5623
|
+
}
|
|
5624
|
+
var AuthorizationError = class extends Error {
|
|
5625
|
+
constructor(action, role) {
|
|
5626
|
+
super(`forbidden: role '${role}' may not perform '${action}'`);
|
|
5627
|
+
this.action = action;
|
|
5628
|
+
this.role = role;
|
|
5629
|
+
this.name = "AuthorizationError";
|
|
5630
|
+
}
|
|
5631
|
+
};
|
|
5632
|
+
function authorize(principal, action) {
|
|
5633
|
+
if (!can(principal.role, action)) throw new AuthorizationError(action, principal.role);
|
|
5634
|
+
}
|
|
5635
|
+
var TenantMismatchError = class extends Error {
|
|
5636
|
+
constructor() {
|
|
5637
|
+
super("forbidden: principal is not scoped to the requested tenant");
|
|
5638
|
+
this.name = "TenantMismatchError";
|
|
5639
|
+
}
|
|
5640
|
+
};
|
|
5641
|
+
function scopeReads(principal) {
|
|
5642
|
+
return principal.tenant;
|
|
5643
|
+
}
|
|
5644
|
+
function assertSameTenant(principal, requestedTenant) {
|
|
5645
|
+
if (requestedTenant !== principal.tenant) throw new TenantMismatchError();
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5351
5648
|
// src/compliance/rulesets/baseline.ts
|
|
5352
5649
|
var baseline = RulesetSchema.parse({
|
|
5353
5650
|
name: "baseline",
|
|
@@ -5635,7 +5932,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
|
|
|
5635
5932
|
|
|
5636
5933
|
// src/mcp/server.ts
|
|
5637
5934
|
var SERVER_NAME = "cartography";
|
|
5638
|
-
var SERVER_VERSION = "2.
|
|
5935
|
+
var SERVER_VERSION = "2.5.0";
|
|
5639
5936
|
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
5640
5937
|
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
5641
5938
|
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
@@ -6037,6 +6334,14 @@ function createMcpServer(opts = {}) {
|
|
|
6037
6334
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
6038
6335
|
},
|
|
6039
6336
|
async (args) => {
|
|
6337
|
+
if (opts.principal) {
|
|
6338
|
+
try {
|
|
6339
|
+
authorize(opts.principal, "discovery");
|
|
6340
|
+
} catch (err) {
|
|
6341
|
+
if (err instanceof AuthorizationError) return json({ error: `forbidden: role '${opts.principal.role}' may not run discovery (operator required)` });
|
|
6342
|
+
throw err;
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6040
6345
|
let sid = resolveSession();
|
|
6041
6346
|
if (args.update) {
|
|
6042
6347
|
if (!sid) return json({ error: "No session to update; run discovery first." });
|
|
@@ -6132,7 +6437,7 @@ function createMcpServer(opts = {}) {
|
|
|
6132
6437
|
}
|
|
6133
6438
|
|
|
6134
6439
|
// src/mcp/transports.ts
|
|
6135
|
-
var
|
|
6440
|
+
var import_node_crypto6 = require("crypto");
|
|
6136
6441
|
var import_node_http = __toESM(require("http"), 1);
|
|
6137
6442
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6138
6443
|
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
@@ -6179,7 +6484,41 @@ function defaultAllowedHosts(host2, port) {
|
|
|
6179
6484
|
return [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
|
|
6180
6485
|
}
|
|
6181
6486
|
|
|
6487
|
+
// src/auth/identity.ts
|
|
6488
|
+
var import_node_crypto5 = require("crypto");
|
|
6489
|
+
function hashToken(token) {
|
|
6490
|
+
return (0, import_node_crypto5.createHash)("sha256").update(token, "utf8").digest("hex");
|
|
6491
|
+
}
|
|
6492
|
+
var SqliteCredentialStore = class {
|
|
6493
|
+
constructor(db) {
|
|
6494
|
+
this.db = db;
|
|
6495
|
+
}
|
|
6496
|
+
count() {
|
|
6497
|
+
return this.db.countCredentials();
|
|
6498
|
+
}
|
|
6499
|
+
findByHash(tokenHash) {
|
|
6500
|
+
return this.db.findCredentialByHash(tokenHash);
|
|
6501
|
+
}
|
|
6502
|
+
};
|
|
6503
|
+
function resolvePrincipal(presentedToken, opts) {
|
|
6504
|
+
const tenant = opts.defaultTenant ?? DEFAULT_TENANT;
|
|
6505
|
+
if (opts.store && opts.store.count() > 0) {
|
|
6506
|
+
if (!presentedToken) return void 0;
|
|
6507
|
+
const rec = opts.store.findByHash(hashToken(presentedToken));
|
|
6508
|
+
return rec ? { subject: rec.subject, tenant: rec.tenant, role: rec.role } : void 0;
|
|
6509
|
+
}
|
|
6510
|
+
if (opts.sharedToken) {
|
|
6511
|
+
if (!presentedToken || !timingSafeEqual(presentedToken, opts.sharedToken)) return void 0;
|
|
6512
|
+
return { subject: "shared-token", tenant, role: "admin" };
|
|
6513
|
+
}
|
|
6514
|
+
if (opts.required) return void 0;
|
|
6515
|
+
return { subject: "anonymous", tenant, role: "admin" };
|
|
6516
|
+
}
|
|
6517
|
+
|
|
6182
6518
|
// src/mcp/transports.ts
|
|
6519
|
+
function samePrincipal(a, b) {
|
|
6520
|
+
return a.subject === b.subject && a.tenant === b.tenant && a.role === b.role;
|
|
6521
|
+
}
|
|
6183
6522
|
async function runStdio(server) {
|
|
6184
6523
|
const transport = new import_stdio.StdioServerTransport();
|
|
6185
6524
|
await server.connect(transport);
|
|
@@ -6224,6 +6563,14 @@ async function runHttp(factory, opts = {}) {
|
|
|
6224
6563
|
assertSafeBind({ host: host2, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
|
|
6225
6564
|
const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host2, port);
|
|
6226
6565
|
const token = opts.token;
|
|
6566
|
+
const authStore = opts.auth?.store;
|
|
6567
|
+
const defaultTenant = opts.defaultTenant;
|
|
6568
|
+
const resolveAuth = (header) => resolvePrincipal(bearerToken(header), {
|
|
6569
|
+
...authStore ? { store: authStore } : {},
|
|
6570
|
+
...token ? { sharedToken: token } : {},
|
|
6571
|
+
...defaultTenant ? { defaultTenant } : {},
|
|
6572
|
+
...opts.auth?.required ? { required: true } : {}
|
|
6573
|
+
});
|
|
6227
6574
|
const transports = /* @__PURE__ */ new Map();
|
|
6228
6575
|
const httpServer = import_node_http.default.createServer(async (req, res) => {
|
|
6229
6576
|
try {
|
|
@@ -6233,7 +6580,8 @@ async function runHttp(factory, opts = {}) {
|
|
|
6233
6580
|
res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
|
|
6234
6581
|
return;
|
|
6235
6582
|
}
|
|
6236
|
-
|
|
6583
|
+
const principal = resolveAuth(req.headers["authorization"]);
|
|
6584
|
+
if (!principal) {
|
|
6237
6585
|
res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
|
|
6238
6586
|
return;
|
|
6239
6587
|
}
|
|
@@ -6260,8 +6608,12 @@ async function runHttp(factory, opts = {}) {
|
|
|
6260
6608
|
const sessionId = req.headers["mcp-session-id"];
|
|
6261
6609
|
const existing = sessionId ? transports.get(sessionId) : void 0;
|
|
6262
6610
|
if (existing) {
|
|
6611
|
+
if (!samePrincipal(existing.principal, principal)) {
|
|
6612
|
+
res.writeHead(403, { "content-type": "application/json" }).end('{"error":"session belongs to a different principal"}');
|
|
6613
|
+
return;
|
|
6614
|
+
}
|
|
6263
6615
|
const body2 = req.method === "POST" ? await readJsonBody(req) : void 0;
|
|
6264
|
-
await existing.handleRequest(req, res, body2);
|
|
6616
|
+
await existing.transport.handleRequest(req, res, body2);
|
|
6265
6617
|
return;
|
|
6266
6618
|
}
|
|
6267
6619
|
if (req.method !== "POST") {
|
|
@@ -6270,18 +6622,18 @@ async function runHttp(factory, opts = {}) {
|
|
|
6270
6622
|
}
|
|
6271
6623
|
const body = await readJsonBody(req);
|
|
6272
6624
|
const transport = new import_streamableHttp.StreamableHTTPServerTransport({
|
|
6273
|
-
sessionIdGenerator: () => (0,
|
|
6625
|
+
sessionIdGenerator: () => (0, import_node_crypto6.randomUUID)(),
|
|
6274
6626
|
enableDnsRebindingProtection: true,
|
|
6275
6627
|
allowedHosts,
|
|
6276
6628
|
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
6277
6629
|
onsessioninitialized: (id) => {
|
|
6278
|
-
transports.set(id, transport);
|
|
6630
|
+
transports.set(id, { transport, principal });
|
|
6279
6631
|
}
|
|
6280
6632
|
});
|
|
6281
6633
|
transport.onclose = () => {
|
|
6282
6634
|
if (transport.sessionId) transports.delete(transport.sessionId);
|
|
6283
6635
|
};
|
|
6284
|
-
await factory().connect(transport);
|
|
6636
|
+
await factory(principal).connect(transport);
|
|
6285
6637
|
await transport.handleRequest(req, res, body);
|
|
6286
6638
|
} catch (err) {
|
|
6287
6639
|
process.stderr.write(`[cartography-mcp] HTTP request failed: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -7908,6 +8260,8 @@ async function runApi(opts) {
|
|
|
7908
8260
|
const token = opts.token;
|
|
7909
8261
|
const graphqlEnabled = opts.graphql !== false;
|
|
7910
8262
|
const defaultTenant = opts.tenant?.defaultTenant ?? DEFAULT_TENANT;
|
|
8263
|
+
const authStore = opts.auth?.store;
|
|
8264
|
+
const rbacMode = !!(authStore && authStore.count() > 0);
|
|
7911
8265
|
const log2 = opts.log ?? (() => {
|
|
7912
8266
|
});
|
|
7913
8267
|
const restDeps = { backend: opts.backend, version: opts.version };
|
|
@@ -7966,23 +8320,44 @@ async function runApi(opts) {
|
|
|
7966
8320
|
finish(r.status);
|
|
7967
8321
|
return;
|
|
7968
8322
|
}
|
|
7969
|
-
|
|
8323
|
+
const principal = resolvePrincipal(bearerToken(req.headers["authorization"]), {
|
|
8324
|
+
...authStore ? { store: authStore } : {},
|
|
8325
|
+
...token ? { sharedToken: token } : {},
|
|
8326
|
+
defaultTenant,
|
|
8327
|
+
...opts.auth?.required ? { required: true } : {}
|
|
8328
|
+
});
|
|
8329
|
+
if (!principal) {
|
|
7970
8330
|
send(res, 401, { error: "unauthorized" }, { "www-authenticate": "Bearer", ...cors });
|
|
7971
8331
|
finish(401);
|
|
7972
8332
|
return;
|
|
7973
8333
|
}
|
|
7974
|
-
let ctx;
|
|
7975
8334
|
try {
|
|
7976
|
-
|
|
7977
|
-
tenantLabel = ctx.tenant;
|
|
8335
|
+
authorize(principal, "read");
|
|
7978
8336
|
} catch (err) {
|
|
7979
|
-
if (err instanceof
|
|
7980
|
-
send(res,
|
|
7981
|
-
finish(
|
|
8337
|
+
if (err instanceof AuthorizationError) {
|
|
8338
|
+
send(res, 403, { error: "forbidden" }, cors);
|
|
8339
|
+
finish(403);
|
|
7982
8340
|
return;
|
|
7983
8341
|
}
|
|
7984
8342
|
throw err;
|
|
7985
8343
|
}
|
|
8344
|
+
let ctx;
|
|
8345
|
+
if (rbacMode) {
|
|
8346
|
+
ctx = { tenant: principal.tenant };
|
|
8347
|
+
tenantLabel = principal.tenant;
|
|
8348
|
+
} else {
|
|
8349
|
+
try {
|
|
8350
|
+
ctx = resolveTenant(req, url, opts.tenant ?? {});
|
|
8351
|
+
tenantLabel = ctx.tenant;
|
|
8352
|
+
} catch (err) {
|
|
8353
|
+
if (err instanceof InvalidTenantError) {
|
|
8354
|
+
send(res, 400, { error: "invalid tenant" }, cors);
|
|
8355
|
+
finish(400);
|
|
8356
|
+
return;
|
|
8357
|
+
}
|
|
8358
|
+
throw err;
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
7986
8361
|
if (graphqlEnabled && path === "/graphql") {
|
|
7987
8362
|
if (req.method === "GET") {
|
|
7988
8363
|
const g = handleGraphqlGet();
|
|
@@ -8052,6 +8427,30 @@ function dispatchRest(ctx, path, url, deps) {
|
|
|
8052
8427
|
}
|
|
8053
8428
|
}
|
|
8054
8429
|
|
|
8430
|
+
// src/auth/types.ts
|
|
8431
|
+
var import_zod9 = require("zod");
|
|
8432
|
+
var ROLES = ["viewer", "operator", "admin"];
|
|
8433
|
+
var RoleSchema = import_zod9.z.enum(ROLES);
|
|
8434
|
+
var ACTIONS = ["read", "discovery", "admin"];
|
|
8435
|
+
var ActionSchema = import_zod9.z.enum(ACTIONS);
|
|
8436
|
+
var PrincipalSchema = import_zod9.z.object({
|
|
8437
|
+
subject: import_zod9.z.string().min(1),
|
|
8438
|
+
tenant: import_zod9.z.string().min(1),
|
|
8439
|
+
role: RoleSchema
|
|
8440
|
+
});
|
|
8441
|
+
var CredentialConfigSchema = import_zod9.z.object({
|
|
8442
|
+
token: import_zod9.z.string().min(1),
|
|
8443
|
+
subject: import_zod9.z.string().min(1),
|
|
8444
|
+
tenant: import_zod9.z.string().optional(),
|
|
8445
|
+
role: RoleSchema.default("viewer")
|
|
8446
|
+
});
|
|
8447
|
+
var AuthConfigSchema = import_zod9.z.object({
|
|
8448
|
+
/** Seed credentials (merged into the SQLite store on startup). */
|
|
8449
|
+
credentials: import_zod9.z.array(CredentialConfigSchema).optional(),
|
|
8450
|
+
/** Reject unauthenticated requests even on loopback (default: loopback dev stays open). */
|
|
8451
|
+
required: import_zod9.z.boolean().optional()
|
|
8452
|
+
});
|
|
8453
|
+
|
|
8055
8454
|
// src/api/start.ts
|
|
8056
8455
|
var import_node_fs5 = require("fs");
|
|
8057
8456
|
var import_node_path5 = require("path");
|
|
@@ -8079,6 +8478,7 @@ function parseApiArgs(argv) {
|
|
|
8079
8478
|
else if (a === "--db") opts.dbPath = argv[++i];
|
|
8080
8479
|
else if (a === "--session") opts.session = argv[++i];
|
|
8081
8480
|
else if (a === "--tenant" || a === "--org") opts.tenant = argv[++i];
|
|
8481
|
+
else if (a === "--auth-required") opts.authRequired = true;
|
|
8082
8482
|
else if (a === "--help" || a === "-h") opts.help = true;
|
|
8083
8483
|
}
|
|
8084
8484
|
return opts;
|
|
@@ -8094,11 +8494,13 @@ async function startApi(opts = {}) {
|
|
|
8094
8494
|
const host2 = opts.host ?? "127.0.0.1";
|
|
8095
8495
|
const port = opts.port ?? 3737;
|
|
8096
8496
|
const version = readVersion();
|
|
8497
|
+
const authStore = new SqliteCredentialStore(db);
|
|
8097
8498
|
const server = await runApi({
|
|
8098
8499
|
host: host2,
|
|
8099
8500
|
port,
|
|
8100
8501
|
backend,
|
|
8101
8502
|
version,
|
|
8503
|
+
auth: { store: authStore, ...opts.authRequired ? { required: true } : {} },
|
|
8102
8504
|
...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
|
|
8103
8505
|
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
8104
8506
|
...token ? { token } : {},
|
|
@@ -8593,13 +8995,13 @@ function createClaudeProvider() {
|
|
|
8593
8995
|
}
|
|
8594
8996
|
|
|
8595
8997
|
// src/providers/shell.ts
|
|
8596
|
-
var
|
|
8998
|
+
var import_zod10 = require("zod");
|
|
8597
8999
|
function createBashTool() {
|
|
8598
9000
|
const shell = IS_WIN ? "powershell" : "posix";
|
|
8599
9001
|
return {
|
|
8600
9002
|
name: "Bash",
|
|
8601
9003
|
description: "Run a read-only shell command (inspect ports, processes, config). Mutating or destructive commands are blocked by the read-only allowlist.",
|
|
8602
|
-
inputShape: { command:
|
|
9004
|
+
inputShape: { command: import_zod10.z.string().describe("The read-only shell command to run") },
|
|
8603
9005
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
8604
9006
|
handler: async (args) => {
|
|
8605
9007
|
const command = String(args["command"] ?? "").trim();
|
|
@@ -11111,9 +11513,9 @@ async function runOnce(cfg, db) {
|
|
|
11111
11513
|
}
|
|
11112
11514
|
|
|
11113
11515
|
// src/sync/hash.ts
|
|
11114
|
-
var
|
|
11516
|
+
var import_node_crypto7 = require("crypto");
|
|
11115
11517
|
function shareHash(kind, payload) {
|
|
11116
|
-
return (0,
|
|
11518
|
+
return (0, import_node_crypto7.createHash)("sha256").update(stableStringify({ kind, payload })).digest("hex");
|
|
11117
11519
|
}
|
|
11118
11520
|
|
|
11119
11521
|
// src/sync/classify.ts
|
|
@@ -11157,7 +11559,7 @@ function classify2(input) {
|
|
|
11157
11559
|
}
|
|
11158
11560
|
|
|
11159
11561
|
// src/sync/push.ts
|
|
11160
|
-
var
|
|
11562
|
+
var import_node_crypto8 = require("crypto");
|
|
11161
11563
|
var PUSH_SCHEMA_VERSION = 1;
|
|
11162
11564
|
var DEFAULT_BATCH = 100;
|
|
11163
11565
|
var DEFAULT_RETRIES = 4;
|
|
@@ -11171,7 +11573,7 @@ function defaultSleep(ms) {
|
|
|
11171
11573
|
}
|
|
11172
11574
|
function batchKey(items) {
|
|
11173
11575
|
const hashes = items.map((i) => i.contentHash).sort();
|
|
11174
|
-
return (0,
|
|
11576
|
+
return (0, import_node_crypto8.createHash)("sha256").update(stableStringify(hashes)).digest("hex");
|
|
11175
11577
|
}
|
|
11176
11578
|
async function pushDeltas(config, items, opts = {}) {
|
|
11177
11579
|
const central = config.centralDb;
|
|
@@ -11377,6 +11779,10 @@ function checkClaudePrerequisites() {
|
|
|
11377
11779
|
}
|
|
11378
11780
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11379
11781
|
0 && (module.exports = {
|
|
11782
|
+
ACTIONS,
|
|
11783
|
+
ActionSchema,
|
|
11784
|
+
AuthConfigSchema,
|
|
11785
|
+
AuthorizationError,
|
|
11380
11786
|
CLIENTS,
|
|
11381
11787
|
CONFIDENCE,
|
|
11382
11788
|
CartographyDB,
|
|
@@ -11385,6 +11791,7 @@ function checkClaudePrerequisites() {
|
|
|
11385
11791
|
ConditionSchema,
|
|
11386
11792
|
ConfigError,
|
|
11387
11793
|
ControlResultSchema,
|
|
11794
|
+
CredentialConfigSchema,
|
|
11388
11795
|
CsvCostSource,
|
|
11389
11796
|
DEFAULT_ANOMALY_THRESHOLDS,
|
|
11390
11797
|
DEFAULT_SERVER_NAME,
|
|
@@ -11393,16 +11800,22 @@ function checkClaudePrerequisites() {
|
|
|
11393
11800
|
INGEST_SCHEMA_VERSION,
|
|
11394
11801
|
IngestEnvelopeSchema,
|
|
11395
11802
|
InvalidTenantError,
|
|
11803
|
+
JiraSink,
|
|
11396
11804
|
LOOPBACK_HOSTS,
|
|
11397
11805
|
MCP_BIN,
|
|
11398
11806
|
NotFoundError,
|
|
11399
11807
|
PACKAGE_NAME,
|
|
11808
|
+
PAGERDUTY_ENQUEUE_URL,
|
|
11400
11809
|
PERSONAL,
|
|
11401
11810
|
PORT_MAP,
|
|
11402
11811
|
PRIVATE_IP,
|
|
11403
11812
|
PUSH_SCHEMA_VERSION,
|
|
11813
|
+
PagerDutySink,
|
|
11814
|
+
PrincipalSchema,
|
|
11404
11815
|
ProviderRegistry,
|
|
11405
11816
|
RELATION_TO_DIRECTION,
|
|
11817
|
+
ROLES,
|
|
11818
|
+
RoleSchema,
|
|
11406
11819
|
RuleCheckSchema,
|
|
11407
11820
|
RulesetSchema,
|
|
11408
11821
|
SCAN_ARG_PATTERNS,
|
|
@@ -11412,10 +11825,13 @@ function checkClaudePrerequisites() {
|
|
|
11412
11825
|
ScannerRegistry,
|
|
11413
11826
|
ScannerShape,
|
|
11414
11827
|
SharingLevelSchema,
|
|
11828
|
+
SlackSink,
|
|
11829
|
+
SqliteCredentialStore,
|
|
11415
11830
|
SqliteQueryBackend,
|
|
11416
11831
|
SqliteStoreBackend,
|
|
11417
11832
|
StdoutSink,
|
|
11418
11833
|
TENANT_HEADER,
|
|
11834
|
+
TenantMismatchError,
|
|
11419
11835
|
VectorStore,
|
|
11420
11836
|
WebhookSink,
|
|
11421
11837
|
applyInstall,
|
|
@@ -11423,7 +11839,9 @@ function checkClaudePrerequisites() {
|
|
|
11423
11839
|
assertReadOnly,
|
|
11424
11840
|
assertSafeBind,
|
|
11425
11841
|
assertSafeScanArg,
|
|
11842
|
+
assertSameTenant,
|
|
11426
11843
|
assignColors,
|
|
11844
|
+
authorize,
|
|
11427
11845
|
bearerToken,
|
|
11428
11846
|
bookmarksScanner,
|
|
11429
11847
|
buildCartographyToolHandlers,
|
|
@@ -11431,6 +11849,7 @@ function checkClaudePrerequisites() {
|
|
|
11431
11849
|
buildOpenApiDocument,
|
|
11432
11850
|
buildReport,
|
|
11433
11851
|
buildSinks,
|
|
11852
|
+
can,
|
|
11434
11853
|
centralDbFromEnv,
|
|
11435
11854
|
checkBearer,
|
|
11436
11855
|
checkPrerequisites,
|
|
@@ -11496,6 +11915,9 @@ function checkClaudePrerequisites() {
|
|
|
11496
11915
|
filterBySeverity,
|
|
11497
11916
|
findAnonViolations,
|
|
11498
11917
|
formatComplianceText,
|
|
11918
|
+
formatJira,
|
|
11919
|
+
formatPagerDuty,
|
|
11920
|
+
formatSlack,
|
|
11499
11921
|
generateDependencyMermaid,
|
|
11500
11922
|
generateDiffMermaid,
|
|
11501
11923
|
generateTopologyMermaid,
|
|
@@ -11504,6 +11926,7 @@ function checkClaudePrerequisites() {
|
|
|
11504
11926
|
globalId,
|
|
11505
11927
|
groupByDomain,
|
|
11506
11928
|
handleGraphqlGet,
|
|
11929
|
+
hashToken,
|
|
11507
11930
|
hexCorners,
|
|
11508
11931
|
hexDistance,
|
|
11509
11932
|
hexNeighbors,
|
|
@@ -11518,6 +11941,7 @@ function checkClaudePrerequisites() {
|
|
|
11518
11941
|
isPersonalHost,
|
|
11519
11942
|
isReadOnlyCommand,
|
|
11520
11943
|
isRemembered,
|
|
11944
|
+
isSecureWebhookUrl,
|
|
11521
11945
|
k8sScanner,
|
|
11522
11946
|
keyMetaOf,
|
|
11523
11947
|
layoutClusters,
|
|
@@ -11556,6 +11980,7 @@ function checkClaudePrerequisites() {
|
|
|
11556
11980
|
pixelToHex,
|
|
11557
11981
|
planInstall,
|
|
11558
11982
|
portsScanner,
|
|
11983
|
+
postJson,
|
|
11559
11984
|
previewShare,
|
|
11560
11985
|
pseudonymize,
|
|
11561
11986
|
pseudonymizeFragment,
|
|
@@ -11568,6 +11993,7 @@ function checkClaudePrerequisites() {
|
|
|
11568
11993
|
renderDiff,
|
|
11569
11994
|
resolveEffectiveLevel,
|
|
11570
11995
|
resolveNlQuery,
|
|
11996
|
+
resolvePrincipal,
|
|
11571
11997
|
resolveSharingLevel,
|
|
11572
11998
|
resolveTenant,
|
|
11573
11999
|
revalidateAnonymized,
|
|
@@ -11587,6 +12013,7 @@ function checkClaudePrerequisites() {
|
|
|
11587
12013
|
safetyHook,
|
|
11588
12014
|
sanitizeUntrusted,
|
|
11589
12015
|
sanitizeValue,
|
|
12016
|
+
scopeReads,
|
|
11590
12017
|
scoreTopology,
|
|
11591
12018
|
securityRelevantChange,
|
|
11592
12019
|
serializeConfig,
|