@aws/cloudformation-validate 1.9.0-beta → 1.10.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/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
- DetailedReport,
2
+ Diagnostic,
3
+ ValidationReport,
3
4
  DiagnosticModel,
4
5
  AdditionalSchemaSource,
5
6
  ExternalRuleSource,
@@ -8,8 +9,6 @@ import type {
8
9
  ResolvedResource,
9
10
  RuleInfo,
10
11
  SourceSpan,
11
- StandardDiagnostic,
12
- StandardReport,
13
12
  ValidateConfig,
14
13
  } from './bindings_wasm';
15
14
  export type {
@@ -29,14 +28,12 @@ export type {
29
28
  ResourceRef,
30
29
  RelatedResource,
31
30
  ViolationContext,
32
- StandardDiagnostic,
33
- DetailedDiagnostic,
31
+ Diagnostic,
34
32
  PhaseMetric,
35
33
  PerformanceMetrics,
36
34
  Summary,
37
35
  ReportMetadata,
38
- StandardReport,
39
- DetailedReport,
36
+ ValidationReport,
40
37
  PseudoParameterOverrides,
41
38
  ValidateConfig,
42
39
  ExternalRuleSource,
@@ -79,9 +76,55 @@ export type JsonValue =
79
76
  | {
80
77
  [key: string]: JsonValue;
81
78
  };
79
+ export type AwsCliOperationKind =
80
+ | 'READ_ONLY'
81
+ | 'CLOUD_FORMATION_CREATE'
82
+ | 'CLOUD_FORMATION_UPDATE'
83
+ | 'CLOUD_FORMATION_DELETE'
84
+ | 'DATA_PLANE_MUTATION'
85
+ | 'UNMAPPED_MUTATION';
86
+ export type AwsCliCommandValidationStatus = 'VALIDATED' | 'SKIPPED';
87
+ export type AwsCliTemplateSource =
88
+ 'TEMPLATE_BODY' | 'CLOUD_CONTROL_DESIRED_STATE' | 'SYNTHESIZED_CREATE' | 'SYNTHESIZED_UPDATE';
89
+ export interface AwsCliCommandOptions {
90
+ servicePrefix?: string;
91
+ httpMethod?: string;
92
+ isReadOnly?: boolean;
93
+ }
94
+ /**
95
+ * Service, operation, and input values for one AWS CLI command.
96
+ *
97
+ * `serviceName` is the canonical botocore service name and is normalized only
98
+ * for ASCII case. Callers adapting an SDK request must translate its native
99
+ * service identity before constructing this request; endpoint and signing-name
100
+ * aliases are never guessed by the validation core.
101
+ */
102
+ export declare class AwsCliCommand {
103
+ readonly serviceName: string;
104
+ readonly operationName: string;
105
+ readonly parameters: Record<string, unknown>;
106
+ readonly servicePrefix?: string;
107
+ readonly httpMethod?: string;
108
+ readonly isReadOnly?: boolean;
109
+ constructor(
110
+ serviceName: string,
111
+ operationName: string,
112
+ parameters: Record<string, unknown>,
113
+ options?: AwsCliCommandOptions,
114
+ );
115
+ }
116
+ export interface AwsCliCommandValidation {
117
+ operationKind: AwsCliOperationKind;
118
+ status: AwsCliCommandValidationStatus;
119
+ templateSource: AwsCliTemplateSource | null;
120
+ resourceTypes: string[];
121
+ reason: string;
122
+ report: ValidationReport | null;
123
+ template: Uint8Array | null;
124
+ }
82
125
  export interface Engine {
83
- validateStandard(template: TemplateFile, config?: ValidateConfig): StandardReport;
84
- validateDetailed(template: TemplateFile, config?: ValidateConfig): DetailedReport;
126
+ validateTemplate(template: TemplateFile, config?: ValidateConfig): ValidationReport;
127
+ validateAwsCliCommand(request: AwsCliCommand): AwsCliCommandValidation;
85
128
  listRules(): RuleInfo[];
86
129
  engineName(): string;
87
130
  free(): void;
@@ -119,6 +162,24 @@ export interface EngineConfig {
119
162
  */
120
163
  schemaValidatorConfig?: SchemaValidatorConfig;
121
164
  }
165
+ /**
166
+ * Configuration for the {@link CompositeEngine}. The built-in rules are always
167
+ * evaluated by the engine's fixed built-in evaluator, so these fields only layer
168
+ * external rules on top - there is no engine-native custom-rule field.
169
+ */
170
+ export interface CompositeEngineConfig {
171
+ /** Custom Rego rules layered on top of the built-in rules. */
172
+ regoRules?: RuleSource[];
173
+ /** Custom CEL rules layered on top of the built-in rules. */
174
+ celRules?: RuleSource[];
175
+ /** CloudFormation Guard DSL rules layered on top of the built-in rules. */
176
+ guardRules?: RuleSource[];
177
+ /**
178
+ * Optional schema validator configuration, observed by both the built-in and
179
+ * external rule evaluation.
180
+ */
181
+ schemaValidatorConfig?: SchemaValidatorConfig;
182
+ }
122
183
  /**
123
184
  * Configuration for the schema validator. Additional schemas are merged on top
124
185
  * of the bundled CloudFormation provider schemas before schema validation.
@@ -150,9 +211,10 @@ export declare class SchemaValidator {
150
211
  constructor(config?: SchemaValidatorConfig);
151
212
  listRules(): RuleInfo[];
152
213
  schemaCount(): number;
153
- validate(template: TemplateFile, region?: string): StandardDiagnostic[];
214
+ validate(template: TemplateFile, region?: string): Diagnostic[];
154
215
  free(): void;
155
216
  }
156
217
  export declare const RegoEngine: new (config?: EngineConfig) => Engine;
157
218
  export declare const CelEngine: new (config?: EngineConfig) => Engine;
219
+ export declare const CompositeEngine: new (config?: CompositeEngineConfig) => Engine;
158
220
  export declare function version(): string;
package/index.js CHANGED
@@ -1,15 +1,203 @@
1
1
  'use strict';
2
2
  Object.defineProperty(exports, '__esModule', { value: true });
3
- exports.CelEngine =
3
+ exports.CompositeEngine =
4
+ exports.CelEngine =
4
5
  exports.RegoEngine =
5
6
  exports.SchemaValidator =
6
7
  exports.TemplateModel =
7
8
  exports.SchemaFile =
8
9
  exports.RuleFile =
9
10
  exports.TemplateFile =
11
+ exports.AwsCliCommand =
10
12
  void 0;
11
13
  exports.version = version;
12
14
  const fs_1 = require('fs');
15
+ /**
16
+ * Service, operation, and input values for one AWS CLI command.
17
+ *
18
+ * `serviceName` is the canonical botocore service name and is normalized only
19
+ * for ASCII case. Callers adapting an SDK request must translate its native
20
+ * service identity before constructing this request; endpoint and signing-name
21
+ * aliases are never guessed by the validation core.
22
+ */
23
+ class AwsCliCommand {
24
+ constructor(serviceName, operationName, parameters, options = {}) {
25
+ this.serviceName = serviceName;
26
+ this.operationName = operationName;
27
+ if (!isPlainRecord(parameters)) {
28
+ throw new TypeError('parameters must be a plain object with string keys');
29
+ }
30
+ const copiedParameters = Object.create(null);
31
+ for (const key of Reflect.ownKeys(parameters)) {
32
+ if (typeof key !== 'string') {
33
+ throw new TypeError('request parameter names must be strings');
34
+ }
35
+ const descriptor = Object.getOwnPropertyDescriptor(parameters, key);
36
+ if (descriptor === undefined || !('value' in descriptor)) {
37
+ throw new TypeError(`request parameter ${JSON.stringify(key)} must be a value property`);
38
+ }
39
+ copiedParameters[key] = descriptor.value;
40
+ }
41
+ this.parameters = copiedParameters;
42
+ this.servicePrefix = options.servicePrefix;
43
+ this.httpMethod = options.httpMethod;
44
+ this.isReadOnly = options.isReadOnly;
45
+ }
46
+ }
47
+ exports.AwsCliCommand = AwsCliCommand;
48
+ const MIN_SIGNED_64 = -(1n << 63n);
49
+ const MAX_SIGNED_64 = (1n << 63n) - 1n;
50
+ const MAX_UNSIGNED_64 = (1n << 64n) - 1n;
51
+ const MAX_REQUEST_VALUE_DEPTH = 64;
52
+ const DATE_GET_TIME = Date.prototype.getTime;
53
+ const DATE_TO_ISO_STRING = Date.prototype.toISOString;
54
+ const UINT8_ARRAY_FOR_EACH = Uint8Array.prototype.forEach;
55
+ function isPlainRecord(value) {
56
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
57
+ return false;
58
+ }
59
+ const prototype = Object.getPrototypeOf(value);
60
+ return prototype === Object.prototype || prototype === null;
61
+ }
62
+ function unsupportedValue(typeName) {
63
+ return { type: 'UNSUPPORTED', type_name: typeName };
64
+ }
65
+ function encodeAwsCliValue(value, depth = 0, ancestors = new Set()) {
66
+ if (depth > MAX_REQUEST_VALUE_DEPTH) {
67
+ return unsupportedValue('recursion depth exceeded');
68
+ }
69
+ if (value === null) {
70
+ return { type: 'NULL' };
71
+ }
72
+ if (typeof value === 'boolean') {
73
+ return { type: 'BOOLEAN', value };
74
+ }
75
+ if (typeof value === 'number') {
76
+ if (!Number.isFinite(value)) {
77
+ return unsupportedValue('non-finite floating-point number');
78
+ }
79
+ if (Number.isInteger(value)) {
80
+ return Number.isSafeInteger(value)
81
+ ? { type: 'INTEGER', value }
82
+ : unsupportedValue('integer outside the JavaScript safe range');
83
+ }
84
+ return { type: 'NUMBER', value };
85
+ }
86
+ if (typeof value === 'bigint') {
87
+ if (value >= MIN_SIGNED_64 && value <= MAX_SIGNED_64) {
88
+ return { type: 'INTEGER', value };
89
+ }
90
+ if (value >= 0n && value <= MAX_UNSIGNED_64) {
91
+ return { type: 'UNSIGNED_INTEGER', value };
92
+ }
93
+ return unsupportedValue('integer outside the 64-bit request range');
94
+ }
95
+ if (typeof value === 'string') {
96
+ return { type: 'STRING', value };
97
+ }
98
+ if (value instanceof Uint8Array) {
99
+ const bytes = [];
100
+ try {
101
+ UINT8_ARRAY_FOR_EACH.call(value, (byte) => {
102
+ bytes.push(byte);
103
+ });
104
+ } catch {
105
+ return unsupportedValue('invalid Uint8Array');
106
+ }
107
+ return { type: 'BYTES', value: bytes };
108
+ }
109
+ if (value instanceof Date) {
110
+ try {
111
+ const timestamp = DATE_GET_TIME.call(value);
112
+ return Number.isFinite(timestamp)
113
+ ? { type: 'STRING', value: DATE_TO_ISO_STRING.call(value) }
114
+ : unsupportedValue('invalid Date');
115
+ } catch {
116
+ return unsupportedValue('invalid Date');
117
+ }
118
+ }
119
+ if (Array.isArray(value)) {
120
+ if (ancestors.has(value)) {
121
+ return unsupportedValue('cyclic array');
122
+ }
123
+ ancestors.add(value);
124
+ try {
125
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
126
+ if (
127
+ lengthDescriptor === undefined ||
128
+ !('value' in lengthDescriptor) ||
129
+ !Number.isSafeInteger(lengthDescriptor.value) ||
130
+ lengthDescriptor.value < 0
131
+ ) {
132
+ return unsupportedValue('array with invalid length');
133
+ }
134
+ const items = [];
135
+ for (let index = 0; index < lengthDescriptor.value; index += 1) {
136
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
137
+ if (descriptor === undefined) {
138
+ return unsupportedValue('sparse array');
139
+ }
140
+ if (!('value' in descriptor)) {
141
+ return unsupportedValue('array with accessor elements');
142
+ }
143
+ items.push(encodeAwsCliValue(descriptor.value, depth + 1, ancestors));
144
+ }
145
+ return { type: 'ARRAY', items };
146
+ } finally {
147
+ ancestors.delete(value);
148
+ }
149
+ }
150
+ if (isPlainRecord(value)) {
151
+ if (ancestors.has(value)) {
152
+ return unsupportedValue('cyclic object');
153
+ }
154
+ ancestors.add(value);
155
+ try {
156
+ const entries = Object.create(null);
157
+ for (const key of Reflect.ownKeys(value)) {
158
+ if (typeof key !== 'string') {
159
+ return unsupportedValue('mapping with non-string keys');
160
+ }
161
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
162
+ if (descriptor === undefined || !('value' in descriptor)) {
163
+ return unsupportedValue('mapping with accessor properties');
164
+ }
165
+ entries[key] = encodeAwsCliValue(descriptor.value, depth + 1, ancestors);
166
+ }
167
+ return { type: 'OBJECT', entries };
168
+ } finally {
169
+ ancestors.delete(value);
170
+ }
171
+ }
172
+ return unsupportedValue(typeof value);
173
+ }
174
+ function toWireAwsCliCommand(request) {
175
+ const parameters = Object.create(null);
176
+ for (const [name, value] of Object.entries(request.parameters)) {
177
+ try {
178
+ parameters[name] = encodeAwsCliValue(value);
179
+ } catch {
180
+ parameters[name] = unsupportedValue('request value inspection failed');
181
+ }
182
+ }
183
+ return {
184
+ serviceName: request.serviceName,
185
+ operationName: request.operationName,
186
+ parameters,
187
+ ...(request.servicePrefix === undefined ? {} : { servicePrefix: request.servicePrefix }),
188
+ ...(request.httpMethod === undefined ? {} : { httpMethod: request.httpMethod }),
189
+ ...(request.isReadOnly === undefined ? {} : { isReadOnly: request.isReadOnly }),
190
+ };
191
+ }
192
+ function fromWireAwsCliCommandValidation(validation) {
193
+ const template = validation.template;
194
+ return {
195
+ ...validation,
196
+ templateSource: validation.templateSource ?? null,
197
+ report: validation.report ?? null,
198
+ template: template == null ? null : Uint8Array.from(template),
199
+ };
200
+ }
13
201
  const bridge = require('./bindings_wasm');
14
202
  class TemplateFile {
15
203
  constructor(path) {
@@ -62,6 +250,16 @@ function toWasmEngineConfig(config) {
62
250
  : undefined,
63
251
  };
64
252
  }
253
+ function toWasmCompositeEngineConfig(config) {
254
+ return {
255
+ regoRules: toExternalRuleSources(config?.regoRules),
256
+ celRules: toExternalRuleSources(config?.celRules),
257
+ guardRules: toExternalRuleSources(config?.guardRules),
258
+ schemaValidatorConfig: config?.schemaValidatorConfig
259
+ ? toWasmSchemaValidatorConfig(config.schemaValidatorConfig)
260
+ : undefined,
261
+ };
262
+ }
65
263
  function toWasmSchemaValidatorConfig(config) {
66
264
  return {
67
265
  additionalSchemas: toAdditionalSchemas(config?.additionalSchemas),
@@ -126,16 +324,19 @@ class SchemaValidator {
126
324
  }
127
325
  }
128
326
  exports.SchemaValidator = SchemaValidator;
129
- function createEngineClass(WasmClass) {
327
+ function createEngineClass(WasmClass, toWasmConfig) {
130
328
  return class {
131
329
  constructor(config) {
132
- this.inner = new WasmClass(toWasmEngineConfig(config));
330
+ this.inner = new WasmClass(toWasmConfig(config));
133
331
  }
134
- validateStandard(template, config) {
135
- return this.inner.validateStandard(template.readBytes(), config ?? {}, template.path);
332
+ validateTemplate(template, config) {
333
+ return this.inner.validateTemplate(template.readBytes(), config ?? {}, template.path);
136
334
  }
137
- validateDetailed(template, config) {
138
- return this.inner.validateDetailed(template.readBytes(), config ?? {}, template.path);
335
+ validateAwsCliCommand(request) {
336
+ if (!(request instanceof AwsCliCommand)) {
337
+ throw new TypeError('request must be an AwsCliCommand');
338
+ }
339
+ return fromWireAwsCliCommandValidation(this.inner.validateAwsCliCommand(toWireAwsCliCommand(request)));
139
340
  }
140
341
  listRules() {
141
342
  return this.inner.listRules();
@@ -148,8 +349,9 @@ function createEngineClass(WasmClass) {
148
349
  }
149
350
  };
150
351
  }
151
- exports.RegoEngine = createEngineClass(bridge.WasmRegoEngine);
152
- exports.CelEngine = createEngineClass(bridge.WasmCelEngine);
352
+ exports.RegoEngine = createEngineClass(bridge.WasmRegoEngine, toWasmEngineConfig);
353
+ exports.CelEngine = createEngineClass(bridge.WasmCelEngine, toWasmEngineConfig);
354
+ exports.CompositeEngine = createEngineClass(bridge.WasmCompositeEngine, toWasmCompositeEngineConfig);
153
355
  function version() {
154
356
  return bridge.version();
155
357
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/cloudformation-validate",
3
- "version": "1.9.0-beta",
3
+ "version": "1.10.0",
4
4
  "description": "Fast, offline, embeddable validation for AWS CloudFormation templates",
5
5
  "keywords": [
6
6
  "aws",