diff --git a/.projen/deps.json b/.projen/deps.json index 647a510f67..abfcb0e1b2 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -133,6 +133,10 @@ "name": "constructs", "version": "^10.0.5", "type": "peer" + }, + { + "name": "yaml", + "type": "runtime" } ], "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." diff --git a/.projenrc.js b/.projenrc.js index c41ff71b52..b5b25bac5d 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -14,6 +14,7 @@ const project = new awscdk.AwsCdkConstructLibrary({ description: 'Check CDK v2 applications for best practices using a combination on available rule packs.', repositoryUrl: 'https://github.com/cdklabs/cdk-nag.git', + deps: ['yaml'], devDeps: ['@aws-cdk/assert@^2.18'], publishToPypi: { distName: 'cdk-nag', diff --git a/package.json b/package.json index 346338a407..c050d90e62 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,9 @@ "aws-cdk-lib": "^2.78.0", "constructs": "^10.0.5" }, + "dependencies": { + "yaml": "^2.3.0" + }, "resolutions": { "@types/babel__traverse": "7.18.2", "@types/prettier": "2.6.0" diff --git a/src/rules/apigw/APIGWRequestValidation.ts b/src/rules/apigw/APIGWRequestValidation.ts index e9fb27a0da..0563d48636 100644 --- a/src/rules/apigw/APIGWRequestValidation.ts +++ b/src/rules/apigw/APIGWRequestValidation.ts @@ -2,9 +2,11 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ +import { readFileSync } from 'fs'; import { parse } from 'path'; -import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnResource, Stack, Stage } from 'aws-cdk-lib'; import { CfnRequestValidator, CfnRestApi } from 'aws-cdk-lib/aws-apigateway'; +import { parse as yamlparse } from 'yaml'; import { NagRuleCompliance, NagRules } from '../../nag-rules'; /** @@ -27,6 +29,35 @@ export default Object.defineProperty( } } } + if (node.bodyS3Location) { + const assetOutdir = NagRules.resolveResourceFromInstrinsic( + node, + Stage.of(node)?.assetOutdir + ); + const assetPath = NagRules.resolveResourceFromInstrinsic( + node, + node.getMetadata('aws:asset:path') + ); + const specFile = yamlparse( + readFileSync(assetOutdir + '/' + assetPath, 'utf-8') + ); + if ('x-amazon-apigateway-request-validators' in specFile) { + for (const prop in specFile[ + 'x-amazon-apigateway-request-validators' + ]) { + if ( + specFile['x-amazon-apigateway-request-validators'][prop] + .validateRequestBody && + specFile['x-amazon-apigateway-request-validators'][prop] + .validateRequestParameters + ) { + found = true; + } else { + found = false; + } + } + } + } if (!found) { return NagRuleCompliance.NON_COMPLIANT; } diff --git a/test/rules/APIGW.test.ts b/test/rules/APIGW.test.ts index d45ef110f5..bd0db8276c 100644 --- a/test/rules/APIGW.test.ts +++ b/test/rules/APIGW.test.ts @@ -2,14 +2,18 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ +import { cx_api } from 'aws-cdk-lib'; import { + ApiDefinition, AuthorizationType, CfnClientCertificate, CfnRequestValidator, CfnRestApi, CfnStage, + InlineApiDefinition, MethodLoggingLevel, RestApi, + SpecRestApi, } from 'aws-cdk-lib/aws-apigateway'; import { CfnRoute, CfnStage as CfnV2Stage } from 'aws-cdk-lib/aws-apigatewayv2'; import { CfnWebACLAssociation } from 'aws-cdk-lib/aws-wafv2'; @@ -268,6 +272,62 @@ describe('Amazon API Gateway', () => { }); validateStack(stack, ruleId, TestType.NON_COMPLIANCE); }); + test('Noncompliance 3', () => { + const apiSpec = new InlineApiDefinition({ + openapi: '3.0.2', + paths: { + '/pets': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Empty', + }, + }, + }, + }, + }, + 'x-amazon-apigateway-integration': { + responses: { + default: { + statusCode: '200', + }, + }, + requestTemplates: { + 'application/json': '{"statusCode": 200}', + }, + passthroughBehavior: 'when_no_match', + type: 'mock', + }, + }, + }, + }, + components: { + schemas: { + Empty: { + title: 'Empty Schema', + type: 'object', + }, + }, + }, + }); + new SpecRestApi(stack, 'SpecRestApi1', { apiDefinition: apiSpec }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + test('Noncompliance 4', () => { + stack.node.setContext( + cx_api.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, + true + ); + new SpecRestApi(stack, 'SpecRestApi2', { + apiDefinition: ApiDefinition.fromAsset( + './test/rules/assets/NonCompliantOpenApi.json' + ), + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); test('Compliance', () => { const compliantRestApi = new RestApi(stack, 'RestApi'); compliantRestApi.addRequestValidator('RequestValidator', { @@ -284,6 +344,18 @@ describe('Amazon API Gateway', () => { }); validateStack(stack, ruleId, TestType.COMPLIANCE); }); + test('Compliance - API import', () => { + stack.node.setContext( + cx_api.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, + true + ); + new SpecRestApi(stack, 'SpecRestApi', { + apiDefinition: ApiDefinition.fromAsset( + './test/rules/assets/CompliantOpenApi.json' + ), + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); }); describe('APIGWSSLEnabled: API Gateway REST API stages are configured with SSL certificates', () => { diff --git a/test/rules/assets/CompliantOpenApi.json b/test/rules/assets/CompliantOpenApi.json new file mode 100644 index 0000000000..73c31b1a0b --- /dev/null +++ b/test/rules/assets/CompliantOpenApi.json @@ -0,0 +1,162 @@ +{ + "swagger": "2.0", + "info": { + "title": "ReqValidators Sample", + "version": "1.0.0" + }, + "schemes": [ + "https" + ], + "basePath": "/v1", + "produces": [ + "application/json" + ], + "x-amazon-apigateway-request-validators" : { + "all" : { + "validateRequestBody" : true, + "validateRequestParameters" : true + }, + "params-only" : { + "validateRequestBody" : true, + "validateRequestParameters" : true + } + }, + "x-amazon-apigateway-request-validator" : "params-only", + "paths": { + "/validation": { + "post": { + "x-amazon-apigateway-request-validator" : "all", + "parameters": [ + { + "in": "header", + "name": "h1", + "required": true + }, + { + "in": "body", + "name": "RequestBodyModel", + "required": true, + "schema": { + "$ref": "#/definitions/RequestBodyModel" + } + } + ], + "responses": { + "200": { + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Error" + } + }, + "headers" : { + "test-method-response-header" : { + "type" : "string" + } + } + } + }, + "security" : [{ + "api_key" : [] + }], + "x-amazon-apigateway-auth" : { + "type" : "none" + }, + "x-amazon-apigateway-integration" : { + "type" : "http", + "uri" : "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod" : "POST", + "requestParameters": { + "integration.request.header.custom_h1": "method.request.header.h1" + }, + "responses" : { + "2\\d{2}" : { + "statusCode" : "200" + }, + "default" : { + "statusCode" : "400", + "responseParameters" : { + "method.response.header.test-method-response-header" : "'static value'" + }, + "responseTemplates" : { + "application/json" : "json 400 response template", + "application/xml" : "xml 400 response template" + } + } + } + } + }, + "get": { + "parameters": [ + { + "name": "q1", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Error" + } + }, + "headers" : { + "test-method-response-header" : { + "type" : "string" + } + } + } + }, + "security" : [{ + "api_key" : [] + }], + "x-amazon-apigateway-auth" : { + "type" : "none" + }, + "x-amazon-apigateway-integration" : { + "type" : "http", + "uri" : "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod" : "GET", + "requestParameters": { + "integration.request.querystring.type": "method.request.querystring.q1" + }, + "responses" : { + "2\\d{2}" : { + "statusCode" : "200" + }, + "default" : { + "statusCode" : "400", + "responseParameters" : { + "method.response.header.test-method-response-header" : "'static value'" + }, + "responseTemplates" : { + "application/json" : "json 400 response template", + "application/xml" : "xml 400 response template" + } + } + } + } + } + } + }, + "definitions": { + "RequestBodyModel": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "type": { "type": "string", "enum": ["dog", "cat", "fish"] }, + "name": { "type": "string" }, + "price": { "type": "number", "minimum": 25, "maximum": 500 } + }, + "required": ["type", "name", "price"] + }, + "Error": { + "type": "object", + "properties": { + + } + } + } + } \ No newline at end of file diff --git a/test/rules/assets/NonCompliantOpenApi.json b/test/rules/assets/NonCompliantOpenApi.json new file mode 100644 index 0000000000..7014d6e39e --- /dev/null +++ b/test/rules/assets/NonCompliantOpenApi.json @@ -0,0 +1,162 @@ +{ + "swagger": "2.0", + "info": { + "title": "ReqValidators Sample", + "version": "1.0.0" + }, + "schemes": [ + "https" + ], + "basePath": "/v1", + "produces": [ + "application/json" + ], + "x-amazon-apigateway-request-validators" : { + "all" : { + "validateRequestBody" : true, + "validateRequestParameters" : false + }, + "params-only" : { + "validateRequestBody" : false, + "validateRequestParameters" : true + } + }, + "x-amazon-apigateway-request-validator" : "params-only", + "paths": { + "/validation": { + "post": { + "x-amazon-apigateway-request-validator" : "all", + "parameters": [ + { + "in": "header", + "name": "h1", + "required": true + }, + { + "in": "body", + "name": "RequestBodyModel", + "required": true, + "schema": { + "$ref": "#/definitions/RequestBodyModel" + } + } + ], + "responses": { + "200": { + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Error" + } + }, + "headers" : { + "test-method-response-header" : { + "type" : "string" + } + } + } + }, + "security" : [{ + "api_key" : [] + }], + "x-amazon-apigateway-auth" : { + "type" : "none" + }, + "x-amazon-apigateway-integration" : { + "type" : "http", + "uri" : "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod" : "POST", + "requestParameters": { + "integration.request.header.custom_h1": "method.request.header.h1" + }, + "responses" : { + "2\\d{2}" : { + "statusCode" : "200" + }, + "default" : { + "statusCode" : "400", + "responseParameters" : { + "method.response.header.test-method-response-header" : "'static value'" + }, + "responseTemplates" : { + "application/json" : "json 400 response template", + "application/xml" : "xml 400 response template" + } + } + } + } + }, + "get": { + "parameters": [ + { + "name": "q1", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Error" + } + }, + "headers" : { + "test-method-response-header" : { + "type" : "string" + } + } + } + }, + "security" : [{ + "api_key" : [] + }], + "x-amazon-apigateway-auth" : { + "type" : "none" + }, + "x-amazon-apigateway-integration" : { + "type" : "http", + "uri" : "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod" : "GET", + "requestParameters": { + "integration.request.querystring.type": "method.request.querystring.q1" + }, + "responses" : { + "2\\d{2}" : { + "statusCode" : "200" + }, + "default" : { + "statusCode" : "400", + "responseParameters" : { + "method.response.header.test-method-response-header" : "'static value'" + }, + "responseTemplates" : { + "application/json" : "json 400 response template", + "application/xml" : "xml 400 response template" + } + } + } + } + } + } + }, + "definitions": { + "RequestBodyModel": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "type": { "type": "string", "enum": ["dog", "cat", "fish"] }, + "name": { "type": "string" }, + "price": { "type": "number", "minimum": 25, "maximum": 500 } + }, + "required": ["type", "name", "price"] + }, + "Error": { + "type": "object", + "properties": { + + } + } + } + } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7886629f44..570b639c8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6674,6 +6674,11 @@ yaml@^2.2.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== +yaml@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.0.tgz#47ebe58ee718f772ce65862beb1db816210589a0" + integrity sha512-8/1wgzdKc7bc9E6my5wZjmdavHLvO/QOmLG1FBugblEvY4IXrLjlViIOmL24HthU042lWTDRO90Fz1Yp66UnMw== + yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"