@intentius/chant-lexicon-azure 0.124.51 → 0.124.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/integrity.json +2 -2
- package/dist/listed-types.d.ts +31 -0
- package/dist/listed-types.d.ts.map +1 -0
- package/dist/listed-types.js +44 -0
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +7 -0
- package/dist/reference-catalog.d.ts +43 -0
- package/dist/reference-catalog.d.ts.map +1 -0
- package/dist/reference-catalog.js +81 -0
- package/package.json +2 -2
- package/src/listed-types.test.ts +68 -0
- package/src/listed-types.ts +65 -0
- package/src/plugin.ts +9 -0
- package/src/reference-catalog.test.ts +144 -0
- package/src/reference-catalog.ts +84 -0
- package/src/testdata/indexed/listing.json +56 -0
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "cbd7212af4cd764e203bd8abb25b0ad3acaa1f99ad654ad55ceea486569ac10a",
|
|
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": "
|
|
40
|
+
"composite": "35bb0b445290640e7c5eca09fb8e9d91dc7346604547adce69576d6ba9bc47d2"
|
|
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
|
+
};
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "0.124.54",
|
|
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.
|
|
82
|
+
"@intentius/chant": "^0.124.54"
|
|
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
|
+
}
|