diff --git a/apps/api/src/cloud-security/aws-command-executor.spec.ts b/apps/api/src/cloud-security/aws-command-executor.spec.ts index 4b875fa53..ff998eac4 100644 --- a/apps/api/src/cloud-security/aws-command-executor.spec.ts +++ b/apps/api/src/cloud-security/aws-command-executor.spec.ts @@ -422,12 +422,32 @@ describe('PutMetricFilterCommand required params + normalization', () => { ]); expect(errors).toEqual( expect.arrayContaining([ - expect.stringMatching(/Required param "filterPattern" is missing/), + // filterPattern must be PRESENT (but may be empty), so a missing one is + // reported via the present-check, not the non-empty REQUIRED_PARAMS one. + expect.stringMatching(/Required param "filterPattern" must be provided/), expect.stringMatching(/Required param "metricTransformations" is missing/), ]), ); }); + it('allows an empty filterPattern (AWS accepts "" — it matches all events)', () => { + const errors = validatePlanSteps([ + step({ + service: 'cloudwatch-logs', + command: 'PutMetricFilterCommand', + params: { + logGroupName: 'lg', + filterName: 'fn', + filterPattern: '', + metricTransformations: [ + { metricName: 'm', metricNamespace: 'CloudTrailMetrics', metricValue: '1' }, + ], + }, + }), + ]); + expect(errors.filter((e) => /filterPattern/.test(e))).toHaveLength(0); + }); + it('does not error when all PutMetricFilter params are present', () => { const errors = validatePlanSteps([ step({ diff --git a/apps/api/src/cloud-security/aws-command-executor.ts b/apps/api/src/cloud-security/aws-command-executor.ts index ec786902c..713ea3e21 100644 --- a/apps/api/src/cloud-security/aws-command-executor.ts +++ b/apps/api/src/cloud-security/aws-command-executor.ts @@ -165,12 +165,18 @@ export const REQUIRED_PARAMS: Record = { StartConfigurationRecorderCommand: ['ConfigurationRecorderName'], PutBucketPolicyCommand: ['Bucket', 'Policy'], CreateTrailCommand: ['Name', 'S3BucketName'], - PutMetricFilterCommand: [ - 'logGroupName', - 'filterName', - 'filterPattern', - 'metricTransformations', - ], + PutMetricFilterCommand: ['logGroupName', 'filterName', 'metricTransformations'], +}; + +/** + * Params that must be PRESENT in the request but may legitimately be an empty + * string — unlike REQUIRED_PARAMS, which also rejects "". For example, AWS + * CloudWatch Logs PutMetricFilter requires `filterPattern` to be supplied but + * accepts an empty pattern (an empty filterPattern matches all log events), so + * we must reject only a missing/null value, not "". + */ +const REQUIRED_PRESENT_PARAMS: Record = { + PutMetricFilterCommand: ['filterPattern'], }; const REQUIRED_PARAM_ONE_OF: Record = { @@ -649,6 +655,18 @@ export function validatePlanSteps(steps: AwsCommandStep[]): string[] { } } + const requiredPresent = REQUIRED_PRESENT_PARAMS[step.command]; + if (requiredPresent) { + for (const key of requiredPresent) { + const value = step.params?.[key]; + // Must be supplied, but an empty string is valid (e.g. an empty + // CloudWatch filterPattern matches all log events). + if (value === undefined || value === null) { + errors.push(`${prefix}: Required param "${key}" must be provided`); + } + } + } + const oneOfGroups = REQUIRED_PARAM_ONE_OF[step.command]; if (oneOfGroups) { for (const group of oneOfGroups) {