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

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,653 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Value } from "@sinclair/typebox/value";
3
+ import { POLICY_VALIDATION_LIMITS, STORAGE_PROPERTY_HARDENING_CONTROLS, PolicyPropertyMapV1Schema, LogicalResourceManifestV1Schema, IacBindingV1Schema, SECRET_FIELD_PATTERN, SECRET_VALUE_PATTERN, assertPolicyValidationJson, calculatePolicyValidationDigest, calculatePolicyValidationHash, } from "@apexops/contracts";
4
+ import { parseJsonProcessOutput } from "./iac-normalizers.js";
5
+ function object(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value)
7
+ ? value
8
+ : undefined;
9
+ }
10
+ function own(value, key) {
11
+ const record = object(value);
12
+ return record !== undefined && Object.hasOwn(record, key) ? record[key] : undefined;
13
+ }
14
+ function text(value) {
15
+ return typeof value === "string" && value.length > 0 ? value : undefined;
16
+ }
17
+ function expression(value, track) {
18
+ if (typeof value === "string")
19
+ return track === "bicep" ? value.trimStart().startsWith("[") : /\$\{|%\{/.test(value);
20
+ if (Array.isArray(value))
21
+ return value.some((child) => expression(child, track));
22
+ return (object(value) !== undefined &&
23
+ Object.values(value).some((child) => expression(child, track)));
24
+ }
25
+ function hasUnknown(value) {
26
+ if (value === true)
27
+ return true;
28
+ if (Array.isArray(value))
29
+ return value.some(hasUnknown);
30
+ return object(value) !== undefined && Object.values(value).some(hasUnknown);
31
+ }
32
+ function validUnknownMask(value, after) {
33
+ if (typeof value === "boolean")
34
+ return true;
35
+ if (Array.isArray(value))
36
+ return (Array.isArray(after) &&
37
+ value.length === after.length &&
38
+ value.every((child, index) => validUnknownMask(child, after[index])));
39
+ return (object(value) !== undefined &&
40
+ object(after) !== undefined &&
41
+ Object.entries(value).every(([key, child]) => Object.hasOwn(after, key) && validUnknownMask(child, own(after, key))));
42
+ }
43
+ function matchingKnownValues(planned, after, unknown) {
44
+ if (unknown === true)
45
+ return true;
46
+ if (Array.isArray(planned) && Array.isArray(after)) {
47
+ return (planned.length === after.length &&
48
+ planned.every((value, index) => matchingKnownValues(value, after[index], Array.isArray(unknown) ? unknown[index] : undefined)));
49
+ }
50
+ const plannedObject = object(planned);
51
+ const afterObject = object(after);
52
+ if (plannedObject !== undefined && afterObject !== undefined) {
53
+ return [...new Set([...Object.keys(plannedObject), ...Object.keys(afterObject)])].every((key) => matchingKnownValues(own(plannedObject, key), own(afterObject, key), own(unknown, key)));
54
+ }
55
+ return planned === after;
56
+ }
57
+ function resourcesFromJson(json, track) {
58
+ if (Buffer.byteLength(json, "utf8") > POLICY_VALIDATION_LIMITS.bytes)
59
+ throw new TypeError("POLICY_VALIDATION_LIMIT_EXCEEDED");
60
+ const parsed = parseJsonProcessOutput(track === "bicep" ? "azure-what-if" : "terraform-plan", json);
61
+ assertPolicyValidationJson(parsed);
62
+ const root = object(parsed);
63
+ if (root === undefined)
64
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
65
+ const resources = [];
66
+ if (track === "bicep") {
67
+ const visit = (value, inheritedUnsupported = false, parentSymbols = []) => {
68
+ const entries = Array.isArray(value)
69
+ ? value.map((entry) => [undefined, entry])
70
+ : object(value) !== undefined
71
+ ? Object.entries(value)
72
+ : undefined;
73
+ if (entries === undefined)
74
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
75
+ for (const [symbol, entry] of entries) {
76
+ const resource = object(entry);
77
+ if (resource === undefined)
78
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
79
+ const unsupported = inheritedUnsupported ||
80
+ Object.hasOwn(resource, "copy") ||
81
+ (Object.hasOwn(resource, "condition") && resource.condition !== true) ||
82
+ resource.existing === true;
83
+ const physicalId = text(own(resource, "id"));
84
+ const symbols = parentSymbols !== null && symbol !== undefined ? [...parentSymbols, symbol] : null;
85
+ resources.push({
86
+ value: resource,
87
+ unsupported,
88
+ ...(text(resource.type) === undefined ? {} : { resourceType: text(resource.type) }),
89
+ ...(physicalId === undefined ? {} : { physicalId }),
90
+ ...(symbols === null ? {} : { codeSymbol: symbols.join("::") }),
91
+ });
92
+ if (Object.hasOwn(resource, "resources"))
93
+ visit(resource.resources, unsupported, symbols);
94
+ if (text(resource.type)?.toLowerCase() === "microsoft.resources/deployments") {
95
+ const properties = object(resource.properties);
96
+ if (properties !== undefined && Object.hasOwn(properties, "template")) {
97
+ const template = object(properties.template);
98
+ if (template === undefined)
99
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
100
+ visit(template.resources, unsupported || Object.hasOwn(properties, "templateLink"), symbols);
101
+ }
102
+ }
103
+ }
104
+ };
105
+ visit(root.resources);
106
+ }
107
+ else {
108
+ if (root.errored === true ||
109
+ root.complete === false ||
110
+ (root.deferred_changes !== undefined &&
111
+ (!Array.isArray(root.deferred_changes) || root.deferred_changes.length > 0))) {
112
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
113
+ }
114
+ if (root.resource_changes !== undefined && !Array.isArray(root.resource_changes))
115
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
116
+ const changes = (root.resource_changes ?? []);
117
+ const visit = (value) => {
118
+ const module = object(value);
119
+ if (module === undefined ||
120
+ (module.resources !== undefined && !Array.isArray(module.resources)) ||
121
+ (module.child_modules !== undefined && !Array.isArray(module.child_modules)))
122
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
123
+ for (const entry of (module.resources ?? [])) {
124
+ const resource = object(entry);
125
+ const address = text(own(resource, "address"));
126
+ const values = object(own(resource, "values"));
127
+ if (resource === undefined || address === undefined || values === undefined)
128
+ throw new Error("POLICY_VALIDATION_INVALID_SOURCE");
129
+ const matchingChanges = changes.filter((change) => own(change, "address") === address);
130
+ const change = own(matchingChanges[0], "change");
131
+ const unknown = own(change, "after_unknown");
132
+ const actions = own(change, "actions");
133
+ const after = object(own(change, "after"));
134
+ const physicalId = text(own(values, "id"));
135
+ resources.push({
136
+ codeSymbol: address,
137
+ ...(text(resource.type) !== undefined && own(matchingChanges[0], "type") === resource.type
138
+ ? { resourceType: text(resource.type) }
139
+ : {}),
140
+ ...(physicalId === undefined ? {} : { physicalId }),
141
+ value: values,
142
+ unknown,
143
+ unsupported: resource.mode !== "managed" ||
144
+ matchingChanges.length !== 1 ||
145
+ own(matchingChanges[0], "mode") !== "managed" ||
146
+ !Array.isArray(actions) ||
147
+ !["create", "update", "no-op", "create,delete", "delete,create"].includes(actions.join(",")) ||
148
+ !actions.every((action) => typeof action === "string" && !action.includes(",")) ||
149
+ !validUnknownMask(unknown, after) ||
150
+ after === undefined ||
151
+ !matchingKnownValues(values, after, unknown),
152
+ });
153
+ }
154
+ for (const child of (module.child_modules ?? []))
155
+ visit(child);
156
+ };
157
+ visit(own(root.planned_values, "root_module"));
158
+ }
159
+ return resources;
160
+ }
161
+ function observe(mapping, request, resources) {
162
+ if (mapping.disposition === "blocked")
163
+ return { outcome: "fail", reason: "blocked" };
164
+ if (mapping.disposition === "exempt")
165
+ return { outcome: "unsupported", reason: "unverified-exemption" };
166
+ if (mapping.disposition === "not-applicable")
167
+ return { outcome: "pass", reason: "not-applicable" };
168
+ if (mapping.effect === "disabled")
169
+ return { outcome: "unsupported", reason: "unsupported-effect" };
170
+ if (!Object.hasOwn(mapping, "expectedValue"))
171
+ return ["modify", "deployIfNotExists"].includes(mapping.effect)
172
+ ? { outcome: "pass", reason: "platform-remediated" }
173
+ : { outcome: "unsupported", reason: "missing-expected-value" };
174
+ return observeBoundProperty(mapping, request, resources);
175
+ }
176
+ function observeBoundProperty(mapping, request, resources) {
177
+ const segments = mapping.propertyPath.split(".");
178
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(mapping.propertyPath) ||
179
+ segments.some((segment) => ["__proto__", "prototype", "constructor"].includes(segment))) {
180
+ return { outcome: "unsupported", reason: "unsupported-path" };
181
+ }
182
+ if (expression(mapping.expectedValue, request.track))
183
+ return { outcome: "unsupported", reason: "unsupported-expression" };
184
+ const binding = object(own(request.logicalResourceManifest, mapping.logicalResourceId));
185
+ if (binding === undefined)
186
+ return { outcome: "unsupported", reason: "resource-not-bound" };
187
+ if (resources === undefined)
188
+ return { outcome: "unsupported", reason: "invalid-source" };
189
+ const physicalId = text(own(binding, "physicalId"));
190
+ if (Object.hasOwn(binding, "physicalId") && physicalId === undefined)
191
+ return { outcome: "unsupported", reason: "unsupported-resource" };
192
+ const codeSymbol = request.track === "terraform"
193
+ ? (text(own(binding, "terraformAddress")) ?? text(own(binding, "codeSymbol")))
194
+ : text(own(binding, "codeSymbol"));
195
+ if (codeSymbol === undefined && (request.track === "terraform" || physicalId === undefined)) {
196
+ return { outcome: "unsupported", reason: "resource-not-bound" };
197
+ }
198
+ const matches = resources.filter((resource) => request.track === "terraform"
199
+ ? resource.codeSymbol === codeSymbol
200
+ : (physicalId !== undefined && resource.physicalId === physicalId) ||
201
+ (codeSymbol !== undefined && resource.codeSymbol === codeSymbol));
202
+ if (matches.length === 0)
203
+ return { outcome: "unsupported", reason: "resource-not-found" };
204
+ if (matches.length !== 1)
205
+ return { outcome: "unsupported", reason: "ambiguous-resource" };
206
+ const resource = matches[0];
207
+ if (resource.unsupported || (physicalId !== undefined && physicalId !== resource.physicalId)) {
208
+ return { outcome: "unsupported", reason: "unsupported-resource" };
209
+ }
210
+ let observed = resource.value;
211
+ let unknown = resource.unknown;
212
+ for (const segment of segments) {
213
+ if (unknown === true || (typeof observed === "string" && expression(observed, request.track))) {
214
+ return { outcome: "unsupported", reason: "unsupported-expression" };
215
+ }
216
+ observed = own(observed, segment);
217
+ unknown = own(unknown, segment);
218
+ }
219
+ if (hasUnknown(unknown) || expression(observed, request.track))
220
+ return { outcome: "unsupported", reason: "unsupported-expression" };
221
+ if (observed === undefined)
222
+ return { outcome: "fail", reason: "missing-property" };
223
+ const observedValueDigest = calculatePolicyValidationDigest(observed);
224
+ const matched = observedValueDigest === calculatePolicyValidationDigest(mapping.expectedValue);
225
+ return { outcome: matched ? "pass" : "fail", reason: matched ? "matched" : "value-mismatch", observedValueDigest };
226
+ }
227
+ export function validateStorageSecurityProperties(request) {
228
+ const { binding, ...source } = request;
229
+ return validateStorageSecurityBindings({ ...source, bindings: { storage: binding } }).storage;
230
+ }
231
+ export function validateStorageSecurityBindings(request) {
232
+ assertPolicyValidationJson(request.bindings);
233
+ if (!["bicep", "terraform"].includes(request.track) ||
234
+ !/^[0-9a-f]{64}$/.test(request.sourceHash) ||
235
+ object(request.bindings) === undefined ||
236
+ Object.keys(request.bindings).length > POLICY_VALIDATION_LIMITS.mappings ||
237
+ Object.entries(request.bindings).some(([id, binding]) => id.length === 0 || object(binding) === undefined) ||
238
+ typeof request.json !== "string")
239
+ throw new TypeError("STORAGE_SECURITY_INVALID_INPUT");
240
+ const controls = STORAGE_PROPERTY_HARDENING_CONTROLS[request.track];
241
+ const expectedType = request.track === "bicep" ? "microsoft.storage/storageaccounts" : "azurerm_storage_account";
242
+ let resources;
243
+ try {
244
+ resources = resourcesFromJson(request.json, request.track).map((resource) => ({
245
+ ...resource,
246
+ unsupported: resource.unsupported || resource.resourceType?.toLowerCase() !== expectedType,
247
+ }));
248
+ }
249
+ catch {
250
+ resources = undefined;
251
+ }
252
+ const inputHash = createHash("sha256").update(request.json).digest("hex");
253
+ return Object.freeze(Object.fromEntries(Object.entries(request.bindings).map(([logicalId, binding]) => {
254
+ const results = controls.map(([propertyPath, expectedValue]) => Object.freeze({
255
+ propertyPath,
256
+ expectedValueDigest: calculatePolicyValidationDigest(expectedValue),
257
+ ...observeBoundProperty({ propertyPath, expectedValue, logicalResourceId: "storage" }, { track: request.track, logicalResourceManifest: { storage: binding } }, resources),
258
+ }));
259
+ const observation = Object.freeze({
260
+ coverage: "storage-account-property-hardening-v1",
261
+ fullBaselineEvaluated: false,
262
+ sourceHash: request.sourceHash,
263
+ bindingHash: calculatePolicyValidationDigest(binding),
264
+ inputHash,
265
+ outcome: results.some(({ outcome }) => outcome === "fail")
266
+ ? "fail"
267
+ : results.some(({ outcome }) => outcome === "unsupported")
268
+ ? "unsupported"
269
+ : "pass",
270
+ results: Object.freeze(results),
271
+ });
272
+ return [logicalId, observation];
273
+ })));
274
+ }
275
+ export function validateBicepStorageDiagnostics(request) {
276
+ assertPolicyValidationJson(request.binding);
277
+ if (!/^[a-f0-9]{64}$/.test(request.sourceHash) ||
278
+ !/^\/subscriptions\/[a-f0-9-]{36}\/resourceGroups\/[^/]+\/providers\/Microsoft\.OperationalInsights\/workspaces\/[^/]+$/i.test(request.workspaceResourceId) ||
279
+ typeof request.json !== "string" ||
280
+ Buffer.byteLength(request.json) > POLICY_VALIDATION_LIMITS.bytes)
281
+ throw new TypeError("STORAGE_DIAGNOSTICS_INVALID_INPUT");
282
+ const bindingHash = calculatePolicyValidationDigest({
283
+ binding: request.binding,
284
+ workspaceResourceId: request.workspaceResourceId,
285
+ });
286
+ const inputHash = createHash("sha256").update(request.json).digest("hex");
287
+ const result = (outcome, reason) => ({
288
+ coverage: "bicep-storage-service-diagnostics-v1",
289
+ fullBaselineEvaluated: false,
290
+ sourceHash: request.sourceHash,
291
+ inputHash,
292
+ bindingHash,
293
+ outcome,
294
+ reason,
295
+ });
296
+ let resources;
297
+ try {
298
+ resources = resourcesFromJson(request.json, "bicep");
299
+ }
300
+ catch {
301
+ return result("unsupported", "invalid-source");
302
+ }
303
+ const matches = resources.filter(({ codeSymbol }) => codeSymbol === request.binding.codeSymbol);
304
+ if (matches.length !== 1 || request.binding.codeSymbol === undefined || request.binding.physicalId !== undefined)
305
+ return result("unsupported", "ambiguous-or-unbound-account");
306
+ const account = matches[0];
307
+ if (account.unsupported ||
308
+ account.codeSymbol.includes("::") ||
309
+ account.resourceType?.toLowerCase() !== "microsoft.storage/storageaccounts" ||
310
+ typeof account.value.name !== "string" ||
311
+ !/^[a-z0-9]{3,24}$/.test(account.value.name) ||
312
+ Object.hasOwn(account.value, "scope") ||
313
+ Object.hasOwn(account.value, "condition"))
314
+ return result("unsupported", "unsupported-account");
315
+ const services = ["blobServices", "fileServices", "queueServices", "tableServices"];
316
+ for (const service of services) {
317
+ const resourceType = `Microsoft.Storage/storageAccounts/${service}`;
318
+ const serviceResources = resources.filter((resource) => resource.resourceType?.toLowerCase() === resourceType.toLowerCase() &&
319
+ (resource.value.name === `${account.value.name}/default` ||
320
+ resource.value.name === `[format('{0}/{1}', '${account.value.name}', 'default')]`));
321
+ if (serviceResources.length !== 1)
322
+ return result("fail", "missing-or-ambiguous-service");
323
+ const target = serviceResources[0];
324
+ if (target.unsupported ||
325
+ target.codeSymbol === undefined ||
326
+ target.codeSymbol.includes("::") ||
327
+ Object.hasOwn(target.value, "scope") ||
328
+ Object.hasOwn(target.value, "condition") ||
329
+ !Array.isArray(target.value.dependsOn) ||
330
+ !target.value.dependsOn.includes(account.codeSymbol))
331
+ return result("unsupported", "unsupported-service");
332
+ const scope = `[resourceId('${resourceType}', '${account.value.name}', 'default')]`;
333
+ const fullNameScope = `[resourceId('${resourceType}', split('${account.value.name}/default', '/')[0], split('${account.value.name}/default', '/')[1])]`;
334
+ const settings = resources.filter((resource) => resource.resourceType?.toLowerCase() === "microsoft.insights/diagnosticsettings" &&
335
+ (resource.value.scope === scope || resource.value.scope === fullNameScope));
336
+ if (settings.length !== 1)
337
+ return result("fail", "missing-or-ambiguous-diagnostics");
338
+ const setting = settings[0];
339
+ if (setting.unsupported ||
340
+ setting.codeSymbol === undefined ||
341
+ setting.codeSymbol.includes("::") ||
342
+ Object.hasOwn(setting.value, "condition") ||
343
+ !Array.isArray(setting.value.dependsOn) ||
344
+ !setting.value.dependsOn.includes(target.codeSymbol))
345
+ return result("unsupported", "unsupported-diagnostics");
346
+ const properties = object(setting.value.properties);
347
+ if (properties === undefined ||
348
+ typeof properties.workspaceId !== "string" ||
349
+ properties.workspaceId.toLowerCase() !== request.workspaceResourceId.toLowerCase())
350
+ return result("fail", "workspace-mismatch");
351
+ const logs = properties.logs;
352
+ const metrics = properties.metrics;
353
+ if (!Array.isArray(logs) || !Array.isArray(metrics))
354
+ return result("fail", "missing-categories");
355
+ const allLogs = logs.some((entry) => own(entry, "categoryGroup") === "allLogs" && own(entry, "enabled") === true);
356
+ const requiredLogs = ["StorageRead", "StorageWrite", "StorageDelete"];
357
+ if ((!allLogs &&
358
+ !requiredLogs.every((category) => logs.some((entry) => own(entry, "category") === category && own(entry, "enabled") === true))) ||
359
+ !metrics.some((entry) => own(entry, "category") === "Transaction" && own(entry, "enabled") === true))
360
+ return result("fail", "disabled-or-missing-categories");
361
+ }
362
+ return result("pass", "matched");
363
+ }
364
+ export function validateBicepResourceParity(request) {
365
+ assertPolicyValidationJson(request.manifest);
366
+ if (request.binding !== undefined) {
367
+ assertPolicyValidationJson(request.binding);
368
+ if (!Value.Check(IacBindingV1Schema, request.binding) ||
369
+ request.binding.track !== "bicep" ||
370
+ request.binding.projectId !== request.manifest.projectId ||
371
+ request.binding.runId !== request.manifest.runId)
372
+ throw new TypeError("RESOURCE_PARITY_INVALID_INPUT");
373
+ }
374
+ if (!Value.Check(LogicalResourceManifestV1Schema, request.manifest) ||
375
+ request.manifest.track !== "bicep" ||
376
+ request.manifest.resources.length > 1000 ||
377
+ !/^[a-f0-9]{64}$/.test(request.sourceHash) ||
378
+ typeof request.json !== "string" ||
379
+ Buffer.byteLength(request.json) > POLICY_VALIDATION_LIMITS.bytes)
380
+ throw new TypeError("RESOURCE_PARITY_INVALID_INPUT");
381
+ const result = (outcome, reason) => ({
382
+ coverage: "bicep-symbolic-resource-parity-v1",
383
+ sourceHash: request.sourceHash,
384
+ manifestHash: calculatePolicyValidationDigest(request.manifest),
385
+ ...(request.binding === undefined ? {} : { bindingHash: calculatePolicyValidationDigest(request.binding) }),
386
+ inputHash: createHash("sha256").update(request.json).digest("hex"),
387
+ outcome,
388
+ reason,
389
+ });
390
+ const expected = request.manifest.resources;
391
+ const byId = new Map(expected.map((resource) => [resource.logicalId, resource]));
392
+ if (byId.size !== expected.length ||
393
+ new Set(expected.map(({ executionAddress }) => executionAddress)).size !== expected.length ||
394
+ expected.some((resource) => resource.ownership !== "managed" ||
395
+ resource.implementationKind !== "resource" ||
396
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resource.executionAddress ?? "") ||
397
+ resource.dependsOn.some((id) => !byId.has(id)) ||
398
+ JSON.stringify([...resource.dependsOn].sort()) !== JSON.stringify([...resource.generatedDependencies].sort())))
399
+ return result("unsupported", "unsupported-resource");
400
+ let observed;
401
+ try {
402
+ observed = resourcesFromJson(request.json, "bicep");
403
+ }
404
+ catch {
405
+ return result("unsupported", "invalid-source");
406
+ }
407
+ if (observed.some((resource) => resource.unsupported ||
408
+ resource.codeSymbol === undefined ||
409
+ resource.codeSymbol.includes("::") ||
410
+ resource.resourceType?.toLowerCase() === "microsoft.resources/deployments" ||
411
+ Object.hasOwn(resource.value, "condition")))
412
+ return result("unsupported", "unsupported-resource");
413
+ if (observed.length !== expected.length ||
414
+ expected.some((resource) => observed.filter(({ codeSymbol }) => codeSymbol === resource.executionAddress).length !== 1))
415
+ return result("fail", "coverage-mismatch");
416
+ for (const resource of expected) {
417
+ const compiled = observed.find(({ codeSymbol }) => codeSymbol === resource.executionAddress);
418
+ if (compiled.resourceType?.toLowerCase() !== resource.type.toLowerCase())
419
+ return result("fail", "type-mismatch");
420
+ const selected = request.binding?.resourceBindings[resource.logicalId];
421
+ const scopeId = selected?.scopeLogicalId;
422
+ if (Object.hasOwn(compiled.value, "scope") || scopeId !== undefined) {
423
+ const target = scopeId === undefined ? undefined : byId.get(scopeId);
424
+ const targetBinding = scopeId === undefined ? undefined : request.binding?.resourceBindings[scopeId];
425
+ const targetCompiled = target === undefined ? undefined : observed.find(({ codeSymbol }) => codeSymbol === target.executionAddress);
426
+ const targetName = targetBinding?.parameters.name;
427
+ if (resource.type.toLowerCase() !== "microsoft.insights/diagnosticsettings" ||
428
+ target === undefined ||
429
+ targetBinding === undefined ||
430
+ targetBinding.scopeLogicalId !== undefined ||
431
+ scopeId === resource.logicalId ||
432
+ !resource.dependsOn.includes(scopeId) ||
433
+ typeof targetName !== "string" ||
434
+ !/^Microsoft\.[A-Za-z0-9.]+(?:\/[A-Za-z][A-Za-z0-9]*)+$/.test(target.type) ||
435
+ targetName.split("/").length !== target.type.split("/").length - 1 ||
436
+ targetName.split("/").some((name) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name)) ||
437
+ targetCompiled?.value.name !== targetName ||
438
+ compiled.value.name !== selected?.parameters.name ||
439
+ target.implementationAddress !== targetBinding.implementation ||
440
+ resource.implementationAddress !== selected?.implementation)
441
+ return result("unsupported", "unsupported-resource");
442
+ const names = targetName.split("/");
443
+ const literalScope = `[resourceId('${target.type}', ${names.map((name) => `'${name}'`).join(", ")})]`;
444
+ const splitScope = `[resourceId('${target.type}', ${names.map((_, index) => `split('${targetName}', '/')[${index}]`).join(", ")})]`;
445
+ if (compiled.value.scope !== literalScope && compiled.value.scope !== splitScope)
446
+ return result("fail", "dependency-mismatch");
447
+ }
448
+ const dependencies = compiled.value.dependsOn ?? [];
449
+ if (!Array.isArray(dependencies) ||
450
+ dependencies.some((dependency) => typeof dependency !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(dependency)))
451
+ return result("unsupported", "unsupported-resource");
452
+ const expectedDependencies = resource.dependsOn.map((id) => byId.get(id).executionAddress).sort();
453
+ if (new Set(dependencies).size !== dependencies.length ||
454
+ JSON.stringify([...dependencies].sort()) !== JSON.stringify(expectedDependencies))
455
+ return result("fail", "dependency-mismatch");
456
+ }
457
+ return result("pass", "matched");
458
+ }
459
+ export function validateBicepStorageBaseline(request) {
460
+ const parity = validateBicepResourceParity(request);
461
+ const result = (outcome, reason) => ({
462
+ coverage: "bicep-storage-only-baseline-v1",
463
+ sourceHash: request.sourceHash,
464
+ inputHash: parity.inputHash,
465
+ manifestHash: parity.manifestHash,
466
+ bindingHash: calculatePolicyValidationDigest(request.binding),
467
+ outcome,
468
+ reason,
469
+ });
470
+ if (parity.outcome !== "pass")
471
+ return result(parity.outcome, "resource-parity");
472
+ const resources = resourcesFromJson(request.json, "bicep");
473
+ const accounts = resources.filter(({ resourceType }) => resourceType?.toLowerCase() === "microsoft.storage/storageaccounts");
474
+ const allowed = new Set([
475
+ "microsoft.storage/storageaccounts",
476
+ "microsoft.insights/diagnosticsettings",
477
+ ...["blobservices", "fileservices", "queueservices", "tableservices"].map((service) => `microsoft.storage/storageaccounts/${service}`),
478
+ ]);
479
+ if (accounts.length === 0 ||
480
+ new Set(accounts.map(({ value }) => value.name)).size !== accounts.length ||
481
+ resources.length !== accounts.length * 9 ||
482
+ resources.some(({ resourceType }) => !allowed.has(resourceType?.toLowerCase() ?? "")))
483
+ return result("unsupported", "unsupported-resource");
484
+ const parsed = JSON.parse(request.json);
485
+ const hasCredential = (value) => {
486
+ if (typeof value === "string")
487
+ return SECRET_VALUE_PATTERN.test(value);
488
+ if (Array.isArray(value))
489
+ return value.some(hasCredential);
490
+ return (object(value) !== undefined &&
491
+ Object.entries(value).some(([key, child]) => (SECRET_FIELD_PATTERN.test(key) && typeof child === "string" && child.length > 0) || hasCredential(child)));
492
+ };
493
+ if (hasCredential(parsed))
494
+ return result("fail", "credential-content");
495
+ const onlyKeys = (value, keys) => object(value) !== undefined && Object.keys(value).every((key) => keys.includes(key));
496
+ const template = object(parsed);
497
+ if (!onlyKeys(template, [
498
+ "$schema",
499
+ "contentVersion",
500
+ "languageVersion",
501
+ "metadata",
502
+ "resources",
503
+ "parameters",
504
+ "variables",
505
+ "outputs",
506
+ ]) ||
507
+ ["parameters", "variables", "outputs"].some((key) => Object.hasOwn(template, key) &&
508
+ (!onlyKeys(template[key], []) || Object.keys(template[key]).length !== 0)))
509
+ return result("unsupported", "unsupported-resource");
510
+ for (const resource of resources) {
511
+ if (!onlyKeys(resource.value, [
512
+ "type",
513
+ "apiVersion",
514
+ "name",
515
+ "location",
516
+ "kind",
517
+ "sku",
518
+ "tags",
519
+ "identity",
520
+ "properties",
521
+ "dependsOn",
522
+ "scope",
523
+ ]))
524
+ return result("unsupported", "unsupported-resource");
525
+ if (resource.resourceType?.toLowerCase() === "microsoft.insights/diagnosticsettings") {
526
+ if (!onlyKeys(resource.value.properties, ["workspaceId", "logs", "metrics", "logAnalyticsDestinationType"]))
527
+ return result("unsupported", "unsupported-resource");
528
+ }
529
+ else if (resource.resourceType?.toLowerCase() !== "microsoft.storage/storageaccounts" &&
530
+ resource.value.properties !== undefined &&
531
+ !onlyKeys(resource.value.properties, []))
532
+ return result("unsupported", "unsupported-resource");
533
+ }
534
+ for (const account of accounts) {
535
+ const properties = object(account.value.properties);
536
+ const network = object(properties?.networkAcls);
537
+ const encryption = object(properties?.encryption);
538
+ const encryptionServices = object(encryption?.services);
539
+ const identity = object(account.value.identity);
540
+ if (!onlyKeys(properties, [
541
+ "minimumTlsVersion",
542
+ "supportsHttpsTrafficOnly",
543
+ "allowBlobPublicAccess",
544
+ "allowSharedKeyAccess",
545
+ "publicNetworkAccess",
546
+ "defaultToOAuthAuthentication",
547
+ "networkAcls",
548
+ "encryption",
549
+ "accessTier",
550
+ ]) ||
551
+ !onlyKeys(network, ["defaultAction", "bypass", "ipRules", "virtualNetworkRules"]) ||
552
+ !onlyKeys(identity, ["type"]) ||
553
+ !onlyKeys(encryption, ["keySource", "requireInfrastructureEncryption", "services"]) ||
554
+ !onlyKeys(encryptionServices, ["blob", "file"]) ||
555
+ !["blob", "file"].every((service) => onlyKeys(own(encryptionServices, service), ["enabled", "keyType"])))
556
+ return result("unsupported", "unsupported-resource");
557
+ if (properties === undefined ||
558
+ !STORAGE_PROPERTY_HARDENING_CONTROLS.bicep.every(([path, expected]) => properties[path.slice("properties.".length)] === expected) ||
559
+ properties.publicNetworkAccess !== "Disabled" ||
560
+ properties.defaultToOAuthAuthentication !== true ||
561
+ network?.defaultAction !== "Deny" ||
562
+ network.bypass !== "None" ||
563
+ !Array.isArray(network.ipRules) ||
564
+ network.ipRules.length !== 0 ||
565
+ !Array.isArray(network.virtualNetworkRules) ||
566
+ network.virtualNetworkRules.length !== 0 ||
567
+ encryption?.keySource !== "Microsoft.Storage" ||
568
+ encryption.requireInfrastructureEncryption !== true ||
569
+ !["blob", "file"].every((service) => own(own(encryptionServices, service), "enabled") === true) ||
570
+ identity?.type !== "SystemAssigned")
571
+ return result("fail", "storage-controls");
572
+ const manifestAccount = request.manifest.resources.find(({ executionAddress }) => executionAddress === account.codeSymbol);
573
+ const services = request.manifest.resources.filter((resource) => resource.type.toLowerCase().startsWith("microsoft.storage/storageaccounts/") &&
574
+ resource.dependsOn.includes(manifestAccount.logicalId));
575
+ const settings = request.manifest.resources.filter(({ logicalId }) => services.some((service) => request.binding.resourceBindings[logicalId]?.scopeLogicalId === service.logicalId));
576
+ const workspaceIds = settings.map(({ logicalId }) => own(request.binding.resourceBindings[logicalId]?.parameters.properties, "workspaceId"));
577
+ if (settings.length !== 4 || workspaceIds.some((id) => typeof id !== "string") || new Set(workspaceIds).size !== 1)
578
+ return result("unsupported", "diagnostic-routing");
579
+ let routing;
580
+ try {
581
+ routing = validateBicepStorageDiagnostics({
582
+ sourceHash: request.sourceHash,
583
+ binding: { codeSymbol: account.codeSymbol },
584
+ workspaceResourceId: workspaceIds[0],
585
+ json: request.json,
586
+ });
587
+ }
588
+ catch {
589
+ return result("unsupported", "diagnostic-routing");
590
+ }
591
+ if (routing.outcome !== "pass")
592
+ return result(routing.outcome, "diagnostic-routing");
593
+ }
594
+ return result("pass", "matched");
595
+ }
596
+ export function validatePolicyProperties(request) {
597
+ assertPolicyValidationJson(request.policyMap);
598
+ assertPolicyValidationJson(request.logicalResourceManifest);
599
+ if (Array.isArray(request.policyMap?.mappings) &&
600
+ request.policyMap.mappings.length > POLICY_VALIDATION_LIMITS.mappings)
601
+ throw new TypeError("POLICY_VALIDATION_LIMIT_EXCEEDED");
602
+ if (!Value.Check(PolicyPropertyMapV1Schema, request.policyMap) ||
603
+ !/^[0-9a-f]{64}$/.test(request.sourceHash) ||
604
+ !/^[0-9a-f]{64}$/.test(request.policyMapHash) ||
605
+ !["bicep", "terraform"].includes(request.track) ||
606
+ typeof request.json !== "string" ||
607
+ object(request.logicalResourceManifest) === undefined) {
608
+ throw new TypeError("POLICY_VALIDATION_INVALID_INPUT");
609
+ }
610
+ const policyMapContentHash = calculatePolicyValidationDigest(request.policyMap);
611
+ const logicalResourceManifestHash = calculatePolicyValidationDigest(request.logicalResourceManifest);
612
+ let resources;
613
+ try {
614
+ resources = resourcesFromJson(request.json, request.track);
615
+ }
616
+ catch {
617
+ resources = undefined;
618
+ }
619
+ const results = request.policyMap.mappings.map((mapping, mappingIndex) => {
620
+ const { expectedValue, ...identity } = mapping;
621
+ return {
622
+ ...identity,
623
+ mappingIndex,
624
+ mappingHash: calculatePolicyValidationDigest(mapping),
625
+ ...(Object.hasOwn(mapping, "expectedValue")
626
+ ? { expectedValueDigest: calculatePolicyValidationDigest(expectedValue) }
627
+ : {}),
628
+ ...observe(mapping, request, resources),
629
+ };
630
+ });
631
+ const receipt = {
632
+ schemaVersion: "1.0.0",
633
+ projectId: request.policyMap.projectId,
634
+ runId: request.policyMap.runId,
635
+ track: request.track,
636
+ sourceHash: request.sourceHash,
637
+ policyMapHash: request.policyMapHash,
638
+ policyMapContentHash,
639
+ logicalResourceManifestHash,
640
+ inputHash: createHash("sha256").update(request.json).digest("hex"),
641
+ outcome: results.some((result) => result.outcome === "fail")
642
+ ? "fail"
643
+ : results.length === 0 || results.some((result) => result.outcome === "unsupported")
644
+ ? "unsupported"
645
+ : "pass",
646
+ results,
647
+ };
648
+ for (const result of results)
649
+ Object.freeze(result);
650
+ Object.freeze(results);
651
+ return Object.freeze({ ...receipt, receiptHash: calculatePolicyValidationHash(receipt) });
652
+ }
653
+ //# sourceMappingURL=policy-validation.js.map