@datasynx/agentic-ai-cartography 2.11.0 → 2.12.1

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/README.md CHANGED
@@ -42,6 +42,7 @@
42
42
  [What it does](#what-it-does) ·
43
43
  [Cross-platform](#cross-platform-support) ·
44
44
  [Features](#features) ·
45
+ [Platform & ecosystem](#platform--ecosystem) ·
45
46
  [CLI commands](#commands) ·
46
47
  [Architecture](#architecture) ·
47
48
  [Safety](#safety) ·
@@ -64,7 +65,7 @@ The topology is exposed with **progressive disclosure** so agents never blow the
64
65
  context window:
65
66
 
66
67
  - **Resources** (read-only context): `cartography://graph/summary` (low-token index — read first), `cartography://nodes/{id}`, `cartography://services`, `cartography://databases`, `cartography://dependencies/{id}`.
67
- - **Tools** (parameterized queries): `query_infrastructure`, `search_topology` (semantic), `get_dependencies` (recursive graph traversal), `list_services`, `get_node`, `get_summary`, `run_discovery`.
68
+ - **Tools** (parameterized queries): `query_infrastructure`, `search_topology` (semantic), `get_dependencies` (recursive graph traversal), `query_natural_language` (plain-English topology questions, LLM-free), `correlate_topology` (collapse the same resource across clouds/on-prem), `get_cost_summary`, `score_compliance`, `classify_drift`, `list_services`, `get_node`, `get_summary`, `run_discovery`.
68
69
  - **Prompts**: `audit-attack-surface`, `map-service-dependencies`, `onboard-to-system`.
69
70
 
70
71
  ### Quick start
@@ -257,11 +258,125 @@ Cartography runs natively on **Linux**, **macOS**, and **Windows** — no WSL re
257
258
  | **Database Discovery** | PostgreSQL, MySQL, MongoDB, Redis, SQLite file scan. Windows: `Get-Service` for DB engine detection |
258
259
  | **Cloud Scanning** | AWS (EC2/RDS/EKS/S3), GCP (Compute/GKE/Cloud Run), Azure (AKS/WebApps), Kubernetes |
259
260
  | **Human-in-the-Loop** | Chat with the agent mid-discovery: type `"hubspot windsurf"` to search for specific tools |
261
+ | **Terraform Import** | First-class `terraform-state` scanner — `*.tfstate` JSON → authoritative IaC nodes/edges, reconciled with observed reality (no `terraform` CLI, no extra credentials) |
262
+ | **Multi-Cloud Correlation** | Collapse the same logical resource discovered across AWS/GCP/Azure/on-prem into canonical entities + confidence-scored `same_as` links (`correlate_topology`, pure & deterministic) |
263
+ | **Intelligence Layer** | Cost attribution (FinOps rollups), compliance scoring (CIS/SOC2/ISO 27001 starters), anomaly detection (orphans / shadow IT), severity-classified drift |
260
264
  | **Export Formats** | Mermaid topology, D3.js interactive graph, Backstage YAML, JSON |
261
265
  | **Safety First** | Strict read-only **allowlist** (not a denylist): only known-safe commands run — shell-aware for POSIX *and* PowerShell, enforced at the command runner as defense-in-depth. 100% read-only |
262
266
 
263
267
  ---
264
268
 
269
+ ## Platform & Ecosystem
270
+
271
+ Beyond the MCP server, Cartography ships a **read-only platform** for teams and an
272
+ **ecosystem** of integrations. Everything below is opt-in, read-only by default, and never
273
+ phones home — the same locked constraints as the core.
274
+
275
+ ### REST / GraphQL API + web dashboard (Phase 4)
276
+
277
+ `cartography api` (and the `cartography-api` binary) exposes the topology over a **read-only**
278
+ HTTP API — REST under `/v1/...` with a published **OpenAPI 3.1** document, and **GraphQL** at
279
+ `/graphql` (SDL + introspection, no `Mutation` type). It reuses the MCP transport's constant-time
280
+ bearer auth + DNS-rebinding hardening and the same tenant-scoped query layer — no new runtime
281
+ dependency (Node's built-in `http`).
282
+
283
+ ```bash
284
+ cartography api # loopback dev — REST + /graphql + dashboard, no token
285
+ curl -s http://127.0.0.1:3737/v1/summary | jq .totals
286
+ # Exposed: a non-loopback bind REQUIRES both --allowed-hosts and --token
287
+ cartography api --host 0.0.0.0 --allowed-hosts cartograph.internal:3737 --token "$TOKEN"
288
+ cartography api --no-graphql # REST only
289
+ cartography api --no-dashboard # disable the / and /app web UI
290
+ ```
291
+
292
+ The same server hosts a self-contained **web dashboard** at `/` and `/app` — a live, interactive
293
+ Canvas topology view with node drill-down (no CDN, no build step, zero new dependency). It fetches
294
+ the live `/v1/*` API, so it inherits the API's auth + RBAC/tenant scoping for free.
295
+ See **[docs/how-to/api-server.md](docs/how-to/api-server.md)** and
296
+ **[docs/how-to/web-dashboard.md](docs/how-to/web-dashboard.md)**.
297
+
298
+ ### Role-based access control (Phase 4)
299
+
300
+ `cartography auth` layers **identity + roles** over the HTTP surfaces. A bearer token resolves to a
301
+ principal `{ subject, tenant, role }`; the server returns **401** for an unknown token, **403** for
302
+ an insufficient role, and **pins every read to the principal's tenant** — no cross-tenant read by
303
+ spoofing a header. Roles are `viewer` ⊂ `operator` (adds `run_discovery`) ⊂ `admin` (adds credential
304
+ admin). Fully backward-compatible: with no credentials configured, the server behaves exactly as
305
+ before. Tokens are stored **hashed** (sha256), printed once on creation.
306
+
307
+ ```bash
308
+ cartography auth add alice --role operator --tenant acme # prints the bearer token ONCE
309
+ cartography auth list # subjects/roles/tenants, never the token
310
+ cartography auth revoke alice
311
+ cartography api --auth-required # require auth even on loopback
312
+ ```
313
+
314
+ See **[docs/how-to/rbac.md](docs/how-to/rbac.md)**.
315
+
316
+ ### Central collector → production (Phase 4)
317
+
318
+ `cartography mcp --server-mode` runs the binary as a self-hostable **central collector** that pools
319
+ every employee's *consented* discovery into one org-wide topology — consent-first, never phones home.
320
+ It exposes an authenticated `POST /ingest` write route (the consent-gated push envelope, server-side
321
+ anonymization re-validation via `--anon-mode reject|strip`, per-org rate limit → `429 + Retry-After`)
322
+ and an org-wide merged `get_summary`. Public `GET /healthz` (liveness) / `GET /readyz` (readiness)
323
+ probes and a `deploy/` Docker bundle make it orchestrator-ready.
324
+
325
+ ```bash
326
+ export CARTOGRAPHY_CENTRAL_TOKEN=$(openssl rand -base64 32)
327
+ cartography mcp --server-mode --host 0.0.0.0 \
328
+ --allowed-hosts cartograph.internal:3737 --org acme --anon-mode reject
329
+ ```
330
+
331
+ For an org-wide store at **10K+ nodes**, opt into a **Neo4j/Memgraph graph-DB backend** for the
332
+ collector's merge + summary path (`neo4j-driver` is an optional dependency; if absent or unreachable
333
+ it **degrades to SQLite**, never fails):
334
+
335
+ ```bash
336
+ cartography mcp --server-mode --store-backend graph \
337
+ --graph-url bolt://graph.internal:7687 --graph-user neo4j --graph-password "$NEO4J_PASSWORD"
338
+ # or via env: CARTOGRAPHY_GRAPH_URL / _USER / _PASSWORD (kept out of process listings)
339
+ ```
340
+
341
+ See **[docs/how-to/self-host-collector.md](docs/how-to/self-host-collector.md)**.
342
+
343
+ ### Backstage live data source (Phase 4)
344
+
345
+ Cartography maps discovered infrastructure to **Backstage catalog entities** (`Component`/`API`/
346
+ `Resource` + `dependsOn` relations). Export a static `catalog-info.yaml` snapshot, or consume the
347
+ **live, tenant-scoped** endpoint `GET /v1/backstage/catalog` on the API server (re-mapped on every
348
+ request, RBAC-pinned). A reference `CartographEntityProvider` lives in
349
+ [`examples/backstage-plugin/`](examples/backstage-plugin/). See
350
+ **[docs/how-to/backstage.md](docs/how-to/backstage.md)**.
351
+
352
+ ### Drift alerting — Slack / PagerDuty / Jira (Phase 4)
353
+
354
+ `cartography drift` classifies topology drift into `info`/`warning`/`critical` and fans it out to
355
+ the **sinks** configured under the `drift` block of `cartography.config.json` — `stdout`, `slack`,
356
+ `pagerduty`, `jira`, or a generic `webhook`. With no config it prints one redacted JSON line and makes
357
+ **no outbound request**. Every sink is hardened identically (`https:`-or-loopback only, bounded
358
+ timeout, body always credential-redacted, only host:port logged, one failing sink never blocks the
359
+ others). See **[docs/how-to/drift-and-ci.md](docs/how-to/drift-and-ci.md)**.
360
+
361
+ ### Kubernetes operator (Phase 5)
362
+
363
+ `cartography operator` runs the **deterministic, LLM-free** discovery continuously **inside** a
364
+ cluster and reports drift between reconcile cycles — a "continuous CMDB for Kubernetes". It's a thin
365
+ reconcile loop (no CRD, no controller-runtime, no agent loop), read-only (`kubectl get …` via the
366
+ allowlist only). The `deploy/k8s/` manifests ship a **read-only** ServiceAccount + ClusterRole
367
+ (`get/list/watch` only — no write verbs).
368
+
369
+ ```bash
370
+ kubectl apply -f deploy/k8s/operator.yaml # in-cluster, single-replica Deployment
371
+ cartography operator --once # local dev against your kube-context
372
+ cartography operator --interval 300 # long-running reconcile loop
373
+ ```
374
+
375
+ See **[docs/how-to/k8s-operator.md](docs/how-to/k8s-operator.md)** and
376
+ **[docs/how-to/terraform-import.md](docs/how-to/terraform-import.md)**.
377
+
378
+ ---
379
+
265
380
  ## Requirements
266
381
 
267
382
  - **Node.js >= 20** (Linux, macOS, or Windows) — that's it for the MCP server and the
@@ -349,10 +464,31 @@ datasynx-cartography drift [base] [current] Severity-classified drift ale
349
464
  --webhook <url> Outbound webhook sink (opt-in; token via CARTOGRAPHY_DRIFT_TOKEN)
350
465
  datasynx-cartography bookmarks View all browser bookmarks
351
466
  datasynx-cartography seed [--file <path>] Manually add infrastructure nodes
467
+ datasynx-cartography cost --file <csv> Enrich nodes with owner/cost (FinOps)
468
+ datasynx-cartography compliance [session] --ruleset <name> Grade against baseline/cis/soc2/iso27001
469
+ datasynx-cartography consent <…> Per-employee sharing policy + anonymization
470
+ datasynx-cartography sync <status|review|push> Opt-in central-DB outbound pipeline
471
+ datasynx-cartography schedule --config <file> Recurring headless discovery + drift
472
+ datasynx-cartography prune [--older-than <days>] Prune old sessions / compact the audit trail
352
473
  datasynx-cartography doctor Check all requirements + cloud CLIs
353
474
  datasynx-cartography docs Full feature reference
354
475
  ```
355
476
 
477
+ ### Platform & Ecosystem
478
+
479
+ ```
480
+ datasynx-cartography mcp [--server-mode] [--http] MCP server / central collector (Phase 4)
481
+ datasynx-cartography api [--no-graphql] [--no-dashboard] REST + GraphQL + web dashboard (Phase 4)
482
+ --host <h> --port <n> --token <secret> --allowed-hosts <list> (non-loopback needs both)
483
+ --tenant <id> / --org <id> Tenant whose topology to serve
484
+ datasynx-cartography auth add <subject> --role <viewer|operator|admin> --tenant <id> RBAC (Phase 4)
485
+ datasynx-cartography auth list | revoke <subject>
486
+ datasynx-cartography operator [--once] [--interval <sec>] Kubernetes operator (Phase 5)
487
+ ```
488
+
489
+ > The `cartography-mcp` and `cartography-api` binaries start the MCP server and the API server
490
+ > directly (used by `server.json` / containers via `npx`).
491
+
356
492
  ---
357
493
 
358
494
  ## Output Files
@@ -380,7 +516,11 @@ datasynx-output/
380
516
 
381
517
  The **MCP server is the headline interface** — LLM-agnostic and the same SQLite graph
382
518
  underneath every entry point. Discovery (deterministic scanners or the optional Claude
383
- loop) writes the graph; any MCP host reads it.
519
+ loop) writes the graph; any MCP host reads it. The Phase 4 **platform** adds read-only
520
+ HTTP surfaces over that same graph — the REST/GraphQL API (`cartography api`), the web
521
+ dashboard, RBAC (`cartography auth`), the self-hostable central collector
522
+ (`mcp --server-mode`, with an optional Neo4j/Memgraph backend), and a live Backstage
523
+ data source — all sharing the MCP transport's bearer auth + tenant scoping.
384
524
 
385
525
  ```
386
526
  ┌──────────────────────────────────────────┐
@@ -888,201 +888,6 @@ function localDiscoveryFn(registry, plugins) {
888
888
  };
889
889
  }
890
890
 
891
- // src/compliance/rulesets/baseline.ts
892
- var baseline = RulesetSchema.parse({
893
- name: "baseline",
894
- version: "1.0.0",
895
- framework: "baseline",
896
- description: "Deterministic baseline hygiene controls scored against signals available today.",
897
- rules: [
898
- {
899
- id: "BASE-1",
900
- control: "BASE-1",
901
- framework: "baseline",
902
- severity: "medium",
903
- title: "Asset has an owner",
904
- rationale: "Every asset should have a clear owning team/person for accountability.",
905
- scope: {},
906
- check: { any: [
907
- { field: "owner", op: "present" },
908
- { field: "domain", op: "present" },
909
- { field: "tags", op: "matches", pattern: "owner_key" },
910
- { field: "metadataKeys", op: "matches", pattern: "owner_key" }
911
- ] }
912
- },
913
- {
914
- id: "BASE-2",
915
- control: "BASE-2",
916
- framework: "baseline",
917
- severity: "low",
918
- title: "Service/data asset is assigned a business domain",
919
- rationale: "Domain assignment enables blast-radius and ownership analysis.",
920
- scope: { groups: ["data", "web"] },
921
- check: { field: "domain", op: "present" }
922
- },
923
- {
924
- id: "BASE-3",
925
- control: "BASE-3",
926
- framework: "baseline",
927
- severity: "high",
928
- title: "Critical asset is discovered with adequate confidence",
929
- rationale: "Low-confidence critical assets indicate incomplete or unreliable discovery.",
930
- scope: { groups: ["data", "infra"] },
931
- check: { field: "confidence", op: "gte", value: 0.5 }
932
- },
933
- {
934
- id: "BASE-4",
935
- control: "BASE-4",
936
- framework: "baseline",
937
- severity: "critical",
938
- title: "No embedded credentials / plaintext DSN in metadata",
939
- rationale: "A connection string with embedded credentials is a direct secret-exposure risk.",
940
- scope: {},
941
- check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
942
- },
943
- {
944
- id: "BASE-5",
945
- control: "BASE-5",
946
- framework: "baseline",
947
- severity: "medium",
948
- title: "Data store carries an acceptable quality score",
949
- rationale: "Where a quality score exists, a low score flags an under-governed data store.",
950
- scope: { groups: ["data"] },
951
- applicableWhen: { field: "qualityScore", op: "present" },
952
- check: { field: "qualityScore", op: "gte", value: 50 }
953
- }
954
- ]
955
- });
956
-
957
- // src/compliance/rulesets/cis.ts
958
- var cis = RulesetSchema.parse({
959
- name: "cis",
960
- version: "0.1.0",
961
- framework: "CIS",
962
- description: "CIS starter subset \u2014 illustrative controls, not a certified benchmark.",
963
- rules: [
964
- {
965
- id: "CIS-1.1",
966
- control: "CIS-1.1",
967
- framework: "CIS",
968
- severity: "critical",
969
- title: "No plaintext credentials in service configuration",
970
- rationale: "CIS hardening forbids embedded secrets in config/metadata.",
971
- scope: {},
972
- check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
973
- },
974
- {
975
- id: "CIS-2.1",
976
- control: "CIS-2.1",
977
- framework: "CIS",
978
- severity: "high",
979
- title: "Data store is not publicly exposed",
980
- rationale: "Data stores should not bind to public/0.0.0.0 addresses.",
981
- scope: { groups: ["data"] },
982
- check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
983
- },
984
- {
985
- id: "CIS-3.1",
986
- control: "CIS-3.1",
987
- framework: "CIS",
988
- severity: "medium",
989
- title: "Asset inventory has an accountable owner",
990
- rationale: "CIS asset management requires an assigned owner.",
991
- scope: {},
992
- check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
993
- }
994
- ]
995
- });
996
-
997
- // src/compliance/rulesets/soc2.ts
998
- var soc2 = RulesetSchema.parse({
999
- name: "soc2",
1000
- version: "0.1.0",
1001
- framework: "SOC2",
1002
- description: "SOC 2 starter subset \u2014 illustrative controls, not a certified control set.",
1003
- rules: [
1004
- {
1005
- id: "CC6.1",
1006
- control: "CC6.1",
1007
- framework: "SOC2",
1008
- severity: "critical",
1009
- title: "Logical access \u2014 no embedded credentials",
1010
- rationale: "CC6.1 logical access controls preclude plaintext secrets in config.",
1011
- scope: {},
1012
- check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
1013
- },
1014
- {
1015
- id: "CC6.6",
1016
- control: "CC6.6",
1017
- framework: "SOC2",
1018
- severity: "high",
1019
- title: "Boundary protection \u2014 data stores not public",
1020
- rationale: "CC6.6 requires protection of system boundaries from external access.",
1021
- scope: { groups: ["data"] },
1022
- check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
1023
- },
1024
- {
1025
- id: "CC1.2",
1026
- control: "CC1.2",
1027
- framework: "SOC2",
1028
- severity: "medium",
1029
- title: "Accountability \u2014 asset has an owner",
1030
- rationale: "CC1.2 establishes accountability; assets need an assigned owner.",
1031
- scope: {},
1032
- check: { any: [{ field: "owner", op: "present" }, { field: "domain", op: "present" }] }
1033
- }
1034
- ]
1035
- });
1036
-
1037
- // src/compliance/rulesets/iso27001.ts
1038
- var iso27001 = RulesetSchema.parse({
1039
- name: "iso27001",
1040
- version: "0.1.0",
1041
- framework: "ISO27001",
1042
- description: "ISO/IEC 27001 Annex A starter subset \u2014 illustrative, not certified.",
1043
- rules: [
1044
- {
1045
- id: "A.8.1",
1046
- control: "A.8.1",
1047
- framework: "ISO27001",
1048
- severity: "medium",
1049
- title: "Inventory of assets \u2014 ownership assigned",
1050
- rationale: "A.8.1 requires an inventory of assets each with an identified owner.",
1051
- scope: {},
1052
- check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
1053
- },
1054
- {
1055
- id: "A.8.12",
1056
- control: "A.8.12",
1057
- framework: "ISO27001",
1058
- severity: "critical",
1059
- title: "Data leakage prevention \u2014 no embedded secrets",
1060
- rationale: "A.8.12 mandates measures against data leakage such as exposed credentials.",
1061
- scope: {},
1062
- check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
1063
- },
1064
- {
1065
- id: "A.8.20",
1066
- control: "A.8.20",
1067
- framework: "ISO27001",
1068
- severity: "high",
1069
- title: "Network security \u2014 data stores not publicly exposed",
1070
- rationale: "A.8.20 requires networks to be secured; data stores should not be public.",
1071
- scope: { groups: ["data"] },
1072
- check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
1073
- }
1074
- ]
1075
- });
1076
-
1077
- // src/compliance/rulesets/registry.ts
1078
- var RULESETS = { baseline, cis, soc2, iso27001 };
1079
- function getRuleset(name) {
1080
- return RULESETS[name];
1081
- }
1082
- function listRulesets() {
1083
- return Object.values(RULESETS).map((r) => ({ name: r.name, version: r.version, framework: r.framework, ruleCount: r.rules.length }));
1084
- }
1085
-
1086
891
  // src/sinks/stdout.ts
1087
892
  var StdoutSink = class {
1088
893
  name = "stdout";
@@ -1439,6 +1244,201 @@ async function runDrift(db, config, opts = {}) {
1439
1244
  return alert;
1440
1245
  }
1441
1246
 
1247
+ // src/compliance/rulesets/baseline.ts
1248
+ var baseline = RulesetSchema.parse({
1249
+ name: "baseline",
1250
+ version: "1.0.0",
1251
+ framework: "baseline",
1252
+ description: "Deterministic baseline hygiene controls scored against signals available today.",
1253
+ rules: [
1254
+ {
1255
+ id: "BASE-1",
1256
+ control: "BASE-1",
1257
+ framework: "baseline",
1258
+ severity: "medium",
1259
+ title: "Asset has an owner",
1260
+ rationale: "Every asset should have a clear owning team/person for accountability.",
1261
+ scope: {},
1262
+ check: { any: [
1263
+ { field: "owner", op: "present" },
1264
+ { field: "domain", op: "present" },
1265
+ { field: "tags", op: "matches", pattern: "owner_key" },
1266
+ { field: "metadataKeys", op: "matches", pattern: "owner_key" }
1267
+ ] }
1268
+ },
1269
+ {
1270
+ id: "BASE-2",
1271
+ control: "BASE-2",
1272
+ framework: "baseline",
1273
+ severity: "low",
1274
+ title: "Service/data asset is assigned a business domain",
1275
+ rationale: "Domain assignment enables blast-radius and ownership analysis.",
1276
+ scope: { groups: ["data", "web"] },
1277
+ check: { field: "domain", op: "present" }
1278
+ },
1279
+ {
1280
+ id: "BASE-3",
1281
+ control: "BASE-3",
1282
+ framework: "baseline",
1283
+ severity: "high",
1284
+ title: "Critical asset is discovered with adequate confidence",
1285
+ rationale: "Low-confidence critical assets indicate incomplete or unreliable discovery.",
1286
+ scope: { groups: ["data", "infra"] },
1287
+ check: { field: "confidence", op: "gte", value: 0.5 }
1288
+ },
1289
+ {
1290
+ id: "BASE-4",
1291
+ control: "BASE-4",
1292
+ framework: "baseline",
1293
+ severity: "critical",
1294
+ title: "No embedded credentials / plaintext DSN in metadata",
1295
+ rationale: "A connection string with embedded credentials is a direct secret-exposure risk.",
1296
+ scope: {},
1297
+ check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
1298
+ },
1299
+ {
1300
+ id: "BASE-5",
1301
+ control: "BASE-5",
1302
+ framework: "baseline",
1303
+ severity: "medium",
1304
+ title: "Data store carries an acceptable quality score",
1305
+ rationale: "Where a quality score exists, a low score flags an under-governed data store.",
1306
+ scope: { groups: ["data"] },
1307
+ applicableWhen: { field: "qualityScore", op: "present" },
1308
+ check: { field: "qualityScore", op: "gte", value: 50 }
1309
+ }
1310
+ ]
1311
+ });
1312
+
1313
+ // src/compliance/rulesets/cis.ts
1314
+ var cis = RulesetSchema.parse({
1315
+ name: "cis",
1316
+ version: "0.1.0",
1317
+ framework: "CIS",
1318
+ description: "CIS starter subset \u2014 illustrative controls, not a certified benchmark.",
1319
+ rules: [
1320
+ {
1321
+ id: "CIS-1.1",
1322
+ control: "CIS-1.1",
1323
+ framework: "CIS",
1324
+ severity: "critical",
1325
+ title: "No plaintext credentials in service configuration",
1326
+ rationale: "CIS hardening forbids embedded secrets in config/metadata.",
1327
+ scope: {},
1328
+ check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
1329
+ },
1330
+ {
1331
+ id: "CIS-2.1",
1332
+ control: "CIS-2.1",
1333
+ framework: "CIS",
1334
+ severity: "high",
1335
+ title: "Data store is not publicly exposed",
1336
+ rationale: "Data stores should not bind to public/0.0.0.0 addresses.",
1337
+ scope: { groups: ["data"] },
1338
+ check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
1339
+ },
1340
+ {
1341
+ id: "CIS-3.1",
1342
+ control: "CIS-3.1",
1343
+ framework: "CIS",
1344
+ severity: "medium",
1345
+ title: "Asset inventory has an accountable owner",
1346
+ rationale: "CIS asset management requires an assigned owner.",
1347
+ scope: {},
1348
+ check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
1349
+ }
1350
+ ]
1351
+ });
1352
+
1353
+ // src/compliance/rulesets/soc2.ts
1354
+ var soc2 = RulesetSchema.parse({
1355
+ name: "soc2",
1356
+ version: "0.1.0",
1357
+ framework: "SOC2",
1358
+ description: "SOC 2 starter subset \u2014 illustrative controls, not a certified control set.",
1359
+ rules: [
1360
+ {
1361
+ id: "CC6.1",
1362
+ control: "CC6.1",
1363
+ framework: "SOC2",
1364
+ severity: "critical",
1365
+ title: "Logical access \u2014 no embedded credentials",
1366
+ rationale: "CC6.1 logical access controls preclude plaintext secrets in config.",
1367
+ scope: {},
1368
+ check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
1369
+ },
1370
+ {
1371
+ id: "CC6.6",
1372
+ control: "CC6.6",
1373
+ framework: "SOC2",
1374
+ severity: "high",
1375
+ title: "Boundary protection \u2014 data stores not public",
1376
+ rationale: "CC6.6 requires protection of system boundaries from external access.",
1377
+ scope: { groups: ["data"] },
1378
+ check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
1379
+ },
1380
+ {
1381
+ id: "CC1.2",
1382
+ control: "CC1.2",
1383
+ framework: "SOC2",
1384
+ severity: "medium",
1385
+ title: "Accountability \u2014 asset has an owner",
1386
+ rationale: "CC1.2 establishes accountability; assets need an assigned owner.",
1387
+ scope: {},
1388
+ check: { any: [{ field: "owner", op: "present" }, { field: "domain", op: "present" }] }
1389
+ }
1390
+ ]
1391
+ });
1392
+
1393
+ // src/compliance/rulesets/iso27001.ts
1394
+ var iso27001 = RulesetSchema.parse({
1395
+ name: "iso27001",
1396
+ version: "0.1.0",
1397
+ framework: "ISO27001",
1398
+ description: "ISO/IEC 27001 Annex A starter subset \u2014 illustrative, not certified.",
1399
+ rules: [
1400
+ {
1401
+ id: "A.8.1",
1402
+ control: "A.8.1",
1403
+ framework: "ISO27001",
1404
+ severity: "medium",
1405
+ title: "Inventory of assets \u2014 ownership assigned",
1406
+ rationale: "A.8.1 requires an inventory of assets each with an identified owner.",
1407
+ scope: {},
1408
+ check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
1409
+ },
1410
+ {
1411
+ id: "A.8.12",
1412
+ control: "A.8.12",
1413
+ framework: "ISO27001",
1414
+ severity: "critical",
1415
+ title: "Data leakage prevention \u2014 no embedded secrets",
1416
+ rationale: "A.8.12 mandates measures against data leakage such as exposed credentials.",
1417
+ scope: {},
1418
+ check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
1419
+ },
1420
+ {
1421
+ id: "A.8.20",
1422
+ control: "A.8.20",
1423
+ framework: "ISO27001",
1424
+ severity: "high",
1425
+ title: "Network security \u2014 data stores not publicly exposed",
1426
+ rationale: "A.8.20 requires networks to be secured; data stores should not be public.",
1427
+ scope: { groups: ["data"] },
1428
+ check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
1429
+ }
1430
+ ]
1431
+ });
1432
+
1433
+ // src/compliance/rulesets/registry.ts
1434
+ var RULESETS = { baseline, cis, soc2, iso27001 };
1435
+ function getRuleset(name) {
1436
+ return RULESETS[name];
1437
+ }
1438
+ function listRulesets() {
1439
+ return Object.values(RULESETS).map((r) => ({ name: r.name, version: r.version, framework: r.framework, ruleCount: r.rules.length }));
1440
+ }
1441
+
1442
1442
  // src/orgkey.ts
1443
1443
  import { randomBytes, hkdfSync } from "crypto";
1444
1444
  import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, statSync } from "fs";
@@ -1494,6 +1494,8 @@ function reversalKey(orgKey) {
1494
1494
  import { createHmac, createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
1495
1495
  var PRIVATE_IP = /\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b/g;
1496
1496
  var HOSTNAME = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
1497
+ var BARE_INTERNAL_HOST = /^[a-z0-9]+(?:-[a-z0-9]+)+$|^[a-z]+\d+$|^\d+[a-z]+$/i;
1498
+ var ANON_TOKEN = /^anon:(?:host|user|path|ip):[a-z2-7]+$/;
1497
1499
  var POSIX_PATH = /(?:^|(?<=\s|=|:|"|'|\())(\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+)/g;
1498
1500
  var WIN_PATH = /\b[A-Za-z]:\\[A-Za-z0-9._\\-]+/g;
1499
1501
  var B32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
@@ -1548,8 +1550,18 @@ function pseudonymizeString(s, orgKey, db) {
1548
1550
  (_m, user, host) => `${pseudonymizeFragment(user, "user", orgKey, db)}@${pseudonymizeFragment(host, "host", orgKey, db)}`
1549
1551
  );
1550
1552
  out = out.replace(HOSTNAME, (m) => pseudonymizeFragment(m, "host", orgKey, db));
1553
+ const trimmed = out.trim();
1554
+ if (out === s && !ANON_TOKEN.test(trimmed) && BARE_INTERNAL_HOST.test(trimmed)) {
1555
+ out = pseudonymizeFragment(trimmed, "host", orgKey, db);
1556
+ }
1551
1557
  return out;
1552
1558
  }
1559
+ function pseudonymizeId(id, orgKey, db) {
1560
+ const segments = id.split(":");
1561
+ if (segments.length <= 1) return pseudonymizeString(id, orgKey, db);
1562
+ const [type, ...rest] = segments;
1563
+ return [type, ...rest.map((seg) => pseudonymizeString(seg, orgKey, db))].join(":");
1564
+ }
1553
1565
  function pseudonymize(value, orgKey, db) {
1554
1566
  if (typeof value === "string") return pseudonymizeString(value, orgKey, db);
1555
1567
  if (Array.isArray(value)) return value.map((v) => pseudonymize(v, orgKey, db));
@@ -1800,7 +1812,7 @@ function correlateTopology(nodes, _edges = []) {
1800
1812
 
1801
1813
  // src/mcp/server.ts
1802
1814
  var SERVER_NAME = "cartography";
1803
- var SERVER_VERSION = "2.11.0";
1815
+ var SERVER_VERSION = "2.12.1";
1804
1816
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
1805
1817
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
1806
1818
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -2812,8 +2824,6 @@ var FQDN = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
2812
2824
  var POSIX_PATH2 = /(?:^|(?<=\s|=|:|"|'|\())(\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+)/g;
2813
2825
  var WIN_PATH2 = /\b[A-Za-z]:\\[A-Za-z0-9._\\-]+/g;
2814
2826
  var HOME_USER = /(?:\/home\/|\/Users\/|[A-Za-z]:\\Users\\)([A-Za-z0-9._-]+)/g;
2815
- var BARE_INTERNAL_HOST = /^[a-z0-9]+(?:-[a-z0-9]+)+$|^[a-z]+\d+$|^\d+[a-z]+$/i;
2816
- var ANON_TOKEN = /^anon:(?:host|user|path|ip):[a-z2-7]+$/;
2817
2827
  function violationsInString(s, path) {
2818
2828
  const out = [];
2819
2829
  const trimmed = s.trim();
@@ -3139,17 +3149,19 @@ async function startMcp(opts = {}) {
3139
3149
  }
3140
3150
 
3141
3151
  export {
3152
+ ScannerRegistry,
3142
3153
  isPersonalHost,
3143
3154
  runLocalDiscovery,
3155
+ runDrift,
3144
3156
  getRuleset,
3145
3157
  listRulesets,
3146
- runDrift,
3147
3158
  loadOrgKey,
3148
3159
  rotateOrgKey,
3149
3160
  pseudonymizeString,
3161
+ pseudonymizeId,
3150
3162
  pseudonymize,
3151
3163
  reversePseudonym,
3152
3164
  parseMcpArgs,
3153
3165
  startMcp
3154
3166
  };
3155
- //# sourceMappingURL=chunk-FFUUNSWP.js.map
3167
+ //# sourceMappingURL=chunk-OIDAXUW5.js.map