@apexops/capabilities 0.10.0-next.5 → 0.11.0-next.0

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.
Files changed (92) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/archetype-source.d.ts +49 -0
  3. package/dist/archetype-source.d.ts.map +1 -0
  4. package/dist/archetype-source.js +421 -0
  5. package/dist/archetype-source.js.map +1 -0
  6. package/dist/bound-envelope-transport.js +1 -1
  7. package/dist/bound-envelope-transport.js.map +1 -1
  8. package/dist/executable-resolution.d.ts +21 -0
  9. package/dist/executable-resolution.d.ts.map +1 -0
  10. package/dist/executable-resolution.js +87 -0
  11. package/dist/executable-resolution.js.map +1 -0
  12. package/dist/governance-baseline.d.ts +87 -0
  13. package/dist/governance-baseline.d.ts.map +1 -0
  14. package/dist/governance-baseline.js +639 -0
  15. package/dist/governance-baseline.js.map +1 -0
  16. package/dist/governance-setup.d.ts +13 -0
  17. package/dist/governance-setup.d.ts.map +1 -0
  18. package/dist/governance-setup.js +231 -0
  19. package/dist/governance-setup.js.map +1 -0
  20. package/dist/iac-generation.d.ts.map +1 -1
  21. package/dist/iac-generation.js +56 -20
  22. package/dist/iac-generation.js.map +1 -1
  23. package/dist/iac-normalizers.d.ts.map +1 -1
  24. package/dist/iac-normalizers.js +19 -1
  25. package/dist/iac-normalizers.js.map +1 -1
  26. package/dist/iac.d.ts +30 -2
  27. package/dist/iac.d.ts.map +1 -1
  28. package/dist/iac.js.map +1 -1
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +6 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/native-iac-providers.d.ts +20 -4
  34. package/dist/native-iac-providers.d.ts.map +1 -1
  35. package/dist/native-iac-providers.js +591 -27
  36. package/dist/native-iac-providers.js.map +1 -1
  37. package/dist/pack-manager.d.ts.map +1 -1
  38. package/dist/pack-manager.js +15 -14
  39. package/dist/pack-manager.js.map +1 -1
  40. package/dist/policy-validation.d.ts +68 -0
  41. package/dist/policy-validation.d.ts.map +1 -0
  42. package/dist/policy-validation.js +653 -0
  43. package/dist/policy-validation.js.map +1 -0
  44. package/dist/process-runner.d.ts.map +1 -1
  45. package/dist/process-runner.js +10 -1
  46. package/dist/process-runner.js.map +1 -1
  47. package/dist/rename-retry.d.ts +12 -0
  48. package/dist/rename-retry.d.ts.map +1 -0
  49. package/dist/rename-retry.js +27 -0
  50. package/dist/rename-retry.js.map +1 -0
  51. package/dist/repository-publish.d.ts +17 -0
  52. package/dist/repository-publish.d.ts.map +1 -0
  53. package/dist/repository-publish.js +124 -0
  54. package/dist/repository-publish.js.map +1 -0
  55. package/dist/terraform-provider-schema.d.ts.map +1 -1
  56. package/dist/terraform-provider-schema.js +6 -2
  57. package/dist/terraform-provider-schema.js.map +1 -1
  58. package/dist/test/archetype-source.test.d.ts +2 -0
  59. package/dist/test/archetype-source.test.d.ts.map +1 -0
  60. package/dist/test/archetype-source.test.js +258 -0
  61. package/dist/test/archetype-source.test.js.map +1 -0
  62. package/dist/test/executable-resolution.test.d.ts +2 -0
  63. package/dist/test/executable-resolution.test.d.ts.map +1 -0
  64. package/dist/test/executable-resolution.test.js +87 -0
  65. package/dist/test/executable-resolution.test.js.map +1 -0
  66. package/dist/test/governance-baseline.test.d.ts +2 -0
  67. package/dist/test/governance-baseline.test.d.ts.map +1 -0
  68. package/dist/test/governance-baseline.test.js +841 -0
  69. package/dist/test/governance-baseline.test.js.map +1 -0
  70. package/dist/test/governance-setup.test.d.ts +2 -0
  71. package/dist/test/governance-setup.test.d.ts.map +1 -0
  72. package/dist/test/governance-setup.test.js +160 -0
  73. package/dist/test/governance-setup.test.js.map +1 -0
  74. package/dist/test/iac-generation.test.js +203 -14
  75. package/dist/test/iac-generation.test.js.map +1 -1
  76. package/dist/test/native-iac-providers.test.js +1482 -5
  77. package/dist/test/native-iac-providers.test.js.map +1 -1
  78. package/dist/test/policy-validation.test.d.ts +2 -0
  79. package/dist/test/policy-validation.test.d.ts.map +1 -0
  80. package/dist/test/policy-validation.test.js +1061 -0
  81. package/dist/test/policy-validation.test.js.map +1 -0
  82. package/dist/test/rename-retry.test.d.ts +2 -0
  83. package/dist/test/rename-retry.test.d.ts.map +1 -0
  84. package/dist/test/rename-retry.test.js +25 -0
  85. package/dist/test/rename-retry.test.js.map +1 -0
  86. package/dist/test/repository-publish.test.d.ts +2 -0
  87. package/dist/test/repository-publish.test.d.ts.map +1 -0
  88. package/dist/test/repository-publish.test.js +123 -0
  89. package/dist/test/repository-publish.test.js.map +1 -0
  90. package/dist/test/terraform-provider-schema.test.js +12 -6
  91. package/dist/test/terraform-provider-schema.test.js.map +1 -1
  92. package/package.json +3 -3
@@ -0,0 +1,1061 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { Value } from "@sinclair/typebox/value";
4
+ import { POLICY_VALIDATION_LIMITS, PolicyValidationV1Schema, assertPolicyValidationJson, calculatePolicyValidationDigest, calculatePolicyValidationHash, hasValidPolicyValidation, } from "@apexops/contracts";
5
+ import { validatePolicyProperties, validateBicepResourceParity, validateBicepStorageDiagnostics, validateBicepStorageBaseline, validateStorageSecurityProperties, validateStorageSecurityBindings, } from "../policy-validation.js";
6
+ const hash = "a".repeat(64);
7
+ function terraformSource(values) {
8
+ const identity = {
9
+ address: "azurerm_storage_account.main",
10
+ mode: "managed",
11
+ type: "azurerm_storage_account",
12
+ name: "main",
13
+ provider_name: "registry.terraform.io/hashicorp/azurerm",
14
+ };
15
+ const after = { id: "/resources/storage", ...values };
16
+ return {
17
+ format_version: "1.2",
18
+ terraform_version: "1.9.8",
19
+ planned_values: { root_module: { resources: [{ ...identity, schema_version: 4, values: after }] } },
20
+ resource_changes: [{ ...identity, change: { actions: ["no-op"], before: after, after, after_unknown: {} } }],
21
+ };
22
+ }
23
+ function nestedValue(depth) {
24
+ let value = true;
25
+ for (let index = 0; index < depth; index++)
26
+ value = { nested: value };
27
+ return value;
28
+ }
29
+ function input(track) {
30
+ const mappings = ["deny", "modify", "deployIfNotExists"].map((effect, index) => ({
31
+ policyAssignmentId: "/assignments/baseline",
32
+ policyDefinitionId: `/definitions/${index}`,
33
+ policyDefinitionReferenceId: `member-${index}`,
34
+ effect: effect,
35
+ logicalResourceId: "storage",
36
+ propertyPath: "properties.security.enabled",
37
+ expectedValue: true,
38
+ disposition: "planned",
39
+ }));
40
+ return {
41
+ track,
42
+ sourceHash: hash,
43
+ policyMapHash: "b".repeat(64),
44
+ policyMap: { schemaVersion: "1.0.0", projectId: "policy-test", runId: "run-1", governanceHash: hash, mappings },
45
+ logicalResourceManifest: {
46
+ storage: { physicalId: "/resources/storage", codeSymbol: "azurerm_storage_account.main" },
47
+ },
48
+ json: JSON.stringify(track === "bicep"
49
+ ? { resources: [{ id: "/resources/storage", properties: { security: { enabled: true } } }] }
50
+ : terraformSource({ properties: { security: { enabled: true } } })),
51
+ };
52
+ }
53
+ describe("bounded policy property validation", () => {
54
+ it("storage-only baseline requires complete resource coverage, controls and accepted diagnostic scopes", () => {
55
+ const workspaceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/log";
56
+ const accountType = "Microsoft.Storage/storageAccounts";
57
+ const resources = {
58
+ storage: {
59
+ type: accountType,
60
+ name: "apexfixture",
61
+ identity: { type: "SystemAssigned" },
62
+ properties: {
63
+ minimumTlsVersion: "TLS1_2",
64
+ supportsHttpsTrafficOnly: true,
65
+ allowBlobPublicAccess: false,
66
+ allowSharedKeyAccess: false,
67
+ publicNetworkAccess: "Disabled",
68
+ defaultToOAuthAuthentication: true,
69
+ networkAcls: { defaultAction: "Deny", bypass: "None", ipRules: [], virtualNetworkRules: [] },
70
+ encryption: {
71
+ keySource: "Microsoft.Storage",
72
+ requireInfrastructureEncryption: true,
73
+ services: { blob: { enabled: true }, file: { enabled: true } },
74
+ },
75
+ },
76
+ },
77
+ };
78
+ const entry = (id, type, dependsOn) => ({
79
+ logicalId: id,
80
+ type,
81
+ implementationAddress: `native:${type}@2023-05-01`,
82
+ executionAddress: id,
83
+ implementationKind: "resource",
84
+ ownership: "managed",
85
+ dependsOn,
86
+ generatedDependencies: dependsOn,
87
+ sourcePath: "main.bicep",
88
+ });
89
+ const manifest = {
90
+ schemaVersion: "1.0.0",
91
+ projectId: "demo",
92
+ runId: "run",
93
+ track: "bicep",
94
+ resources: [entry("storage", accountType, [])],
95
+ };
96
+ const binding = {
97
+ schemaVersion: "1.0.0",
98
+ projectId: "demo",
99
+ runId: "run",
100
+ track: "bicep",
101
+ intentHash: hash,
102
+ resourceBindings: {
103
+ storage: {
104
+ implementation: manifest.resources[0].implementationAddress,
105
+ version: "2023-05-01",
106
+ parameters: { name: "apexfixture" },
107
+ },
108
+ },
109
+ };
110
+ for (const service of ["blobServices", "fileServices", "queueServices", "tableServices"]) {
111
+ const type = `${accountType}/${service}`;
112
+ const diagnostic = `${service}Diagnostic`;
113
+ manifest.resources.push(entry(service, type, ["storage"]), entry(diagnostic, "Microsoft.Insights/diagnosticSettings", [service]));
114
+ binding.resourceBindings[service] = {
115
+ implementation: `native:${type}@2023-05-01`,
116
+ version: "2023-05-01",
117
+ parameters: { name: "apexfixture/default" },
118
+ };
119
+ binding.resourceBindings[diagnostic] = {
120
+ implementation: "native:Microsoft.Insights/diagnosticSettings@2023-05-01",
121
+ version: "2023-05-01",
122
+ scopeLogicalId: service,
123
+ parameters: { name: "logs", properties: { workspaceId } },
124
+ };
125
+ resources[service] = { type, name: "apexfixture/default", dependsOn: ["storage"] };
126
+ resources[diagnostic] = {
127
+ type: "Microsoft.Insights/diagnosticSettings",
128
+ name: "logs",
129
+ dependsOn: [service],
130
+ scope: `[resourceId('${type}', 'apexfixture', 'default')]`,
131
+ properties: {
132
+ workspaceId,
133
+ logs: [{ categoryGroup: "allLogs", enabled: true }],
134
+ metrics: [{ category: "Transaction", enabled: true }],
135
+ },
136
+ };
137
+ }
138
+ const check = (source) => validateBicepStorageBaseline({ sourceHash: hash, manifest, binding, json: JSON.stringify(source) });
139
+ assert.equal(check({ resources }).outcome, "pass");
140
+ for (const [key, value] of [
141
+ ["allowSharedKeyAccess", true],
142
+ ["publicNetworkAccess", "Enabled"],
143
+ ["defaultToOAuthAuthentication", false],
144
+ ["encryption", {}],
145
+ ["networkAcls", {}],
146
+ ]) {
147
+ const changed = structuredClone(resources);
148
+ changed.storage.properties[key] = value;
149
+ assert.notEqual(check({ resources: changed }).outcome, "pass");
150
+ }
151
+ for (const key of Object.keys(resources.storage.properties)) {
152
+ const changed = structuredClone(resources);
153
+ delete changed.storage.properties[key];
154
+ assert.notEqual(check({ resources: changed }).outcome, "pass", key);
155
+ }
156
+ const noIdentity = structuredClone(resources);
157
+ delete noIdentity.storage.identity;
158
+ assert.notEqual(check({ resources: noIdentity }).outcome, "pass");
159
+ for (const section of ["outputs", "variables", "parameters", "functions"]) {
160
+ assert.equal(check({ resources, [section]: { unchecked: "value" } }).outcome, "unsupported");
161
+ }
162
+ for (const property of ["isLocalUserEnabled", "isSftpEnabled", "unknownControl"]) {
163
+ const changed = structuredClone(resources);
164
+ changed.storage.properties[property] = true;
165
+ assert.equal(check({ resources: changed }).outcome, "unsupported");
166
+ }
167
+ const unreviewedService = structuredClone(resources);
168
+ unreviewedService.blobServices.properties = { cors: { corsRules: [{ allowedOrigins: ["*"] }] } };
169
+ assert.equal(check({ resources: unreviewedService }).outcome, "unsupported");
170
+ const unsupportedManifest = structuredClone(manifest);
171
+ unsupportedManifest.resources.push(entry("foreign", "Microsoft.KeyVault/vaults", []));
172
+ assert.equal(validateBicepStorageBaseline({
173
+ sourceHash: hash,
174
+ manifest: unsupportedManifest,
175
+ binding,
176
+ json: JSON.stringify({ resources: { ...resources, foreign: { type: "Microsoft.KeyVault/vaults" } } }),
177
+ }).reason, "unsupported-resource");
178
+ assert.equal(check({ resources, outputs: { private: { type: "string", value: "AccountKey=must-not-leak" } } }).reason, "credential-content");
179
+ const missing = structuredClone(resources);
180
+ delete missing.blobServicesDiagnostic;
181
+ assert.notEqual(check({ resources: missing }).outcome, "pass");
182
+ const changed = structuredClone(resources);
183
+ changed.blobServicesDiagnostic.properties.workspaceId = `${workspaceId}foreign`;
184
+ assert.equal(check({ resources: changed }).reason, "diagnostic-routing");
185
+ });
186
+ it("storage diagnostics require all service scopes and the exact accepted workspace", () => {
187
+ const workspaceResourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/log";
188
+ const resources = {
189
+ storage: { type: "Microsoft.Storage/storageAccounts", name: "apexfixture" },
190
+ };
191
+ for (const service of ["blobServices", "fileServices", "queueServices", "tableServices"]) {
192
+ const type = `Microsoft.Storage/storageAccounts/${service}`;
193
+ resources[service] = { type, name: "[format('{0}/{1}', 'apexfixture', 'default')]", dependsOn: ["storage"] };
194
+ resources[`${service}Diagnostic`] = {
195
+ type: "Microsoft.Insights/diagnosticSettings",
196
+ scope: `[resourceId('${type}', 'apexfixture', 'default')]`,
197
+ dependsOn: [service],
198
+ properties: {
199
+ workspaceId: workspaceResourceId,
200
+ logs: [{ categoryGroup: "allLogs", enabled: true }],
201
+ metrics: [{ category: "Transaction", enabled: true }],
202
+ },
203
+ };
204
+ }
205
+ const check = (source) => validateBicepStorageDiagnostics({
206
+ sourceHash: hash,
207
+ binding: { codeSymbol: "storage" },
208
+ workspaceResourceId,
209
+ json: JSON.stringify(source),
210
+ });
211
+ const receipt = check({ resources });
212
+ assert.equal(receipt.outcome, "pass");
213
+ const fullNames = structuredClone(resources);
214
+ for (const service of ["blobServices", "fileServices", "queueServices", "tableServices"]) {
215
+ fullNames[service].name = "apexfixture/default";
216
+ fullNames[`${service}Diagnostic`].scope =
217
+ `[resourceId('Microsoft.Storage/storageAccounts/${service}', split('apexfixture/default', '/')[0], split('apexfixture/default', '/')[1])]`;
218
+ }
219
+ assert.equal(check({ resources: fullNames }).outcome, "pass");
220
+ fullNames.blobServicesDiagnostic.scope = String(fullNames.blobServicesDiagnostic.scope).replace("[1]", "[0]");
221
+ assert.notEqual(check({ resources: fullNames }).outcome, "pass");
222
+ assert.equal(receipt.fullBaselineEvaluated, false);
223
+ assert.doesNotMatch(JSON.stringify(receipt), /apexfixture|subscriptions/);
224
+ for (const mutation of [
225
+ "missing-service",
226
+ "missing-setting",
227
+ "wrong-scope",
228
+ "wrong-workspace",
229
+ "disabled-log",
230
+ "disabled-metric",
231
+ "condition",
232
+ "missing-dependency",
233
+ ]) {
234
+ const changed = structuredClone(resources);
235
+ const setting = changed.blobServicesDiagnostic;
236
+ const properties = setting.properties;
237
+ if (mutation === "missing-service")
238
+ delete changed.blobServices;
239
+ if (mutation === "missing-setting")
240
+ delete changed.blobServicesDiagnostic;
241
+ if (mutation === "wrong-scope")
242
+ setting.scope = String(setting.scope).replace("apexfixture", "foreignaccount");
243
+ if (mutation === "wrong-workspace")
244
+ properties.workspaceId += "foreign";
245
+ if (mutation === "disabled-log")
246
+ properties.logs[0].enabled = false;
247
+ if (mutation === "disabled-metric")
248
+ properties.metrics[0].enabled = false;
249
+ if (mutation === "condition")
250
+ setting.condition = false;
251
+ if (mutation === "missing-dependency")
252
+ setting.dependsOn = [];
253
+ assert.notEqual(check({ resources: changed }).outcome, "pass", mutation);
254
+ }
255
+ assert.equal(check({ resources: { ...resources, storage: { ...resources.storage, name: "[parameters('name')]" } } }).outcome, "unsupported");
256
+ assert.equal(check({}).reason, "invalid-source");
257
+ });
258
+ it("compiled Bicep parity checks exact resource coverage, types and dependencies", () => {
259
+ const resource = {
260
+ logicalId: "storage",
261
+ type: "Microsoft.Storage/storageAccounts",
262
+ implementationAddress: "native",
263
+ executionAddress: "storage",
264
+ implementationKind: "resource",
265
+ ownership: "managed",
266
+ dependsOn: [],
267
+ generatedDependencies: [],
268
+ sourcePath: "main.bicep",
269
+ };
270
+ const manifest = {
271
+ schemaVersion: "1.0.0",
272
+ projectId: "demo",
273
+ runId: "run",
274
+ track: "bicep",
275
+ resources: [
276
+ resource,
277
+ {
278
+ ...resource,
279
+ logicalId: "second",
280
+ executionAddress: "second",
281
+ dependsOn: ["storage"],
282
+ generatedDependencies: ["storage"],
283
+ },
284
+ ],
285
+ };
286
+ const resources = { storage: { type: resource.type }, second: { type: resource.type, dependsOn: ["storage"] } };
287
+ const check = (source, expected = manifest) => validateBicepResourceParity({ sourceHash: hash, manifest: expected, json: JSON.stringify(source) });
288
+ assert.equal(check({ resources }).outcome, "pass");
289
+ assert.equal(check({ resources: { ...resources, extra: { type: resource.type } } }).reason, "coverage-mismatch");
290
+ assert.equal(check({ resources: { storage: resources.storage } }).reason, "coverage-mismatch");
291
+ assert.equal(check({ resources: { ...resources, storage: { type: "Microsoft.KeyVault/vaults" } } }).reason, "type-mismatch");
292
+ assert.equal(check({ resources: { ...resources, second: { type: resource.type } } }).reason, "dependency-mismatch");
293
+ assert.equal(check({ resources: { ...resources, second: { type: resource.type, dependsOn: ["[resourceId('x','y')]"] } } })
294
+ .outcome, "unsupported");
295
+ for (const patch of [{ condition: true }, { copy: {} }, { scope: "[resourceGroup().id]" }, { existing: true }])
296
+ assert.equal(check({ resources: { ...resources, storage: { ...resources.storage, ...patch } } }).outcome, "unsupported");
297
+ assert.equal(check({ resources }, { ...manifest, resources: [{ ...resource, ownership: "existing" }] }).outcome, "unsupported");
298
+ assert.equal(check({ resources: [] }).outcome, "fail");
299
+ assert.equal(validateBicepResourceParity({ sourceHash: hash, manifest, json: "not JSON" }).reason, "invalid-source");
300
+ assert.equal(check({ resources }).manifestHash, calculatePolicyValidationDigest(manifest));
301
+ });
302
+ it("compiled diagnostic parity requires the exact accepted scope binding", () => {
303
+ const targetType = "Microsoft.Storage/storageAccounts/blobServices";
304
+ const diagnosticType = "Microsoft.Insights/diagnosticSettings";
305
+ const target = {
306
+ logicalId: "blob",
307
+ type: targetType,
308
+ implementationAddress: `native:${targetType}@2023-05-01`,
309
+ executionAddress: "blob",
310
+ implementationKind: "resource",
311
+ ownership: "managed",
312
+ dependsOn: [],
313
+ generatedDependencies: [],
314
+ sourcePath: "main.bicep",
315
+ };
316
+ const manifest = {
317
+ schemaVersion: "1.0.0",
318
+ projectId: "demo",
319
+ runId: "run",
320
+ track: "bicep",
321
+ resources: [
322
+ target,
323
+ {
324
+ ...target,
325
+ logicalId: "diag",
326
+ type: diagnosticType,
327
+ implementationAddress: `native:${diagnosticType}@2021-05-01-preview`,
328
+ executionAddress: "diag",
329
+ dependsOn: ["blob"],
330
+ generatedDependencies: ["blob"],
331
+ },
332
+ ],
333
+ };
334
+ const binding = {
335
+ schemaVersion: "1.0.0",
336
+ projectId: "demo",
337
+ runId: "run",
338
+ track: "bicep",
339
+ intentHash: hash,
340
+ resourceBindings: {
341
+ blob: {
342
+ implementation: target.implementationAddress,
343
+ version: "2023-05-01",
344
+ parameters: { name: "account/default" },
345
+ },
346
+ diag: {
347
+ implementation: manifest.resources[1].implementationAddress,
348
+ version: "2021-05-01-preview",
349
+ scopeLogicalId: "blob",
350
+ parameters: { name: "logs" },
351
+ },
352
+ },
353
+ };
354
+ const scope = `[resourceId('${targetType}', split('account/default', '/')[0], split('account/default', '/')[1])]`;
355
+ const resources = {
356
+ blob: { type: targetType, name: "account/default" },
357
+ diag: { type: diagnosticType, name: "logs", scope, dependsOn: ["blob"] },
358
+ };
359
+ const check = (source, selected = binding) => validateBicepResourceParity({ sourceHash: hash, manifest, binding: selected, json: JSON.stringify(source) });
360
+ assert.equal(check({ resources }).outcome, "pass");
361
+ assert.equal(check({ resources }).bindingHash, calculatePolicyValidationDigest(binding));
362
+ assert.equal(check({
363
+ resources: {
364
+ ...resources,
365
+ diag: { ...resources.diag, scope: scope.replaceAll("account/default", "foreign/default") },
366
+ },
367
+ }).outcome, "fail");
368
+ assert.equal(check({ resources: { ...resources, blob: { ...resources.blob, name: "foreign/default" } } }).outcome, "unsupported");
369
+ assert.equal(check({ resources: { ...resources, diag: { ...resources.diag, dependsOn: [] } } }).outcome, "fail");
370
+ const missing = structuredClone(binding);
371
+ delete missing.resourceBindings.diag.scopeLogicalId;
372
+ assert.equal(check({ resources }, missing).outcome, "unsupported");
373
+ assert.equal(validateBicepResourceParity({ sourceHash: hash, manifest, json: JSON.stringify({ resources }) }).outcome, "unsupported");
374
+ });
375
+ it("keeps batched storage observations resource-specific and bounded", () => {
376
+ const secure = {
377
+ type: "Microsoft.Storage/storageAccounts",
378
+ properties: {
379
+ minimumTlsVersion: "TLS1_2",
380
+ supportsHttpsTrafficOnly: true,
381
+ allowBlobPublicAccess: false,
382
+ allowSharedKeyAccess: false,
383
+ },
384
+ };
385
+ const source = {
386
+ resources: { secure, insecure: { ...secure, properties: { ...secure.properties, allowSharedKeyAccess: true } } },
387
+ };
388
+ const request = {
389
+ track: "bicep",
390
+ sourceHash: hash,
391
+ json: JSON.stringify(source),
392
+ bindings: { secure: { codeSymbol: "secure" }, insecure: { codeSymbol: "insecure" } },
393
+ };
394
+ const result = validateStorageSecurityBindings(request);
395
+ assert.equal(result.secure.outcome, "pass");
396
+ assert.equal(result.insecure.outcome, "fail");
397
+ assert.equal(result.secure.inputHash, result.insecure.inputHash);
398
+ assert.notEqual(result.secure.bindingHash, result.insecure.bindingHash);
399
+ assert.ok(Object.isFrozen(result));
400
+ assert.ok(Object.isFrozen(result.secure.results));
401
+ assert.throws(() => validateStorageSecurityBindings({
402
+ ...request,
403
+ bindings: Object.fromEntries(Array.from({ length: 1001 }, (_, index) => [`storage${index}`, { codeSymbol: "secure" }])),
404
+ }), /INVALID_INPUT/);
405
+ });
406
+ it("reports storage property hardening without claiming full security-baseline coverage", () => {
407
+ for (const track of ["bicep", "terraform"]) {
408
+ const source = (insecure = false, foreign = false) => {
409
+ if (track === "bicep")
410
+ return {
411
+ resources: {
412
+ storage: {
413
+ type: foreign ? "Microsoft.KeyVault/vaults" : "Microsoft.Storage/storageAccounts",
414
+ properties: {
415
+ minimumTlsVersion: "TLS1_2",
416
+ supportsHttpsTrafficOnly: true,
417
+ allowBlobPublicAccess: false,
418
+ allowSharedKeyAccess: insecure,
419
+ },
420
+ },
421
+ },
422
+ };
423
+ const plan = terraformSource({
424
+ min_tls_version: "TLS1_2",
425
+ https_traffic_only_enabled: true,
426
+ allow_nested_items_to_be_public: false,
427
+ shared_access_key_enabled: insecure,
428
+ });
429
+ if (foreign)
430
+ plan.planned_values.root_module.resources[0].type = "azurerm_key_vault";
431
+ return plan;
432
+ };
433
+ const binding = track === "bicep" ? { codeSymbol: "storage" } : { terraformAddress: "azurerm_storage_account.main" };
434
+ const request = { track, sourceHash: hash, binding, json: JSON.stringify(source()) };
435
+ const result = validateStorageSecurityProperties(request);
436
+ assert.equal(result.outcome, "pass");
437
+ assert.equal(result.coverage, "storage-account-property-hardening-v1");
438
+ assert.equal(result.fullBaselineEvaluated, false);
439
+ assert.equal(result.results.length, 4);
440
+ assert.ok(result.results.every(({ outcome }) => outcome === "pass"));
441
+ assert.equal(result.bindingHash, calculatePolicyValidationDigest(binding));
442
+ assert.equal(validateStorageSecurityProperties({ ...request, json: JSON.stringify(source(true)) }).outcome, "fail");
443
+ assert.equal(validateStorageSecurityProperties({ ...request, json: JSON.stringify(source(false, true)) }).outcome, "unsupported");
444
+ }
445
+ });
446
+ it("storage hardening requires every concrete property on the exact resource", () => {
447
+ const bicepProperties = {
448
+ minimumTlsVersion: "TLS1_2",
449
+ supportsHttpsTrafficOnly: true,
450
+ allowBlobPublicAccess: false,
451
+ allowSharedKeyAccess: false,
452
+ };
453
+ const terraformProperties = {
454
+ min_tls_version: "TLS1_2",
455
+ https_traffic_only_enabled: true,
456
+ allow_nested_items_to_be_public: false,
457
+ shared_access_key_enabled: false,
458
+ };
459
+ for (const track of ["bicep", "terraform"]) {
460
+ const binding = track === "bicep" ? { codeSymbol: "storage" } : { terraformAddress: "azurerm_storage_account.main" };
461
+ const makeSource = (values, unknown = false) => {
462
+ if (track === "bicep")
463
+ return {
464
+ resources: {
465
+ storage: { type: "Microsoft.Storage/storageAccounts", properties: values },
466
+ sibling: { type: "Microsoft.Storage/storageAccounts", properties: bicepProperties },
467
+ },
468
+ };
469
+ const plan = terraformSource(values);
470
+ if (unknown)
471
+ plan.resource_changes[0].change.after_unknown = { shared_access_key_enabled: true };
472
+ return plan;
473
+ };
474
+ const valid = track === "bicep" ? bicepProperties : terraformProperties;
475
+ const evaluate = (json) => validateStorageSecurityProperties({ track, binding, sourceHash: hash, json });
476
+ for (const property of Object.keys(valid)) {
477
+ const missing = { ...valid };
478
+ delete missing[property];
479
+ const result = evaluate(JSON.stringify(makeSource(missing)));
480
+ assert.equal(result.outcome, "fail");
481
+ assert.ok(result.results.some(({ reason }) => reason === "missing-property"));
482
+ for (const bad of [null, "private-value-must-not-leak", {}, []]) {
483
+ const failed = evaluate(JSON.stringify(makeSource({ ...valid, [property]: bad })));
484
+ assert.equal(failed.outcome, "fail");
485
+ assert.equal(JSON.stringify(failed).includes("private-value-must-not-leak"), false);
486
+ }
487
+ }
488
+ assert.equal(evaluate("invalid-json").outcome, "unsupported");
489
+ assert.equal(evaluate("{}").outcome, "unsupported");
490
+ if (track === "bicep") {
491
+ const unresolved = makeSource({ ...valid, allowSharedKeyAccess: "[parameters('sharedKey')]" });
492
+ assert.equal(evaluate(JSON.stringify(unresolved)).outcome, "unsupported");
493
+ }
494
+ else {
495
+ assert.equal(evaluate(JSON.stringify(makeSource(valid, true))).outcome, "unsupported");
496
+ const duplicate = terraformSource(valid);
497
+ duplicate.planned_values.root_module.resources.push(duplicate.planned_values.root_module.resources[0]);
498
+ assert.equal(evaluate(JSON.stringify(duplicate)).results[0].reason, "ambiguous-resource");
499
+ const wrongType = terraformSource(valid);
500
+ wrongType.resource_changes[0].type = "azurerm_key_vault";
501
+ assert.equal(evaluate(JSON.stringify(wrongType)).outcome, "unsupported");
502
+ }
503
+ assert.throws(() => validateStorageSecurityProperties({ track, sourceHash: hash, binding: nestedValue(80), json: "{}" }), /LIMIT_EXCEEDED/);
504
+ assert.throws(() => validateStorageSecurityProperties({ track, sourceHash: "bad", binding, json: "{}" }), /INVALID_INPUT/);
505
+ }
506
+ });
507
+ for (const track of ["bicep", "terraform"]) {
508
+ it(`${track}: requires an exact physical ID whenever the binding supplies one`, () => {
509
+ const request = input(track);
510
+ for (const physicalId of ["/resources/storage", "/resources/other", "/RESOURCES/storage", undefined]) {
511
+ const values = { properties: { security: { enabled: true } } };
512
+ const source = terraformSource(values);
513
+ const resourceValues = source.planned_values.root_module.resources[0].values;
514
+ if (physicalId === undefined)
515
+ delete resourceValues.id;
516
+ else
517
+ resourceValues.id = physicalId;
518
+ const json = JSON.stringify(track === "terraform"
519
+ ? source
520
+ : {
521
+ languageVersion: "2.0",
522
+ resources: {
523
+ "azurerm_storage_account.main": {
524
+ ...values,
525
+ ...(physicalId === undefined ? {} : { id: physicalId }),
526
+ },
527
+ },
528
+ });
529
+ const receipt = validatePolicyProperties({ ...request, json });
530
+ assert.equal(receipt.outcome, physicalId === "/resources/storage" ? "pass" : "unsupported");
531
+ assert.equal(validatePolicyProperties({
532
+ ...request,
533
+ json,
534
+ logicalResourceManifest: { storage: { codeSymbol: "azurerm_storage_account.main" } },
535
+ }).outcome, "pass");
536
+ }
537
+ assert.equal(validatePolicyProperties({
538
+ ...request,
539
+ logicalResourceManifest: { storage: { codeSymbol: "azurerm_storage_account.main", physicalId: "" } },
540
+ }).outcome, "unsupported");
541
+ });
542
+ it(`${track}: rejects deeply nested expected values and manifests before recursive work`, () => {
543
+ const request = input(track);
544
+ request.policyMap.mappings[0].expectedValue = nestedValue(6000);
545
+ assert.throws(() => validatePolicyProperties(request), {
546
+ name: "TypeError",
547
+ message: "POLICY_VALIDATION_LIMIT_EXCEEDED",
548
+ });
549
+ assert.throws(() => validatePolicyProperties({
550
+ ...input(track),
551
+ logicalResourceManifest: { storage: nestedValue(6000) },
552
+ }), { name: "TypeError", message: "POLICY_VALIDATION_LIMIT_EXCEEDED" });
553
+ });
554
+ it(`${track}: treats 66KB deeply nested observed JSON as unsupported`, () => {
555
+ const request = input(track);
556
+ const json = request.json.replaceAll('"enabled":true', `"enabled":${'{"nested":'.repeat(6000)}true${"}".repeat(6000)}`);
557
+ assert.ok(Buffer.byteLength(json) >= 66_000);
558
+ const receipt = validatePolicyProperties({ ...request, json });
559
+ assert.equal(receipt.outcome, "unsupported");
560
+ assert.equal(receipt.results[0].reason, "invalid-source");
561
+ assert.equal(hasValidPolicyValidation(receipt, receipt), true);
562
+ });
563
+ it(`${track}: checks every deny, modify and DINE mapping and validates the receipt`, () => {
564
+ const request = input(track);
565
+ const receipt = validatePolicyProperties(request);
566
+ assert.equal(receipt.outcome, "pass");
567
+ assert.deepEqual(receipt.results.map(({ outcome }) => outcome), ["pass", "pass", "pass"]);
568
+ assert.equal(Value.Check(PolicyValidationV1Schema, receipt), true);
569
+ assert.equal(hasValidPolicyValidation(receipt, receipt), true);
570
+ assert.equal(hasValidPolicyValidation(receipt, { ...receipt, sourceHash: "c".repeat(64) }), false);
571
+ assert.equal(hasValidPolicyValidation(receipt, { ...receipt, policyMapHash: "c".repeat(64) }), false);
572
+ assert.deepEqual(validatePolicyProperties(request), receipt);
573
+ });
574
+ it(`${track}: fails mismatches and missing properties without recording raw values`, () => {
575
+ const request = input(track);
576
+ request.policyMap.mappings[0].expectedValue = "sensitive-expected-value";
577
+ request.policyMap.mappings[1].propertyPath = "properties.missing";
578
+ const receipt = validatePolicyProperties(request);
579
+ assert.equal(receipt.outcome, "fail");
580
+ assert.deepEqual(receipt.results.map(({ outcome }) => outcome), ["fail", "fail", "pass"]);
581
+ assert.equal(JSON.stringify(receipt).includes("sensitive-expected-value"), false);
582
+ });
583
+ it(`${track}: absent expectedValue and unverified exemptions cannot pass`, () => {
584
+ const request = input(track);
585
+ delete request.policyMap.mappings[0].expectedValue;
586
+ request.policyMap.mappings[1].disposition = "exempt";
587
+ request.policyMap.mappings[2].disposition = "blocked";
588
+ const receipt = validatePolicyProperties(request);
589
+ assert.deepEqual(receipt.results.map(({ outcome }) => outcome), ["unsupported", "unsupported", "fail"]);
590
+ assert.equal(receipt.outcome, "fail");
591
+ });
592
+ it(`${track}: not-applicable rows and platform-remediated effects pass without a property check`, () => {
593
+ const request = input(track);
594
+ request.policyMap.mappings[0].disposition = "not-applicable";
595
+ request.policyMap.mappings[0].reason = "No designed resource has this type";
596
+ request.policyMap.mappings[1].effect = "deployIfNotExists";
597
+ delete request.policyMap.mappings[1].expectedValue;
598
+ request.policyMap.mappings[2].effect = "deny";
599
+ delete request.policyMap.mappings[2].expectedValue;
600
+ const receipt = validatePolicyProperties(request);
601
+ assert.deepEqual(receipt.results.map(({ outcome, reason }) => [outcome, reason]), [
602
+ ["pass", "not-applicable"],
603
+ ["pass", "platform-remediated"],
604
+ ["unsupported", "missing-expected-value"],
605
+ ]);
606
+ assert.equal(hasValidPolicyValidation(receipt, receipt), true);
607
+ });
608
+ it(`${track}: out-of-scope logical resources and dangerous paths are unsupported`, () => {
609
+ const request = input(track);
610
+ request.policyMap.mappings[0].logicalResourceId = "logresource";
611
+ request.policyMap.mappings[1].propertyPath = "properties.__proto__.enabled";
612
+ request.policyMap.mappings[2].propertyPath = "properties.security[0]";
613
+ assert.deepEqual(validatePolicyProperties(request).results.map(({ outcome }) => outcome), ["unsupported", "unsupported", "unsupported"]);
614
+ });
615
+ it(`${track}: preserves distinct same-effect initiative member identities`, () => {
616
+ const request = input(track);
617
+ request.policyMap.mappings[1].effect = "deny";
618
+ const receipt = validatePolicyProperties(request);
619
+ assert.equal(receipt.results.length, 3);
620
+ assert.notEqual(receipt.results[0].mappingHash, receipt.results[1].mappingHash);
621
+ assert.equal(receipt.results[1].policyDefinitionReferenceId, "member-1");
622
+ });
623
+ it(`${track}: rejects stale bindings, mutated results and extra receipt fields`, () => {
624
+ const receipt = validatePolicyProperties(input(track));
625
+ for (const field of [
626
+ "sourceHash",
627
+ "policyMapHash",
628
+ "policyMapContentHash",
629
+ "logicalResourceManifestHash",
630
+ "inputHash",
631
+ ]) {
632
+ assert.equal(hasValidPolicyValidation(receipt, { ...receipt, [field]: "c".repeat(64) }), false);
633
+ }
634
+ assert.equal(hasValidPolicyValidation(receipt, { ...receipt, track: track === "bicep" ? "terraform" : "bicep" }), false);
635
+ assert.equal(hasValidPolicyValidation({ ...receipt, results: receipt.results.slice(1) }, receipt), false);
636
+ assert.equal(Value.Check(PolicyValidationV1Schema, { ...receipt, sourceHash: "stale" }), false);
637
+ assert.equal(Value.Check(PolicyValidationV1Schema, { ...receipt, observedValue: "secret" }), false);
638
+ const altered = structuredClone(receipt);
639
+ delete altered.results[0].expectedValueDigest;
640
+ const { receiptHash: ignoredHash, ...body } = altered;
641
+ assert.equal(ignoredHash.length, 64);
642
+ altered.receiptHash = calculatePolicyValidationHash(body);
643
+ assert.equal(hasValidPolicyValidation(altered, receipt), false);
644
+ assert.equal(Object.isFrozen(receipt), true);
645
+ assert.equal(Object.isFrozen(receipt.results), true);
646
+ assert.equal(Object.isFrozen(receipt.results[0]), true);
647
+ });
648
+ it(`${track}: binds exact supplied hashes and content without trusting dispositions`, () => {
649
+ const request = input(track);
650
+ request.policyMap.mappings[0].disposition = "satisfied";
651
+ request.policyMap.mappings[0].expectedValue = false;
652
+ delete request.policyMap.mappings[2].policyDefinitionId;
653
+ delete request.policyMap.mappings[2].policyDefinitionReferenceId;
654
+ const receipt = validatePolicyProperties(request);
655
+ assert.equal(receipt.results[0].outcome, "fail");
656
+ assert.equal(receipt.sourceHash, request.sourceHash);
657
+ assert.equal(receipt.policyMapHash, request.policyMapHash);
658
+ assert.equal(receipt.policyMapContentHash, calculatePolicyValidationDigest(request.policyMap));
659
+ assert.equal(receipt.logicalResourceManifestHash, calculatePolicyValidationDigest(request.logicalResourceManifest));
660
+ assert.equal(Object.hasOwn(receipt.results[2], "policyDefinitionId"), false);
661
+ assert.equal(hasValidPolicyValidation(receipt, receipt), true);
662
+ const reordered = {
663
+ ...request,
664
+ policyMap: { ...request.policyMap, mappings: [...request.policyMap.mappings].reverse() },
665
+ };
666
+ assert.notEqual(validatePolicyProperties(reordered).receiptHash, receipt.receiptHash);
667
+ });
668
+ it(`${track}: treats malformed output and an empty map as unsupported`, () => {
669
+ const request = input(track);
670
+ for (const json of ["not-json secret-value", "[]", "{}", "null"]) {
671
+ const receipt = validatePolicyProperties({ ...request, json });
672
+ assert.equal(receipt.outcome, "unsupported");
673
+ assert.equal(receipt.results[0].reason, "invalid-source");
674
+ assert.equal(JSON.stringify(receipt).includes("secret-value"), false);
675
+ }
676
+ request.policyMap.mappings = [];
677
+ const receipt = validatePolicyProperties(request);
678
+ assert.equal(receipt.outcome, "unsupported");
679
+ assert.equal(hasValidPolicyValidation(receipt, receipt), true);
680
+ });
681
+ it(`${track}: rejects prototype, alias, indexed and wildcard paths`, () => {
682
+ for (const propertyPath of [
683
+ "constructor.name",
684
+ "properties.prototype.enabled",
685
+ "properties..enabled",
686
+ "properties.*",
687
+ "properties.security[0]",
688
+ "Microsoft.Storage/storageAccounts/enabled",
689
+ ]) {
690
+ const request = input(track);
691
+ request.policyMap.mappings[0].propertyPath = propertyPath;
692
+ assert.equal(validatePolicyProperties(request).results[0].reason, "unsupported-path");
693
+ }
694
+ });
695
+ it(`${track}: compares concrete JSON values structurally and omits source secrets`, () => {
696
+ const request = input(track);
697
+ const values = {
698
+ properties: {
699
+ security: { secret: "sensitive-observed-value", enabled: true },
700
+ nullable: null,
701
+ count: 0,
702
+ disabled: false,
703
+ items: ["one", "two"],
704
+ },
705
+ };
706
+ const json = JSON.stringify(track === "bicep" ? { resources: [{ id: "/resources/storage", ...values }] } : terraformSource(values));
707
+ for (const [propertyPath, expectedValue] of [
708
+ ["properties.security", { enabled: true, secret: "sensitive-observed-value" }],
709
+ ["properties.nullable", null],
710
+ ["properties.count", 0],
711
+ ["properties.disabled", false],
712
+ ["properties.items", ["one", "two"]],
713
+ ]) {
714
+ request.policyMap.mappings[0].propertyPath = propertyPath;
715
+ request.policyMap.mappings[0].expectedValue = expectedValue;
716
+ const receipt = validatePolicyProperties({ ...request, json });
717
+ assert.equal(receipt.results[0].outcome, "pass");
718
+ assert.equal(JSON.stringify(receipt).includes("sensitive-observed-value"), false);
719
+ }
720
+ request.policyMap.mappings[0].expectedValue = ["two", "one"];
721
+ assert.equal(validatePolicyProperties({ ...request, json }).results[0].outcome, "fail");
722
+ });
723
+ it(`${track}: checks mixed deny, modify and DINE properties independently`, () => {
724
+ const request = input(track);
725
+ const paths = ["properties.minimumTlsVersion", "tags.owner", "properties.diagnostics.enabled"];
726
+ const expected = ["TLS1_2", "operations", true];
727
+ for (const [index, mapping] of request.policyMap.mappings.entries()) {
728
+ mapping.propertyPath = paths[index];
729
+ mapping.expectedValue = expected[index];
730
+ }
731
+ const values = {
732
+ properties: { minimumTlsVersion: "TLS1_2", diagnostics: { enabled: true } },
733
+ tags: { owner: "operations" },
734
+ };
735
+ const json = JSON.stringify(track === "bicep" ? { resources: [{ id: "/resources/storage", ...values }] } : terraformSource(values));
736
+ assert.equal(validatePolicyProperties({ ...request, json }).outcome, "pass");
737
+ for (const [index, mapping] of request.policyMap.mappings.entries()) {
738
+ mapping.expectedValue = "wrong";
739
+ assert.equal(validatePolicyProperties({ ...request, json }).results[index].reason, "value-mismatch");
740
+ mapping.expectedValue = expected[index];
741
+ mapping.propertyPath = "properties.absent";
742
+ assert.equal(validatePolicyProperties({ ...request, json }).results[index].reason, "missing-property");
743
+ mapping.propertyPath = paths[index];
744
+ }
745
+ });
746
+ }
747
+ it("uses compiled ARM symbolic keys, not inferred resource names", () => {
748
+ const request = input("bicep");
749
+ const logicalResourceManifest = { storage: { codeSymbol: "azurerm_storage_account.main" } };
750
+ const json = JSON.stringify({
751
+ languageVersion: "2.0",
752
+ resources: {
753
+ "azurerm_storage_account.main": {
754
+ type: "Microsoft.Storage/storageAccounts",
755
+ apiVersion: "2025-01-01",
756
+ name: "[parameters('storageName')]",
757
+ properties: { security: { enabled: true } },
758
+ },
759
+ },
760
+ });
761
+ assert.equal(validatePolicyProperties({ ...request, logicalResourceManifest, json }).outcome, "pass");
762
+ assert.equal(validatePolicyProperties({ ...request, json }).results[0].reason, "unsupported-resource");
763
+ const nameOnly = JSON.stringify({ resources: [{ name: "storage", properties: { security: { enabled: true } } }] });
764
+ assert.equal(validatePolicyProperties({ ...request, json: nameOnly }).results[0].reason, "resource-not-found");
765
+ });
766
+ it("binds Bicep deployment-template children by qualified symbols without crossing siblings", () => {
767
+ const request = input("bicep");
768
+ const deployment = (enabled) => ({
769
+ type: "Microsoft.Resources/deployments",
770
+ properties: { template: { resources: { storage: { properties: { security: { enabled } } } } } },
771
+ });
772
+ const source = { resources: { primary: deployment(true), sibling: deployment(false) } };
773
+ const evaluate = (codeSymbol, json = JSON.stringify(source)) => validatePolicyProperties({
774
+ ...request,
775
+ json,
776
+ logicalResourceManifest: { storage: { codeSymbol } },
777
+ });
778
+ assert.equal(evaluate("primary::storage").outcome, "pass");
779
+ assert.equal(evaluate("sibling::storage").outcome, "fail");
780
+ assert.equal(evaluate("storage").results[0].reason, "resource-not-found");
781
+ assert.equal(evaluate("missing::storage").results[0].reason, "resource-not-found");
782
+ for (const extra of [{ condition: false }, { condition: "[parameters('enabled')]" }, { copy: { count: 2 } }]) {
783
+ const json = JSON.stringify({ resources: { primary: { ...deployment(true), ...extra } } });
784
+ assert.equal(evaluate("primary::storage", json).results[0].reason, "unsupported-resource");
785
+ }
786
+ const unresolved = JSON.stringify({ resources: { primary: deployment("[parameters('security')]") } });
787
+ assert.equal(evaluate("primary::storage", unresolved).results[0].reason, "unsupported-expression");
788
+ const nested = {
789
+ resources: { outer: { type: "Microsoft.Resources/deployments", properties: { template: source } } },
790
+ };
791
+ assert.equal(evaluate("outer::primary::storage", JSON.stringify(nested)).outcome, "pass");
792
+ const child = deployment(true).properties.template.resources.storage;
793
+ const ambiguous = { resources: { primary: deployment(true), "primary::storage": child } };
794
+ assert.equal(evaluate("primary::storage", JSON.stringify(ambiguous)).results[0].reason, "ambiguous-resource");
795
+ const unnamed = { resources: [{ ...deployment(true), name: "primary" }] };
796
+ assert.equal(evaluate("primary::storage", JSON.stringify(unnamed)).results[0].reason, "resource-not-found");
797
+ for (const properties of [{ template: null }, { template: {} }]) {
798
+ assert.equal(evaluate("primary::storage", JSON.stringify({ resources: { primary: { type: "Microsoft.Resources/deployments", properties } } })).results[0].reason, "invalid-source");
799
+ }
800
+ const linked = { ...deployment(true).properties, templateLink: { uri: "https://example.invalid/template.json" } };
801
+ assert.equal(evaluate("primary::storage", JSON.stringify({ resources: { primary: { type: "Microsoft.Resources/deployments", properties: linked } } })).results[0].reason, "unsupported-resource");
802
+ });
803
+ it("rejects ARM expressions at the leaf, ancestor and inside a compared object", () => {
804
+ for (const security of [
805
+ { enabled: "[parameters('secret')]" },
806
+ "[variables('settings')]",
807
+ { enabled: { nested: "[reference('id')]" } },
808
+ ]) {
809
+ const request = input("bicep");
810
+ const receipt = validatePolicyProperties({
811
+ ...request,
812
+ json: JSON.stringify({ resources: [{ id: "/resources/storage", properties: { security } }] }),
813
+ });
814
+ assert.equal(receipt.results[0].reason, "unsupported-expression");
815
+ assert.equal(JSON.stringify(receipt).includes("parameters"), false);
816
+ }
817
+ });
818
+ it("rejects ambiguous ARM bindings, loops and conditional resources", () => {
819
+ const request = input("bicep");
820
+ const resource = { id: "/resources/storage", properties: { security: { enabled: true } } };
821
+ assert.equal(validatePolicyProperties({ ...request, json: JSON.stringify({ resources: [resource, resource] }) }).results[0]
822
+ .reason, "ambiguous-resource");
823
+ for (const extra of [{ condition: false }, { condition: "[parameters('enabled')]" }, { copy: { count: 2 } }]) {
824
+ assert.equal(validatePolicyProperties({ ...request, json: JSON.stringify({ resources: [{ ...resource, ...extra }] }) })
825
+ .results[0].reason, "unsupported-resource");
826
+ }
827
+ });
828
+ it("uses exact Terraform child-module addresses and honors after_unknown masks", () => {
829
+ const request = input("terraform");
830
+ const address = 'module.storage.azurerm_storage_account.main["east"]';
831
+ const logicalResourceManifest = { storage: { terraformAddress: address } };
832
+ const resource = { address, mode: "managed", values: { properties: { security: { enabled: true } } } };
833
+ const source = {
834
+ planned_values: { root_module: { child_modules: [{ address: "module.storage", resources: [resource] }] } },
835
+ resource_changes: [
836
+ { address, mode: "managed", change: { actions: ["create"], after: resource.values, after_unknown: {} } },
837
+ ],
838
+ };
839
+ assert.equal(validatePolicyProperties({ ...request, logicalResourceManifest, json: JSON.stringify(source) }).outcome, "pass");
840
+ assert.equal(validatePolicyProperties({ ...request, json: JSON.stringify(source) }).results[0].reason, "resource-not-found");
841
+ for (const after_unknown of [true, { properties: true }, { properties: { security: { enabled: true } } }]) {
842
+ const receipt = validatePolicyProperties({
843
+ ...request,
844
+ logicalResourceManifest,
845
+ json: JSON.stringify({
846
+ ...source,
847
+ resource_changes: [
848
+ { ...source.resource_changes[0], change: { ...source.resource_changes[0].change, after_unknown } },
849
+ ],
850
+ }),
851
+ });
852
+ assert.equal(receipt.results[0].reason, "unsupported-expression");
853
+ }
854
+ });
855
+ it("does not accept malformed Terraform unknown metadata or unresolved template strings", () => {
856
+ const request = input("terraform");
857
+ const source = terraformSource({ properties: { security: { enabled: true } } });
858
+ for (const after_unknown of [
859
+ "unknown",
860
+ [],
861
+ [true],
862
+ { properties: [true] },
863
+ { properties: { security: { enabled: {} } } },
864
+ { properties: { security: { enabled: "unknown" } } },
865
+ ]) {
866
+ const json = JSON.stringify({
867
+ ...source,
868
+ resource_changes: [
869
+ { ...source.resource_changes[0], change: { ...source.resource_changes[0].change, after_unknown } },
870
+ ],
871
+ });
872
+ assert.equal(validatePolicyProperties({ ...request, json }).outcome, "unsupported");
873
+ }
874
+ const json = JSON.stringify(terraformSource({ properties: { security: { enabled: "${var.enabled}" } } }));
875
+ assert.equal(validatePolicyProperties({ ...request, json }).results[0].reason, "unsupported-expression");
876
+ });
877
+ it("fails closed on invalid caller contracts without leaking inputs", () => {
878
+ const request = input("bicep");
879
+ assert.throws(() => validatePolicyProperties({ ...request, sourceHash: "bad" }), /POLICY_VALIDATION_INVALID_INPUT/);
880
+ assert.throws(() => validatePolicyProperties({ ...request, policyMapHash: "bad" }), /POLICY_VALIDATION_INVALID_INPUT/);
881
+ assert.throws(() => validatePolicyProperties({
882
+ ...request,
883
+ policyMap: { ...request.policyMap, secret: "sensitive" },
884
+ }), /POLICY_VALIDATION_INVALID_INPUT/);
885
+ });
886
+ it("checks concrete Azure storage properties in Terraform show plan JSON", () => {
887
+ const physicalId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-policy/providers/Microsoft.Storage/storageAccounts/stpolicy";
888
+ const source = terraformSource({
889
+ id: physicalId,
890
+ name: "stpolicy",
891
+ resource_group_name: "rg-policy",
892
+ location: "swedencentral",
893
+ account_tier: "Standard",
894
+ account_replication_type: "LRS",
895
+ min_tls_version: "TLS1_2",
896
+ https_traffic_only_enabled: true,
897
+ allow_nested_items_to_be_public: false,
898
+ shared_access_key_enabled: false,
899
+ tags: { owner: "operations" },
900
+ });
901
+ const request = input("terraform");
902
+ request.policyMap.mappings = [
903
+ ["min_tls_version", "TLS1_2"],
904
+ ["https_traffic_only_enabled", true],
905
+ ["allow_nested_items_to_be_public", false],
906
+ ].map(([propertyPath, expectedValue], index) => ({
907
+ ...request.policyMap.mappings[index],
908
+ propertyPath: propertyPath,
909
+ expectedValue,
910
+ }));
911
+ for (const actions of [["no-op"], ["update"], ["create"], ["delete", "create"], ["create", "delete"]]) {
912
+ source.resource_changes[0].change.actions = actions;
913
+ assert.equal(validatePolicyProperties({
914
+ ...request,
915
+ logicalResourceManifest: { storage: { terraformAddress: "azurerm_storage_account.main", physicalId } },
916
+ json: JSON.stringify(source),
917
+ }).outcome, "pass");
918
+ }
919
+ });
920
+ it("requires complete, unambiguous Terraform change evidence for every evaluated resource", () => {
921
+ const request = input("terraform");
922
+ const source = terraformSource({ properties: { security: { enabled: true } } });
923
+ const change = source.resource_changes[0];
924
+ const { resource_changes: omitted, ...withoutChanges } = source;
925
+ assert.equal(omitted.length, 1);
926
+ for (const json of [
927
+ JSON.stringify(withoutChanges),
928
+ ...[
929
+ [],
930
+ [null],
931
+ [{ ...change, address: "azurerm_storage_account.other" }],
932
+ [change, change],
933
+ [{ ...change, mode: "data" }],
934
+ [{ address: change.address, change: change.change }],
935
+ [{ ...change, change: null }],
936
+ ...["actions", "after_unknown", "after"].map((field) => {
937
+ const partial = { ...change.change };
938
+ delete partial[field];
939
+ return [{ ...change, change: partial }];
940
+ }),
941
+ ...[
942
+ [],
943
+ ["delete"],
944
+ ["read"],
945
+ ["forget"],
946
+ ["invalid"],
947
+ ["create,delete"],
948
+ ["update", "update"],
949
+ "update",
950
+ null,
951
+ ].map((actions) => [{ ...change, change: { ...change.change, actions } }]),
952
+ ...[null, [], false, { id: "/resources/storage", properties: { security: { enabled: false } } }].map((after) => [{ ...change, change: { ...change.change, after } }]),
953
+ ].map((resource_changes) => JSON.stringify({ ...source, resource_changes })),
954
+ ]) {
955
+ const receipt = validatePolicyProperties({ ...request, json });
956
+ assert.equal(receipt.outcome, "unsupported");
957
+ assert.equal(receipt.results[0].reason, "unsupported-resource");
958
+ }
959
+ const otherAddress = "azurerm_storage_account.other";
960
+ const secondResource = { ...source.planned_values.root_module.resources[0], address: otherAddress };
961
+ source.planned_values.root_module.resources.push(secondResource);
962
+ request.policyMap.mappings[1].logicalResourceId = "other";
963
+ const logicalResourceManifest = { ...request.logicalResourceManifest, other: { terraformAddress: otherAddress } };
964
+ assert.deepEqual(validatePolicyProperties({ ...request, logicalResourceManifest, json: JSON.stringify(source) }).results.map(({ outcome }) => outcome), ["pass", "unsupported", "pass"]);
965
+ source.resource_changes.push({ ...change, address: otherAddress });
966
+ assert.equal(validatePolicyProperties({ ...request, logicalResourceManifest, json: JSON.stringify(source) }).outcome, "pass");
967
+ });
968
+ it("allows unknown unrelated Terraform properties without treating them as physical-ID evidence", () => {
969
+ const request = input("terraform");
970
+ const source = terraformSource({ properties: { security: { enabled: true } } });
971
+ const values = { properties: { security: { enabled: true } } };
972
+ const json = JSON.stringify({
973
+ ...source,
974
+ planned_values: { root_module: { resources: [{ ...source.planned_values.root_module.resources[0], values }] } },
975
+ resource_changes: [
976
+ {
977
+ ...source.resource_changes[0],
978
+ change: { actions: ["create"], before: null, after: { ...values, id: null }, after_unknown: { id: true } },
979
+ },
980
+ ],
981
+ });
982
+ assert.equal(validatePolicyProperties({ ...request, json }).outcome, "unsupported");
983
+ assert.equal(validatePolicyProperties({
984
+ ...request,
985
+ json,
986
+ logicalResourceManifest: { storage: { codeSymbol: "azurerm_storage_account.main" } },
987
+ }).outcome, "pass");
988
+ });
989
+ it("bounds JSON depth, nodes and UTF-8 bytes, preserving valid JSON digest behavior", () => {
990
+ assert.doesNotThrow(() => calculatePolicyValidationDigest(nestedValue(POLICY_VALIDATION_LIMITS.depth)));
991
+ assert.doesNotThrow(() => assertPolicyValidationJson(Array(POLICY_VALIDATION_LIMITS.nodes - 1).fill(null)));
992
+ assert.doesNotThrow(() => assertPolicyValidationJson("a".repeat(POLICY_VALIDATION_LIMITS.bytes - 2)));
993
+ for (const value of [
994
+ nestedValue(POLICY_VALIDATION_LIMITS.depth + 1),
995
+ Array(POLICY_VALIDATION_LIMITS.nodes).fill(null),
996
+ "a".repeat(POLICY_VALIDATION_LIMITS.bytes - 1),
997
+ "\u00e9".repeat(POLICY_VALIDATION_LIMITS.bytes / 2),
998
+ ])
999
+ assert.throws(() => calculatePolicyValidationDigest(value), {
1000
+ name: "TypeError",
1001
+ message: "POLICY_VALIDATION_LIMIT_EXCEEDED",
1002
+ });
1003
+ const shared = { enabled: true };
1004
+ assert.equal(calculatePolicyValidationDigest([shared, shared]), calculatePolicyValidationDigest([{ enabled: true }, { enabled: true }]));
1005
+ const cyclic = {};
1006
+ cyclic.self = cyclic;
1007
+ for (const value of [cyclic, { nested: undefined }, { nested: Number.NaN }]) {
1008
+ assert.throws(() => calculatePolicyValidationDigest(value), {
1009
+ name: "TypeError",
1010
+ message: "POLICY_VALIDATION_NON_JSON_VALUE",
1011
+ });
1012
+ }
1013
+ });
1014
+ it("bounds mapping counts and oversized or wide source JSON before evaluation", () => {
1015
+ const request = input("bicep");
1016
+ request.policyMap.mappings = Array(POLICY_VALIDATION_LIMITS.mappings).fill(request.policyMap.mappings[0]);
1017
+ assert.equal(validatePolicyProperties(request).outcome, "pass");
1018
+ request.policyMap.mappings.push(request.policyMap.mappings[0]);
1019
+ assert.throws(() => validatePolicyProperties(request), {
1020
+ name: "TypeError",
1021
+ message: "POLICY_VALIDATION_LIMIT_EXCEEDED",
1022
+ });
1023
+ for (const track of ["bicep", "terraform"]) {
1024
+ const valid = input(track);
1025
+ for (const json of [
1026
+ valid.json + " ".repeat(POLICY_VALIDATION_LIMITS.bytes),
1027
+ valid.json.replaceAll('"enabled":true', `"enabled":${JSON.stringify(Array(POLICY_VALIDATION_LIMITS.nodes).fill(null))}`),
1028
+ ])
1029
+ assert.equal(validatePolicyProperties({ ...valid, json }).results[0].reason, "invalid-source");
1030
+ const wide = input(track);
1031
+ wide.policyMap.mappings[0].expectedValue = Array(POLICY_VALIDATION_LIMITS.nodes).fill(null);
1032
+ assert.throws(() => validatePolicyProperties(wide), {
1033
+ name: "TypeError",
1034
+ message: "POLICY_VALIDATION_LIMIT_EXCEEDED",
1035
+ });
1036
+ }
1037
+ });
1038
+ it("returns false for invalid contract data including deeply nested expected values", () => {
1039
+ const receipt = validatePolicyProperties(input("bicep"));
1040
+ const deep = nestedValue(6000);
1041
+ const cyclic = {};
1042
+ cyclic.self = cyclic;
1043
+ for (const value of [
1044
+ { ...receipt, expectedValue: deep },
1045
+ { ...receipt, results: [{ ...receipt.results[0], expectedValue: deep }] },
1046
+ { ...receipt, results: [{ ...receipt.results[0], expectedValueDigest: deep }] },
1047
+ { ...receipt, results: Array(POLICY_VALIDATION_LIMITS.mappings + 1).fill(receipt.results[0]) },
1048
+ cyclic,
1049
+ ])
1050
+ assert.equal(hasValidPolicyValidation(value, receipt), false);
1051
+ assert.equal(hasValidPolicyValidation(receipt, { ...receipt, sourceHash: deep }), false);
1052
+ assert.equal(hasValidPolicyValidation(receipt, null), false);
1053
+ const request = input("bicep");
1054
+ request.policyMap.mappings[0].expectedValue = cyclic;
1055
+ assert.throws(() => validatePolicyProperties(request), {
1056
+ name: "TypeError",
1057
+ message: "POLICY_VALIDATION_NON_JSON_VALUE",
1058
+ });
1059
+ });
1060
+ });
1061
+ //# sourceMappingURL=policy-validation.test.js.map