@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/archetype-source.d.ts +49 -0
- package/dist/archetype-source.d.ts.map +1 -0
- package/dist/archetype-source.js +421 -0
- package/dist/archetype-source.js.map +1 -0
- package/dist/bound-envelope-transport.js +1 -1
- package/dist/bound-envelope-transport.js.map +1 -1
- package/dist/executable-resolution.d.ts +21 -0
- package/dist/executable-resolution.d.ts.map +1 -0
- package/dist/executable-resolution.js +87 -0
- package/dist/executable-resolution.js.map +1 -0
- package/dist/governance-baseline.d.ts +87 -0
- package/dist/governance-baseline.d.ts.map +1 -0
- package/dist/governance-baseline.js +639 -0
- package/dist/governance-baseline.js.map +1 -0
- package/dist/governance-setup.d.ts +13 -0
- package/dist/governance-setup.d.ts.map +1 -0
- package/dist/governance-setup.js +231 -0
- package/dist/governance-setup.js.map +1 -0
- package/dist/iac-generation.d.ts.map +1 -1
- package/dist/iac-generation.js +56 -20
- package/dist/iac-generation.js.map +1 -1
- package/dist/iac-normalizers.d.ts.map +1 -1
- package/dist/iac-normalizers.js +19 -1
- package/dist/iac-normalizers.js.map +1 -1
- package/dist/iac.d.ts +30 -2
- package/dist/iac.d.ts.map +1 -1
- package/dist/iac.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/native-iac-providers.d.ts +20 -4
- package/dist/native-iac-providers.d.ts.map +1 -1
- package/dist/native-iac-providers.js +591 -27
- package/dist/native-iac-providers.js.map +1 -1
- package/dist/pack-manager.d.ts.map +1 -1
- package/dist/pack-manager.js +15 -14
- package/dist/pack-manager.js.map +1 -1
- package/dist/policy-validation.d.ts +68 -0
- package/dist/policy-validation.d.ts.map +1 -0
- package/dist/policy-validation.js +653 -0
- package/dist/policy-validation.js.map +1 -0
- package/dist/process-runner.d.ts.map +1 -1
- package/dist/process-runner.js +10 -1
- package/dist/process-runner.js.map +1 -1
- package/dist/rename-retry.d.ts +12 -0
- package/dist/rename-retry.d.ts.map +1 -0
- package/dist/rename-retry.js +27 -0
- package/dist/rename-retry.js.map +1 -0
- package/dist/repository-publish.d.ts +17 -0
- package/dist/repository-publish.d.ts.map +1 -0
- package/dist/repository-publish.js +124 -0
- package/dist/repository-publish.js.map +1 -0
- package/dist/terraform-provider-schema.d.ts.map +1 -1
- package/dist/terraform-provider-schema.js +6 -2
- package/dist/terraform-provider-schema.js.map +1 -1
- package/dist/test/archetype-source.test.d.ts +2 -0
- package/dist/test/archetype-source.test.d.ts.map +1 -0
- package/dist/test/archetype-source.test.js +258 -0
- package/dist/test/archetype-source.test.js.map +1 -0
- package/dist/test/executable-resolution.test.d.ts +2 -0
- package/dist/test/executable-resolution.test.d.ts.map +1 -0
- package/dist/test/executable-resolution.test.js +87 -0
- package/dist/test/executable-resolution.test.js.map +1 -0
- package/dist/test/governance-baseline.test.d.ts +2 -0
- package/dist/test/governance-baseline.test.d.ts.map +1 -0
- package/dist/test/governance-baseline.test.js +841 -0
- package/dist/test/governance-baseline.test.js.map +1 -0
- package/dist/test/governance-setup.test.d.ts +2 -0
- package/dist/test/governance-setup.test.d.ts.map +1 -0
- package/dist/test/governance-setup.test.js +160 -0
- package/dist/test/governance-setup.test.js.map +1 -0
- package/dist/test/iac-generation.test.js +203 -14
- package/dist/test/iac-generation.test.js.map +1 -1
- package/dist/test/native-iac-providers.test.js +1482 -5
- package/dist/test/native-iac-providers.test.js.map +1 -1
- package/dist/test/policy-validation.test.d.ts +2 -0
- package/dist/test/policy-validation.test.d.ts.map +1 -0
- package/dist/test/policy-validation.test.js +1061 -0
- package/dist/test/policy-validation.test.js.map +1 -0
- package/dist/test/rename-retry.test.d.ts +2 -0
- package/dist/test/rename-retry.test.d.ts.map +1 -0
- package/dist/test/rename-retry.test.js +25 -0
- package/dist/test/rename-retry.test.js.map +1 -0
- package/dist/test/repository-publish.test.d.ts +2 -0
- package/dist/test/repository-publish.test.d.ts.map +1 -0
- package/dist/test/repository-publish.test.js +123 -0
- package/dist/test/repository-publish.test.js.map +1 -0
- package/dist/test/terraform-provider-schema.test.js +12 -6
- package/dist/test/terraform-provider-schema.test.js.map +1 -1
- 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
|