@intentius/chant-lexicon-azure 0.124.52 → 0.124.55

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "d37be4f0fa4234cd078e74b706321a00c03670354d86196d0e8c3838b05d03d1",
4
+ "manifest.json": "ed16dc12ffb0921b68b72c1e81b2981c39030cdbd0841989888c975e470232b2",
5
5
  "meta.json": "d2521221ed616d882ded4296f0071685bd74e4b645d55dabc9685733c37dc419",
6
6
  "types/index.d.ts": "29631032bda9533849178968290e65d72250eab0b8bab424f2d9ef9b4e8546a4",
7
7
  "rules/hardcoded-location.ts": "0b895449c8c94066e81721c46be24249231029fe13b471477456df5c4e45b8bf",
@@ -37,5 +37,5 @@
37
37
  "skills/chant-azure-patterns.md": "2d8cb8bbeda73b695856cec4ddaf5b3a0efce76e4ee6e3cf14b90ca69dee5757",
38
38
  "skills/chant-azure-aks.md": "2d4e0098c1a22b54ffadd410564d9df0f3a04cb4eb7c6261f20ae33106e7aca8"
39
39
  },
40
- "composite": "098b5964e22cd9fee3590356f60392cf51b47f3927c1ad388ae159196da54da9"
40
+ "composite": "c4984d5df82100b89d6749649d6f80c691a939db21a4b10dc450216f827f743c"
41
41
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Resource Graph's type names as azure lexicon kinds (#4521).
3
+ *
4
+ * behold#1029 lists Azure through Resource Graph, which lowercases every
5
+ * type and joins a child to its parent with `/`
6
+ * (`microsoft.network/virtualnetworks/subnets`). The lexicon's kinds come
7
+ * from the ARM schema ids, mixed case with `_` between a child and its parent
8
+ * (`Microsoft.Network/virtualNetworks_subnets`). So the mapping is a lookup:
9
+ * every kind, through {@link armType} and lowercased, is the listed type it
10
+ * answers to. ARM type names are case-insensitive, so no two kinds share one.
11
+ *
12
+ * A Resource Graph row has ARM's resource shape (`properties` beside
13
+ * `location`, `sku`, `identity`), and the deep reader flattens that shape
14
+ * before the hooks run (./deep-observe.ts `buildLiveProperties`), so an
15
+ * indexed row is flattened the same way.
16
+ */
17
+ import type { ListedTypeMapping } from "@intentius/chant/lifecycle/type-listing";
18
+ /** `Microsoft.Network/virtualNetworks_subnets` as Resource Graph spells it: `microsoft.network/virtualnetworks/subnets`. */
19
+ export declare function azureListedTypeForKind(kind: string): string;
20
+ /** Listed type to kind, over a set of kinds. */
21
+ export declare function azureListedTypeIndex(kinds: Iterable<string>): Map<string, string>;
22
+ /**
23
+ * The azure kind for a Resource Graph type, or undefined. Without a registry
24
+ * nothing maps: Resource Graph's lowercase spelling cannot be turned back
25
+ * into the kind's casing by rule.
26
+ */
27
+ export declare function azureKindForListedType(listedType: string, index?: Map<string, string> | null): string | undefined;
28
+ /** A Resource Graph row in the tree the deep reader compares. */
29
+ export declare function azureObservedTree(properties: Record<string, unknown>): Record<string, unknown>;
30
+ export declare const azureListedTypes: ListedTypeMapping;
31
+ //# sourceMappingURL=listed-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listed-types.d.ts","sourceRoot":"","sources":["../src/listed-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAKjF,4HAA4H;AAC5H,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,gDAAgD;AAChD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAIjF;AAeD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,GAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAuB,GAAG,MAAM,GAAG,SAAS,CAEnI;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE9F;AAED,eAAO,MAAM,gBAAgB,EAAE,iBAI9B,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { armType } from "./arm-type.js";
2
+ import { buildLiveProperties } from "./deep-observe.js";
3
+ import { loadApiVersions } from "./serializer.js";
4
+ /** `Microsoft.Network/virtualNetworks_subnets` as Resource Graph spells it: `microsoft.network/virtualnetworks/subnets`. */
5
+ export function azureListedTypeForKind(kind) {
6
+ return armType(kind).toLowerCase();
7
+ }
8
+ /** Listed type to kind, over a set of kinds. */
9
+ export function azureListedTypeIndex(kinds) {
10
+ const out = new Map();
11
+ for (const kind of kinds)
12
+ out.set(azureListedTypeForKind(kind), kind);
13
+ return out;
14
+ }
15
+ let cachedIndex;
16
+ /** Built from the generated registry, or null in a checkout that has not generated. */
17
+ function generatedIndex() {
18
+ if (cachedIndex !== undefined)
19
+ return cachedIndex;
20
+ try {
21
+ cachedIndex = azureListedTypeIndex(loadApiVersions().keys());
22
+ }
23
+ catch {
24
+ cachedIndex = null;
25
+ }
26
+ return cachedIndex;
27
+ }
28
+ /**
29
+ * The azure kind for a Resource Graph type, or undefined. Without a registry
30
+ * nothing maps: Resource Graph's lowercase spelling cannot be turned back
31
+ * into the kind's casing by rule.
32
+ */
33
+ export function azureKindForListedType(listedType, index = generatedIndex()) {
34
+ return index?.get(listedType.toLowerCase());
35
+ }
36
+ /** A Resource Graph row in the tree the deep reader compares. */
37
+ export function azureObservedTree(properties) {
38
+ return buildLiveProperties(properties);
39
+ }
40
+ export const azureListedTypes = {
41
+ substrate: "azure",
42
+ kindFor: (listedType) => azureKindForListedType(listedType),
43
+ observedTree: (properties) => azureObservedTree(properties),
44
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azure",
3
- "version": "0.124.52",
3
+ "version": "0.124.55",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Azure",
6
6
  "intrinsics": [
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,0BAA0B,CAAC;AA2B5E;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,aAoezB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,0BAA0B,CAAC;AA6B5E;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,aA2ezB,CAAC"}
package/dist/plugin.js CHANGED
@@ -9,6 +9,8 @@ import { join, dirname } from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import { moduleSourceDir } from "@intentius/chant/module-source";
11
11
  import { azureSerializer } from "./serializer.js";
12
+ import { azureListedTypes } from "./listed-types.js";
13
+ import { azureReferenceCatalog } from "./reference-catalog.js";
12
14
  import { azureGraphEdges } from "./graph-edges.js";
13
15
  import { hardcodedLocationRule } from "./lint/rules/hardcoded-location.js";
14
16
  import { storageHttpsRule } from "./lint/rules/storage-https.js";
@@ -465,6 +467,11 @@ export const tags = defaultTags([
465
467
  },
466
468
  /** The noise rules the deep pass applies to both the live and declared trees. */
467
469
  deepNormalizationHooks: azureDeepNormalizationHooks,
470
+ // Cloud inventory type names to kinds, for an indexed resource (#4521).
471
+ listedTypes: azureListedTypes,
472
+ // How its resources reference each other, for chant's catalog pass over an
473
+ // index and a declared graph (#4535; behold#1005 retires behold's matcher on it).
474
+ referenceCatalog: azureReferenceCatalog,
468
475
  /** Runtime health (#4177): Container Apps. Implementation in ./health.ts. */
469
476
  healthTypes: ["Microsoft.App/containerApps"],
470
477
  async observeHealth(options) {
@@ -0,0 +1,43 @@
1
+ import type { ReferenceCatalog } from "@intentius/chant/graph-refs";
2
+ /**
3
+ * Azure reference catalog (#4535): how Azure resources name each other, so
4
+ * chant's catalog pass (`reconstructIndexedEdges`, #4522) draws their edges
5
+ * over an index and over a declared graph, and behold can retire its name
6
+ * matcher (behold#1005).
7
+ *
8
+ * ## Shape
9
+ *
10
+ * The paths are the flattened ARM shape: `properties.*` beside `location`,
11
+ * `sku` and `identity`. That is the tree a Resource Graph row becomes
12
+ * (./listed-types.ts `azureObservedTree`), the tree the deep reader compares
13
+ * (./deep-observe.ts `buildLiveProperties`), and the shape the lexicon's
14
+ * declared props already have, so one rule reads all three. Sub-resources
15
+ * inside a row (`ipConfigurations[]`, `frontendIPConfigurations[]`) keep
16
+ * their own `properties` level, because ARM nests them that way.
17
+ *
18
+ * ## Values are ARM resource ids
19
+ *
20
+ * An Azure reference is a resource id. An indexed node is matched by its
21
+ * native id, which for Azure is the row's `id`, so the targets need no
22
+ * identity rule for it. The identities below are the other values a
23
+ * reference reads: a workspace's `customerId`, an app's FQDN.
24
+ *
25
+ * Over a declared graph the same paths usually hold ARM expressions
26
+ * (`[resourceId(...)]`), which match no node; the lexicon's `graphEdges`
27
+ * hook (./graph-edges.ts) already draws those references from the
28
+ * expressions themselves.
29
+ *
30
+ * ## What this does not do
31
+ *
32
+ * - Matching is exact. ARM compares ids case-insensitively, and a reference
33
+ * can spell `resourceGroups` where the target's id spells
34
+ * `resourcegroups`. Such a reference is dangling, never joined to the
35
+ * wrong node.
36
+ * - `identity.userAssignedIdentities` names its identities in object keys,
37
+ * which `readPath` cannot read. A Container App's identity edge comes from
38
+ * `graphEdges` on a declared graph and from nothing over an index.
39
+ * - A subnet's parent VNet is in the subnet's own id. No attribute names it,
40
+ * so there is no containment rule for it.
41
+ */
42
+ export declare const azureReferenceCatalog: ReferenceCatalog;
43
+ //# sourceMappingURL=reference-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-catalog.d.ts","sourceRoot":"","sources":["../src/reference-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,qBAAqB,EAAE,gBAyCnC,CAAC"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Azure reference catalog (#4535): how Azure resources name each other, so
3
+ * chant's catalog pass (`reconstructIndexedEdges`, #4522) draws their edges
4
+ * over an index and over a declared graph, and behold can retire its name
5
+ * matcher (behold#1005).
6
+ *
7
+ * ## Shape
8
+ *
9
+ * The paths are the flattened ARM shape: `properties.*` beside `location`,
10
+ * `sku` and `identity`. That is the tree a Resource Graph row becomes
11
+ * (./listed-types.ts `azureObservedTree`), the tree the deep reader compares
12
+ * (./deep-observe.ts `buildLiveProperties`), and the shape the lexicon's
13
+ * declared props already have, so one rule reads all three. Sub-resources
14
+ * inside a row (`ipConfigurations[]`, `frontendIPConfigurations[]`) keep
15
+ * their own `properties` level, because ARM nests them that way.
16
+ *
17
+ * ## Values are ARM resource ids
18
+ *
19
+ * An Azure reference is a resource id. An indexed node is matched by its
20
+ * native id, which for Azure is the row's `id`, so the targets need no
21
+ * identity rule for it. The identities below are the other values a
22
+ * reference reads: a workspace's `customerId`, an app's FQDN.
23
+ *
24
+ * Over a declared graph the same paths usually hold ARM expressions
25
+ * (`[resourceId(...)]`), which match no node; the lexicon's `graphEdges`
26
+ * hook (./graph-edges.ts) already draws those references from the
27
+ * expressions themselves.
28
+ *
29
+ * ## What this does not do
30
+ *
31
+ * - Matching is exact. ARM compares ids case-insensitively, and a reference
32
+ * can spell `resourceGroups` where the target's id spells
33
+ * `resourcegroups`. Such a reference is dangling, never joined to the
34
+ * wrong node.
35
+ * - `identity.userAssignedIdentities` names its identities in object keys,
36
+ * which `readPath` cannot read. A Container App's identity edge comes from
37
+ * `graphEdges` on a declared graph and from nothing over an index.
38
+ * - A subnet's parent VNet is in the subnet's own id. No attribute names it,
39
+ * so there is no containment rule for it.
40
+ */
41
+ export const azureReferenceCatalog = {
42
+ identities: [
43
+ { kind: "Microsoft.OperationalInsights/workspaces", ids: ["customerId"] },
44
+ { kind: "Microsoft.App/containerApps", ids: ["configuration.ingress.fqdn", "latestRevisionFqdn"] },
45
+ { kind: "Microsoft.App/managedEnvironments", ids: ["defaultDomain", "staticIp"] },
46
+ { kind: "Microsoft.Network/publicIPAddresses", ids: ["ipAddress", "dnsSettings.fqdn"] },
47
+ { kind: "Microsoft.Web/sites", ids: ["defaultHostName"] },
48
+ ],
49
+ refs: [
50
+ // ── containment (→ boundary boxes) ──
51
+ { from: "Microsoft.App/managedEnvironments", path: "vnetConfiguration.infrastructureSubnetId", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
52
+ { from: "Microsoft.App/containerApps", path: "managedEnvironmentId", targetKind: "Microsoft.App/managedEnvironments", relation: "containment", label: "in environment" },
53
+ // The newer API versions write the same id under this name too.
54
+ { from: "Microsoft.App/containerApps", path: "environmentId", targetKind: "Microsoft.App/managedEnvironments", relation: "containment", label: "in environment" },
55
+ { from: "Microsoft.Network/networkInterfaces", path: "ipConfigurations[].properties.subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
56
+ { from: "Microsoft.Network/privateEndpoints", path: "subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
57
+ { from: "Microsoft.ContainerService/managedClusters", path: "agentPoolProfiles[].vnetSubnetID", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
58
+ { from: "Microsoft.Web/sites", path: "virtualNetworkSubnetId", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
59
+ { from: "Microsoft.ContainerInstance/containerGroups", path: "subnetIds[].id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
60
+ { from: "Microsoft.Network/applicationGateways", path: "gatewayIPConfigurations[].properties.subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
61
+ { from: "Microsoft.Network/loadBalancers", path: "frontendIPConfigurations[].properties.subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
62
+ // ── references (→ edges) ──
63
+ // A managed environment names its workspace by the workspace's customer
64
+ // id, not its resource id.
65
+ { from: "Microsoft.App/managedEnvironments", path: "appLogsConfiguration.logAnalyticsConfiguration.customerId", targetKind: "Microsoft.OperationalInsights/workspaces", relation: "reference", label: "logs to" },
66
+ { from: "Microsoft.App/containerApps", path: "configuration.registries[].identity", targetKind: "Microsoft.ManagedIdentity/userAssignedIdentities", relation: "reference", label: "pulls as", optional: true },
67
+ { from: "Microsoft.Network/virtualNetworks_subnets", path: "networkSecurityGroup.id", targetKind: "Microsoft.Network/networkSecurityGroups", relation: "reference", label: "nsg" },
68
+ { from: "Microsoft.Network/virtualNetworks_subnets", path: "routeTable.id", targetKind: "Microsoft.Network/routeTables", relation: "reference", label: "routes" },
69
+ { from: "Microsoft.Network/virtualNetworks_subnets", path: "natGateway.id", targetKind: "Microsoft.Network/natGateways", relation: "reference", label: "egress via" },
70
+ // A VNet row from Resource Graph carries its subnets inline.
71
+ { from: "Microsoft.Network/virtualNetworks", path: "subnets[].properties.networkSecurityGroup.id", targetKind: "Microsoft.Network/networkSecurityGroups", relation: "reference", label: "nsg" },
72
+ { from: "Microsoft.Network/virtualNetworks", path: "subnets[].properties.routeTable.id", targetKind: "Microsoft.Network/routeTables", relation: "reference", label: "routes" },
73
+ { from: "Microsoft.Network/networkInterfaces", path: "networkSecurityGroup.id", targetKind: "Microsoft.Network/networkSecurityGroups", relation: "reference", label: "nsg" },
74
+ { from: "Microsoft.Network/networkInterfaces", path: "ipConfigurations[].properties.publicIPAddress.id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
75
+ { from: "Microsoft.Network/natGateways", path: "publicIpAddresses[].id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
76
+ { from: "Microsoft.Network/loadBalancers", path: "frontendIPConfigurations[].properties.publicIPAddress.id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
77
+ { from: "Microsoft.Network/applicationGateways", path: "frontendIPConfigurations[].properties.publicIPAddress.id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
78
+ { from: "Microsoft.Compute/virtualMachines", path: "networkProfile.networkInterfaces[].id", targetKind: "Microsoft.Network/networkInterfaces", relation: "reference", label: "nic" },
79
+ { from: "Microsoft.Web/sites", path: "serverFarmId", targetKind: "Microsoft.Web/serverfarms", relation: "reference", label: "on plan" },
80
+ ],
81
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-azure",
3
- "version": "0.124.52",
3
+ "version": "0.124.55",
4
4
  "description": "Azure lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -79,6 +79,6 @@
79
79
  "typescript": "^5.9.3"
80
80
  },
81
81
  "peerDependencies": {
82
- "@intentius/chant": "^0.124.52"
82
+ "@intentius/chant": "^0.124.55"
83
83
  }
84
84
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * #4521 on the azure lexicon: Resource Graph's lowercase type names mapped to
3
+ * kinds, and a fixture listing typed into nodes through the azure hooks.
4
+ */
5
+ import { readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { describe, expect, it } from "vitest";
8
+ import { typeListing, type ListingLexicon, type ListingResult } from "@intentius/chant/lifecycle/type-listing";
9
+ import { MASKED } from "@intentius/chant/deep-observation";
10
+ import { azureKindForListedType, azureListedTypeForKind, azureListedTypeIndex } from "./listed-types";
11
+ import { azurePlugin } from "./plugin";
12
+
13
+ const here = import.meta.dirname;
14
+ const surface = JSON.parse(readFileSync(join(here, "..", "surface.snapshot.json"), "utf-8")) as {
15
+ entries: Record<string, { kind: string; resourceType: string }>;
16
+ };
17
+ // A kind is a resource type; the surface can name one type under two exports
18
+ // (AppService and WebApp are both Microsoft.Web/sites), so the kinds are a set.
19
+ const KINDS = [...new Set(Object.values(surface.entries).filter((e) => e.kind === "resource").map((e) => e.resourceType))];
20
+ const INDEX = azureListedTypeIndex(KINDS);
21
+ const listing = JSON.parse(readFileSync(join(here, "testdata", "indexed", "listing.json"), "utf-8")) as ListingResult;
22
+
23
+ const azure: ListingLexicon = {
24
+ name: azurePlugin.name,
25
+ deepNormalizationHooks: azurePlugin.deepNormalizationHooks,
26
+ listedTypes: { ...azurePlugin.listedTypes!, kindFor: (t) => azureKindForListedType(t, INDEX) },
27
+ };
28
+
29
+ describe("Resource Graph types to azure kinds", () => {
30
+ it("every azure resource kind has a lowercase ARM type that maps back to it, and no two kinds share one", () => {
31
+ const missing = KINDS.filter((k) => azureKindForListedType(azureListedTypeForKind(k), INDEX) !== k);
32
+ expect(missing).toEqual([]);
33
+ expect(INDEX.size).toBe(KINDS.length);
34
+ });
35
+
36
+ it("child types join with / as ARM spells them, in any case", () => {
37
+ expect(azureListedTypeForKind("Microsoft.Network/virtualNetworks_subnets")).toBe("microsoft.network/virtualnetworks/subnets");
38
+ expect(azureKindForListedType("microsoft.storage/storageaccounts", INDEX)).toBe("Microsoft.Storage/storageAccounts");
39
+ expect(azureKindForListedType("Microsoft.Storage/storageAccounts", INDEX)).toBe("Microsoft.Storage/storageAccounts");
40
+ expect(azureKindForListedType("microsoft.contoso/widgets", INDEX)).toBeUndefined();
41
+ });
42
+ });
43
+
44
+ describe("the azure fixture listing as typed nodes", () => {
45
+ const doc = typeListing(listing, [azure]);
46
+
47
+ it("produces typed nodes, and keeps the unmapped type as an unknown-kind node", () => {
48
+ expect(doc.graph.nodes.map((n) => [n.listedType, n.kind, n.lexicon, n.unmappedKind ?? false])).toEqual([
49
+ ["microsoft.contoso/widgets", "microsoft.contoso/widgets", "azure", true],
50
+ ["microsoft.network/virtualnetworks/subnets", "Microsoft.Network/virtualNetworks_subnets", "azure", false],
51
+ ["microsoft.storage/storageaccounts", "Microsoft.Storage/storageAccounts", "azure", false],
52
+ ]);
53
+ });
54
+
55
+ it("flattens properties as the deep reader does, and prunes what the azure hooks prune", () => {
56
+ const st = doc.graph.nodes.find((n) => n.listedType === "microsoft.storage/storageaccounts")!;
57
+ expect(st.attrs.minimumTlsVersion).toBe("TLS1_2");
58
+ expect(st.attrs.sku).toEqual({ name: "Standard_LRS", tier: "Standard" });
59
+ expect(st.attrs.properties).toBeUndefined();
60
+ expect(st.attrs.id).toBeUndefined();
61
+ expect(st.attrs.provisioningState).toBeUndefined();
62
+ });
63
+
64
+ it("masks a secret on an unmapped node too", () => {
65
+ const w = doc.graph.nodes.find((n) => n.unmappedKind)!;
66
+ expect((w.attrs.properties as Record<string, unknown>).connectionString).toBe(MASKED);
67
+ });
68
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Resource Graph's type names as azure lexicon kinds (#4521).
3
+ *
4
+ * behold#1029 lists Azure through Resource Graph, which lowercases every
5
+ * type and joins a child to its parent with `/`
6
+ * (`microsoft.network/virtualnetworks/subnets`). The lexicon's kinds come
7
+ * from the ARM schema ids, mixed case with `_` between a child and its parent
8
+ * (`Microsoft.Network/virtualNetworks_subnets`). So the mapping is a lookup:
9
+ * every kind, through {@link armType} and lowercased, is the listed type it
10
+ * answers to. ARM type names are case-insensitive, so no two kinds share one.
11
+ *
12
+ * A Resource Graph row has ARM's resource shape (`properties` beside
13
+ * `location`, `sku`, `identity`), and the deep reader flattens that shape
14
+ * before the hooks run (./deep-observe.ts `buildLiveProperties`), so an
15
+ * indexed row is flattened the same way.
16
+ */
17
+ import type { ListedTypeMapping } from "@intentius/chant/lifecycle/type-listing";
18
+ import { armType } from "./arm-type";
19
+ import { buildLiveProperties, type ArmResourceShowResponse } from "./deep-observe";
20
+ import { loadApiVersions } from "./serializer";
21
+
22
+ /** `Microsoft.Network/virtualNetworks_subnets` as Resource Graph spells it: `microsoft.network/virtualnetworks/subnets`. */
23
+ export function azureListedTypeForKind(kind: string): string {
24
+ return armType(kind).toLowerCase();
25
+ }
26
+
27
+ /** Listed type to kind, over a set of kinds. */
28
+ export function azureListedTypeIndex(kinds: Iterable<string>): Map<string, string> {
29
+ const out = new Map<string, string>();
30
+ for (const kind of kinds) out.set(azureListedTypeForKind(kind), kind);
31
+ return out;
32
+ }
33
+
34
+ let cachedIndex: Map<string, string> | null | undefined;
35
+
36
+ /** Built from the generated registry, or null in a checkout that has not generated. */
37
+ function generatedIndex(): Map<string, string> | null {
38
+ if (cachedIndex !== undefined) return cachedIndex;
39
+ try {
40
+ cachedIndex = azureListedTypeIndex(loadApiVersions().keys());
41
+ } catch {
42
+ cachedIndex = null;
43
+ }
44
+ return cachedIndex;
45
+ }
46
+
47
+ /**
48
+ * The azure kind for a Resource Graph type, or undefined. Without a registry
49
+ * nothing maps: Resource Graph's lowercase spelling cannot be turned back
50
+ * into the kind's casing by rule.
51
+ */
52
+ export function azureKindForListedType(listedType: string, index: Map<string, string> | null = generatedIndex()): string | undefined {
53
+ return index?.get(listedType.toLowerCase());
54
+ }
55
+
56
+ /** A Resource Graph row in the tree the deep reader compares. */
57
+ export function azureObservedTree(properties: Record<string, unknown>): Record<string, unknown> {
58
+ return buildLiveProperties(properties as ArmResourceShowResponse);
59
+ }
60
+
61
+ export const azureListedTypes: ListedTypeMapping = {
62
+ substrate: "azure",
63
+ kindFor: (listedType) => azureKindForListedType(listedType),
64
+ observedTree: (properties) => azureObservedTree(properties),
65
+ };
package/src/plugin.ts CHANGED
@@ -15,6 +15,8 @@ import { join, dirname } from "path";
15
15
  import { fileURLToPath } from "url";
16
16
  import { moduleSourceDir } from "@intentius/chant/module-source";
17
17
  import { azureSerializer } from "./serializer";
18
+ import { azureListedTypes } from "./listed-types";
19
+ import { azureReferenceCatalog } from "./reference-catalog";
18
20
  import { azureGraphEdges } from "./graph-edges";
19
21
  import { hardcodedLocationRule } from "./lint/rules/hardcoded-location";
20
22
  import { storageHttpsRule } from "./lint/rules/storage-https";
@@ -495,6 +497,13 @@ export const tags = defaultTags([
495
497
  /** The noise rules the deep pass applies to both the live and declared trees. */
496
498
  deepNormalizationHooks: azureDeepNormalizationHooks,
497
499
 
500
+ // Cloud inventory type names to kinds, for an indexed resource (#4521).
501
+ listedTypes: azureListedTypes,
502
+
503
+ // How its resources reference each other, for chant's catalog pass over an
504
+ // index and a declared graph (#4535; behold#1005 retires behold's matcher on it).
505
+ referenceCatalog: azureReferenceCatalog,
506
+
498
507
  /** Runtime health (#4177): Container Apps. Implementation in ./health.ts. */
499
508
  healthTypes: ["Microsoft.App/containerApps"],
500
509
 
@@ -0,0 +1,144 @@
1
+ /**
2
+ * #4535: the azure reference catalog. Each rule draws its edge on a fixture
3
+ * of its own, the catalog keeps behold#1023's ruling (typed rules, no edge on
4
+ * ambiguity, scoped join keys), and a Container Apps site in Resource Graph's
5
+ * shape (the resources example-three-cloud's azure member declares) gets its
6
+ * topology.
7
+ */
8
+ import { describe, expect, it } from "vitest";
9
+ import type { IRNode } from "@intentius/chant/graph-ir";
10
+ import { reconstructIndexedEdges, refRuleId, type ReferenceCatalog } from "@intentius/chant/graph-refs";
11
+ import { azureReferenceCatalog } from "./reference-catalog";
12
+ import { azureObservedTree } from "./listed-types";
13
+ import { azurePlugin } from "./plugin";
14
+
15
+ const LEXICON = "azure";
16
+ const SUB = "/subscriptions/00000000-0000-0000-0000-000000000001";
17
+ const RG = `${SUB}/resourceGroups/local`;
18
+
19
+ /** attrs with `value` written at a catalog path (`a.b`, `arr[].c`). */
20
+ function atPath(path: string, value: string): Record<string, unknown> {
21
+ const parts = path.split(".");
22
+ const build = (i: number): unknown => {
23
+ if (i === parts.length) return value;
24
+ const part = parts[i];
25
+ const isArr = part.endsWith("[]");
26
+ const inner = build(i + 1);
27
+ return { [isArr ? part.slice(0, -2) : part]: isArr ? [inner] : inner };
28
+ };
29
+ return build(0) as Record<string, unknown>;
30
+ }
31
+
32
+ /** A node of `kind` named `value`: by its native id (an ARM id), or by the identity path a non-id reference reads. */
33
+ function target(catalog: ReferenceCatalog, kind: string, id: string, value: string, byIdentity: boolean): IRNode {
34
+ const path = catalog.identities.find((r) => r.kind === kind)?.ids[0];
35
+ return byIdentity && path ? { id, kind, lexicon: LEXICON, attrs: atPath(path, value) } : { id, kind, lexicon: LEXICON, attrs: {}, nativeId: value };
36
+ }
37
+
38
+ /** A Resource Graph row as a node: flattened as ./listed-types.ts flattens it, named by its `id`. */
39
+ function row(kind: string, id: string, properties: Record<string, unknown>, extra: Record<string, unknown> = {}): IRNode {
40
+ return { id, kind, lexicon: LEXICON, nativeId: id, location: "westeurope", attrs: azureObservedTree({ name: id.split("/").pop(), location: "westeurope", properties, ...extra }) };
41
+ }
42
+
43
+ const scope = (n: IRNode) => ({ account: `azure${SUB}`, ...(n.location ? { region: n.location } : {}) });
44
+
45
+ describe("azure reference catalog", () => {
46
+ it("is the plugin's referenceCatalog", () => {
47
+ expect(azurePlugin.referenceCatalog).toBe(azureReferenceCatalog);
48
+ });
49
+
50
+ it("names a target kind on every rule (behold#1023's ruling)", () => {
51
+ expect(azureReferenceCatalog.refs.filter((r) => !(r.targetKind ?? r.indexedTargetKind))).toEqual([]);
52
+ });
53
+
54
+ it("has no two rules for one holder kind, path and target kind", () => {
55
+ const keys = azureReferenceCatalog.refs.map((r) => `${r.from}|${r.path}|${r.targetKind}`);
56
+ expect(keys.filter((k, i) => keys.indexOf(k) !== i)).toEqual([]);
57
+ });
58
+
59
+ describe("each rule draws its edge", () => {
60
+ for (const [i, rule] of azureReferenceCatalog.refs.entries()) {
61
+ const to = rule.targetKind ?? rule.indexedTargetKind!;
62
+ it(`${rule.from} ${rule.path} -> ${to}`, () => {
63
+ // An `.id` or `*Id` path holds an ARM id; anything else (a customer
64
+ // id, an identity's id in a registry entry) names the target by a
65
+ // value its identity rule lists.
66
+ const byId = /(^|\.)(id|[A-Za-z]+Id|[A-Za-z]+ID)$/.test(rule.path) || rule.path.endsWith("[].id");
67
+ const value = byId ? `${RG}/providers/${to.replace("_", "/")}/target-${i}` : `value-${i}`;
68
+ const holder: IRNode = { id: `holder-${i}`, kind: rule.from, lexicon: LEXICON, attrs: atPath(rule.path, value) };
69
+ const nodes = [holder, target(azureReferenceCatalog, to, `target-${i}`, value, !byId)];
70
+ const out = reconstructIndexedEdges(nodes, [{ lexicon: LEXICON, catalog: azureReferenceCatalog }], () => ({ account: "azure/sub" }));
71
+ const edge = out.edges.find((e) => e.from === holder.id && e.to === `target-${i}`);
72
+ expect(edge?.rule).toBe(refRuleId(LEXICON, rule.from, rule.path));
73
+ expect(edge?.relation).toBe(rule.relation);
74
+ });
75
+ }
76
+ });
77
+
78
+ describe("a Container Apps site in Resource Graph's shape", () => {
79
+ const vnet = `${RG}/providers/Microsoft.Network/virtualNetworks/three-cloud-vnet`;
80
+ const subnet = `${vnet}/subnets/apps`;
81
+ const logs = `${RG}/providers/Microsoft.OperationalInsights/workspaces/three-cloud-logs`;
82
+ const identity = `${RG}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/three-cloud-web`;
83
+ const env = `${RG}/providers/Microsoft.App/managedEnvironments/three-cloud`;
84
+ const app = `${RG}/providers/Microsoft.App/containerApps/web`;
85
+ const nodes: IRNode[] = [
86
+ row("Microsoft.Network/virtualNetworks", vnet, { addressSpace: { addressPrefixes: ["10.40.0.0/16"] }, subnets: [{ id: subnet, name: "apps", properties: { addressPrefix: "10.40.0.0/23" } }] }),
87
+ row("Microsoft.Network/virtualNetworks_subnets", subnet, { addressPrefix: "10.40.0.0/23" }),
88
+ row("Microsoft.OperationalInsights/workspaces", logs, { customerId: "6f1c2b3a-0000-4000-8000-000000000001", retentionInDays: 30 }),
89
+ row("Microsoft.ManagedIdentity/userAssignedIdentities", identity, {}),
90
+ row("Microsoft.App/managedEnvironments", env, {
91
+ vnetConfiguration: { infrastructureSubnetId: subnet },
92
+ appLogsConfiguration: { destination: "log-analytics", logAnalyticsConfiguration: { customerId: "6f1c2b3a-0000-4000-8000-000000000001" } },
93
+ }),
94
+ row(
95
+ "Microsoft.App/containerApps",
96
+ app,
97
+ { managedEnvironmentId: env, environmentId: env, configuration: { ingress: { fqdn: "web.three-cloud.westeurope.azurecontainerapps.io", external: true } } },
98
+ { identity: { type: "UserAssigned", userAssignedIdentities: { [identity]: {} } } },
99
+ ),
100
+ ];
101
+ const out = reconstructIndexedEdges(nodes, [{ lexicon: LEXICON, catalog: azureReferenceCatalog }], scope);
102
+ const short = (s: string) => s.split("/").slice(-2).join("/");
103
+
104
+ it("draws the environment's subnet and workspace and the app's environment", () => {
105
+ expect(out.edges.map((e) => `${short(e.from)} -> ${short(e.to)} [${e.viaAttr}] ${e.relation}`)).toEqual([
106
+ "containerApps/web -> managedEnvironments/three-cloud [environmentId] containment",
107
+ "containerApps/web -> managedEnvironments/three-cloud [managedEnvironmentId] containment",
108
+ "managedEnvironments/three-cloud -> subnets/apps [vnetConfiguration] containment",
109
+ "managedEnvironments/three-cloud -> workspaces/three-cloud-logs [appLogsConfiguration] reference",
110
+ ]);
111
+ expect(out.containment.map((c) => `${short(c.child)} in ${short(c.parent)}`)).toEqual([
112
+ "containerApps/web in managedEnvironments/three-cloud",
113
+ "managedEnvironments/three-cloud in subnets/apps",
114
+ ]);
115
+ });
116
+
117
+ it("leaves the identity, named in an object key, without an edge rather than guessing", () => {
118
+ expect(out.edges.some((e) => e.to === identity)).toBe(false);
119
+ expect(out.dangling).toEqual([]);
120
+ });
121
+ });
122
+
123
+ it("does not join a reference to the same id in another subscription", () => {
124
+ const subnet = `${RG}/providers/Microsoft.Network/virtualNetworks/v/subnets/apps`;
125
+ const nodes: IRNode[] = [
126
+ { ...row("Microsoft.App/managedEnvironments", "env", { vnetConfiguration: { infrastructureSubnetId: subnet } }) },
127
+ { ...row("Microsoft.Network/virtualNetworks_subnets", subnet, {}), id: "other-sub-subnet" },
128
+ ];
129
+ const out = reconstructIndexedEdges(nodes, [{ lexicon: LEXICON, catalog: azureReferenceCatalog }], (n) => ({ account: n.id === "env" ? "azure/sub-a" : "azure/sub-b", region: "westeurope" }));
130
+ expect(out.edges).toEqual([]);
131
+ expect(out.dangling.map((d) => d.value)).toEqual([subnet]);
132
+ });
133
+
134
+ it("draws no edge when a customer id names two workspaces, and records both", () => {
135
+ const nodes: IRNode[] = [
136
+ row("Microsoft.App/managedEnvironments", "env", { appLogsConfiguration: { logAnalyticsConfiguration: { customerId: "c-1" } } }),
137
+ row("Microsoft.OperationalInsights/workspaces", "ws-a", { customerId: "c-1" }),
138
+ row("Microsoft.OperationalInsights/workspaces", "ws-b", { customerId: "c-1" }),
139
+ ];
140
+ const out = reconstructIndexedEdges(nodes, [{ lexicon: LEXICON, catalog: azureReferenceCatalog }], scope);
141
+ expect(out.edges).toEqual([]);
142
+ expect(out.ambiguous.map((a) => a.candidates)).toEqual([["ws-a", "ws-b"]]);
143
+ });
144
+ });
@@ -0,0 +1,84 @@
1
+ import type { ReferenceCatalog } from "@intentius/chant/graph-refs";
2
+
3
+ /**
4
+ * Azure reference catalog (#4535): how Azure resources name each other, so
5
+ * chant's catalog pass (`reconstructIndexedEdges`, #4522) draws their edges
6
+ * over an index and over a declared graph, and behold can retire its name
7
+ * matcher (behold#1005).
8
+ *
9
+ * ## Shape
10
+ *
11
+ * The paths are the flattened ARM shape: `properties.*` beside `location`,
12
+ * `sku` and `identity`. That is the tree a Resource Graph row becomes
13
+ * (./listed-types.ts `azureObservedTree`), the tree the deep reader compares
14
+ * (./deep-observe.ts `buildLiveProperties`), and the shape the lexicon's
15
+ * declared props already have, so one rule reads all three. Sub-resources
16
+ * inside a row (`ipConfigurations[]`, `frontendIPConfigurations[]`) keep
17
+ * their own `properties` level, because ARM nests them that way.
18
+ *
19
+ * ## Values are ARM resource ids
20
+ *
21
+ * An Azure reference is a resource id. An indexed node is matched by its
22
+ * native id, which for Azure is the row's `id`, so the targets need no
23
+ * identity rule for it. The identities below are the other values a
24
+ * reference reads: a workspace's `customerId`, an app's FQDN.
25
+ *
26
+ * Over a declared graph the same paths usually hold ARM expressions
27
+ * (`[resourceId(...)]`), which match no node; the lexicon's `graphEdges`
28
+ * hook (./graph-edges.ts) already draws those references from the
29
+ * expressions themselves.
30
+ *
31
+ * ## What this does not do
32
+ *
33
+ * - Matching is exact. ARM compares ids case-insensitively, and a reference
34
+ * can spell `resourceGroups` where the target's id spells
35
+ * `resourcegroups`. Such a reference is dangling, never joined to the
36
+ * wrong node.
37
+ * - `identity.userAssignedIdentities` names its identities in object keys,
38
+ * which `readPath` cannot read. A Container App's identity edge comes from
39
+ * `graphEdges` on a declared graph and from nothing over an index.
40
+ * - A subnet's parent VNet is in the subnet's own id. No attribute names it,
41
+ * so there is no containment rule for it.
42
+ */
43
+ export const azureReferenceCatalog: ReferenceCatalog = {
44
+ identities: [
45
+ { kind: "Microsoft.OperationalInsights/workspaces", ids: ["customerId"] },
46
+ { kind: "Microsoft.App/containerApps", ids: ["configuration.ingress.fqdn", "latestRevisionFqdn"] },
47
+ { kind: "Microsoft.App/managedEnvironments", ids: ["defaultDomain", "staticIp"] },
48
+ { kind: "Microsoft.Network/publicIPAddresses", ids: ["ipAddress", "dnsSettings.fqdn"] },
49
+ { kind: "Microsoft.Web/sites", ids: ["defaultHostName"] },
50
+ ],
51
+ refs: [
52
+ // ── containment (→ boundary boxes) ──
53
+ { from: "Microsoft.App/managedEnvironments", path: "vnetConfiguration.infrastructureSubnetId", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
54
+ { from: "Microsoft.App/containerApps", path: "managedEnvironmentId", targetKind: "Microsoft.App/managedEnvironments", relation: "containment", label: "in environment" },
55
+ // The newer API versions write the same id under this name too.
56
+ { from: "Microsoft.App/containerApps", path: "environmentId", targetKind: "Microsoft.App/managedEnvironments", relation: "containment", label: "in environment" },
57
+ { from: "Microsoft.Network/networkInterfaces", path: "ipConfigurations[].properties.subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
58
+ { from: "Microsoft.Network/privateEndpoints", path: "subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
59
+ { from: "Microsoft.ContainerService/managedClusters", path: "agentPoolProfiles[].vnetSubnetID", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
60
+ { from: "Microsoft.Web/sites", path: "virtualNetworkSubnetId", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
61
+ { from: "Microsoft.ContainerInstance/containerGroups", path: "subnetIds[].id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
62
+ { from: "Microsoft.Network/applicationGateways", path: "gatewayIPConfigurations[].properties.subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
63
+ { from: "Microsoft.Network/loadBalancers", path: "frontendIPConfigurations[].properties.subnet.id", targetKind: "Microsoft.Network/virtualNetworks_subnets", relation: "containment", label: "in subnet" },
64
+
65
+ // ── references (→ edges) ──
66
+ // A managed environment names its workspace by the workspace's customer
67
+ // id, not its resource id.
68
+ { from: "Microsoft.App/managedEnvironments", path: "appLogsConfiguration.logAnalyticsConfiguration.customerId", targetKind: "Microsoft.OperationalInsights/workspaces", relation: "reference", label: "logs to" },
69
+ { from: "Microsoft.App/containerApps", path: "configuration.registries[].identity", targetKind: "Microsoft.ManagedIdentity/userAssignedIdentities", relation: "reference", label: "pulls as", optional: true },
70
+ { from: "Microsoft.Network/virtualNetworks_subnets", path: "networkSecurityGroup.id", targetKind: "Microsoft.Network/networkSecurityGroups", relation: "reference", label: "nsg" },
71
+ { from: "Microsoft.Network/virtualNetworks_subnets", path: "routeTable.id", targetKind: "Microsoft.Network/routeTables", relation: "reference", label: "routes" },
72
+ { from: "Microsoft.Network/virtualNetworks_subnets", path: "natGateway.id", targetKind: "Microsoft.Network/natGateways", relation: "reference", label: "egress via" },
73
+ // A VNet row from Resource Graph carries its subnets inline.
74
+ { from: "Microsoft.Network/virtualNetworks", path: "subnets[].properties.networkSecurityGroup.id", targetKind: "Microsoft.Network/networkSecurityGroups", relation: "reference", label: "nsg" },
75
+ { from: "Microsoft.Network/virtualNetworks", path: "subnets[].properties.routeTable.id", targetKind: "Microsoft.Network/routeTables", relation: "reference", label: "routes" },
76
+ { from: "Microsoft.Network/networkInterfaces", path: "networkSecurityGroup.id", targetKind: "Microsoft.Network/networkSecurityGroups", relation: "reference", label: "nsg" },
77
+ { from: "Microsoft.Network/networkInterfaces", path: "ipConfigurations[].properties.publicIPAddress.id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
78
+ { from: "Microsoft.Network/natGateways", path: "publicIpAddresses[].id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
79
+ { from: "Microsoft.Network/loadBalancers", path: "frontendIPConfigurations[].properties.publicIPAddress.id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
80
+ { from: "Microsoft.Network/applicationGateways", path: "frontendIPConfigurations[].properties.publicIPAddress.id", targetKind: "Microsoft.Network/publicIPAddresses", relation: "reference", label: "address" },
81
+ { from: "Microsoft.Compute/virtualMachines", path: "networkProfile.networkInterfaces[].id", targetKind: "Microsoft.Network/networkInterfaces", relation: "reference", label: "nic" },
82
+ { from: "Microsoft.Web/sites", path: "serverFarmId", targetKind: "Microsoft.Web/serverfarms", relation: "reference", label: "on plan" },
83
+ ],
84
+ };
@@ -0,0 +1,56 @@
1
+ {
2
+ "listing": "v1",
3
+ "source": { "name": "behold-lister", "version": "0.0.0-fixture" },
4
+ "scope": { "substrate": "azure", "scopeId": "00000000-0000-0000-0000-000000000001", "locations": "*", "types": "*" },
5
+ "startedAt": "2026-10-26T09:00:00.000Z",
6
+ "completedAt": "2026-10-26T09:00:02.000Z",
7
+ "resources": [
8
+ {
9
+ "listedType": "microsoft.storage/storageaccounts",
10
+ "location": "eastus",
11
+ "nativeId": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/shop/providers/Microsoft.Storage/storageAccounts/shopassets",
12
+ "cloudIds": [
13
+ { "kind": "provider-id", "value": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/shop/providers/Microsoft.Storage/storageAccounts/shopassets" },
14
+ { "kind": "provider-id", "value": "/subscriptions/00000000-0000-0000-0000-000000000001/resourcegroups/shop/providers/microsoft.storage/storageaccounts/shopassets" }
15
+ ],
16
+ "tags": { "tofu-estate": "shop" },
17
+ "properties": {
18
+ "id": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/shop/providers/Microsoft.Storage/storageAccounts/shopassets",
19
+ "name": "shopassets",
20
+ "type": "microsoft.storage/storageaccounts",
21
+ "location": "eastus",
22
+ "resourceGroup": "shop",
23
+ "subscriptionId": "00000000-0000-0000-0000-000000000001",
24
+ "kind": "StorageV2",
25
+ "sku": { "name": "Standard_LRS", "tier": "Standard" },
26
+ "properties": {
27
+ "minimumTlsVersion": "TLS1_2",
28
+ "supportsHttpsTrafficOnly": true,
29
+ "provisioningState": "Succeeded",
30
+ "primaryEndpoints": { "blob": "https://shopassets.blob.core.windows.net/" }
31
+ }
32
+ },
33
+ "observedAt": "2026-10-26T09:00:01.000Z"
34
+ },
35
+ {
36
+ "listedType": "microsoft.network/virtualnetworks/subnets",
37
+ "location": "eastus",
38
+ "nativeId": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/shop/providers/Microsoft.Network/virtualNetworks/shop-vnet/subnets/apps",
39
+ "cloudIds": [{ "kind": "provider-id", "value": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/shop/providers/Microsoft.Network/virtualNetworks/shop-vnet/subnets/apps" }],
40
+ "tags": {},
41
+ "properties": { "name": "apps", "type": "microsoft.network/virtualnetworks/subnets", "properties": { "addressPrefix": "10.0.1.0/24" } },
42
+ "observedAt": "2026-10-26T09:00:01.000Z"
43
+ },
44
+ {
45
+ "listedType": "microsoft.contoso/widgets",
46
+ "location": "eastus",
47
+ "nativeId": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/shop/providers/Microsoft.Contoso/widgets/w1",
48
+ "cloudIds": [{ "kind": "provider-id", "value": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/shop/providers/Microsoft.Contoso/widgets/w1" }],
49
+ "tags": {},
50
+ "properties": { "name": "w1", "type": "microsoft.contoso/widgets", "properties": { "connectionString": "<masked>", "size": 3 } },
51
+ "observedAt": "2026-10-26T09:00:01.000Z"
52
+ }
53
+ ],
54
+ "coverage": [{ "listedType": "*", "location": "*", "verdict": "listed" }],
55
+ "notes": []
56
+ }