@datasynx/agentic-ai-cartography 2.10.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/dist/index.js CHANGED
@@ -4652,6 +4652,8 @@ function reversalKey(orgKey) {
4652
4652
  // src/anonymize.ts
4653
4653
  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;
4654
4654
  var HOSTNAME = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
4655
+ var BARE_INTERNAL_HOST = /^[a-z0-9]+(?:-[a-z0-9]+)+$|^[a-z]+\d+$|^\d+[a-z]+$/i;
4656
+ var ANON_TOKEN = /^anon:(?:host|user|path|ip):[a-z2-7]+$/;
4655
4657
  var POSIX_PATH = /(?:^|(?<=\s|=|:|"|'|\())(\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+)/g;
4656
4658
  var WIN_PATH = /\b[A-Za-z]:\\[A-Za-z0-9._\\-]+/g;
4657
4659
  var B32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
@@ -4706,8 +4708,18 @@ function pseudonymizeString(s, orgKey, db) {
4706
4708
  (_m, user, host2) => `${pseudonymizeFragment(user, "user", orgKey, db)}@${pseudonymizeFragment(host2, "host", orgKey, db)}`
4707
4709
  );
4708
4710
  out = out.replace(HOSTNAME, (m) => pseudonymizeFragment(m, "host", orgKey, db));
4711
+ const trimmed = out.trim();
4712
+ if (out === s && !ANON_TOKEN.test(trimmed) && BARE_INTERNAL_HOST.test(trimmed)) {
4713
+ out = pseudonymizeFragment(trimmed, "host", orgKey, db);
4714
+ }
4709
4715
  return out;
4710
4716
  }
4717
+ function pseudonymizeId(id, orgKey, db) {
4718
+ const segments = id.split(":");
4719
+ if (segments.length <= 1) return pseudonymizeString(id, orgKey, db);
4720
+ const [type, ...rest] = segments;
4721
+ return [type, ...rest.map((seg) => pseudonymizeString(seg, orgKey, db))].join(":");
4722
+ }
4711
4723
  function pseudonymize(value, orgKey, db) {
4712
4724
  if (typeof value === "string") return pseudonymizeString(value, orgKey, db);
4713
4725
  if (Array.isArray(value)) return value.map((v) => pseudonymize(v, orgKey, db));
@@ -4734,8 +4746,6 @@ var FQDN = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
4734
4746
  var POSIX_PATH2 = /(?:^|(?<=\s|=|:|"|'|\())(\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+)/g;
4735
4747
  var WIN_PATH2 = /\b[A-Za-z]:\\[A-Za-z0-9._\\-]+/g;
4736
4748
  var HOME_USER = /(?:\/home\/|\/Users\/|[A-Za-z]:\\Users\\)([A-Za-z0-9._-]+)/g;
4737
- var BARE_INTERNAL_HOST = /^[a-z0-9]+(?:-[a-z0-9]+)+$|^[a-z]+\d+$|^\d+[a-z]+$/i;
4738
- var ANON_TOKEN = /^anon:(?:host|user|path|ip):[a-z2-7]+$/;
4739
4749
  function violationsInString(s, path) {
4740
4750
  const out = [];
4741
4751
  const trimmed = s.trim();
@@ -5125,9 +5135,10 @@ function resolveEffectiveLevel(node, policy) {
5125
5135
  function applySharingLevel(node, level, orgKey, db) {
5126
5136
  if (level === "none") return null;
5127
5137
  if (level === "full") return { ...node, metadata: { ...node.metadata ?? {} }, tags: [...node.tags ?? []] };
5138
+ const { globalId: _g, contentHash: _h, ...rest } = node;
5128
5139
  return {
5129
- ...node,
5130
- id: pseudonymizeString(node.id, orgKey, db),
5140
+ ...rest,
5141
+ id: pseudonymizeId(node.id, orgKey, db),
5131
5142
  name: pseudonymizeString(node.name, orgKey, db),
5132
5143
  metadata: pseudonymize(node.metadata ?? {}, orgKey, db),
5133
5144
  tags: (node.tags ?? []).map((t) => pseudonymizeString(t, orgKey, db))
@@ -5979,7 +5990,7 @@ function correlateTopology(nodes, _edges = []) {
5979
5990
 
5980
5991
  // src/mcp/server.ts
5981
5992
  var SERVER_NAME = "cartography";
5982
- var SERVER_VERSION = "2.10.0";
5993
+ var SERVER_VERSION = "2.12.1";
5983
5994
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5984
5995
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5985
5996
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -7389,9 +7400,132 @@ var serviceConfigScanner = {
7389
7400
  }
7390
7401
  };
7391
7402
 
7403
+ // src/scanners/terraform.ts
7404
+ import { readFileSync as readFileSync4 } from "fs";
7405
+ var TYPE_RULES = [
7406
+ [/(db_instance|_rds|sql_database|sql_instance|database_instance|cosmosdb|dynamodb|spanner|bigtable|documentdb|redshift)/, "database_server"],
7407
+ [/(elasticache|_redis|memcached|memorystore)/, "cache_server"],
7408
+ [/(s3_bucket|storage_bucket|gcs_bucket|storage_account|_blob)/, "database"],
7409
+ [/(_sqs|_queue|servicebus_queue)/, "queue"],
7410
+ [/(_sns|_topic|pubsub_topic|servicebus_topic)/, "topic"],
7411
+ [/(kafka|_msk|event_hub|kinesis)/, "message_broker"],
7412
+ [/(_eks|_gke|_aks|kubernetes_cluster|container_cluster)/, "k8s_cluster"],
7413
+ [/(ecs_|_container|fargate)/, "container"],
7414
+ [/(lambda|cloud_function|cloudfunctions|function_app|cloud_run)/, "web_service"],
7415
+ [/(_lb$|load_balancer|_alb|_elb|application_gateway)/, "web_service"],
7416
+ [/(api_gateway|apigateway)/, "api_endpoint"],
7417
+ [/(_instance|virtual_machine|_vm$|compute_instance)/, "host"]
7418
+ ];
7419
+ function terraformTypeToNode(tfType) {
7420
+ const t = tfType.toLowerCase();
7421
+ for (const [re, nt] of TYPE_RULES) if (re.test(t)) return nt;
7422
+ return "unknown";
7423
+ }
7424
+ var IDENTITY_ATTRS = ["id", "arn", "region", "location", "instance_type", "engine", "machine_type"];
7425
+ var OWNER_TAGS = ["Owner", "owner", "Team", "team"];
7426
+ var SAFE_TAG_KEYS = /* @__PURE__ */ new Set(["Name", "name", "Owner", "owner", "Team", "team", "Env", "env", "Environment", "environment", "Service", "service", "Component", "component", "App", "app", "Project", "project", "Tier", "tier", "Role", "role"]);
7427
+ var SECRET_KEY = /pass|secret|token|key|pwd|cred|private/i;
7428
+ function attrTags(tags) {
7429
+ if (!tags || typeof tags !== "object") return [];
7430
+ return Object.entries(tags).filter(([k]) => SAFE_TAG_KEYS.has(k) && !SECRET_KEY.test(k)).map(([k, v]) => `${k}:${redactSecrets(String(v))}`);
7431
+ }
7432
+ function parseTerraformState(json2) {
7433
+ let state;
7434
+ try {
7435
+ state = JSON.parse(json2);
7436
+ } catch {
7437
+ return { nodes: [], edges: [] };
7438
+ }
7439
+ const resources = Array.isArray(state?.resources) ? state.resources : [];
7440
+ const nodes = [];
7441
+ const edges = [];
7442
+ const addrToId = /* @__PURE__ */ new Map();
7443
+ for (const raw of resources) {
7444
+ const r = raw;
7445
+ if (r.mode && r.mode !== "managed") continue;
7446
+ if (typeof r.type !== "string" || typeof r.name !== "string") continue;
7447
+ const address = `${r.type}.${r.name}`;
7448
+ const nt = terraformTypeToNode(r.type);
7449
+ const id = `${nt}:terraform:${address}`;
7450
+ if (addrToId.has(address)) continue;
7451
+ addrToId.set(address, id);
7452
+ const inst = Array.isArray(r.instances) ? r.instances[0] : void 0;
7453
+ const attrs = inst?.attributes ?? {};
7454
+ const identity = { source: "terraform", tfType: r.type };
7455
+ for (const k of IDENTITY_ATTRS) if (attrs[k] !== void 0) identity[k] = attrs[k];
7456
+ const owner = OWNER_TAGS.map((k) => attrs.tags?.[k]).find((v) => typeof v === "string");
7457
+ nodes.push({
7458
+ id,
7459
+ type: nt,
7460
+ name: address,
7461
+ discoveredVia: "terraform-state",
7462
+ confidence: 0.9,
7463
+ // IaC is authoritative declared intent.
7464
+ metadata: redactValue(identity),
7465
+ tags: attrTags(attrs.tags),
7466
+ ...owner ? { owner } : {}
7467
+ });
7468
+ }
7469
+ for (const raw of resources) {
7470
+ const r = raw;
7471
+ if (r.mode && r.mode !== "managed") continue;
7472
+ if (typeof r.type !== "string" || typeof r.name !== "string") continue;
7473
+ const srcId = addrToId.get(`${r.type}.${r.name}`);
7474
+ if (!srcId) continue;
7475
+ const inst = Array.isArray(r.instances) ? r.instances[0] : void 0;
7476
+ const deps = Array.isArray(inst?.dependencies) ? inst.dependencies : [];
7477
+ for (const dep of deps) {
7478
+ if (typeof dep !== "string") continue;
7479
+ const tgtId = addrToId.get(dep) ?? addrToId.get(dep.split("[")[0]);
7480
+ if (!tgtId || tgtId === srcId) continue;
7481
+ edges.push({ sourceId: srcId, targetId: tgtId, relationship: "depends_on", evidence: evidenceLine("config-declared", `terraform depends_on ${dep}`), confidence: 0.85 });
7482
+ }
7483
+ }
7484
+ return { nodes, edges };
7485
+ }
7486
+ function stateDirs() {
7487
+ return [".", "./terraform", "./infra", "./infrastructure", "./deploy", "./terraform/environments"];
7488
+ }
7489
+ function hintPath(hint) {
7490
+ if (!hint) return void 0;
7491
+ const m = /(?:^|[\s,])tfstate=([^\s,]+)/.exec(hint);
7492
+ return m ? m[1] : void 0;
7493
+ }
7494
+ function resolveStatePath(ctx) {
7495
+ const explicit = hintPath(ctx.hint);
7496
+ if (explicit) return explicit;
7497
+ const found = (ctx.findFiles ?? findFiles)(stateDirs(), ["*.tfstate"], 4, 20).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
7498
+ return found[0];
7499
+ }
7500
+ function readStateFile(path) {
7501
+ try {
7502
+ return readFileSync4(path, "utf8");
7503
+ } catch {
7504
+ return "";
7505
+ }
7506
+ }
7507
+ var terraformScanner = {
7508
+ id: "terraform-state",
7509
+ title: "Terraform state (IaC)",
7510
+ platforms: "all",
7511
+ // No shell commands — the state file is read directly via node:fs, so an
7512
+ // operator-supplied path can never inject a command (no `cat "${path}"` interpolation).
7513
+ detect(ctx) {
7514
+ return resolveStatePath(ctx) !== void 0;
7515
+ },
7516
+ async scan(ctx) {
7517
+ const path = resolveStatePath(ctx);
7518
+ if (!path) return { nodes: [], edges: [] };
7519
+ const json2 = (ctx.readFile ?? readStateFile)(path);
7520
+ if (!json2) return { nodes: [], edges: [] };
7521
+ const result = parseTerraformState(json2);
7522
+ return { ...result, report: `terraform-state: ${result.nodes.length} resources, ${result.edges.length} dependencies from ${path}` };
7523
+ }
7524
+ };
7525
+
7392
7526
  // src/scanners/registry.ts
7393
7527
  function defaultRegistry() {
7394
- return new ScannerRegistry().register(bookmarksScanner).register(installedAppsScanner).register(portsScanner).register(cloudAwsScanner).register(cloudGcpScanner).register(cloudAzureScanner).register(k8sScanner).register(databasesScanner).register(connectionsScanner).register(serviceConfigScanner);
7528
+ return new ScannerRegistry().register(bookmarksScanner).register(installedAppsScanner).register(portsScanner).register(cloudAwsScanner).register(cloudGcpScanner).register(cloudAzureScanner).register(k8sScanner).register(databasesScanner).register(connectionsScanner).register(serviceConfigScanner).register(terraformScanner);
7395
7529
  }
7396
7530
 
7397
7531
  // src/scanners/loader.ts
@@ -8759,13 +8893,13 @@ var AuthConfigSchema = z9.object({
8759
8893
  });
8760
8894
 
8761
8895
  // src/api/start.ts
8762
- import { readFileSync as readFileSync4 } from "fs";
8896
+ import { readFileSync as readFileSync5 } from "fs";
8763
8897
  import { dirname as dirname3, resolve } from "path";
8764
8898
  import { fileURLToPath } from "url";
8765
8899
  function readVersion() {
8766
8900
  try {
8767
8901
  const dir = import.meta.dirname ?? dirname3(fileURLToPath(import.meta.url));
8768
- return JSON.parse(readFileSync4(resolve(dir, "..", "package.json"), "utf-8")).version ?? "0.0.0";
8902
+ return JSON.parse(readFileSync5(resolve(dir, "..", "package.json"), "utf-8")).version ?? "0.0.0";
8769
8903
  } catch {
8770
8904
  return "0.0.0";
8771
8905
  }
@@ -8896,7 +9030,7 @@ function defaultServerEntry(opts = {}) {
8896
9030
  }
8897
9031
 
8898
9032
  // src/installer/install.ts
8899
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
9033
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
8900
9034
  import { dirname as dirname4 } from "path";
8901
9035
  import { homedir as homedir3 } from "os";
8902
9036
  function currentOs() {
@@ -8913,7 +9047,7 @@ function planInstall(spec, ctx, opts) {
8913
9047
  throw new Error(`${spec.label} does not support the "${ctx.scope}" scope.`);
8914
9048
  }
8915
9049
  const fileExists = existsSync4(path);
8916
- const before = fileExists ? readFileSync5(path, "utf8") : "";
9050
+ const before = fileExists ? readFileSync6(path, "utf8") : "";
8917
9051
  const existing = parseConfig(before, spec.format);
8918
9052
  const merged = spec.apply(existing, opts.serverName ?? DEFAULT_SERVER_NAME, opts.entry);
8919
9053
  const after = serializeConfig(merged, spec.format);
@@ -9778,8 +9912,65 @@ Use ask_user when you need context from the user.`;
9778
9912
  }
9779
9913
  }
9780
9914
 
9915
+ // src/k8s/operator.ts
9916
+ function k8sRegistry() {
9917
+ return new ScannerRegistry().register(k8sScanner);
9918
+ }
9919
+ function isInCluster(env = process.env) {
9920
+ return typeof env["KUBERNETES_SERVICE_HOST"] === "string" && env["KUBERNETES_SERVICE_HOST"].length > 0;
9921
+ }
9922
+ function pruneToRetention(db, keep, tenant) {
9923
+ const stale = db.getSessions(tenant).slice(Math.max(1, keep));
9924
+ for (const s of stale) db.deleteSession(s.id);
9925
+ return stale.length;
9926
+ }
9927
+ async function runOperatorCycle(db, config, opts = {}) {
9928
+ const sessionId = db.createSession("discover", config);
9929
+ const discover = opts.discover ?? ((d, s) => runLocalDiscovery(d, s, { registry: k8sRegistry() }));
9930
+ const res = await discover(db, sessionId);
9931
+ const sess = db.getSession(sessionId);
9932
+ if (sess && !sess.name) db.setSessionName(sessionId, deriveSessionName(db.getGraphSummary(sessionId), sess.startedAt));
9933
+ const driftFn = opts.drift ?? ((d, c) => runDrift(d, c));
9934
+ const drift = await driftFn(db, config);
9935
+ pruneToRetention(db, opts.retain ?? 10, normalizeTenant(config.organization));
9936
+ return { sessionId, nodes: res.nodes, edges: res.edges, drift };
9937
+ }
9938
+ async function runOperator(db, config, opts = {}) {
9939
+ const log2 = opts.log ?? ((m) => process.stderr.write(m + "\n"));
9940
+ const intervalMs = opts.intervalMs ?? 5 * 6e4;
9941
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve3) => {
9942
+ if (opts.signal?.aborted) {
9943
+ resolve3();
9944
+ return;
9945
+ }
9946
+ const t = setTimeout(() => {
9947
+ opts.signal?.removeEventListener?.("abort", onAbort);
9948
+ resolve3();
9949
+ }, ms);
9950
+ const onAbort = () => {
9951
+ clearTimeout(t);
9952
+ resolve3();
9953
+ };
9954
+ opts.signal?.addEventListener?.("abort", onAbort, { once: true });
9955
+ }));
9956
+ log2(`Cartograph Kubernetes operator (in-cluster: ${isInCluster()}, interval: ${Math.round(intervalMs / 1e3)}s${opts.once ? ", single pass" : ""})`);
9957
+ for (; ; ) {
9958
+ try {
9959
+ const c = await runOperatorCycle(db, config, opts);
9960
+ log2(
9961
+ `reconcile: session ${c.sessionId} \u2014 ${c.nodes} nodes, ${c.edges} edges` + (c.drift ? `, drift ${c.drift.severity} (${c.drift.items.length} change${c.drift.items.length === 1 ? "" : "s"})` : ", no drift")
9962
+ );
9963
+ } catch (err) {
9964
+ log2(`reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
9965
+ }
9966
+ if (opts.once || opts.signal?.aborted) return;
9967
+ await sleep(intervalMs);
9968
+ if (opts.signal?.aborted) return;
9969
+ }
9970
+ }
9971
+
9781
9972
  // src/cost.ts
9782
- import { readFileSync as readFileSync6 } from "fs";
9973
+ import { readFileSync as readFileSync7 } from "fs";
9783
9974
  import { resolve as resolve2 } from "path";
9784
9975
  function splitCsvLine(line) {
9785
9976
  const out = [];
@@ -9858,7 +10049,7 @@ var CsvCostSource = class {
9858
10049
  }
9859
10050
  id;
9860
10051
  async fetch() {
9861
- const text = readFileSync6(resolve2(this.opts.filePath), "utf-8");
10052
+ const text = readFileSync7(resolve2(this.opts.filePath), "utf-8");
9862
10053
  const records = parseCostCsv(text);
9863
10054
  const match = this.opts.match ?? "nodeId";
9864
10055
  const out = /* @__PURE__ */ new Map();
@@ -11623,7 +11814,7 @@ function formatComplianceText(report) {
11623
11814
  }
11624
11815
 
11625
11816
  // src/config.ts
11626
- import { readFileSync as readFileSync7 } from "fs";
11817
+ import { readFileSync as readFileSync8 } from "fs";
11627
11818
  var ConfigError = class extends Error {
11628
11819
  constructor(message) {
11629
11820
  super(message);
@@ -11648,7 +11839,7 @@ function loadConfig(path) {
11648
11839
  function readConfigFile(path) {
11649
11840
  let raw;
11650
11841
  try {
11651
- raw = readFileSync7(path, "utf-8");
11842
+ raw = readFileSync8(path, "utf-8");
11652
11843
  } catch (err) {
11653
11844
  throw new ConfigError(
11654
11845
  `Cannot read config file ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -12006,14 +12197,14 @@ function runSyncClassify(db, sessionId, config, opts = {}) {
12006
12197
 
12007
12198
  // src/preflight.ts
12008
12199
  import { execSync as execSync2 } from "child_process";
12009
- import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
12200
+ import { existsSync as existsSync5, readFileSync as readFileSync9 } from "fs";
12010
12201
  import { join as join6 } from "path";
12011
12202
  function isOAuthLoggedIn() {
12012
12203
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
12013
12204
  const credFile = join6(home, ".claude", ".credentials.json");
12014
12205
  if (!existsSync5(credFile)) return false;
12015
12206
  try {
12016
- const creds = JSON.parse(readFileSync8(credFile, "utf8"));
12207
+ const creds = JSON.parse(readFileSync9(credFile, "utf8"));
12017
12208
  const oauth = creds["claudeAiOauth"];
12018
12209
  return typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
12019
12210
  } catch {
@@ -12065,9 +12256,11 @@ function checkClaudePrerequisites() {
12065
12256
  }
12066
12257
  export {
12067
12258
  ACTIONS,
12259
+ ANON_TOKEN,
12068
12260
  ActionSchema,
12069
12261
  AuthConfigSchema,
12070
12262
  AuthorizationError,
12263
+ BARE_INTERNAL_HOST,
12071
12264
  CLIENTS,
12072
12265
  CONFIDENCE,
12073
12266
  CORRELATION_CONFIDENCE,
@@ -12231,11 +12424,13 @@ export {
12231
12424
  hostname,
12232
12425
  ingestEnvelope,
12233
12426
  installedAppsScanner,
12427
+ isInCluster,
12234
12428
  isLoopbackHost,
12235
12429
  isPersonalHost,
12236
12430
  isReadOnlyCommand,
12237
12431
  isRemembered,
12238
12432
  isSecureWebhookUrl,
12433
+ k8sRegistry,
12239
12434
  k8sScanner,
12240
12435
  keyMetaOf,
12241
12436
  layoutClusters,
@@ -12272,6 +12467,7 @@ export {
12272
12467
  parseNginxUpstreams,
12273
12468
  parseNlQuery,
12274
12469
  parseScanHint,
12470
+ parseTerraformState,
12275
12471
  pixelToHex,
12276
12472
  planInstall,
12277
12473
  portsScanner,
@@ -12279,6 +12475,7 @@ export {
12279
12475
  previewShare,
12280
12476
  pseudonymize,
12281
12477
  pseudonymizeFragment,
12478
+ pseudonymizeId,
12282
12479
  pseudonymizeString,
12283
12480
  pushDeltas,
12284
12481
  readConfigFile,
@@ -12301,6 +12498,8 @@ export {
12301
12498
  runHttp,
12302
12499
  runLocalDiscovery,
12303
12500
  runOnce,
12501
+ runOperator,
12502
+ runOperatorCycle,
12304
12503
  runStdio,
12305
12504
  runSyncClassify,
12306
12505
  safeEnv,
@@ -12321,6 +12520,8 @@ export {
12321
12520
  stableStringify,
12322
12521
  startApi,
12323
12522
  stripSensitive,
12523
+ terraformScanner,
12524
+ terraformTypeToNode,
12324
12525
  timingSafeEqual,
12325
12526
  toBackstageEntities,
12326
12527
  validateScanner,