nexarch 0.12.15 → 0.12.18
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.
|
@@ -3,7 +3,7 @@ import { execFileSync } from "child_process";
|
|
|
3
3
|
import { resolve } from "path";
|
|
4
4
|
import { callMcpTool } from "../lib/mcp.js";
|
|
5
5
|
import { parseTerraformStateBuffer, projectTerraformState, inferEnvironmentFromTags, environmentSubtypeFor, } from "../lib/terraform-projection.js";
|
|
6
|
-
import {
|
|
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
9
|
function readResult(raw) {
|
|
@@ -189,6 +189,7 @@ export function buildIngestPayload(params) {
|
|
|
189
189
|
const identityEntityRefByName = new Map();
|
|
190
190
|
const identityRefByPrincipalId = new Map();
|
|
191
191
|
const catalogueByResourceType = new Map(catalogue.map((entry) => [entry.resourceType, entry]));
|
|
192
|
+
const canonicalTechnologies = new Set();
|
|
192
193
|
for (const resource of projection.resources) {
|
|
193
194
|
const entityRef = `${resource.entityTypeCode}:${slug(resource.name ?? resource.address)}`;
|
|
194
195
|
entities.push({
|
|
@@ -219,6 +220,22 @@ export function buildIngestPayload(params) {
|
|
|
219
220
|
relationships.push({ relationshipTypeCode: "runs_on", fromEntityRef: entityRef, toEntityRef: environmentRef });
|
|
220
221
|
relationships.push({ relationshipTypeCode: "runs_on", fromEntityRef: entityRef, toEntityRef: platformRef });
|
|
221
222
|
}
|
|
223
|
+
// What product is this? Without it the graph holds a resource that knows
|
|
224
|
+
// where it runs and whose cloud it is on, but not what it *is* — the only
|
|
225
|
+
// record of that being a raw `terraform_type` attribute, which no reader
|
|
226
|
+
// can join across environments or tenants. The canonical entry is shared
|
|
227
|
+
// vocabulary, so one query answers "everything we run on Azure AI
|
|
228
|
+
// Services" across every environment and every IaC tool.
|
|
229
|
+
const canonicalRef = catalogueByResourceType.get(resource.resourceType)?.canonicalEntityRef;
|
|
230
|
+
if (canonicalRef && !canonicalTechnologies.has(canonicalRef)) {
|
|
231
|
+
canonicalTechnologies.add(canonicalRef);
|
|
232
|
+
}
|
|
233
|
+
if (canonicalRef) {
|
|
234
|
+
// `instance_of`, not `runs_on`: this vault does not run on Azure Key
|
|
235
|
+
// Vault, it is one. runs_on is a hosting relation and is already covered
|
|
236
|
+
// above by the edges to the platform and the environment.
|
|
237
|
+
relationships.push({ relationshipTypeCode: "instance_of", fromEntityRef: entityRef, toEntityRef: canonicalRef });
|
|
238
|
+
}
|
|
222
239
|
if (resource.name) {
|
|
223
240
|
entityRefByTypeAndName.set(`${resource.resourceType}::${resource.name}`, entityRef);
|
|
224
241
|
entityRefByName.set(resource.name, entityRef);
|
|
@@ -298,8 +315,38 @@ export function buildIngestPayload(params) {
|
|
|
298
315
|
addTopologyEdge("depends_on", entityRef, identityEntityRefByName.get(identityName));
|
|
299
316
|
}
|
|
300
317
|
}
|
|
318
|
+
// The canonical technologies referenced above, upserted so the edges have
|
|
319
|
+
// something to land on. Named from the ref rather than invented per tenant:
|
|
320
|
+
// these are shared vocabulary, and a tenant-local "Azure AI Services" that
|
|
321
|
+
// differs by a word defeats the point of having a canonical entry at all.
|
|
322
|
+
for (const canonicalRef of canonicalTechnologies) {
|
|
323
|
+
const canonicalTypeCode = canonicalRef.split(":")[1] ?? "platform_component";
|
|
324
|
+
entities.push({
|
|
325
|
+
entityRef: canonicalRef,
|
|
326
|
+
entityTypeCode: canonicalTypeCode,
|
|
327
|
+
name: canonicalTechnologyName(canonicalRef),
|
|
328
|
+
description: "Cloud service referenced by infrastructure ingestion.",
|
|
329
|
+
attributes: { source: "terraform_ingest", canonical: true },
|
|
330
|
+
});
|
|
331
|
+
// A managed service belongs to the cloud that offers it. Only from a
|
|
332
|
+
// platform_component: part_of does not accept a technology_component
|
|
333
|
+
// source, so a canonical that is one is left unattached rather than sent
|
|
334
|
+
// to fail governance.
|
|
335
|
+
if (canonicalTypeCode === "platform_component") {
|
|
336
|
+
relationships.push({ relationshipTypeCode: "part_of", fromEntityRef: canonicalRef, toEntityRef: platformRef });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
301
339
|
return { entities, relationships, environmentRef };
|
|
302
340
|
}
|
|
341
|
+
/** Turns global:platform_component:azure_ai_services into "Azure AI Services". */
|
|
342
|
+
function canonicalTechnologyName(canonicalRef) {
|
|
343
|
+
const slugPart = canonicalRef.split(":").slice(2).join(":") || canonicalRef;
|
|
344
|
+
return slugPart
|
|
345
|
+
.split("_")
|
|
346
|
+
.filter(Boolean)
|
|
347
|
+
.map((word) => (word.length <= 3 && word === word.toLowerCase() && ["ai", "api", "dns", "sql", "cdn"].includes(word) ? word.toUpperCase() : word[0].toUpperCase() + word.slice(1)))
|
|
348
|
+
.join(" ");
|
|
349
|
+
}
|
|
303
350
|
function chunk(items, size) {
|
|
304
351
|
const out = [];
|
|
305
352
|
for (let i = 0; i < items.length; i += size)
|
|
@@ -321,7 +368,7 @@ export async function ingestInfra(args) {
|
|
|
321
368
|
// Fetched once and used for both projection and payload: two different
|
|
322
369
|
// catalogues within one ingest would produce entities whose attributes and
|
|
323
370
|
// whose type mapping disagreed about what a resource is.
|
|
324
|
-
const catalogueResult = await
|
|
371
|
+
const catalogueResult = await loadIacCatalogue();
|
|
325
372
|
if (!asJson)
|
|
326
373
|
console.log(` ${describeCatalogue(catalogueResult)}`);
|
|
327
374
|
const catalogue = catalogueResult.entries;
|
|
@@ -338,6 +385,22 @@ export async function ingestInfra(args) {
|
|
|
338
385
|
` 4. if this repository has several root modules, ingest each separately with --dir\n` +
|
|
339
386
|
` The estate may also simply not have been applied yet.`);
|
|
340
387
|
}
|
|
388
|
+
// Nothing recognised is a coverage problem, and has to be said before the
|
|
389
|
+
// environment is even considered. Otherwise a root whose every resource type
|
|
390
|
+
// is missing from the catalogue fails with "could not determine the
|
|
391
|
+
// environment — 0 of those carried any tags", which sends the reader off to
|
|
392
|
+
// tag resources that would still not be ingested afterwards. The environment
|
|
393
|
+
// is the next question only once there is something to attach it to.
|
|
394
|
+
if (projection.stats.resourcesProjected === 0) {
|
|
395
|
+
const seen = projection.unknownTypes
|
|
396
|
+
.slice(0, 12)
|
|
397
|
+
.map((entry) => ` ${String(entry.count).padStart(3)} ${entry.resourceType}`)
|
|
398
|
+
.join("\n");
|
|
399
|
+
throw new Error(`Nothing in this root module is in the catalogue yet — read ${projection.stats.resourcesSeen} resources, matched none.\n` +
|
|
400
|
+
` Tagging or naming an environment will not help: there would still be nothing to ingest.\n` +
|
|
401
|
+
`\n Resource types found here:\n${seen}\n` +
|
|
402
|
+
`\n These need adding to the platform catalogue before this root can be ingested.`);
|
|
403
|
+
}
|
|
341
404
|
const { environment, how } = resolveEnvironment(projection, environmentOverride, workingDir, statePath ? null : workingDir);
|
|
342
405
|
projection.environment = environment;
|
|
343
406
|
// Azure is the only provider the seed catalogue covers today; resolving it
|
|
@@ -359,6 +422,26 @@ export async function ingestInfra(args) {
|
|
|
359
422
|
// Reference resolution is an optimisation, not a requirement.
|
|
360
423
|
}
|
|
361
424
|
}
|
|
425
|
+
// Types the catalogue does not cover are filed as reference candidates, the
|
|
426
|
+
// same way an unrecognised package name is. Printing them to a console nobody
|
|
427
|
+
// aggregates meant the platform could never learn which services customers
|
|
428
|
+
// actually run: curation happened when someone happened to notice. A resource
|
|
429
|
+
// type is vendor vocabulary — `azurerm_static_web_app` contains nothing of the
|
|
430
|
+
// tenant's — so it is safe to file in a cross-tenant queue, where seen_count
|
|
431
|
+
// accumulates the evidence for covering it.
|
|
432
|
+
//
|
|
433
|
+
// Resolving doubles as filing: a type someone has already promoted comes back
|
|
434
|
+
// resolved, and only genuine misses become candidates.
|
|
435
|
+
if (!dryRun && projection.unknownTypes.length > 0) {
|
|
436
|
+
try {
|
|
437
|
+
await callMcpTool("nexarch_resolve_reference", {
|
|
438
|
+
names: projection.unknownTypes.slice(0, 200).map((entry) => entry.resourceType),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Coverage reporting must never fail an ingest: the estate is the point.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
362
445
|
const { entities, relationships } = buildIngestPayload({ projection, environment, platformRef, platformName, catalogue });
|
|
363
446
|
const report = {
|
|
364
447
|
ok: true,
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback catalogue for Terraform resource types.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED FILE — DO NOT EDIT BY HAND.
|
|
5
|
+
* Regenerate with: node web/scripts/export-iac-catalogue.mjs
|
|
6
|
+
*
|
|
7
|
+
* The catalogue proper is platform reference data, curated in
|
|
8
|
+
* `iac_catalogue_entry` and served to every tenant, so supporting a new
|
|
9
|
+
* service is a data change rather than a CLI release. This file is what the CLI
|
|
10
|
+
* falls back to when it cannot reach the platform — offline, or a CI runner with
|
|
11
|
+
* no route to the gateway. It is generated from that table so it can never get
|
|
12
|
+
* ahead of it: editing it by hand recreates the two-sources-of-truth problem
|
|
13
|
+
* moving the catalogue out of the CLI was meant to end.
|
|
14
|
+
*
|
|
15
|
+
* 30 entries, exported 2026-08-21.
|
|
16
|
+
*/
|
|
17
|
+
export const IAC_CATALOGUE_SEED = [
|
|
18
|
+
{
|
|
19
|
+
resourceType: "azurerm_container_app_environment",
|
|
20
|
+
entityTypeCode: "platform_component",
|
|
21
|
+
entitySubtypeCode: "platform_runtime",
|
|
22
|
+
role: "compute_environment",
|
|
23
|
+
attributes: ["name", "location", "resource_group_name", "default_domain", "infrastructure_subnet_id", "internal_load_balancer_enabled", "static_ip_address", "zone_redundancy_enabled"],
|
|
24
|
+
references: { "infrastructure_subnet_id": "azurerm_subnet" },
|
|
25
|
+
canonicalEntityRef: "global:platform_component:azure_container_apps",
|
|
26
|
+
cloudResourceType: "Microsoft.App/managedEnvironments",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
resourceType: "azurerm_service_plan",
|
|
30
|
+
entityTypeCode: "platform_component",
|
|
31
|
+
entitySubtypeCode: "platform_runtime",
|
|
32
|
+
role: "compute_environment",
|
|
33
|
+
attributes: ["name", "location", "resource_group_name", "sku_name", "os_type", "worker_count"],
|
|
34
|
+
canonicalEntityRef: "global:platform_component:azure_app_service",
|
|
35
|
+
cloudResourceType: "Microsoft.Web/serverfarms",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
resourceType: "azurerm_cognitive_account",
|
|
39
|
+
entityTypeCode: "platform_component",
|
|
40
|
+
entitySubtypeCode: "platform_service",
|
|
41
|
+
role: "compute_host",
|
|
42
|
+
attributes: ["name", "location", "resource_group_name", "kind", "sku_name", "endpoint", "custom_subdomain_name", "public_network_access_enabled", "local_auth_enabled"],
|
|
43
|
+
canonicalEntityRef: "global:platform_component:azure_ai_services",
|
|
44
|
+
cloudResourceType: "Microsoft.CognitiveServices/accounts",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
resourceType: "azurerm_container_app",
|
|
48
|
+
entityTypeCode: "platform_component",
|
|
49
|
+
entitySubtypeCode: "platform_runtime",
|
|
50
|
+
role: "compute_host",
|
|
51
|
+
bindable: true,
|
|
52
|
+
attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "revision_mode", "workload_profile_name"],
|
|
53
|
+
references: { "container_app_environment_id": "azurerm_container_app_environment" },
|
|
54
|
+
canonicalEntityRef: "global:platform_component:azure_container_apps",
|
|
55
|
+
cloudResourceType: "Microsoft.App/containerApps",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
resourceType: "azurerm_container_app_job",
|
|
59
|
+
entityTypeCode: "platform_component",
|
|
60
|
+
entitySubtypeCode: "platform_runtime",
|
|
61
|
+
role: "compute_host",
|
|
62
|
+
bindable: true,
|
|
63
|
+
attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "replica_timeout_in_seconds", "workload_profile_name"],
|
|
64
|
+
references: { "container_app_environment_id": "azurerm_container_app_environment" },
|
|
65
|
+
canonicalEntityRef: "global:platform_component:azure_container_apps",
|
|
66
|
+
cloudResourceType: "Microsoft.App/jobs",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
resourceType: "azurerm_linux_web_app",
|
|
70
|
+
entityTypeCode: "platform_component",
|
|
71
|
+
entitySubtypeCode: "platform_runtime",
|
|
72
|
+
role: "compute_host",
|
|
73
|
+
bindable: true,
|
|
74
|
+
attributes: ["name", "location", "resource_group_name", "service_plan_id", "https_only", "default_hostname"],
|
|
75
|
+
references: { "service_plan_id": "azurerm_service_plan" },
|
|
76
|
+
canonicalEntityRef: "global:platform_component:azure_app_service",
|
|
77
|
+
cloudResourceType: "Microsoft.Web/sites",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
resourceType: "azurerm_static_web_app",
|
|
81
|
+
entityTypeCode: "platform_component",
|
|
82
|
+
entitySubtypeCode: "platform_service",
|
|
83
|
+
role: "compute_host",
|
|
84
|
+
bindable: true,
|
|
85
|
+
attributes: ["name", "location", "resource_group_name", "default_host_name", "sku_tier", "sku_size", "public_network_access_enabled", "preview_environments_enabled", "repository_url"],
|
|
86
|
+
canonicalEntityRef: "global:platform_component:azure_static_web_apps",
|
|
87
|
+
cloudResourceType: "Microsoft.Web/staticSites",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
resourceType: "azurerm_cosmosdb_account",
|
|
91
|
+
entityTypeCode: "data_store",
|
|
92
|
+
entitySubtypeCode: "store_document",
|
|
93
|
+
role: "data_store",
|
|
94
|
+
attributes: ["name", "location", "resource_group_name", "kind", "offer_type", "public_network_access_enabled", "endpoint"],
|
|
95
|
+
canonicalEntityRef: "global:platform_component:azure_cosmos_db",
|
|
96
|
+
cloudResourceType: "Microsoft.DocumentDB/databaseAccounts",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
resourceType: "azurerm_postgresql_flexible_server",
|
|
100
|
+
entityTypeCode: "data_store",
|
|
101
|
+
entitySubtypeCode: "store_relational",
|
|
102
|
+
role: "data_store",
|
|
103
|
+
attributes: ["name", "location", "resource_group_name", "fqdn", "version", "sku_name", "storage_mb", "storage_tier", "public_network_access_enabled", "delegated_subnet_id", "private_dns_zone_id", "backup_retention_days", "geo_redundant_backup_enabled", "zone"],
|
|
104
|
+
canonicalEntityRef: "global:platform_component:azure_database_for_postgresql",
|
|
105
|
+
cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
resourceType: "azurerm_postgresql_flexible_server_database",
|
|
109
|
+
entityTypeCode: "data_store",
|
|
110
|
+
entitySubtypeCode: "store_relational",
|
|
111
|
+
role: "data_store",
|
|
112
|
+
attributes: ["name", "server_id", "charset", "collation"],
|
|
113
|
+
canonicalEntityRef: "global:platform_component:azure_database_for_postgresql",
|
|
114
|
+
cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers/databases",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
resourceType: "azurerm_storage_account",
|
|
118
|
+
entityTypeCode: "data_store",
|
|
119
|
+
entitySubtypeCode: "store_object",
|
|
120
|
+
role: "data_store",
|
|
121
|
+
attributes: ["name", "location", "resource_group_name", "account_kind", "account_tier", "account_replication_type", "access_tier", "https_traffic_only_enabled", "min_tls_version", "public_network_access_enabled", "allow_nested_items_to_be_public", "shared_access_key_enabled", "primary_blob_endpoint", "primary_location", "secondary_location"],
|
|
122
|
+
canonicalEntityRef: "global:platform_component:azure_storage",
|
|
123
|
+
cloudResourceType: "Microsoft.Storage/storageAccounts",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
resourceType: "azurerm_storage_container",
|
|
127
|
+
entityTypeCode: "data_store",
|
|
128
|
+
entitySubtypeCode: "store_object",
|
|
129
|
+
role: "data_store",
|
|
130
|
+
attributes: ["name", "container_access_type", "storage_account_id", "has_immutability_policy"],
|
|
131
|
+
canonicalEntityRef: "global:platform_component:azure_storage",
|
|
132
|
+
cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
resourceType: "azurerm_cdn_frontdoor_endpoint",
|
|
136
|
+
entityTypeCode: "platform_component",
|
|
137
|
+
entitySubtypeCode: "platform_service",
|
|
138
|
+
role: "edge",
|
|
139
|
+
attributes: ["name", "host_name", "enabled", "cdn_frontdoor_profile_id"],
|
|
140
|
+
references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
|
|
141
|
+
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
142
|
+
cloudResourceType: "Microsoft.Cdn/profiles/afdEndpoints",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
resourceType: "azurerm_cdn_frontdoor_firewall_policy",
|
|
146
|
+
entityTypeCode: "policy_control",
|
|
147
|
+
role: "edge",
|
|
148
|
+
attributes: ["name", "location", "resource_group_name", "sku_name", "mode", "enabled"],
|
|
149
|
+
canonicalEntityRef: "global:platform_component:azure_web_application_firewall",
|
|
150
|
+
cloudResourceType: "Microsoft.Network/frontDoorWebApplicationFirewallPolicies",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
resourceType: "azurerm_cdn_frontdoor_profile",
|
|
154
|
+
entityTypeCode: "platform_component",
|
|
155
|
+
entitySubtypeCode: "platform_service",
|
|
156
|
+
role: "edge",
|
|
157
|
+
attributes: ["name", "location", "resource_group_name", "sku_name", "response_timeout_seconds"],
|
|
158
|
+
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
159
|
+
cloudResourceType: "Microsoft.Cdn/profiles",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
resourceType: "azurerm_static_web_app_custom_domain",
|
|
163
|
+
entityTypeCode: "platform_component",
|
|
164
|
+
entitySubtypeCode: "platform_service",
|
|
165
|
+
role: "edge",
|
|
166
|
+
attributes: ["domain_name", "validation_type"],
|
|
167
|
+
references: { "static_web_app_id": "azurerm_static_web_app" },
|
|
168
|
+
canonicalEntityRef: "global:platform_component:azure_static_web_apps",
|
|
169
|
+
cloudResourceType: "Microsoft.Web/staticSites/customDomains",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
resourceType: "azurerm_traffic_manager_external_endpoint",
|
|
173
|
+
entityTypeCode: "platform_component",
|
|
174
|
+
entitySubtypeCode: "platform_service",
|
|
175
|
+
role: "edge",
|
|
176
|
+
attributes: ["name", "target", "endpoint_location", "enabled", "priority", "weight", "always_serve_enabled"],
|
|
177
|
+
references: { "profile_id": "azurerm_traffic_manager_profile" },
|
|
178
|
+
canonicalEntityRef: "global:platform_component:azure_traffic_manager",
|
|
179
|
+
cloudResourceType: "Microsoft.Network/trafficManagerProfiles/externalEndpoints",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
resourceType: "azurerm_traffic_manager_profile",
|
|
183
|
+
entityTypeCode: "platform_component",
|
|
184
|
+
entitySubtypeCode: "platform_service",
|
|
185
|
+
role: "edge",
|
|
186
|
+
attributes: ["name", "resource_group_name", "fqdn", "traffic_routing_method", "profile_status", "traffic_view_enabled", "max_return"],
|
|
187
|
+
canonicalEntityRef: "global:platform_component:azure_traffic_manager",
|
|
188
|
+
cloudResourceType: "Microsoft.Network/trafficManagerProfiles",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
resourceType: "azurerm_role_assignment",
|
|
192
|
+
entityTypeCode: "policy_control",
|
|
193
|
+
role: "identity",
|
|
194
|
+
attributes: ["name", "principal_id", "principal_type", "role_definition_name", "scope"],
|
|
195
|
+
canonicalEntityRef: "global:platform_component:azure_rbac",
|
|
196
|
+
cloudResourceType: "Microsoft.Authorization/roleAssignments",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
resourceType: "azurerm_user_assigned_identity",
|
|
200
|
+
entityTypeCode: "platform_component",
|
|
201
|
+
entitySubtypeCode: "platform_control_plane",
|
|
202
|
+
role: "identity",
|
|
203
|
+
attributes: ["name", "location", "resource_group_name", "client_id", "principal_id", "tenant_id"],
|
|
204
|
+
canonicalEntityRef: "global:platform_component:azure_managed_identity",
|
|
205
|
+
cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
resourceType: "azurerm_servicebus_namespace",
|
|
209
|
+
entityTypeCode: "platform_component",
|
|
210
|
+
entitySubtypeCode: "platform_service",
|
|
211
|
+
role: "messaging",
|
|
212
|
+
attributes: ["name", "location", "resource_group_name", "sku", "public_network_access_enabled"],
|
|
213
|
+
canonicalEntityRef: "global:platform_component:azure_service_bus",
|
|
214
|
+
cloudResourceType: "Microsoft.ServiceBus/namespaces",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
resourceType: "azurerm_servicebus_queue",
|
|
218
|
+
entityTypeCode: "platform_component",
|
|
219
|
+
entitySubtypeCode: "platform_service",
|
|
220
|
+
role: "messaging",
|
|
221
|
+
attributes: ["name", "namespace_id", "max_delivery_count", "requires_session"],
|
|
222
|
+
references: { "namespace_id": "azurerm_servicebus_namespace" },
|
|
223
|
+
canonicalEntityRef: "global:platform_component:azure_service_bus",
|
|
224
|
+
cloudResourceType: "Microsoft.ServiceBus/namespaces/queues",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
resourceType: "azurerm_private_dns_zone",
|
|
228
|
+
entityTypeCode: "platform_component",
|
|
229
|
+
entitySubtypeCode: "platform_service",
|
|
230
|
+
role: "network_boundary",
|
|
231
|
+
attributes: ["name", "resource_group_name", "number_of_record_sets"],
|
|
232
|
+
canonicalEntityRef: "global:platform_component:azure_private_dns",
|
|
233
|
+
cloudResourceType: "Microsoft.Network/privateDnsZones",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
resourceType: "azurerm_private_endpoint",
|
|
237
|
+
entityTypeCode: "platform_component",
|
|
238
|
+
entitySubtypeCode: "platform_service",
|
|
239
|
+
role: "network_boundary",
|
|
240
|
+
attributes: ["name", "location", "resource_group_name", "subnet_id"],
|
|
241
|
+
references: { "subnet_id": "azurerm_subnet" },
|
|
242
|
+
canonicalEntityRef: "global:platform_component:azure_private_link",
|
|
243
|
+
cloudResourceType: "Microsoft.Network/privateEndpoints",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
resourceType: "azurerm_subnet",
|
|
247
|
+
entityTypeCode: "platform_component",
|
|
248
|
+
entitySubtypeCode: "platform_service",
|
|
249
|
+
role: "network_boundary",
|
|
250
|
+
attributes: ["name", "resource_group_name", "virtual_network_name", "address_prefixes", "default_outbound_access_enabled", "private_endpoint_network_policies"],
|
|
251
|
+
nameReferences: { "virtual_network_name": "azurerm_virtual_network" },
|
|
252
|
+
canonicalEntityRef: "global:platform_component:azure_virtual_network",
|
|
253
|
+
cloudResourceType: "Microsoft.Network/virtualNetworks/subnets",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
resourceType: "azurerm_virtual_network",
|
|
257
|
+
entityTypeCode: "platform_component",
|
|
258
|
+
entitySubtypeCode: "platform_service",
|
|
259
|
+
role: "network_boundary",
|
|
260
|
+
attributes: ["name", "location", "resource_group_name", "address_space", "guid"],
|
|
261
|
+
canonicalEntityRef: "global:platform_component:azure_virtual_network",
|
|
262
|
+
cloudResourceType: "Microsoft.Network/virtualNetworks",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
resourceType: "azurerm_log_analytics_workspace",
|
|
266
|
+
entityTypeCode: "platform_component",
|
|
267
|
+
entitySubtypeCode: "platform_service",
|
|
268
|
+
role: "observability",
|
|
269
|
+
attributes: ["name", "location", "resource_group_name", "sku", "retention_in_days", "workspace_id"],
|
|
270
|
+
canonicalEntityRef: "global:platform_component:azure_monitor",
|
|
271
|
+
cloudResourceType: "Microsoft.OperationalInsights/workspaces",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
resourceType: "azurerm_container_registry",
|
|
275
|
+
entityTypeCode: "platform_component",
|
|
276
|
+
entitySubtypeCode: "platform_service",
|
|
277
|
+
role: "registry",
|
|
278
|
+
attributes: ["name", "location", "resource_group_name", "login_server", "sku", "admin_enabled", "public_network_access_enabled"],
|
|
279
|
+
canonicalEntityRef: "global:platform_component:azure_container_registry",
|
|
280
|
+
cloudResourceType: "Microsoft.ContainerRegistry/registries",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
resourceType: "azurerm_resource_group",
|
|
284
|
+
entityTypeCode: "platform_component",
|
|
285
|
+
entitySubtypeCode: "platform_service",
|
|
286
|
+
role: "resource_group",
|
|
287
|
+
attributes: ["name", "location"],
|
|
288
|
+
canonicalEntityRef: "global:platform_component:azure_resource_group",
|
|
289
|
+
cloudResourceType: "Microsoft.Resources/resourceGroups",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
resourceType: "azurerm_key_vault",
|
|
293
|
+
entityTypeCode: "platform_component",
|
|
294
|
+
entitySubtypeCode: "platform_service",
|
|
295
|
+
role: "secret_store",
|
|
296
|
+
attributes: ["name", "location", "resource_group_name", "vault_uri", "sku_name", "enable_rbac_authorization", "rbac_authorization_enabled", "public_network_access_enabled", "purge_protection_enabled", "soft_delete_retention_days"],
|
|
297
|
+
canonicalEntityRef: "global:platform_component:azure_key_vault",
|
|
298
|
+
cloudResourceType: "Microsoft.KeyVault/vaults",
|
|
299
|
+
},
|
|
300
|
+
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { callMcpTool } from "./mcp.js";
|
|
2
|
+
import { IAC_CATALOGUE_SEED } from "./iac-catalogue-seed.js";
|
|
3
|
+
/**
|
|
4
|
+
* Rejects a payload that would widen what may be read.
|
|
5
|
+
*
|
|
6
|
+
* A served catalogue is a network response, and this one decides what leaves
|
|
7
|
+
* the machine. An entry with no attributes is meaningless — it would project a
|
|
8
|
+
* resource stripped of all evidence — and an entry missing its type mapping
|
|
9
|
+
* cannot be applied at all, so neither is accepted quietly. Terraform's own
|
|
10
|
+
* `sensitive_values` gate still runs behind whatever survives here.
|
|
11
|
+
*/
|
|
12
|
+
function usableEntries(payload) {
|
|
13
|
+
return (payload.entries ?? []).filter((entry) => typeof entry?.resourceType === "string" &&
|
|
14
|
+
typeof entry?.entityTypeCode === "string" &&
|
|
15
|
+
Array.isArray(entry?.attributes) &&
|
|
16
|
+
entry.attributes.length > 0 &&
|
|
17
|
+
entry.attributes.every((attribute) => typeof attribute === "string"));
|
|
18
|
+
}
|
|
19
|
+
export async function loadIacCatalogue() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await callMcpTool("nexarch_get_iac_catalogue", {});
|
|
22
|
+
const payload = JSON.parse(raw.content?.[0]?.text ?? "{}");
|
|
23
|
+
const entries = usableEntries(payload);
|
|
24
|
+
// An empty or unparseable response is not a reason to ingest nothing: the
|
|
25
|
+
// bundled copy is known-good and was generated from this same table.
|
|
26
|
+
if (entries.length === 0) {
|
|
27
|
+
return { entries: IAC_CATALOGUE_SEED, source: "bundled", reason: "the platform returned no usable entries" };
|
|
28
|
+
}
|
|
29
|
+
return { entries, source: "platform", reason: null };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
return { entries: IAC_CATALOGUE_SEED, source: "bundled", reason: message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** One line describing which catalogue was used, so a stale ingest is never silent. */
|
|
37
|
+
export function describeCatalogue(result) {
|
|
38
|
+
if (result.source === "platform")
|
|
39
|
+
return `Catalogue: ${result.entries.length} resource types from the platform.`;
|
|
40
|
+
return `Catalogue: ${result.entries.length} resource types from the bundled copy — ${result.reason}.\n Types added to the platform since this CLI was published will not be recognised.`;
|
|
41
|
+
}
|