nexarch 0.12.18 → 0.12.22

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.
@@ -6,6 +6,45 @@ import { parseTerraformStateBuffer, projectTerraformState, inferEnvironmentFromT
6
6
  import { loadIacCatalogue, describeCatalogue } from "../lib/iac-catalogue.js";
7
7
  import { discoverRootModules, environmentFromPath } from "../lib/terraform-detect.js";
8
8
  const ENTITY_BATCH = 50;
9
+ /**
10
+ * Terraform provider prefix -> the platform it provisions.
11
+ *
12
+ * Keyed off the resource type prefix rather than a provider block, because a
13
+ * prefix is available for every resource the projector already read and
14
+ * needs no second pass over the raw state. `resolveNames` are tried against
15
+ * the reference library first, so the platform entity stays canonical rather
16
+ * than a tenant inventing its own "AWS" every time; `fallbackRef`/`fallbackName`
17
+ * are what is used when that resolution is skipped (a dry run) or fails.
18
+ */
19
+ const PROVIDER_PLATFORMS = [
20
+ { prefix: "azurerm_", resolveNames: ["azure", "microsoft azure"], fallbackRef: "platform:azure", fallbackName: "Microsoft Azure" },
21
+ { prefix: "aws_", resolveNames: ["aws", "amazon web services"], fallbackRef: "platform:aws", fallbackName: "Amazon Web Services" },
22
+ { prefix: "google_", resolveNames: ["gcp", "google cloud platform"], fallbackRef: "platform:gcp", fallbackName: "Google Cloud Platform" },
23
+ { prefix: "google-beta_", resolveNames: ["gcp", "google cloud platform"], fallbackRef: "platform:gcp", fallbackName: "Google Cloud Platform" },
24
+ ];
25
+ /**
26
+ * Picks the platform an estate is provisioned on from the resource types
27
+ * actually seen — projected and unrecognised alike, since even a type the
28
+ * catalogue does not cover yet still carries its provider prefix. Ties and
29
+ * mixed-provider states resolve to whichever provider has the most
30
+ * resources: real multi-cloud-in-one-root estates are rare, and a single
31
+ * dominant platform is a far better default than reverting to a hardcoded
32
+ * one. A root module ingested with none of the providers above recognised
33
+ * gets no free-text guess — better to say nothing than invent a platform
34
+ * name from an unfamiliar prefix.
35
+ */
36
+ function detectPlatform(resourceTypes) {
37
+ const counts = new Map();
38
+ for (const resourceType of resourceTypes) {
39
+ const platform = PROVIDER_PLATFORMS.find((p) => resourceType.startsWith(p.prefix));
40
+ if (platform)
41
+ counts.set(platform.prefix, (counts.get(platform.prefix) ?? 0) + 1);
42
+ }
43
+ if (counts.size === 0)
44
+ return null;
45
+ const [topPrefix] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
46
+ return PROVIDER_PLATFORMS.find((p) => p.prefix === topPrefix) ?? null;
47
+ }
9
48
  function readResult(raw) {
10
49
  return JSON.parse(raw.content?.[0]?.text ?? "{}");
11
50
  }
@@ -174,7 +213,13 @@ export function buildIngestPayload(params) {
174
213
  attributes: { source: "terraform_ingest", terraform_version: projection.terraformVersion },
175
214
  },
176
215
  ];
177
- const relationships = [];
216
+ const relationships = [
217
+ // The environment lives on the platform. Without this edge the two nodes
218
+ // this payload always creates float beside each other, and no query can
219
+ // answer "which cloud is dev on" — every resource knows both, but the
220
+ // environment itself knows neither.
221
+ { relationshipTypeCode: "runs_on", fromEntityRef: environmentRef, toEntityRef: platformRef },
222
+ ];
178
223
  // Lookup tables for the topology pass below, populated alongside entities so
179
224
  // a single loop over projection.resources is enough for both.
180
225
  const entityRefByTypeAndName = new Map();
@@ -403,14 +448,21 @@ export async function ingestInfra(args) {
403
448
  }
404
449
  const { environment, how } = resolveEnvironment(projection, environmentOverride, workingDir, statePath ? null : workingDir);
405
450
  projection.environment = environment;
406
- // Azure is the only provider the seed catalogue covers today; resolving it
407
- // through the reference library keeps the platform entity canonical rather
408
- // than inventing a second "Azure" for every tenant.
409
- let platformRef = "platform:azure";
410
- let platformName = "Microsoft Azure";
411
- if (!dryRun) {
451
+ // Detected from what is actually in state, not assumed: an estate is read
452
+ // once and the provider it is on follows from that, the same way the
453
+ // catalogue itself covers whichever providers have entries rather than
454
+ // just Azure. Resolving the detected platform through the reference
455
+ // library keeps the entity canonical rather than inventing a second "AWS"
456
+ // for every tenant.
457
+ const detectedPlatform = detectPlatform([
458
+ ...projection.resources.map((r) => r.resourceType),
459
+ ...projection.unknownTypes.map((u) => u.resourceType),
460
+ ]);
461
+ let platformRef = detectedPlatform?.fallbackRef ?? "platform:unknown";
462
+ let platformName = detectedPlatform?.fallbackName ?? "Unrecognised cloud platform";
463
+ if (!dryRun && detectedPlatform) {
412
464
  try {
413
- const raw = await callMcpTool("nexarch_resolve_reference", { names: ["azure"] });
465
+ const raw = await callMcpTool("nexarch_resolve_reference", { names: detectedPlatform.resolveNames });
414
466
  const resolved = readResult(raw);
415
467
  const hit = (resolved.results ?? []).find((r) => r.resolved && r.entityTypeCode === "platform" && r.canonicalExternalRef);
416
468
  if (hit?.canonicalExternalRef) {
@@ -65,15 +65,17 @@ function printNextSteps(steps, registered) {
65
65
  */
66
66
  function reportDryRun(params) {
67
67
  const { asJson, projectRef, detection, entities, relationships } = params;
68
- const nextSteps = nextStepsFor(projectRef, detection);
68
+ const isLibrary = detection.repoKind === "module_library";
69
+ const nextSteps = isLibrary ? [] : nextStepsFor(projectRef, detection);
69
70
  if (asJson) {
70
71
  process.stdout.write(`${JSON.stringify({
71
72
  ok: true,
72
73
  dryRun: true,
73
74
  wrote: false,
74
75
  projectRef,
75
- registrationStatus: "skeleton_only",
76
- enrichmentCompleted: false,
76
+ repoKind: detection.repoKind,
77
+ registrationStatus: isLibrary ? "complete" : "skeleton_only",
78
+ enrichmentCompleted: isLibrary,
77
79
  detection,
78
80
  plan: { entities, relationships },
79
81
  nextSteps,
@@ -88,6 +90,20 @@ function reportDryRun(params) {
88
90
  for (const rel of relationships)
89
91
  console.log(` ${rel.fromEntityRef} --${rel.relationshipTypeCode}--> ${rel.toEntityRef}`);
90
92
  }
93
+ // A module library has no estate, so registration really is the whole job —
94
+ // and saying "run the commands above" when there are none is the same defect
95
+ // as the missing handoff, pointed the other way.
96
+ if (isLibrary) {
97
+ console.log(`\nThis repository publishes reusable modules; it does not provision an estate,`);
98
+ console.log(`so there is nothing to ingest and registration is the whole job.`);
99
+ if (detection.publishedModules.length > 0) {
100
+ console.log(`\n Modules published here:`);
101
+ for (const name of detection.publishedModules)
102
+ console.log(` ${name}`);
103
+ }
104
+ console.log(`\nNothing was written. Re-run without --dry-run to register.`);
105
+ return;
106
+ }
91
107
  // The dry run used to stop here, which made registration look like the whole
92
108
  // job: a reader saw four entities and no hint that seven ingest runs were the
93
109
  // actual work. A preview has to preview the handoff too.
@@ -116,6 +132,8 @@ export async function runInfrastructureOnboarding(options, detection) {
116
132
  entityRef: projectRef,
117
133
  entityTypeCode: "project",
118
134
  entitySubtypeCode: "project_infrastructure",
135
+ // Both shapes are infrastructure projects; repo_kind above carries which,
136
+ // rather than inventing a subtype the ontology does not define.
119
137
  name: displayName,
120
138
  description: `Infrastructure-as-code repository provisioning the estate (${detection.signals.join(", ")}).`,
121
139
  attributes: {
@@ -124,6 +142,8 @@ export async function runInfrastructureOnboarding(options, detection) {
124
142
  terraform_file_count: detection.terraformFileCount,
125
143
  has_remote_backend: detection.hasRemoteBackend,
126
144
  root_module_count: detection.rootModules.length,
145
+ repo_kind: detection.repoKind,
146
+ ...(detection.publishedModules.length > 0 ? { published_modules: detection.publishedModules.slice(0, 40) } : {}),
127
147
  ...(detection.ciSystem ? { ci_system: detection.ciSystem } : {}),
128
148
  ...(detection.moduleDirectories.length > 0 ? { terraform_modules: detection.moduleDirectories.slice(0, 25) } : {}),
129
149
  },
@@ -177,11 +197,23 @@ export async function runInfrastructureOnboarding(options, detection) {
177
197
  // the map up front — rather than letting the caller discover it through a
178
198
  // failure — is what lets an agent finish the job unaided.
179
199
  const roots = detection.rootModules;
180
- const nextSteps = nextStepsFor(projectRef, detection);
181
- if (!asJson && roots.length > 1)
200
+ const isLibrary = detection.repoKind === "module_library";
201
+ const nextSteps = isLibrary ? [] : nextStepsFor(projectRef, detection);
202
+ if (!asJson && !isLibrary && roots.length > 1)
182
203
  printNextSteps(nextSteps, true);
204
+ if (!asJson && isLibrary) {
205
+ console.log(`\nThis repository publishes reusable modules; it does not provision an estate.`);
206
+ console.log(`There is no state to read, so there is nothing to ingest — and that is the`);
207
+ console.log(`correct outcome rather than a gap. Its modules become architecture when a`);
208
+ console.log(`repository that *does* provision an estate calls them and is ingested.`);
209
+ if (detection.publishedModules.length > 0) {
210
+ console.log(`\n Modules published here:`);
211
+ for (const name of detection.publishedModules)
212
+ console.log(` ${name}`);
213
+ }
214
+ }
183
215
  let ingested = false;
184
- if (!skipIngest && roots.length <= 1) {
216
+ if (!skipIngest && !isLibrary && roots.length <= 1) {
185
217
  const shouldIngest = nonInteractive ? false : await confirm("\nRead the current state and populate the graph now?");
186
218
  if (shouldIngest) {
187
219
  try {
@@ -199,7 +231,7 @@ export async function runInfrastructureOnboarding(options, detection) {
199
231
  // caller's report honest: the numbers it needs to decide "am I finished?"
200
232
  // come from the command that did the work, not from a convention it half
201
233
  // remembers.
202
- const outstanding = ingested ? nextSteps.filter((step) => step.rootModule !== "." && roots.length > 1) : nextSteps;
234
+ const outstanding = isLibrary ? [] : ingested ? nextSteps.filter((step) => step.rootModule !== "." && roots.length > 1) : nextSteps;
203
235
  const { file, snippet } = ciSnippetFor(detection.ciSystem, detection.ciFile);
204
236
  if (asJson) {
205
237
  process.stdout.write(`${JSON.stringify({
@@ -214,11 +246,13 @@ export async function runInfrastructureOnboarding(options, detection) {
214
246
  }, null, 2)}\n`);
215
247
  return;
216
248
  }
217
- console.log(`\nInfrastructure changes continuously, so ingestion belongs in the pipeline, not in someone's memory.`);
218
- console.log(`Add this to ${file}:\n`);
219
- console.log(snippet);
220
- console.log(`\nNEXARCH_TOKEN is a service credential — create one in the workspace under`);
221
- console.log(`Discovery Agents, then store it as a masked CI variable.`);
249
+ if (!isLibrary) {
250
+ console.log(`\nInfrastructure changes continuously, so ingestion belongs in the pipeline, not in someone's memory.`);
251
+ console.log(`Add this to ${file}:\n`);
252
+ console.log(snippet);
253
+ console.log(`\nNEXARCH_TOKEN is a service credential create one in the workspace under`);
254
+ console.log(`Discovery → Agents, then store it as a masked CI variable.`);
255
+ }
222
256
  if (outstanding.length > 0) {
223
257
  console.log(`\nThis run registered the repository only. Until the ${outstanding.length} ingest command${outstanding.length === 1 ? "" : "s"} above`);
224
258
  console.log(`have run, the graph knows the repo exists but nothing it provisions.`);
@@ -1003,7 +1003,63 @@ export function scanProject(dir) {
1003
1003
  };
1004
1004
  }
1005
1005
  // ─── Relationship type selection ──────────────────────────────────────────────
1006
- function pickRelationshipType(toEntityTypeCode, toEntitySubtypeCode, _fromEntityTypeCode = "application") {
1006
+ /**
1007
+ * The relationship pairs this workspace's ontology actually permits.
1008
+ *
1009
+ * Populated once per run from the ingest contract. Empty means the contract
1010
+ * could not be read, in which case selection falls back to its preferences
1011
+ * unchecked — a scan that records slightly wrong relationships is worth more
1012
+ * than a scan that refuses to run because a read failed.
1013
+ */
1014
+ let allowedLinks = new Set();
1015
+ function linkKey(from, rel, to) {
1016
+ return `${from}|${rel}|${to}`;
1017
+ }
1018
+ export async function loadAllowedLinks(companyId) {
1019
+ try {
1020
+ const raw = await callMcpTool("nexarch_get_ingest_contract", { companyId }, { companyId });
1021
+ const parsed = parseToolText(raw);
1022
+ const links = parsed.ontology?.allowedLinks ?? [];
1023
+ allowedLinks = new Set(links.map((l) => linkKey(l.fromEntityTypeCode, l.relationshipTypeCode, l.toEntityTypeCode)));
1024
+ return allowedLinks.size;
1025
+ }
1026
+ catch {
1027
+ allowedLinks = new Set();
1028
+ return 0;
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Narrows a preferred relationship type to one the ontology accepts.
1033
+ *
1034
+ * The preference below encodes what a relationship *means*; this decides
1035
+ * whether the graph will take it from this particular source. Those came apart
1036
+ * in practice: a technology_component depending on a managed service was
1037
+ * offered `integrates_with`, which only accepts application-shaped sources, and
1038
+ * every such write was refused server-side. Refusals are silent unless someone
1039
+ * reads telemetry, so the scan looked clean and the edges simply were not there.
1040
+ *
1041
+ * Checking here rather than hoping also means an ontology change cannot quietly
1042
+ * invalidate a shipped CLI: the contract is read at run time, so the constraint
1043
+ * the gateway will apply is the constraint used to choose.
1044
+ */
1045
+ function conformRelationshipType(preferred, fromEntityTypeCode, toEntityTypeCode) {
1046
+ if (allowedLinks.size === 0)
1047
+ return preferred;
1048
+ if (allowedLinks.has(linkKey(fromEntityTypeCode, preferred, toEntityTypeCode)))
1049
+ return preferred;
1050
+ // depends_on is the broadest runtime statement and the gateway's own
1051
+ // suggestion when it rejects one of these; part_of covers containment when
1052
+ // even that is not permitted.
1053
+ for (const fallback of ["depends_on", "runs_on", "part_of"]) {
1054
+ if (fallback !== preferred && allowedLinks.has(linkKey(fromEntityTypeCode, fallback, toEntityTypeCode)))
1055
+ return fallback;
1056
+ }
1057
+ // Nothing legal connects these two. Returning the preference lets the write
1058
+ // be refused with the gateway's own explanation, which is more useful than a
1059
+ // silently dropped relationship.
1060
+ return preferred;
1061
+ }
1062
+ function pickRelationshipType(toEntityTypeCode, toEntitySubtypeCode, fromEntityTypeCode = "application") {
1007
1063
  switch (toEntityTypeCode) {
1008
1064
  case "model":
1009
1065
  return "uses_model";
@@ -1134,6 +1190,11 @@ export async function initProject(args) {
1134
1190
  return;
1135
1191
  }
1136
1192
  }
1193
+ // Read before anything is chosen: relationship selection below narrows its
1194
+ // preferences to what this workspace's ontology actually accepts, and an
1195
+ // empty set means every preference goes through unchecked.
1196
+ const linkCount = await loadAllowedLinks(creds.companyId);
1197
+ logProgress("contract.loaded", `allowedLinks=${linkCount}`);
1137
1198
  if (!asJson)
1138
1199
  console.log(`Scanning ${dir}…`);
1139
1200
  logProgress("scan.start", dir);
@@ -1511,7 +1572,7 @@ export async function initProject(args) {
1511
1572
  if (!rootDepNames.has(r.input) && !rootDepNames.has(r.normalised))
1512
1573
  continue;
1513
1574
  const depSpec = rootDepVersions.get(r.input) ?? rootDepVersions.get(r.normalised);
1514
- addRel(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode), projectExternalKey, r.canonicalExternalRef, 0.9, {
1575
+ addRel(conformRelationshipType(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode), entityTypeOverride, r.entityTypeCode), projectExternalKey, r.canonicalExternalRef, 0.9, {
1515
1576
  source: depSpec?.source ?? "manifest_scan",
1516
1577
  detected_at: nowIso,
1517
1578
  ...buildVersionAttributes(depSpec?.versionRaw ?? null, depSpec?.source ?? "manifest_scan"),
@@ -1541,7 +1602,7 @@ export async function initProject(args) {
1541
1602
  for (const dep of sp.depSpecs) {
1542
1603
  const r = resolvedByInput.get(dep.name);
1543
1604
  if (r?.canonicalExternalRef && r.entityTypeCode) {
1544
- addRel(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType), sp.externalKey, r.canonicalExternalRef, 0.9, {
1605
+ addRel(conformRelationshipType(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType), sp.entityType, r.entityTypeCode), sp.externalKey, r.canonicalExternalRef, 0.9, {
1545
1606
  source: dep.source,
1546
1607
  detected_at: nowIso,
1547
1608
  ...buildVersionAttributes(dep.versionRaw, dep.source),
@@ -1829,7 +1890,7 @@ export async function initProject(args) {
1829
1890
  .map((d) => resolvedByInput.get(d.name))
1830
1891
  .filter((r) => !!r?.canonicalExternalRef)
1831
1892
  .map((r) => {
1832
- const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType);
1893
+ const relationshipTypeCode = conformRelationshipType(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType), sp.entityType, r.entityTypeCode);
1833
1894
  const relationshipKey = `${relationshipTypeCode}::${sp.externalKey}::${r.canonicalExternalRef}`;
1834
1895
  return {
1835
1896
  canonicalExternalRef: r.canonicalExternalRef,
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import { proposalsStart } from "./commands/proposals-start.js";
28
28
  import { registerRuntime } from "./commands/register-runtime.js";
29
29
  import { ingestInfra } from "./commands/ingest-infra.js";
30
30
  import { verifyTrust } from "./commands/verify-trust.js";
31
+ import { cliBuild, formatBuild } from "./lib/version.js";
31
32
  const [, , command, ...args] = process.argv;
32
33
  const commands = {
33
34
  login,
@@ -75,6 +76,17 @@ async function main() {
75
76
  return;
76
77
  }
77
78
  }
79
+ // Answered before dispatch, and before any credential is required: "which
80
+ // copy am I running" has to work when nothing else does.
81
+ if (command === "--version" || command === "-v" || command === "version") {
82
+ const build = cliBuild();
83
+ if (args.includes("--json"))
84
+ process.stdout.write(`${JSON.stringify(build, null, 2)}
85
+ `);
86
+ else
87
+ console.log(formatBuild(build));
88
+ return;
89
+ }
78
90
  const handler = commands[command ?? ""];
79
91
  if (!handler) {
80
92
  console.log(`
@@ -83,6 +95,7 @@ nexarch — Your architecture workspace for AI delivery.
83
95
  Usage:
84
96
  nexarch login Authenticate in browser and store company-scoped credentials
85
97
  Option: --company <id>
98
+ nexarch --version Print the version and the path this copy runs from
86
99
  nexarch logout Remove stored credentials
87
100
  nexarch status Check connection and show architecture summary
88
101
  nexarch verify-trust Verify this repo's Nexarch trust attestation. Reads the token
@@ -12,9 +12,18 @@
12
12
  * ahead of it: editing it by hand recreates the two-sources-of-truth problem
13
13
  * moving the catalogue out of the CLI was meant to end.
14
14
  *
15
- * 30 entries, exported 2026-08-21.
15
+ * 86 entries, exported 2026-08-22.
16
16
  */
17
17
  export const IAC_CATALOGUE_SEED = [
18
+ {
19
+ resourceType: "aws_ecs_cluster",
20
+ entityTypeCode: "platform_component",
21
+ entitySubtypeCode: "platform_runtime",
22
+ role: "compute_environment",
23
+ attributes: ["name"],
24
+ canonicalEntityRef: "global:platform_component:aws_ecs",
25
+ cloudResourceType: "AWS::ECS::Cluster",
26
+ },
18
27
  {
19
28
  resourceType: "azurerm_container_app_environment",
20
29
  entityTypeCode: "platform_component",
@@ -34,6 +43,71 @@ export const IAC_CATALOGUE_SEED = [
34
43
  canonicalEntityRef: "global:platform_component:azure_app_service",
35
44
  cloudResourceType: "Microsoft.Web/serverfarms",
36
45
  },
46
+ {
47
+ resourceType: "aws_ecs_service",
48
+ entityTypeCode: "platform_component",
49
+ entitySubtypeCode: "platform_runtime",
50
+ role: "compute_host",
51
+ bindable: true,
52
+ attributes: ["name", "cluster", "task_definition", "desired_count", "launch_type"],
53
+ references: { "cluster": "aws_ecs_cluster" },
54
+ nameReferences: { "task_definition": "aws_ecs_task_definition" },
55
+ canonicalEntityRef: "global:platform_component:aws_ecs",
56
+ cloudResourceType: "AWS::ECS::Service",
57
+ },
58
+ {
59
+ resourceType: "aws_ecs_task_definition",
60
+ entityTypeCode: "platform_component",
61
+ entitySubtypeCode: "platform_runtime",
62
+ role: "compute_host",
63
+ attributes: ["family", "network_mode", "requires_compatibilities", "cpu", "memory"],
64
+ canonicalEntityRef: "global:platform_component:aws_ecs",
65
+ cloudResourceType: "AWS::ECS::TaskDefinition",
66
+ nameAttribute: "family",
67
+ },
68
+ {
69
+ resourceType: "aws_instance",
70
+ entityTypeCode: "platform_component",
71
+ entitySubtypeCode: "platform_runtime",
72
+ role: "compute_host",
73
+ bindable: true,
74
+ attributes: ["ami", "instance_type", "availability_zone", "subnet_id", "vpc_security_group_ids", "private_ip", "associate_public_ip_address", "iam_instance_profile"],
75
+ references: { "subnet_id": "aws_subnet" },
76
+ canonicalEntityRef: "global:platform_component:aws_ec2",
77
+ cloudResourceType: "AWS::EC2::Instance",
78
+ },
79
+ {
80
+ resourceType: "aws_lambda_function",
81
+ entityTypeCode: "platform_component",
82
+ entitySubtypeCode: "platform_runtime",
83
+ role: "compute_host",
84
+ bindable: true,
85
+ attributes: ["function_name", "runtime", "handler", "memory_size", "timeout", "role", "architectures", "package_type"],
86
+ references: { "role": "aws_iam_role" },
87
+ canonicalEntityRef: "global:platform_component:aws_lambda",
88
+ cloudResourceType: "AWS::Lambda::Function",
89
+ nameAttribute: "function_name",
90
+ },
91
+ {
92
+ resourceType: "azurerm_ai_foundry",
93
+ entityTypeCode: "platform_component",
94
+ entitySubtypeCode: "platform_service",
95
+ role: "compute_host",
96
+ attributes: ["name", "location", "resource_group_name", "storage_account_id", "key_vault_id", "public_network_access"],
97
+ references: { "key_vault_id": "azurerm_key_vault", "storage_account_id": "azurerm_storage_account" },
98
+ canonicalEntityRef: "global:platform_component:azure_ai_foundry",
99
+ cloudResourceType: "Microsoft.MachineLearningServices/workspaces",
100
+ },
101
+ {
102
+ resourceType: "azurerm_ai_foundry_project",
103
+ entityTypeCode: "platform_component",
104
+ entitySubtypeCode: "platform_service",
105
+ role: "compute_host",
106
+ attributes: ["name", "location", "ai_services_hub_id", "description"],
107
+ references: { "ai_services_hub_id": "azurerm_ai_foundry" },
108
+ canonicalEntityRef: "global:platform_component:azure_ai_foundry",
109
+ cloudResourceType: "Microsoft.MachineLearningServices/workspaces/projects",
110
+ },
37
111
  {
38
112
  resourceType: "azurerm_cognitive_account",
39
113
  entityTypeCode: "platform_component",
@@ -86,6 +160,45 @@ export const IAC_CATALOGUE_SEED = [
86
160
  canonicalEntityRef: "global:platform_component:azure_static_web_apps",
87
161
  cloudResourceType: "Microsoft.Web/staticSites",
88
162
  },
163
+ {
164
+ resourceType: "aws_db_instance",
165
+ entityTypeCode: "data_store",
166
+ entitySubtypeCode: "store_relational",
167
+ role: "data_store",
168
+ attributes: ["identifier", "engine", "engine_version", "instance_class", "allocated_storage", "publicly_accessible", "multi_az", "db_subnet_group_name"],
169
+ canonicalEntityRef: "global:platform_component:aws_rds",
170
+ cloudResourceType: "AWS::RDS::DBInstance",
171
+ nameAttribute: "identifier",
172
+ },
173
+ {
174
+ resourceType: "aws_dynamodb_table",
175
+ entityTypeCode: "data_store",
176
+ entitySubtypeCode: "store_document",
177
+ role: "data_store",
178
+ attributes: ["name", "billing_mode", "hash_key", "range_key", "stream_enabled"],
179
+ canonicalEntityRef: "global:platform_component:aws_dynamodb",
180
+ cloudResourceType: "AWS::DynamoDB::Table",
181
+ },
182
+ {
183
+ resourceType: "aws_elasticache_cluster",
184
+ entityTypeCode: "data_store",
185
+ entitySubtypeCode: "store_cache",
186
+ role: "data_store",
187
+ attributes: ["cluster_id", "engine", "engine_version", "node_type", "num_cache_nodes"],
188
+ canonicalEntityRef: "global:platform_component:aws_elasticache",
189
+ cloudResourceType: "AWS::ElastiCache::CacheCluster",
190
+ nameAttribute: "cluster_id",
191
+ },
192
+ {
193
+ resourceType: "aws_s3_bucket",
194
+ entityTypeCode: "data_store",
195
+ entitySubtypeCode: "store_object",
196
+ role: "data_store",
197
+ attributes: ["bucket", "bucket_prefix", "force_destroy"],
198
+ canonicalEntityRef: "global:platform_component:aws_s3",
199
+ cloudResourceType: "AWS::S3::Bucket",
200
+ nameAttribute: "bucket",
201
+ },
89
202
  {
90
203
  resourceType: "azurerm_cosmosdb_account",
91
204
  entityTypeCode: "data_store",
@@ -131,6 +244,65 @@ export const IAC_CATALOGUE_SEED = [
131
244
  canonicalEntityRef: "global:platform_component:azure_storage",
132
245
  cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers",
133
246
  },
247
+ {
248
+ resourceType: "aws_cloudfront_distribution",
249
+ entityTypeCode: "platform_component",
250
+ entitySubtypeCode: "platform_service",
251
+ role: "edge",
252
+ attributes: ["enabled", "comment", "default_root_object", "domain_name"],
253
+ canonicalEntityRef: "global:platform_component:aws_cloudfront",
254
+ cloudResourceType: "AWS::CloudFront::Distribution",
255
+ nameAttribute: "domain_name",
256
+ },
257
+ {
258
+ resourceType: "aws_lb",
259
+ entityTypeCode: "platform_component",
260
+ entitySubtypeCode: "platform_service",
261
+ role: "edge",
262
+ attributes: ["name", "internal", "load_balancer_type", "security_groups", "subnets", "dns_name"],
263
+ canonicalEntityRef: "global:platform_component:aws_elastic_load_balancing",
264
+ cloudResourceType: "AWS::ElasticLoadBalancingV2::LoadBalancer",
265
+ },
266
+ {
267
+ resourceType: "aws_lb_listener",
268
+ entityTypeCode: "platform_component",
269
+ entitySubtypeCode: "platform_service",
270
+ role: "edge",
271
+ attributes: ["port", "protocol", "load_balancer_arn"],
272
+ references: { "load_balancer_arn": "aws_lb" },
273
+ canonicalEntityRef: "global:platform_component:aws_elastic_load_balancing",
274
+ cloudResourceType: "AWS::ElasticLoadBalancingV2::Listener",
275
+ },
276
+ {
277
+ resourceType: "aws_lb_target_group",
278
+ entityTypeCode: "platform_component",
279
+ entitySubtypeCode: "platform_service",
280
+ role: "edge",
281
+ attributes: ["name", "port", "protocol", "target_type", "vpc_id"],
282
+ references: { "vpc_id": "aws_vpc" },
283
+ canonicalEntityRef: "global:platform_component:aws_elastic_load_balancing",
284
+ cloudResourceType: "AWS::ElasticLoadBalancingV2::TargetGroup",
285
+ },
286
+ {
287
+ resourceType: "azurerm_cdn_frontdoor_custom_domain",
288
+ entityTypeCode: "platform_component",
289
+ entitySubtypeCode: "platform_service",
290
+ role: "edge",
291
+ attributes: ["name", "host_name", "cdn_frontdoor_profile_id", "dns_zone_id"],
292
+ references: { "dns_zone_id": "azurerm_dns_zone", "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
293
+ canonicalEntityRef: "global:platform_component:azure_front_door",
294
+ cloudResourceType: "Microsoft.Cdn/profiles/customDomains",
295
+ },
296
+ {
297
+ resourceType: "azurerm_cdn_frontdoor_custom_domain_association",
298
+ entityTypeCode: "platform_component",
299
+ entitySubtypeCode: "platform_service",
300
+ role: "edge",
301
+ attributes: ["cdn_frontdoor_custom_domain_id"],
302
+ references: { "cdn_frontdoor_custom_domain_id": "azurerm_cdn_frontdoor_custom_domain" },
303
+ canonicalEntityRef: "global:platform_component:azure_front_door",
304
+ cloudResourceType: "Microsoft.Cdn/profiles/customDomains/associations",
305
+ },
134
306
  {
135
307
  resourceType: "azurerm_cdn_frontdoor_endpoint",
136
308
  entityTypeCode: "platform_component",
@@ -149,6 +321,26 @@ export const IAC_CATALOGUE_SEED = [
149
321
  canonicalEntityRef: "global:platform_component:azure_web_application_firewall",
150
322
  cloudResourceType: "Microsoft.Network/frontDoorWebApplicationFirewallPolicies",
151
323
  },
324
+ {
325
+ resourceType: "azurerm_cdn_frontdoor_origin",
326
+ entityTypeCode: "platform_component",
327
+ entitySubtypeCode: "platform_service",
328
+ role: "edge",
329
+ attributes: ["name", "host_name", "enabled", "certificate_name_check_enabled", "cdn_frontdoor_origin_group_id"],
330
+ references: { "cdn_frontdoor_origin_group_id": "azurerm_cdn_frontdoor_origin_group" },
331
+ canonicalEntityRef: "global:platform_component:azure_front_door",
332
+ cloudResourceType: "Microsoft.Cdn/profiles/originGroups/origins",
333
+ },
334
+ {
335
+ resourceType: "azurerm_cdn_frontdoor_origin_group",
336
+ entityTypeCode: "platform_component",
337
+ entitySubtypeCode: "platform_service",
338
+ role: "edge",
339
+ attributes: ["name", "session_affinity_enabled", "cdn_frontdoor_profile_id"],
340
+ references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
341
+ canonicalEntityRef: "global:platform_component:azure_front_door",
342
+ cloudResourceType: "Microsoft.Cdn/profiles/originGroups",
343
+ },
152
344
  {
153
345
  resourceType: "azurerm_cdn_frontdoor_profile",
154
346
  entityTypeCode: "platform_component",
@@ -158,6 +350,25 @@ export const IAC_CATALOGUE_SEED = [
158
350
  canonicalEntityRef: "global:platform_component:azure_front_door",
159
351
  cloudResourceType: "Microsoft.Cdn/profiles",
160
352
  },
353
+ {
354
+ resourceType: "azurerm_cdn_frontdoor_route",
355
+ entityTypeCode: "platform_component",
356
+ entitySubtypeCode: "platform_service",
357
+ role: "edge",
358
+ attributes: ["name", "enabled", "patterns_to_match", "supported_protocols", "forwarding_protocol", "https_redirect_enabled", "cdn_frontdoor_endpoint_id", "cdn_frontdoor_origin_group_id"],
359
+ references: { "cdn_frontdoor_endpoint_id": "azurerm_cdn_frontdoor_endpoint", "cdn_frontdoor_origin_group_id": "azurerm_cdn_frontdoor_origin_group" },
360
+ canonicalEntityRef: "global:platform_component:azure_front_door",
361
+ cloudResourceType: "Microsoft.Cdn/profiles/afdEndpoints/routes",
362
+ },
363
+ {
364
+ resourceType: "azurerm_cdn_frontdoor_security_policy",
365
+ entityTypeCode: "policy_control",
366
+ role: "edge",
367
+ attributes: ["name", "cdn_frontdoor_profile_id"],
368
+ references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
369
+ canonicalEntityRef: "global:platform_component:azure_web_application_firewall",
370
+ cloudResourceType: "Microsoft.Cdn/profiles/securityPolicies",
371
+ },
161
372
  {
162
373
  resourceType: "azurerm_static_web_app_custom_domain",
163
374
  entityTypeCode: "platform_component",
@@ -187,6 +398,100 @@ export const IAC_CATALOGUE_SEED = [
187
398
  canonicalEntityRef: "global:platform_component:azure_traffic_manager",
188
399
  cloudResourceType: "Microsoft.Network/trafficManagerProfiles",
189
400
  },
401
+ {
402
+ resourceType: "aws_iam_policy",
403
+ entityTypeCode: "policy_control",
404
+ role: "governance",
405
+ attributes: ["name", "path", "description"],
406
+ canonicalEntityRef: "global:platform_component:aws_iam",
407
+ cloudResourceType: "AWS::IAM::ManagedPolicy",
408
+ },
409
+ {
410
+ resourceType: "aws_iam_role_policy_attachment",
411
+ entityTypeCode: "policy_control",
412
+ role: "governance",
413
+ attributes: ["role", "policy_arn"],
414
+ canonicalEntityRef: "global:platform_component:aws_iam",
415
+ cloudResourceType: "AWS::IAM::RolePolicyAttachment",
416
+ },
417
+ {
418
+ resourceType: "azurerm_policy_definition_built_in",
419
+ entityTypeCode: "policy_control",
420
+ role: "governance",
421
+ attributes: ["name", "display_name", "policy_type", "mode"],
422
+ canonicalEntityRef: "global:platform_component:azure_policy",
423
+ cloudResourceType: "Microsoft.Authorization/policyDefinitions",
424
+ },
425
+ {
426
+ resourceType: "azurerm_policy_set_definition",
427
+ entityTypeCode: "policy_control",
428
+ role: "governance",
429
+ attributes: ["name", "display_name", "policy_type", "management_group_id"],
430
+ canonicalEntityRef: "global:platform_component:azure_policy",
431
+ cloudResourceType: "Microsoft.Authorization/policySetDefinitions",
432
+ },
433
+ {
434
+ resourceType: "azurerm_security_center_contact",
435
+ entityTypeCode: "policy_control",
436
+ role: "governance",
437
+ attributes: ["name", "alert_notifications", "alerts_to_admins"],
438
+ canonicalEntityRef: "global:platform_component:azure_defender_for_cloud",
439
+ cloudResourceType: "Microsoft.Security/securityContacts",
440
+ },
441
+ {
442
+ resourceType: "azurerm_security_center_subscription_pricing",
443
+ entityTypeCode: "policy_control",
444
+ role: "governance",
445
+ attributes: ["tier", "resource_type"],
446
+ canonicalEntityRef: "global:platform_component:azure_defender_for_cloud",
447
+ cloudResourceType: "Microsoft.Security/pricings",
448
+ },
449
+ {
450
+ resourceType: "azurerm_security_center_workspace",
451
+ entityTypeCode: "policy_control",
452
+ role: "governance",
453
+ attributes: ["scope", "workspace_id"],
454
+ references: { "workspace_id": "azurerm_log_analytics_workspace" },
455
+ canonicalEntityRef: "global:platform_component:azure_defender_for_cloud",
456
+ cloudResourceType: "Microsoft.Security/workspaceSettings",
457
+ },
458
+ {
459
+ resourceType: "azurerm_storage_container_immutability_policy",
460
+ entityTypeCode: "policy_control",
461
+ role: "governance",
462
+ attributes: ["storage_container_resource_manager_id", "immutability_period_in_days", "protected_append_writes_all_enabled"],
463
+ references: { "storage_container_resource_manager_id": "azurerm_storage_container" },
464
+ canonicalEntityRef: "global:platform_component:azure_storage",
465
+ cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies",
466
+ },
467
+ {
468
+ resourceType: "azurerm_subscription_policy_assignment",
469
+ entityTypeCode: "policy_control",
470
+ role: "governance",
471
+ attributes: ["name", "display_name", "policy_definition_id", "subscription_id", "enforce"],
472
+ references: { "policy_definition_id": "azurerm_policy_set_definition" },
473
+ canonicalEntityRef: "global:platform_component:azure_policy",
474
+ cloudResourceType: "Microsoft.Authorization/policyAssignments",
475
+ },
476
+ {
477
+ resourceType: "aws_iam_role",
478
+ entityTypeCode: "platform_component",
479
+ entitySubtypeCode: "platform_control_plane",
480
+ role: "identity",
481
+ attributes: ["name", "path", "description", "max_session_duration"],
482
+ canonicalEntityRef: "global:platform_component:aws_iam",
483
+ cloudResourceType: "AWS::IAM::Role",
484
+ },
485
+ {
486
+ resourceType: "azurerm_federated_identity_credential",
487
+ entityTypeCode: "platform_component",
488
+ entitySubtypeCode: "platform_control_plane",
489
+ role: "identity",
490
+ attributes: ["name", "resource_group_name", "audience", "issuer", "subject", "parent_id"],
491
+ references: { "parent_id": "azurerm_user_assigned_identity" },
492
+ canonicalEntityRef: "global:platform_component:azure_managed_identity",
493
+ cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials",
494
+ },
190
495
  {
191
496
  resourceType: "azurerm_role_assignment",
192
497
  entityTypeCode: "policy_control",
@@ -204,6 +509,34 @@ export const IAC_CATALOGUE_SEED = [
204
509
  canonicalEntityRef: "global:platform_component:azure_managed_identity",
205
510
  cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities",
206
511
  },
512
+ {
513
+ resourceType: "aws_sns_topic",
514
+ entityTypeCode: "platform_component",
515
+ entitySubtypeCode: "platform_service",
516
+ role: "messaging",
517
+ attributes: ["name", "display_name", "fifo_topic"],
518
+ canonicalEntityRef: "global:platform_component:aws_sns",
519
+ cloudResourceType: "AWS::SNS::Topic",
520
+ },
521
+ {
522
+ resourceType: "aws_sns_topic_subscription",
523
+ entityTypeCode: "platform_component",
524
+ entitySubtypeCode: "platform_service",
525
+ role: "messaging",
526
+ attributes: ["protocol", "topic_arn"],
527
+ references: { "topic_arn": "aws_sns_topic" },
528
+ canonicalEntityRef: "global:platform_component:aws_sns",
529
+ cloudResourceType: "AWS::SNS::Subscription",
530
+ },
531
+ {
532
+ resourceType: "aws_sqs_queue",
533
+ entityTypeCode: "platform_component",
534
+ entitySubtypeCode: "platform_service",
535
+ role: "messaging",
536
+ attributes: ["name", "fifo_queue", "visibility_timeout_seconds", "message_retention_seconds", "delay_seconds"],
537
+ canonicalEntityRef: "global:platform_component:aws_sqs",
538
+ cloudResourceType: "AWS::SQS::Queue",
539
+ },
207
540
  {
208
541
  resourceType: "azurerm_servicebus_namespace",
209
542
  entityTypeCode: "platform_component",
@@ -223,6 +556,133 @@ export const IAC_CATALOGUE_SEED = [
223
556
  canonicalEntityRef: "global:platform_component:azure_service_bus",
224
557
  cloudResourceType: "Microsoft.ServiceBus/namespaces/queues",
225
558
  },
559
+ {
560
+ resourceType: "aws_db_subnet_group",
561
+ entityTypeCode: "platform_component",
562
+ entitySubtypeCode: "platform_service",
563
+ role: "network_boundary",
564
+ attributes: ["name", "vpc_id"],
565
+ references: { "vpc_id": "aws_vpc" },
566
+ canonicalEntityRef: "global:platform_component:aws_rds",
567
+ cloudResourceType: "AWS::RDS::DBSubnetGroup",
568
+ },
569
+ {
570
+ resourceType: "aws_internet_gateway",
571
+ entityTypeCode: "platform_component",
572
+ entitySubtypeCode: "platform_service",
573
+ role: "network_boundary",
574
+ attributes: ["vpc_id"],
575
+ references: { "vpc_id": "aws_vpc" },
576
+ canonicalEntityRef: "global:platform_component:aws_vpc",
577
+ cloudResourceType: "AWS::EC2::InternetGateway",
578
+ },
579
+ {
580
+ resourceType: "aws_nat_gateway",
581
+ entityTypeCode: "platform_component",
582
+ entitySubtypeCode: "platform_service",
583
+ role: "network_boundary",
584
+ attributes: ["subnet_id", "allocation_id", "connectivity_type"],
585
+ references: { "subnet_id": "aws_subnet" },
586
+ canonicalEntityRef: "global:platform_component:aws_vpc",
587
+ cloudResourceType: "AWS::EC2::NatGateway",
588
+ },
589
+ {
590
+ resourceType: "aws_route53_record",
591
+ entityTypeCode: "platform_component",
592
+ entitySubtypeCode: "platform_service",
593
+ role: "network_boundary",
594
+ attributes: ["name", "type", "ttl", "zone_id"],
595
+ canonicalEntityRef: "global:platform_component:aws_route53",
596
+ cloudResourceType: "AWS::Route53::RecordSet",
597
+ },
598
+ {
599
+ resourceType: "aws_route53_zone",
600
+ entityTypeCode: "platform_component",
601
+ entitySubtypeCode: "platform_service",
602
+ role: "network_boundary",
603
+ attributes: ["name", "comment"],
604
+ canonicalEntityRef: "global:platform_component:aws_route53",
605
+ cloudResourceType: "AWS::Route53::HostedZone",
606
+ },
607
+ {
608
+ resourceType: "aws_route_table",
609
+ entityTypeCode: "platform_component",
610
+ entitySubtypeCode: "platform_service",
611
+ role: "network_boundary",
612
+ attributes: ["vpc_id"],
613
+ references: { "vpc_id": "aws_vpc" },
614
+ canonicalEntityRef: "global:platform_component:aws_vpc",
615
+ cloudResourceType: "AWS::EC2::RouteTable",
616
+ },
617
+ {
618
+ resourceType: "aws_security_group",
619
+ entityTypeCode: "platform_component",
620
+ entitySubtypeCode: "platform_service",
621
+ role: "network_boundary",
622
+ attributes: ["name", "description", "vpc_id"],
623
+ references: { "vpc_id": "aws_vpc" },
624
+ canonicalEntityRef: "global:platform_component:aws_security_group",
625
+ cloudResourceType: "AWS::EC2::SecurityGroup",
626
+ },
627
+ {
628
+ resourceType: "aws_subnet",
629
+ entityTypeCode: "platform_component",
630
+ entitySubtypeCode: "platform_service",
631
+ role: "network_boundary",
632
+ attributes: ["cidr_block", "availability_zone", "vpc_id", "map_public_ip_on_launch"],
633
+ references: { "vpc_id": "aws_vpc" },
634
+ canonicalEntityRef: "global:platform_component:aws_vpc",
635
+ cloudResourceType: "AWS::EC2::Subnet",
636
+ nameAttribute: "id",
637
+ },
638
+ {
639
+ resourceType: "aws_vpc",
640
+ entityTypeCode: "platform_component",
641
+ entitySubtypeCode: "platform_service",
642
+ role: "network_boundary",
643
+ attributes: ["cidr_block", "enable_dns_support", "enable_dns_hostnames", "instance_tenancy"],
644
+ canonicalEntityRef: "global:platform_component:aws_vpc",
645
+ cloudResourceType: "AWS::EC2::VPC",
646
+ nameAttribute: "id",
647
+ },
648
+ {
649
+ resourceType: "azurerm_dns_cname_record",
650
+ entityTypeCode: "platform_component",
651
+ entitySubtypeCode: "platform_service",
652
+ role: "network_boundary",
653
+ attributes: ["name", "resource_group_name", "zone_name", "ttl", "record"],
654
+ nameReferences: { "zone_name": "azurerm_dns_zone" },
655
+ canonicalEntityRef: "global:platform_component:azure_dns",
656
+ cloudResourceType: "Microsoft.Network/dnsZones/CNAME",
657
+ },
658
+ {
659
+ resourceType: "azurerm_dns_txt_record",
660
+ entityTypeCode: "platform_component",
661
+ entitySubtypeCode: "platform_service",
662
+ role: "network_boundary",
663
+ attributes: ["name", "resource_group_name", "zone_name", "ttl"],
664
+ nameReferences: { "zone_name": "azurerm_dns_zone" },
665
+ canonicalEntityRef: "global:platform_component:azure_dns",
666
+ cloudResourceType: "Microsoft.Network/dnsZones/TXT",
667
+ },
668
+ {
669
+ resourceType: "azurerm_dns_zone",
670
+ entityTypeCode: "platform_component",
671
+ entitySubtypeCode: "platform_service",
672
+ role: "network_boundary",
673
+ attributes: ["name", "resource_group_name", "number_of_record_sets", "name_servers"],
674
+ canonicalEntityRef: "global:platform_component:azure_dns",
675
+ cloudResourceType: "Microsoft.Network/dnsZones",
676
+ },
677
+ {
678
+ resourceType: "azurerm_network_security_group",
679
+ entityTypeCode: "platform_component",
680
+ entitySubtypeCode: "platform_service",
681
+ role: "network_boundary",
682
+ attributes: ["name", "location", "resource_group_name"],
683
+ canonicalEntityRef: "global:platform_component:azure_network_security_group",
684
+ cloudResourceType: "Microsoft.Network/networkSecurityGroups",
685
+ },
226
686
  {
227
687
  resourceType: "azurerm_private_dns_zone",
228
688
  entityTypeCode: "platform_component",
@@ -232,6 +692,17 @@ export const IAC_CATALOGUE_SEED = [
232
692
  canonicalEntityRef: "global:platform_component:azure_private_dns",
233
693
  cloudResourceType: "Microsoft.Network/privateDnsZones",
234
694
  },
695
+ {
696
+ resourceType: "azurerm_private_dns_zone_virtual_network_link",
697
+ entityTypeCode: "platform_component",
698
+ entitySubtypeCode: "platform_service",
699
+ role: "network_boundary",
700
+ attributes: ["name", "resource_group_name", "private_dns_zone_name", "virtual_network_id", "registration_enabled"],
701
+ references: { "virtual_network_id": "azurerm_virtual_network" },
702
+ nameReferences: { "private_dns_zone_name": "azurerm_private_dns_zone" },
703
+ canonicalEntityRef: "global:platform_component:azure_private_dns",
704
+ cloudResourceType: "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
705
+ },
235
706
  {
236
707
  resourceType: "azurerm_private_endpoint",
237
708
  entityTypeCode: "platform_component",
@@ -252,6 +723,16 @@ export const IAC_CATALOGUE_SEED = [
252
723
  canonicalEntityRef: "global:platform_component:azure_virtual_network",
253
724
  cloudResourceType: "Microsoft.Network/virtualNetworks/subnets",
254
725
  },
726
+ {
727
+ resourceType: "azurerm_subnet_network_security_group_association",
728
+ entityTypeCode: "platform_component",
729
+ entitySubtypeCode: "platform_service",
730
+ role: "network_boundary",
731
+ attributes: ["subnet_id", "network_security_group_id"],
732
+ references: { "subnet_id": "azurerm_subnet", "network_security_group_id": "azurerm_network_security_group" },
733
+ canonicalEntityRef: "global:platform_component:azure_network_security_group",
734
+ cloudResourceType: "Microsoft.Network/virtualNetworks/subnets/networkSecurityGroupAssociations",
735
+ },
255
736
  {
256
737
  resourceType: "azurerm_virtual_network",
257
738
  entityTypeCode: "platform_component",
@@ -261,6 +742,25 @@ export const IAC_CATALOGUE_SEED = [
261
742
  canonicalEntityRef: "global:platform_component:azure_virtual_network",
262
743
  cloudResourceType: "Microsoft.Network/virtualNetworks",
263
744
  },
745
+ {
746
+ resourceType: "aws_cloudwatch_log_group",
747
+ entityTypeCode: "platform_component",
748
+ entitySubtypeCode: "platform_service",
749
+ role: "observability",
750
+ attributes: ["name", "retention_in_days"],
751
+ canonicalEntityRef: "global:platform_component:aws_cloudwatch",
752
+ cloudResourceType: "AWS::Logs::LogGroup",
753
+ },
754
+ {
755
+ resourceType: "aws_cloudwatch_metric_alarm",
756
+ entityTypeCode: "platform_component",
757
+ entitySubtypeCode: "platform_service",
758
+ role: "observability",
759
+ attributes: ["alarm_name", "comparison_operator", "evaluation_periods", "metric_name", "namespace", "period", "statistic", "threshold"],
760
+ canonicalEntityRef: "global:platform_component:aws_cloudwatch",
761
+ cloudResourceType: "AWS::CloudWatch::Alarm",
762
+ nameAttribute: "alarm_name",
763
+ },
264
764
  {
265
765
  resourceType: "azurerm_log_analytics_workspace",
266
766
  entityTypeCode: "platform_component",
@@ -270,6 +770,25 @@ export const IAC_CATALOGUE_SEED = [
270
770
  canonicalEntityRef: "global:platform_component:azure_monitor",
271
771
  cloudResourceType: "Microsoft.OperationalInsights/workspaces",
272
772
  },
773
+ {
774
+ resourceType: "azurerm_monitor_diagnostic_setting",
775
+ entityTypeCode: "platform_component",
776
+ entitySubtypeCode: "platform_service",
777
+ role: "observability",
778
+ attributes: ["name", "target_resource_id", "log_analytics_workspace_id", "storage_account_id"],
779
+ references: { "storage_account_id": "azurerm_storage_account", "log_analytics_workspace_id": "azurerm_log_analytics_workspace" },
780
+ canonicalEntityRef: "global:platform_component:azure_monitor",
781
+ cloudResourceType: "Microsoft.Insights/diagnosticSettings",
782
+ },
783
+ {
784
+ resourceType: "aws_ecr_repository",
785
+ entityTypeCode: "platform_component",
786
+ entitySubtypeCode: "platform_service",
787
+ role: "registry",
788
+ attributes: ["name", "image_tag_mutability", "force_delete"],
789
+ canonicalEntityRef: "global:platform_component:aws_ecr",
790
+ cloudResourceType: "AWS::ECR::Repository",
791
+ },
273
792
  {
274
793
  resourceType: "azurerm_container_registry",
275
794
  entityTypeCode: "platform_component",
@@ -288,6 +807,26 @@ export const IAC_CATALOGUE_SEED = [
288
807
  canonicalEntityRef: "global:platform_component:azure_resource_group",
289
808
  cloudResourceType: "Microsoft.Resources/resourceGroups",
290
809
  },
810
+ {
811
+ resourceType: "aws_kms_key",
812
+ entityTypeCode: "platform_component",
813
+ entitySubtypeCode: "platform_service",
814
+ role: "secret_store",
815
+ attributes: ["description", "key_usage", "deletion_window_in_days", "enable_key_rotation"],
816
+ canonicalEntityRef: "global:platform_component:aws_kms",
817
+ cloudResourceType: "AWS::KMS::Key",
818
+ nameAttribute: "id",
819
+ },
820
+ {
821
+ resourceType: "aws_secretsmanager_secret",
822
+ entityTypeCode: "platform_component",
823
+ entitySubtypeCode: "platform_service",
824
+ role: "secret_store",
825
+ attributes: ["name", "description", "kms_key_id"],
826
+ references: { "kms_key_id": "aws_kms_key" },
827
+ canonicalEntityRef: "global:platform_component:aws_secrets_manager",
828
+ cloudResourceType: "AWS::SecretsManager::Secret",
829
+ },
291
830
  {
292
831
  resourceType: "azurerm_key_vault",
293
832
  entityTypeCode: "platform_component",
package/dist/lib/mcp.js CHANGED
@@ -1,21 +1,8 @@
1
1
  import https from "https";
2
- import { readFileSync } from "fs";
3
- import { fileURLToPath } from "url";
4
- import { dirname, join } from "path";
5
2
  import { requireCredentials } from "./credentials.js";
3
+ import { cliVersion } from "./version.js";
6
4
  const MCP_GATEWAY_URL = "https://mcp.nexarch.ai";
7
- function readCliVersion() {
8
- try {
9
- const here = dirname(fileURLToPath(import.meta.url));
10
- const pkgPath = join(here, "..", "..", "package.json");
11
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
12
- return pkg.version ?? "0.0.0";
13
- }
14
- catch {
15
- return "0.0.0";
16
- }
17
- }
18
- const CLI_VERSION = readCliVersion();
5
+ const CLI_VERSION = cliVersion();
19
6
  const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.NEXARCH_MCP_TIMEOUT_MS ?? "90000", 10) || 90_000;
20
7
  const REQUEST_RETRIES = Math.max(0, Number.parseInt(process.env.NEXARCH_MCP_RETRIES ?? "2", 10) || 2);
21
8
  function sleep(ms) {
@@ -2,6 +2,17 @@ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
2
2
  import { join, sep } from "path";
3
3
  const SCAN_DEPTH = 8;
4
4
  const IGNORED_DIRS = new Set(["node_modules", ".git", ".terraform", "dist", "build", ".next"]);
5
+ /**
6
+ * Path segments whose contents demonstrate Terraform rather than run it.
7
+ *
8
+ * An `examples/complete` directory configures a provider and looks exactly like
9
+ * an estate root by every local signal — which is why terraform-aws-vpc
10
+ * reported thirteen of them as environments while missing the modules that are
11
+ * the point of the repository. Worse than missing the real architecture: the
12
+ * examples are mutually exclusive demonstrations, so graphing them together
13
+ * describes an estate that has never existed anywhere.
14
+ */
15
+ const NON_ESTATE_SEGMENT = /(^|[\\/])(examples?|docs?|test|tests|fixtures?|testdata|_example|sample|samples)([\\/]|$)/i;
5
16
  function collectTerraformFiles(dir, depth = 0, out = []) {
6
17
  if (depth > SCAN_DEPTH)
7
18
  return out;
@@ -115,6 +126,43 @@ function collectModuleDirectories(dir, depth = 0, out = []) {
115
126
  }
116
127
  return out;
117
128
  }
129
+ /**
130
+ * Recognises a repository that publishes reusable modules rather than running an estate.
131
+ *
132
+ * The Terraform registry convention is a module at the repository root —
133
+ * `main.tf` with `variables.tf` and `outputs.tf` beside it — deliberately
134
+ * configuring no provider and no backend, because both are supplied by whoever
135
+ * calls it. Every signal that identifies an estate is therefore absent by
136
+ * design, and the signals that *are* present belong to the examples directory.
137
+ *
138
+ * This matters more than tidiness. terraform-aws-vpc has no estate to ingest:
139
+ * asking it for one and getting thirteen mutually exclusive demonstrations is
140
+ * worse than getting nothing, because nothing is honest.
141
+ */
142
+ function detectPublishedModules(dir) {
143
+ const rootFiles = (() => {
144
+ try {
145
+ return readdirSync(dir);
146
+ }
147
+ catch {
148
+ return [];
149
+ }
150
+ })();
151
+ const hasRootModule = rootFiles.includes("variables.tf") && rootFiles.includes("outputs.tf") && rootFiles.some((f) => f.endsWith(".tf") && f !== "variables.tf" && f !== "outputs.tf");
152
+ const modules = [];
153
+ const modulesDir = join(dir, "modules");
154
+ try {
155
+ for (const child of readdirSync(modulesDir)) {
156
+ const full = join(modulesDir, child);
157
+ if (statSync(full).isDirectory() && readdirSync(full).some((f) => f.endsWith(".tf")))
158
+ modules.push(child);
159
+ }
160
+ }
161
+ catch {
162
+ // No modules directory; a single-module repository is still a library.
163
+ }
164
+ return { isLibrary: hasRootModule, modules: modules.sort() };
165
+ }
118
166
  const ROOT_SCAN_DEPTH = 4;
119
167
  const ENVIRONMENT_DIR = /^(dev|development|test|qa|uat|sit|stag|staging|preprod|pre-prod|prod|production|live|dr)$/i;
120
168
  /**
@@ -159,7 +207,8 @@ export function discoverRootModules(repoDir) {
159
207
  }
160
208
  const tfFiles = entries.filter((entry) => entry.endsWith(".tf"));
161
209
  const isUnderModules = /(^|[\\/])modules([\\/]|$)/i.test(relative);
162
- if (tfFiles.length > 0 && !isUnderModules) {
210
+ const isDemonstration = NON_ESTATE_SEGMENT.test(relative);
211
+ if (tfFiles.length > 0 && !isUnderModules && !isDemonstration) {
163
212
  const signals = [];
164
213
  let hasBackend = false;
165
214
  let hasLocalBackend = false;
@@ -206,7 +255,13 @@ export function discoverRootModules(repoDir) {
206
255
  signals.push("tfvars");
207
256
  if (underStacksDir)
208
257
  signals.push("stacks directory");
209
- if (hasBackend || hasProvider || initialised || hasPluginDir || (underStacksDir && hasTfvars) || (underStacksDir && environmentHint)) {
258
+ // A provider block alone no longer qualifies. It is the one signal an
259
+ // example shares with a real estate, and treating it as sufficient is
260
+ // what let demonstration fixtures in. What distinguishes an estate is
261
+ // evidence that state exists or is configured somewhere: a backend, an
262
+ // initialised working directory, or a conventional stacks layout.
263
+ const isEstateRoot = hasBackend || initialised || hasPluginDir || (underStacksDir && (hasTfvars || Boolean(environmentHint)));
264
+ if (isEstateRoot) {
210
265
  found.push({ dir, relative: relative || ".", signals, environmentHint, initialised, usesLocalBackend: hasLocalBackend });
211
266
  // A root module's subdirectories are its own modules, not further roots.
212
267
  return;
@@ -278,7 +333,17 @@ export function detectInfrastructureProject(dir) {
278
333
  if (rootModules.length > 0) {
279
334
  signals.push(`${rootModules.length} root module${rootModules.length === 1 ? "" : "s"}`);
280
335
  }
281
- const isInfrastructure = rootModules.length > 0 || (files.length > 0 && (files.length >= 3 || hasRemoteBackend || hasProviderBlock || moduleDirectories.length > 0));
336
+ const library = detectPublishedModules(dir);
337
+ // An estate is claimed only by root modules that survived the tightened
338
+ // acceptance above. A repository with a root module and no estate roots is
339
+ // publishing modules, not running anything.
340
+ const repoKind = rootModules.length > 0 ? "estate" : library.isLibrary ? "module_library" : "unknown";
341
+ if (repoKind === "module_library") {
342
+ signals.push(library.modules.length > 0 ? `publishes ${library.modules.length + 1} modules` : "publishes a reusable module");
343
+ }
344
+ const isInfrastructure = rootModules.length > 0 ||
345
+ repoKind === "module_library" ||
346
+ (files.length > 0 && (files.length >= 3 || hasRemoteBackend || hasProviderBlock || moduleDirectories.length > 0));
282
347
  return {
283
348
  isInfrastructure,
284
349
  rootModules,
@@ -287,6 +352,8 @@ export function detectInfrastructureProject(dir) {
287
352
  hasRemoteBackend,
288
353
  hasProviderBlock,
289
354
  moduleDirectories,
355
+ repoKind,
356
+ publishedModules: repoKind === "module_library" ? library.modules : [],
290
357
  ...detectCiSystem(dir),
291
358
  };
292
359
  }
@@ -208,7 +208,7 @@ export function projectTerraformState(params) {
208
208
  entitySubtypeCode: entry.entitySubtypeCode ?? null,
209
209
  role: entry.role,
210
210
  mode: typeof resource.mode === "string" ? resource.mode : "managed",
211
- name: typeof values.name === "string" ? values.name : null,
211
+ name: typeof values[entry.nameAttribute ?? "name"] === "string" ? values[entry.nameAttribute ?? "name"] : null,
212
212
  attributes,
213
213
  tags: projectTags(values, sensitiveValues),
214
214
  identityIds: projectIdentityIds(values),
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from "fs";
2
+ import { dirname, join, resolve } from "path";
3
+ import { fileURLToPath } from "url";
4
+ export function cliBuild() {
5
+ let version = "0.0.0";
6
+ let installPath = "unknown";
7
+ try {
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ const root = resolve(here, "..", "..");
10
+ installPath = root;
11
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
12
+ version = pkg.version ?? "0.0.0";
13
+ }
14
+ catch {
15
+ // A CLI that cannot read its own manifest should still run; it just cannot
16
+ // say which build it is.
17
+ }
18
+ return {
19
+ version,
20
+ installPath,
21
+ viaNpx: /[\\/]_npx[\\/]/.test(installPath),
22
+ nodeVersion: process.version,
23
+ };
24
+ }
25
+ /** Just the version, for the MCP client's user agent. */
26
+ export function cliVersion() {
27
+ return cliBuild().version;
28
+ }
29
+ /** The block printed by `nexarch --version`. */
30
+ export function formatBuild(build) {
31
+ return [
32
+ `nexarch ${build.version}`,
33
+ ` running from ${build.installPath}${build.viaNpx ? " (npx cache)" : ""}`,
34
+ ` node ${build.nodeVersion}`,
35
+ build.viaNpx
36
+ ? ""
37
+ : `\n This is an installed copy, not a fresh download. If it is older than you\n expect, a global install is shadowing \`npx nexarch@latest\`:\n npm rm -g nexarch`,
38
+ ]
39
+ .filter(Boolean)
40
+ .join("\n");
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.12.18",
3
+ "version": "0.12.22",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",