├── .npmrc ├── images └── how-it-works.png ├── .gitignore ├── .eslintrc.json ├── runners ├── AutoRemediateOrganizations-002.runner.js ├── AutoRemediateS3-012.runner.js ├── AutoRemediateIAM-001.runner.js ├── AutoRemediateIAM-038.runner.js ├── AutoRemediateEBS-009.runner.js ├── AutoRemediateKMS-002.runner.js ├── AutoRemediateKMS-004.runner.js ├── AutoRemediateRS-019.runner.js ├── AutoRemediateS3-016.runner.js ├── AutoRemediateRDS-023.runner.js ├── AutoRemediateS3-014.runner.js ├── AutoRemediateEC2-002.runner.js ├── AutoRemediateEC2-005.runner.js ├── AutoRemediateEC2-019.runner.js ├── AutoRemediateGD-001.runner.js ├── AutoRemediateRS-023.runner.js ├── AutoRemediatLamda-003.runner.js ├── AutoRemediateVPC-001.runner.js ├── AutoRemediateKinesis-001.runner.js ├── AutoRemediateRDS-006.runner.js ├── AutoRemediateCT-003.runner.js ├── AutoRemediateSQS-004.runner.js ├── AutoRemediateTrustedAdvisor-003.runner.js └── AutoRemediateS3-001.runner.js ├── functions ├── Utils.js ├── AutoRemediateKMS-002.js ├── AutoRemediateTrustedAdvisor-003.js ├── AutoRemediateOrganizations-002.js ├── AutoRemediateCT-003.js ├── AutoRemediateS3-012.js ├── AutoRemediateEC2-040.js ├── AutoRemediateKMS-004.js ├── AutoRemediateEC2-039.js ├── AutoRemediateEC2-043.js ├── AutoRemediateEC2-002.js ├── AutoRemediateEC2-038.js ├── AutoRemediateEC2-003.js ├── AutoRemediateEC2-004.js ├── AutoRemediateEC2-045.js ├── AutoRemediateGD-001.js ├── AutoRemediateRDS-006.js ├── AutoRemediateRDS-008.js ├── AutoRemediateEC2-005.js ├── AutoRemediateEC2-008.js ├── AutoRemediateS3-002.js ├── AutoRemediateS3-003.js ├── AutoRemediateEC2-006.js ├── AutoRemediateS3-004.js ├── AutoRemediateS3-005.js ├── AutoRemediateRS-001.js ├── AutoRemediateS3-006.js ├── AutoRemediateS3-008.js ├── AutoRemediateS3-007.js ├── AutoRemediateEBS-009.js ├── AutoRemediateRDS-023.js ├── AutoRemediateS3-009.js ├── AutoRemediateCFM-005.js ├── AutoRemediateRS-019.js ├── AutoRemediateS3-010.js ├── AutoRemediateEC2-019.js ├── AutoRemediateRS-023.js ├── AutoRemediateIAM-001.js ├── AutoRemediateIAM-038.js ├── AutoRemediateCT-001.js ├── AutoRemediateSQS-004.js ├── AutoRemediateKinesis-001.js ├── AutoRemediateOrchestrator.js ├── AutoRemediateS3-001.js ├── AutoRemediateLambda-003.js ├── AutoRemediateIAM-029.js ├── AutoRemediateS3-014.js ├── AutoRemediateS3-016.js ├── config.json ├── AutoRemediateVPC-001.js └── AutoRemediateConfig-001.js ├── jest.config.js ├── LICENSE.md ├── utils ├── S3_utils.js └── security_group_access_revoker.js ├── .github └── workflows │ ├── pull-request.yml │ └── codeql-analysis.yml ├── package.json ├── test ├── utils │ └── S3_utils.test.js ├── AutoRemediateS3-003.test.js ├── AutoRemediateS3-002.test.js ├── AutoRemediateS3-004.test.js ├── AutoRemediateS3-005.test.js ├── AutoRemediateS3-006.test.js ├── AutoRemediateS3-008.test.js ├── AutoRemediateS3-009.test.js ├── AutoRemediateS3-010.test.js └── AutoRemediateS3-007.test.js ├── exclude-rules.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | always-auth=false 3 | -------------------------------------------------------------------------------- /images/how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudconformity/auto-remediate/HEAD/images/how-it-works.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .env 4 | *.env 5 | .serverless 6 | .idea 7 | coverage 8 | *.swp 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "jest/globals": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "standard", 9 | "plugin:jest/recommended", 10 | "plugin:jest/style" 11 | ], 12 | "plugins": [ 13 | "standard", 14 | "jest" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /runners/AutoRemediateOrganizations-002.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | 3 | } 4 | 5 | const AutoRemediate = require('../functions/AutoRemediateOrganizations-002') 6 | 7 | AutoRemediate.handler(event, {}, function (err, data) { 8 | console.log(err) 9 | console.log('data', JSON.stringify(data, null, 2)) 10 | }) 11 | -------------------------------------------------------------------------------- /runners/AutoRemediateS3-012.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'YOUR_BUCKET_NAME' 3 | } 4 | 5 | const AutoRemediate = require('../functions/AutoRemediateS3-012') 6 | 7 | AutoRemediate.handler(event, {}, function (err, data) { 8 | console.log(err) 9 | console.log('data', JSON.stringify(data, null, 2)) 10 | }) 11 | -------------------------------------------------------------------------------- /runners/AutoRemediateIAM-001.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'AKIA----------------' 3 | } 4 | 5 | const AutoRemediate = require('../functions/AutoRemediateIAM-001') 6 | 7 | AutoRemediate.handler(event, {}, function (err, data) { 8 | console.log(err) 9 | console.log('data', JSON.stringify(data, null, 2)) 10 | }) 11 | -------------------------------------------------------------------------------- /runners/AutoRemediateIAM-038.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'AKIA----------------' 3 | } 4 | 5 | const AutoRemediate = require('../functions/AutoRemediateIAM-038') 6 | 7 | AutoRemediate.handler(event, {}, function (err, data) { 8 | console.log(err) 9 | console.log('data', JSON.stringify(data, null, 2)) 10 | }) 11 | -------------------------------------------------------------------------------- /runners/AutoRemediateEBS-009.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'snap-41df37a1', 3 | region: 'us-east-1' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateEBS-009') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateKMS-002.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'your_kms_key_id', 3 | region: 'us-east-1' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateKMS-002') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateKMS-004.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'your_kms_key_id', 3 | region: 'us-east-1' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateKMS-004') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateRS-019.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'YOUR_RS_CLUSTER_ID', 3 | region: 'us-east-1' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateRS-019') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateS3-016.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'privatetest2', 3 | region: 'ap-southeast-2' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateS3-016') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateRDS-023.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'my-db-snapshot', 3 | region: 'ap-southeast-2b' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateRDS-023') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateS3-014.runner.js: -------------------------------------------------------------------------------- 1 | 2 | const event = { 3 | resource: 'privatetest2', 4 | region: 'ap-southeast-2' 5 | } 6 | 7 | const AutoRemediate = require('../functions/AutoRemediateS3-014') 8 | 9 | AutoRemediate.handler(event, {}, function (err, data) { 10 | console.log(err) 11 | console.log('data', JSON.stringify(data, null, 2)) 12 | }) 13 | -------------------------------------------------------------------------------- /runners/AutoRemediateEC2-002.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'YOUR_SECURITY_GROUP_NAME', 3 | region: 'us-east-1' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateEC2-002') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateEC2-005.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'YOUR_SECURITY_GROUP_NAME', 3 | region: 'us-east-1' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateEC2-005') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateEC2-019.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'ami-0afd9aa15c54e1210', 3 | region: 'ap-southeast-2' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateEC2-019') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateGD-001.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | ruleId: 'GD-001', 3 | region: 'us-east-1', 4 | status: 'FAILURE' 5 | } 6 | 7 | const AutoRemediate = require('../functions/AutoRemediateGD-001') 8 | 9 | AutoRemediate.handler(event, {}, function (err, data) { 10 | console.log(err) 11 | console.log('data', JSON.stringify(data, null, 2)) 12 | }) 13 | -------------------------------------------------------------------------------- /runners/AutoRemediateRS-023.runner.js: -------------------------------------------------------------------------------- 1 | 2 | const event = { 3 | resource: 'your_redshift_parmatet_group', 4 | region: 'us-east-1' 5 | } 6 | 7 | const AutoRemediate = require('../functions/AutoRemediateRS-023') 8 | 9 | AutoRemediate.handler(event, {}, function (err, data) { 10 | console.log(err) 11 | console.log('data', JSON.stringify(data, null, 2)) 12 | }) 13 | -------------------------------------------------------------------------------- /runners/AutoRemediatLamda-003.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | resource: 'auto-remediate-v1-AutoRemediateIAM-038', 3 | region: 'ap-southeast-2' 4 | } 5 | 6 | const AutoRemediate = require('../functions/AutoRemediateLambda-003') 7 | 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateVPC-001.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | ruleId: 'VPC-001', 3 | resource: 'vpc-68c3000d', 4 | region: 'ap-southeast-2', 5 | status: 'FAILURE' 6 | } 7 | const AutoRemediate = require('../functions/AutoRemediateVPC-001') 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateKinesis-001.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | ruleId: 'Kinesis-001', 3 | resource: 'mystream', 4 | region: 'ap-southeast-2', 5 | status: 'FAILURE' 6 | } 7 | const AutoRemediate = require('../functions/AutoRemediateKinesis-001') 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateRDS-006.runner.js: -------------------------------------------------------------------------------- 1 | 2 | const event = { 3 | ruleId: 'RDS-006', 4 | resource: 'MY-RDS-INSTANCE', 5 | region: 'ap-southeast-2', 6 | status: 'FAILURE' 7 | } 8 | const AutoRemediate = require('../functions/AutoRemediateRDS-006') 9 | AutoRemediate.handler(event, {}, function (err, data) { 10 | console.log(err) 11 | console.log('data', JSON.stringify(data, null, 2)) 12 | }) 13 | -------------------------------------------------------------------------------- /runners/AutoRemediateCT-003.runner.js: -------------------------------------------------------------------------------- 1 | 2 | const event = { 3 | ruleId: 'CT-003', 4 | resource: 'cloudtrail-global-logging-s3-bucket', 5 | region: 'ap-southeast-2', 6 | status: 'FAILURE' 7 | } 8 | const AutoRemediate = require('../functions/AutoRemediateCT-003') 9 | AutoRemediate.handler(event, {}, function (err, data) { 10 | console.log(err) 11 | console.log('data', JSON.stringify(data, null, 2)) 12 | }) 13 | -------------------------------------------------------------------------------- /runners/AutoRemediateSQS-004.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | ruleId: 'SQS-004', 3 | resource: 'https://sqs.ap-southeast-2.amazonaws.com/12345678/mysqs', 4 | region: 'ap-southeast-2', 5 | status: 'FAILURE' 6 | } 7 | const AutoRemediate = require('../functions/AutoRemediateSQS-004') 8 | AutoRemediate.handler(event, {}, function (err, data) { 9 | console.log(err) 10 | console.log('data', JSON.stringify(data, null, 2)) 11 | }) 12 | -------------------------------------------------------------------------------- /runners/AutoRemediateTrustedAdvisor-003.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | ruleId: 'TrustedAdvisor-003', 3 | resource: 'your_aws_key_id', 4 | region: 'ap-southeast-2', 5 | status: 'FAILURE' 6 | } 7 | 8 | const AutoRemediate = require('../functions/AutoRemediateTrustedAdvisor-003') 9 | 10 | AutoRemediate.handler(event, {}, function (err, data) { 11 | console.log(err) 12 | console.log('data', JSON.stringify(data, null, 2)) 13 | }) 14 | -------------------------------------------------------------------------------- /functions/Utils.js: -------------------------------------------------------------------------------- 1 | const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts') 2 | 3 | module.exports = { 4 | /** 5 | * Returns AWS Account ID whose credentials are used to call the API 6 | * 7 | */ 8 | getAccountId: async function () { 9 | const STS = new STSClient({ apiVersion: '2011-06-15' }) 10 | 11 | const { Account: account } = await STS.send(new GetCallerIdentityCommand({})) 12 | return account 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /runners/AutoRemediateS3-001.runner.js: -------------------------------------------------------------------------------- 1 | const event = { 2 | ruleId: 'S3-001', 3 | ruleTitle: "S3 Bucket Public 'READ' Access", 4 | service: 'S3', 5 | region: 'us-east-1', 6 | riskLevel: 'VERY_HIGH', 7 | resource: 'open-bucket-created-by-mike', 8 | status: 'FAILURE' 9 | } 10 | 11 | const AutoRemediate = require('../functions/AutoRemediateS3-001') 12 | 13 | AutoRemediate.handler(event, {}, function (err, data) { 14 | console.log(err) 15 | console.log('data', JSON.stringify(data, null, 2)) 16 | }) 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | restoreMocks: true, 4 | resetModules: true, 5 | testEnvironment: 'node', 6 | testPathIgnorePatterns: ['/node_modules', '/runners'], 7 | collectCoverageFrom: ['functions/**/*.js', 'utils/**/*.js'], 8 | coverageReporters: ['json', 'lcov', 'json-summary', 'text'], 9 | coverageThreshold: { 10 | global: { 11 | statements: 15, 12 | branches: 10, 13 | functions: 15, 14 | lines: 15 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /functions/AutoRemediateKMS-002.js: -------------------------------------------------------------------------------- 1 | 2 | const { KMSClient, EnableKeyRotationCommand } = require('@aws-sdk/client-kms') 3 | 4 | /** 5 | * Lambda function to Enabled KMS Key Rotation 6 | */ 7 | 8 | const handler = async (event) => { 9 | console.log('Enable AWS KMS Key Rotation - Received event:', JSON.stringify(event, null, 2)) 10 | 11 | if (!event || !event.resource) { 12 | return handleError('Invalid event') 13 | } 14 | 15 | const params = { 16 | KeyId: event.resource 17 | } 18 | 19 | const KMS = new KMSClient({ region: event.region }) 20 | 21 | try { 22 | const result = await KMS.send(new EnableKeyRotationCommand(params)) 23 | console.log('Result', result) 24 | return 'Successfully processed event' 25 | } catch (err) { 26 | console.log('Error', err) 27 | return handleError(err.message ? err.message : 'Enable Key Rotation failed') 28 | } 29 | 30 | function handleError (message) { 31 | message = message || 'Failed to process request.' 32 | throw new Error(message) 33 | } 34 | } 35 | 36 | module.exports = { handler } 37 | -------------------------------------------------------------------------------- /functions/AutoRemediateTrustedAdvisor-003.js: -------------------------------------------------------------------------------- 1 | 2 | const { IAMClient, UpdateAccessKeyCommand } = require('@aws-sdk/client-iam') 3 | 4 | /** 5 | * Lambda function disable exposed Amazon IAM access keys 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log('Exposed IAM Access Keys - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.resource) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | AccessKeyId: event.resource, 18 | Status: 'Inactive' 19 | } 20 | 21 | const IAM = new IAMClient() 22 | 23 | try { 24 | const result = await IAM.send(new UpdateAccessKeyCommand(params)) 25 | console.log('Result', result) 26 | return 'Successfully processed event' 27 | } catch (err) { 28 | console.log('Error', err) 29 | return handleError(err.message ? err.message : 'update Access Key failed') 30 | } 31 | 32 | function handleError (message) { 33 | message = message || 'Failed to process request.' 34 | throw new Error(message) 35 | } 36 | } 37 | 38 | module.exports = { handler } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateOrganizations-002.js: -------------------------------------------------------------------------------- 1 | 2 | const { OrganizationsClient, EnableAllFeaturesCommand } = require('@aws-sdk/client-organizations') 3 | 4 | /** 5 | * Lambda function to enable All Features for Organizations 6 | */ 7 | 8 | const handler = async (event) => { 9 | console.log('Enable All Features - Received event:', JSON.stringify(event, null, 2)) 10 | 11 | if (!event || !event.region) { 12 | return handleError('Invalid event') 13 | } 14 | 15 | const params = { 16 | 17 | } 18 | 19 | const Organizations = new OrganizationsClient({ region: event.region }) 20 | 21 | try { 22 | const result = await Organizations.send(new EnableAllFeaturesCommand(params)) 23 | console.log('Result', result) 24 | return 'Successfully processed event' 25 | } catch (err) { 26 | console.log('Error', err) 27 | return handleError(err.message ? err.message : 'Enable All Features failed') 28 | } 29 | 30 | function handleError (message) { 31 | message = message || 'Failed to process request.' 32 | throw new Error(message) 33 | } 34 | } 35 | 36 | module.exports = { handler } 37 | -------------------------------------------------------------------------------- /functions/AutoRemediateCT-003.js: -------------------------------------------------------------------------------- 1 | const { S3Client, PutBucketAclCommand } = require('@aws-sdk/client-s3') 2 | /** 3 | * Lambda function to disable access to any AWS CloudTrail logging buckets that are publicly accessible 4 | * 5 | */ 6 | module.exports.handler = async (event) => { 7 | console.log(' Publicly Shared AWS CloudTrail Logging Buckets - Received event:', JSON.stringify(event, null, 2)) 8 | if (!event || !event.resource || !event.region) { 9 | return handleError('Invalid event') 10 | } 11 | 12 | const params = { 13 | Bucket: event.resource, 14 | ACL: 'private' 15 | } 16 | 17 | const S3 = new S3Client({ region: event.region }) 18 | 19 | try { 20 | const result = await S3.send(new PutBucketAclCommand(params)) 21 | console.log('Result', result) 22 | return 'Successfully processed event' 23 | } catch (err) { 24 | console.log('Error', err) 25 | return handleError(err.message ? err.message : 'Failed to putBucketAcl') 26 | } 27 | 28 | function handleError (message) { 29 | message = message || 'Failed to process request.' 30 | throw new Error(message) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-012.js: -------------------------------------------------------------------------------- 1 | 2 | const { S3Client, PutBucketVersioningCommand } = require('@aws-sdk/client-s3') 3 | 4 | /** 5 | * Lambda function to enable versioning on an s3 bucket 6 | */ 7 | 8 | const handler = async (event) => { 9 | console.log('S3BucketVersioning - Received event:', JSON.stringify(event, null, 2)) 10 | 11 | if (!event || !event.resource) { 12 | return handleError('Invalid event') 13 | } 14 | 15 | const params = { 16 | Bucket: event.resource, 17 | VersioningConfiguration: { 18 | Status: 'Enabled' 19 | } 20 | } 21 | 22 | const S3 = new S3Client() 23 | 24 | try { 25 | const result = await S3.send(new PutBucketVersioningCommand(params)) 26 | console.log('Result', result) 27 | return 'Successfully processed event' 28 | } catch (err) { 29 | console.log('Error', err) 30 | return handleError(err.message ? err.message : 'enabling bucket versioning failed') 31 | } 32 | 33 | function handleError (message) { 34 | message = message || 'Failed to process request.' 35 | throw new Error(message) 36 | } 37 | } 38 | 39 | module.exports = { handler } 40 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-040.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_RPC_PORT = 135 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 135 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedRPCAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_RPC_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing RPC access on port 135 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateKMS-004.js: -------------------------------------------------------------------------------- 1 | 2 | const { KMSClient, CancelKeyDeletionCommand } = require('@aws-sdk/client-kms') 3 | 4 | /** 5 | * Lambda function to make AWS KMS Customer Master Keys (CMK) that has been scheduled for deletion canceled 6 | */ 7 | 8 | const handler = async (event) => { 9 | console.log('Recover KMS Customer Master Keys - Received event:', JSON.stringify(event, null, 2)) 10 | 11 | if (!event || !event.resource) { 12 | return handleError('Invalid event') 13 | } 14 | 15 | const params = { 16 | KeyId: event.resource 17 | } 18 | 19 | const KMS = new KMSClient({ region: event.region }) 20 | 21 | try { 22 | const result = await KMS.send(new CancelKeyDeletionCommand(params)) 23 | console.log('Result', result) 24 | return 'Successfully processed event' 25 | } catch (err) { 26 | console.log('Error', err) 27 | return handleError(err.message ? err.message : 'Cancel Key Deletion failed') 28 | } 29 | 30 | function handleError (message) { 31 | message = message || 'Failed to process request.' 32 | throw new Error(message) 33 | } 34 | } 35 | 36 | module.exports = { handler } 37 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-039.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_SMTP_PORT = 25 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 25 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedSMTPAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_SMTP_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing smtp access on port 25 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-043.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_CIFS_PORT = 445 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 445 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedCIFSAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_CIFS_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing CIFS access on port 445 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-002.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_SSH_PORT = 22 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 22 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedSSHAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_SSH_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing SSH database access on port 22 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-038.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_TELNET_PORT = 23 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 23 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedTELNETAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_TELNET_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing telnet access on port 23 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cloud Conformity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-003.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_RDP_PORT = 3389 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 3389 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedRDPAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_RDP_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing RDP database access on port RDP failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-004.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_ORACLE_PORT = 1521 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 1521 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedORACLEAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_ORACLE_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing Oracle database access on port 1521 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-045.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_MONGO_PORT = 27017 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 27017 from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedMONGOAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_MONGO_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing MONGO database access on port 27017 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateGD-001.js: -------------------------------------------------------------------------------- 1 | 2 | const { GuardDutyClient, CreateDetectorCommand } = require('@aws-sdk/client-guardduty') 3 | 4 | /** 5 | * Lambda function to make Amazon GuardDuty service is currently enabled in order to protect your AWS environment and infrastructure 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log('GuardDuty In Use - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.region) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | Enable: true 18 | } 19 | 20 | const GuardDuty = new GuardDutyClient({ region: event.region }) 21 | 22 | try { 23 | const result = await GuardDuty.send(new CreateDetectorCommand(params)) 24 | console.log('Result', result) 25 | return 'Successfully processed event' 26 | } catch (err) { 27 | console.log('Error', err) 28 | return handleError(err.message ? err.message : 'Failed to enable GuardDuty') 29 | } 30 | 31 | function handleError (message) { 32 | message = message || 'Failed to process request.' 33 | throw new Error(message) 34 | } 35 | } 36 | 37 | module.exports = { handler } 38 | -------------------------------------------------------------------------------- /utils/S3_utils.js: -------------------------------------------------------------------------------- 1 | const { GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 2 | const isEqual = require('lodash.isequal') 3 | const filterAclGrants = function (acl, grantToRemove) { 4 | return { 5 | Owner: acl.Owner, 6 | Grants: acl.Grants.filter(grant => !isEqual(grant, grantToRemove)) 7 | } 8 | } 9 | 10 | const getAcl = async (s3, bucketName) => { 11 | var getAclParams = { 12 | Bucket: bucketName 13 | } 14 | return s3.send(new GetBucketAclCommand(getAclParams)) 15 | } 16 | 17 | const putAcl = async (s3, bucketName, acl) => { 18 | const putAclParams = { 19 | Bucket: bucketName, 20 | AccessControlPolicy: acl 21 | } 22 | return s3.send(new PutBucketAclCommand(putAclParams)) 23 | } 24 | 25 | const filterAcl = async (s3, bucketName, grantToRemove) => { 26 | const acl = await getAcl(s3, bucketName) 27 | const filteredAcl = await filterAclGrants(acl, grantToRemove) 28 | const putAclResponse = await putAcl(s3, bucketName, filteredAcl) 29 | 30 | console.log('result>' + JSON.stringify(putAclResponse)) 31 | } 32 | 33 | module.exports = { 34 | filterAclGrants: filterAclGrants, 35 | filterAcl: filterAcl 36 | } 37 | -------------------------------------------------------------------------------- /functions/AutoRemediateRDS-006.js: -------------------------------------------------------------------------------- 1 | const { RDSClient, ModifyDBInstanceCommand } = require('@aws-sdk/client-rds') 2 | /** 3 | * Lambda function to enable Update Minor Version flag for AWS RDS 4 | * 5 | */ 6 | const handler = async (event) => { 7 | console.log(' Update Minor Version for RDS - Received event:', JSON.stringify(event, null, 2)) 8 | if (!event || !event.resource || !event.region) { 9 | return handleError('Invalid event') 10 | } 11 | 12 | const params = { 13 | DBInstanceIdentifier: event.resource, 14 | AutoMinorVersionUpgrade: true, 15 | ApplyImmediately: true 16 | 17 | } 18 | 19 | const RDS = new RDSClient({ region: event.region }) 20 | 21 | try { 22 | const result = await RDS.send(new ModifyDBInstanceCommand(params)) 23 | console.log('Result', result) 24 | return 'Successfully processed event' 25 | } catch (err) { 26 | console.log('Error', err) 27 | return handleError(err.message ? err.message : 'Failed to modify DB Instance') 28 | } 29 | 30 | function handleError (message) { 31 | message = message || 'Failed to process request.' 32 | throw new Error(message) 33 | } 34 | } 35 | 36 | module.exports = { handler } 37 | -------------------------------------------------------------------------------- /functions/AutoRemediateRDS-008.js: -------------------------------------------------------------------------------- 1 | 2 | const { RDSClient, ModifyDBInstanceCommand } = require('@aws-sdk/client-rds') 3 | 4 | /** 5 | * Lambda function to automatically remediate publicly accessible RDS instance 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log('PubliclyAccessible - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.ccrn || !event.resource || !event.region) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | DBInstanceIdentifier: event.resource, 18 | PubliclyAccessible: false 19 | } 20 | 21 | const RDS = new RDSClient({ region: event.region }) 22 | 23 | try { 24 | const result = await RDS.send(new ModifyDBInstanceCommand(params)) 25 | console.log('Result', result) 26 | return 'Successfully processed event' 27 | } catch (err) { 28 | console.log('Error', err) 29 | return handleError(err.message ? err.message : 'Failed to modify DB Instance') 30 | } 31 | 32 | function handleError (message) { 33 | message = message || 'Failed to process request.' 34 | throw new Error(message) 35 | } 36 | } 37 | 38 | module.exports = { handler } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-005.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_MYSQL_PORT = 3306 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 3306 (used by MYSQL Database Server) from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedMYSQLAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_MYSQL_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing mysql database access on port 3306 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-008.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_MSSQL_PORT = 1433 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 1433 (used by MSSQL Database Server) from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedMSSQLAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_MSSQL_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing mssql database access on port 1433 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-002.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils/S3_utils') 2 | const { S3Client } = require('@aws-sdk/client-s3') 3 | 4 | const CCRuleCode = 'S3-002' 5 | const CCRuleName = 'BucketPublicReadAcpAccess' 6 | 7 | const readAcpAllUsers = { 8 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 9 | } 10 | 11 | function handleError (message) { 12 | message = message || 'Failed to process request.' 13 | throw new Error(message) 14 | } 15 | 16 | // look for and remove S3BucketPublicReadAccess 17 | const handler = async (event) => { 18 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 19 | 20 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 21 | return handleError('Invalid event') 22 | } 23 | 24 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 25 | try { 26 | await utils.filterAcl(s3, event.resource, readAcpAllUsers) 27 | return 'Success' 28 | } catch (err) { 29 | console.log(err, err.stack) 30 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 31 | } 32 | } 33 | 34 | module.exports = { 35 | handler: handler 36 | } 37 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-003.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-003' 6 | const CCRuleName = 'BucketPublicWriteAccess' 7 | 8 | const writeAllUsers = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3BucketPublicReadAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, writeAllUsers) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-006.js: -------------------------------------------------------------------------------- 1 | const AccessRevoker = require('../utils/security_group_access_revoker') 2 | 3 | const TCP_PSS_PORT = 5432 4 | const PROTOCOL = 'tcp' 5 | 6 | /** 7 | * Lambda function to remove the inbound rule that allow unrestricted access through TCP port 5432 (used by PostgreSQL Database Server) from the selected EC2 security group. 8 | */ 9 | 10 | module.exports.handler = (event, context, callback) => { 11 | console.log('UnrestrictedPOSTGRESAccess - Received event:', JSON.stringify(event, null, 2)) 12 | 13 | if (!event || !event.resource || !event.region) { 14 | return handleError('Invalid event') 15 | } 16 | 17 | AccessRevoker.revoke(PROTOCOL, TCP_PSS_PORT, event.resource, event.region, function (err, result) { 18 | if (err) { 19 | console.log('Error', err) 20 | return handleError(err.message ? err.message : 'removing postgresql database access on port 5432 failed') 21 | } 22 | console.log('Result', result) 23 | return callback(null, 'Successfully processed event') 24 | }) 25 | 26 | function handleError (message) { 27 | message = message || 'Failed to process request.' 28 | return callback(new Error(message)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-004.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-004' 6 | const CCRuleName = 'BucketPublicWriteAcpAccess' 7 | 8 | const writeAllUsers = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3BucketPublicReadAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, writeAllUsers) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-005.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-005' 6 | const CCRuleName = 'BucketPublicReadAcpAccess' 7 | 8 | const readAcpAllUsers = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'FULL_CONTROL' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3BucketPublicReadAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, readAcpAllUsers) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateRS-001.js: -------------------------------------------------------------------------------- 1 | 2 | const { RedshiftClient, ModifyClusterCommand } = require('@aws-sdk/client-redshift') 3 | 4 | /** 5 | * Lambda function to make Redshift Cluster private to minimise security risks 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log('ClusterPubliclyAccessible - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.ccrn || !event.resource || !event.region) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | ClusterIdentifier: event.resource, 18 | PubliclyAccessible: false 19 | } 20 | 21 | const Redshift = new RedshiftClient({ region: event.region }) 22 | 23 | try { 24 | const result = await Redshift.send(new ModifyClusterCommand(params)) 25 | console.log('Result', result) 26 | return 'Successfully processed event' 27 | } catch (err) { 28 | console.log('Error', err) 29 | return handleError(err.message ? err.message : 'Failed to make Redshift cluster private') 30 | } 31 | 32 | function handleError (message) { 33 | message = message || 'Failed to process request.' 34 | throw new Error(message) 35 | } 36 | } 37 | 38 | module.exports = { handler } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-006.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-006' 6 | const CCRuleName = 'BucketAuthenticatedUsersReadAccess' 7 | 8 | const readAuthenticatedUsers = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3 BucketAuthenticatedUsersReadAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, readAuthenticatedUsers) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-008.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-008' 6 | const CCRuleName = 'BucketAuthenticatedUsersWriteAccess' 7 | 8 | const writeAuthenticatedUsers = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3 BucketAuthenticatedUsersWriteAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, writeAuthenticatedUsers) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-007.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-007' 6 | const CCRuleName = 'BucketAuthenticatedUsersReadAcpAccess' 7 | 8 | const readAcpAuthenticatedUsers = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3 BucketAuthenticatedUsersReadAcpAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, readAcpAuthenticatedUsers) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateEBS-009.js: -------------------------------------------------------------------------------- 1 | const { EC2Client, ModifySnapshotAttributeCommand } = require('@aws-sdk/client-ec2') 2 | /** 3 | * Lambda function enforce Elastic Block Store (EBS) volume snapshots not to be public 4 | * 5 | */ 6 | const handler = async (event) => { 7 | console.log('Publicly Accessible EBS Snapshot - Received event:', JSON.stringify(event, null, 2)) 8 | if (!event || !event.resource || !event.region) { 9 | return handleError('Invalid event') 10 | } 11 | 12 | const params = { 13 | SnapshotId: event.resource, 14 | Attribute: 'createVolumePermission', 15 | OperationType: 'remove', 16 | GroupNames: [ 17 | 'all' 18 | ] 19 | 20 | } 21 | 22 | const EC2 = new EC2Client({ region: event.region }) 23 | 24 | try { 25 | const result = await EC2.send(new ModifySnapshotAttributeCommand(params)) 26 | console.log('Result', result) 27 | return 'Successfully processed event' 28 | } catch (err) { 29 | console.log('Error', err) 30 | return handleError(err.message ? err.message : 'Failed to modify DB Instance') 31 | } 32 | 33 | function handleError (message) { 34 | message = message || 'Failed to process request.' 35 | throw new Error(message) 36 | } 37 | } 38 | 39 | module.exports = { handler } 40 | -------------------------------------------------------------------------------- /functions/AutoRemediateRDS-023.js: -------------------------------------------------------------------------------- 1 | 2 | const { RDSClient, ModifyDBSnapshotAttributeCommand } = require('@aws-sdk/client-rds') 3 | 4 | /** 5 | * Lambda function to enforce AWS Relational Database Service (RDS) database snapshots not to be shared publicly 6 | */ 7 | 8 | const handler = async (event) => { 9 | console.log('RDS Snapshot Publicly Accessible - Received event:', JSON.stringify(event, null, 2)) 10 | 11 | if (!event || !event.resource) { 12 | return handleError('Invalid event') 13 | } 14 | 15 | const params = { 16 | DBSnapshotIdentifier: event.resource, 17 | AttributeName: 'restore', 18 | ValuesToRemove: [ 19 | 'all' 20 | ] 21 | 22 | } 23 | 24 | const RDS = new RDSClient({ region: event.region }) 25 | 26 | try { 27 | const result = await RDS.send(new ModifyDBSnapshotAttributeCommand(params)) 28 | console.log('Result', result) 29 | return 'Successfully processed event' 30 | } catch (err) { 31 | console.log('Error', err) 32 | return handleError(err.message ? err.message : 'Modify Db Snapshot failed') 33 | } 34 | 35 | function handleError (message) { 36 | message = message || 'Failed to process request.' 37 | throw new Error(message) 38 | } 39 | } 40 | 41 | module.exports = { handler } 42 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-009.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-009' 6 | const CCRuleName = 'BucketAuthenticatedUsersWriteAcpAccess' 7 | 8 | const authenticatedUsersWriteAcpAccess = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE_ACP' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3 BucketAuthenticatedUsersWriteAcpAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, authenticatedUsersWriteAcpAccess) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateCFM-005.js: -------------------------------------------------------------------------------- 1 | 2 | const { CloudFormationClient, UpdateTerminationProtectionCommand } = require('@aws-sdk/client-cloudformation') 3 | 4 | /** 5 | * Lambda function to enable CloudFormation Stack Termination Protection 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log('StackTerminationProtection - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.ccrn || !event.resource || !event.region) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | EnableTerminationProtection: true, 18 | StackName: event.resource 19 | } 20 | 21 | const CloudFormation = new CloudFormationClient({ region: event.region }) 22 | 23 | try { 24 | const result = await CloudFormation.send(new UpdateTerminationProtectionCommand(params)) 25 | console.log('Result', result) 26 | return 'Successfully processed event' 27 | } catch (err) { 28 | console.log('Error', err) 29 | return handleError(err.message ? err.message : 'Failed to enable CloudFormation Stack Termination Protection') 30 | } 31 | 32 | function handleError (message) { 33 | message = message || 'Failed to process request.' 34 | throw new Error(message) 35 | } 36 | } 37 | 38 | module.exports = { handler } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateRS-019.js: -------------------------------------------------------------------------------- 1 | 2 | const { RedshiftClient, ModifyClusterCommand } = require('@aws-sdk/client-redshift') 3 | const CONFIG = require('./config')['AutoRemediateRS-019'] 4 | 5 | /** 6 | * Lambda function to Enable Automated Snapshot Retention Period for AWS Redshift 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log('AWS Redshift Automated Snapshot Retention Period - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.resource) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | ClusterIdentifier: event.resource, 18 | AutomatedSnapshotRetentionPeriod: CONFIG['RetentionPeriod'] 19 | 20 | } 21 | 22 | const Redshift = new RedshiftClient({ region: event.region }) 23 | 24 | try { 25 | const result = await Redshift.send(new ModifyClusterCommand(params)) 26 | console.log('Result', result) 27 | return 'Successfully processed event' 28 | } catch (err) { 29 | console.log('Error', err) 30 | return handleError(err.message ? err.message : 'Modify Cluster failed') 31 | } 32 | 33 | function handleError (message) { 34 | message = message || 'Failed to process request.' 35 | throw new Error(message) 36 | } 37 | } 38 | 39 | module.exports = { handler } 40 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-010.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../utils/S3_utils') 3 | const { S3Client } = require('@aws-sdk/client-s3') 4 | 5 | const CCRuleCode = 'S3-010' 6 | const CCRuleName = 'BucketAuthenticatedUsersFullControlAccess' 7 | 8 | const authenticatedUsersFullControlAccess = { 9 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'FULL_CONTROL' 10 | } 11 | 12 | function handleError (message) { 13 | message = message || 'Failed to process request.' 14 | throw new Error(message) 15 | } 16 | 17 | // look for and remove S3 BucketAuthenticatedUsersFullControlAccess 18 | const handler = async (event) => { 19 | console.log('S3', CCRuleName, ' - Received event:', JSON.stringify(event, null, 2)) 20 | 21 | if (!event || !event.resource || event.ruleId !== CCRuleCode) { 22 | return handleError('Invalid event') 23 | } 24 | 25 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 26 | 27 | try { 28 | await utils.filterAcl(s3, event.resource, authenticatedUsersFullControlAccess) 29 | return 'Success' 30 | } catch (err) { 31 | console.log(err, err.stack) 32 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 33 | } 34 | } 35 | 36 | module.exports = { 37 | handler: handler 38 | } 39 | -------------------------------------------------------------------------------- /functions/AutoRemediateEC2-019.js: -------------------------------------------------------------------------------- 1 | 2 | const { EC2Client, ModifyImageAttributeCommand } = require('@aws-sdk/client-ec2') 3 | 4 | /** 5 | * Lambda function to disable access to EC2 AMI that publicly shared with other AWS accounts 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log('Publicly Shared AMI - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.resource || !event.region) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | ImageId: event.resource, 18 | LaunchPermission: { 19 | 20 | Remove: [ 21 | { 22 | Group: 'all' 23 | } 24 | 25 | ] 26 | } 27 | } 28 | 29 | const Ec2 = new EC2Client({ region: event.region }) 30 | 31 | try { 32 | const result = await Ec2.send(new ModifyImageAttributeCommand(params)) 33 | console.log('Result', result) 34 | return 'Successfully processed event' 35 | } catch (err) { 36 | console.log('Error', err) 37 | return handleError(err.message ? err.message : 'Failed to update ImageAttribute') 38 | } 39 | 40 | function handleError (message) { 41 | message = message || 'Failed to process request.' 42 | throw new Error(message) 43 | } 44 | } 45 | 46 | module.exports = { handler } 47 | -------------------------------------------------------------------------------- /functions/AutoRemediateRS-023.js: -------------------------------------------------------------------------------- 1 | 2 | const { RedshiftClient, ModifyClusterParameterGroupCommand } = require('@aws-sdk/client-redshift') 3 | 4 | /** 5 | * Lambda function to enable Redshift User Activity Logging 6 | */ 7 | 8 | const handler = async (event) => { 9 | console.log('Enable Redshift User Activity Logging - Received event:', JSON.stringify(event, null, 2)) 10 | 11 | if (!event || !event.resource) { 12 | return handleError('Invalid event') 13 | } 14 | 15 | const params = { 16 | ParameterGroupName: event.resource, 17 | Parameters: [{ 18 | ParameterName: 'enable_user_activity_logging', 19 | ParameterValue: 'true' 20 | } 21 | ] 22 | } 23 | 24 | const Redshift = new RedshiftClient({ region: event.region }) 25 | 26 | try { 27 | const result = await Redshift.send(new ModifyClusterParameterGroupCommand(params)) 28 | console.log('Result', result) 29 | return 'Successfully processed event' 30 | } catch (err) { 31 | console.log('Error', err) 32 | return handleError(err.message ? err.message : 'modify cluster parameter group failed') 33 | } 34 | 35 | function handleError (message) { 36 | message = message || 'Failed to process request.' 37 | throw new Error(message) 38 | } 39 | } 40 | 41 | module.exports = { handler } 42 | -------------------------------------------------------------------------------- /functions/AutoRemediateIAM-001.js: -------------------------------------------------------------------------------- 1 | const { ClientIam, UpdateAccessKeyCommand } = require('@aws-sdk/client-iam') 2 | 3 | /** 4 | * Lambda function to deactivate access keys older than 30 days 5 | */ 6 | 7 | const handler = async (event) => { 8 | console.log('AccessKeysRotated30Days - Received event:', JSON.stringify(event, null, 2)) 9 | if (!event || !event.resource || !event.extradata) { 10 | return handleError('Invalid event') 11 | } 12 | 13 | const userName = (event.extradata || []).find(data => data.name === 'UserName') 14 | 15 | if (!userName || !userName.value) { 16 | return handleError('Cannot find IAM Username') 17 | } 18 | 19 | const params = { 20 | AccessKeyId: event.resource, 21 | Status: 'Inactive', 22 | UserName: userName.value 23 | } 24 | 25 | const IAM = new ClientIam() 26 | 27 | try { 28 | const result = await IAM.send(new UpdateAccessKeyCommand(params)) 29 | console.log('Result', result) 30 | return 'Successfully processed event' 31 | } catch (err) { 32 | console.log('Error', err) 33 | return handleError(err.message ? err.message : 'update Access Key failed') 34 | } 35 | 36 | function handleError (message) { 37 | message = message || 'Failed to process request.' 38 | throw new Error(message) 39 | } 40 | } 41 | 42 | module.exports = { handler } 43 | -------------------------------------------------------------------------------- /utils/security_group_access_revoker.js: -------------------------------------------------------------------------------- 1 | const { EC2Client, RevokeSecurityGroupIngressCommand } = require('@aws-sdk/client-ec2') 2 | 3 | const revokeSecurityGroupAccess = async (protocol, port, resource, region, callback) => { 4 | const ec2 = new EC2Client({ region: region }) 5 | 6 | const params = { 7 | GroupId: resource, 8 | IpPermissions: [ 9 | { 10 | FromPort: port, 11 | ToPort: port, 12 | IpProtocol: protocol, 13 | IpRanges: [{ CidrIp: '0.0.0.0/0' }] 14 | } 15 | ] 16 | } 17 | try { 18 | const revokeIngressResponse = await ec2.send(new RevokeSecurityGroupIngressCommand(params)) 19 | console.log('Revoking %s, %s on 0.0.0.0/0 succeeded') 20 | console.log(revokeIngressResponse) 21 | } catch (err) { 22 | console.log('Revoking %s, %s on 0.0.0.0/0 failed') 23 | console.error(err) 24 | } 25 | 26 | try { 27 | params.IpPermissions[0].IpRanges[0].CidrIp = '::/0' 28 | const revokeIngressResponse = await ec2.send(new RevokeSecurityGroupIngressCommand(params)) 29 | console.log('Revoking %s, %s on ::/0 succeeded') 30 | console.log(revokeIngressResponse) 31 | } catch (err) { 32 | console.log('Revoking %s, %s on ::/0 failed') 33 | console.error(err) 34 | } 35 | 36 | return callback(null, 'done') 37 | } 38 | 39 | exports.revoke = revokeSecurityGroupAccess 40 | -------------------------------------------------------------------------------- /functions/AutoRemediateIAM-038.js: -------------------------------------------------------------------------------- 1 | 2 | const { IAMClient, UpdateAccessKeyCommand } = require('@aws-sdk/client-iam') 3 | 4 | /** 5 | * Lambda function to deactivate access keys older than 90 days 6 | */ 7 | 8 | const handler = async (event) => { 9 | console.log('AWS IAM Access Keys Rotation- 90 Days - Received event:', JSON.stringify(event, null, 2)) 10 | if (!event || !event.resource || !event.extradata) { 11 | return handleError('Invalid event') 12 | } 13 | 14 | const UserName = (event.extradata || []).filter(data => data.name === 'UserName') 15 | 16 | if (UserName.length !== 1 && !UserName[0].value) { 17 | return handleError('Cannot find IAM Username') 18 | } 19 | 20 | const params = { 21 | AccessKeyId: event.resource, 22 | Status: 'Inactive', 23 | UserName: UserName[0].value 24 | } 25 | 26 | const IAM = new IAMClient() 27 | 28 | try { 29 | const result = await IAM.send(new UpdateAccessKeyCommand(params)) 30 | console.log('Result', result) 31 | return 'Successfully processed event' 32 | } catch (err) { 33 | console.log('Error', err) 34 | return handleError(err.message ? err.message : 'update Access Key failed') 35 | } 36 | 37 | function handleError (message) { 38 | message = message || 'Failed to process request.' 39 | throw new Error(message) 40 | } 41 | } 42 | 43 | module.exports = { handler } 44 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | env: 10 | ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }} 11 | ARTIFACTORY_TOKEN_EMAIL: ${{ secrets.ARTIFACTORY_TOKEN_EMAIL }} 12 | ARTIFACTORY_URL: ${{ secrets.ARTIFACTORY_URL_VIRTUAL }} 13 | steps: 14 | - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # refers to actions/checkout@v3.1.0 15 | with: 16 | persist-credentials: false 17 | 18 | # Install Node.js 19 | - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # refers to actions/setup-node@v3.5.1 20 | with: 21 | node-version: 18 22 | 23 | - run: npm i npm@9 -g 24 | - run: npm config set registry https://$ARTIFACTORY_URL 25 | - run: npm config set replace-registry-host npm.pkg.github.com 26 | - run: npm config set //$ARTIFACTORY_URL:_auth "$(echo -n $ARTIFACTORY_TOKEN_EMAIL:$ARTIFACTORY_TOKEN | base64 -w 0)" 27 | - run: npm config set //$ARTIFACTORY_URL:email "$ARTIFACTORY_TOKEN_EMAIL" 28 | 29 | # Install your dependencies 30 | - name: Installing package.json... 31 | run: npm ci 32 | 33 | # Run ESLint 34 | - name: Running ESLint... 35 | run: npm run lint 36 | 37 | # Run Tests 38 | - name: Running tests... 39 | run: npm run test -------------------------------------------------------------------------------- /functions/AutoRemediateCT-001.js: -------------------------------------------------------------------------------- 1 | 2 | const { CloudTrailClient, UpdateTrailCommand } = require('@aws-sdk/client-cloudtrail') 3 | 4 | const CONFIG = require('./config')['AutoRemediateCT-001'] 5 | 6 | /** 7 | * Lambda function to automatically remediate CloudTrail not Global 8 | */ 9 | const handler = async (event) => { 10 | console.log('CloudTrail not Global - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.region || event.ruleId !== 'CT-001') { 13 | return handleError('Invalid event') 14 | } 15 | 16 | const params = { 17 | Name: CONFIG['Name'], 18 | S3BucketName: CONFIG['S3BucketName'], 19 | IncludeGlobalServiceEvents: CONFIG['IncludeGlobalServiceEvents'], 20 | IsMultiRegionTrail: CONFIG['IsMultiRegionTrail'], 21 | S3KeyPrefix: CONFIG['S3KeyPrefix'] 22 | } 23 | 24 | const CloudTrail = new CloudTrailClient({ apiVersion: '2013-11-01' }) 25 | 26 | try { 27 | const result = await CloudTrail.send(new UpdateTrailCommand(params)) 28 | console.log('Result', result) 29 | return 'Successfully processed event' 30 | } catch (err) { 31 | console.log('Error' + err) 32 | return handleError(err.message ? err.message : 'Make CloudTrail global in account failed') 33 | } 34 | 35 | function handleError (message) { 36 | message = message || 'Failed to process request.' 37 | throw new Error(message) 38 | } 39 | } 40 | 41 | module.exports = { handler } 42 | -------------------------------------------------------------------------------- /functions/AutoRemediateSQS-004.js: -------------------------------------------------------------------------------- 1 | const { SQSClient, SetQueueAttributesCommand } = require('@aws-sdk/client-sqs') 2 | const CONFIG = require('./config') 3 | 4 | /** 5 | * Lambda function to enable Amazon Simple Queue Service (SQS) queues' message encryption using Server-Side Encryption (SSE) 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log(' Enable AWS SQS Server-Side Encryption for messages - Received event:', JSON.stringify(event, null, 2)) 11 | if (!event || !event.resource || !event.region) { 12 | return handleError('Invalid event') 13 | } 14 | if (!CONFIG['AutoRemediateSQS-004']['KmsKeyId']) { 15 | return handleError('Missing KmsKeyId value') 16 | } 17 | const params = { 18 | Attributes: { 19 | KmsMasterKeyId: CONFIG['AutoRemediateSQS-004']['KmsKeyId'] 20 | }, 21 | QueueUrl: event.resource 22 | } 23 | const Sqs = new SQSClient({ region: event.region, apiVersion: '2012-11-05' }) 24 | try { 25 | const result = await Sqs.send(new SetQueueAttributesCommand(params)) 26 | console.log('Result', result) 27 | return 'Successfully processed event' 28 | } catch (err) { 29 | console.log('Error', err) 30 | return handleError(err.message ? err.message : 'Failed to setQueueAttributes ,KmsMasterKeyId ') 31 | } 32 | 33 | function handleError (message) { 34 | message = message || 'Failed to process request.' 35 | throw new Error(message) 36 | } 37 | } 38 | 39 | module.exports = { handler } 40 | -------------------------------------------------------------------------------- /functions/AutoRemediateKinesis-001.js: -------------------------------------------------------------------------------- 1 | const CONFIG = require('./config') 2 | const { KinesisClient, StartStreamEncryptionCommand } = require('@aws-sdk/client-kinesis') 3 | 4 | /** 5 | * Lambda function to enable AWS Kinesis streams encryption using Server-Side Encryption (SSE) 6 | * 7 | */ 8 | 9 | const handler = async (event) => { 10 | console.log(' Enable Kinesis Server-Side Encryption - Received event:', JSON.stringify(event, null, 2)) 11 | 12 | if (!event || !event.resource || !event.region) { 13 | return handleError('Invalid event') 14 | } 15 | 16 | if (!CONFIG['AutoRemediateKinesis-001']['KmsKeyId']) { 17 | return handleError('Missing KmsKeyId value') 18 | } 19 | 20 | const params = { 21 | EncryptionType: 'KMS', 22 | KeyId: CONFIG['AutoRemediateKinesis-001']['KmsKeyId'], 23 | StreamName: event.resource 24 | } 25 | 26 | const Kinesis = new KinesisClient({ region: event.region, apiVersion: '2012-11-05' }) 27 | 28 | try { 29 | const result = await Kinesis.send(new StartStreamEncryptionCommand(params)) 30 | console.log('Result', result) 31 | return 'Successfully processed event' 32 | } catch (err) { 33 | console.log('Error', err) 34 | return handleError(err.message ? err.message : 'Failed to enable Kinesis Server Side Encryption') 35 | } 36 | 37 | function handleError (message) { 38 | message = message || 'Failed to process request.' 39 | throw new Error(message) 40 | } 41 | } 42 | 43 | module.exports = { handler } 44 | -------------------------------------------------------------------------------- /functions/AutoRemediateOrchestrator.js: -------------------------------------------------------------------------------- 1 | const CONFIG = require('./config') 2 | const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda') 3 | 4 | const handler = async (event) => { 5 | console.log('Received event: ', JSON.stringify(event, null, 2)) 6 | console.log('Config settings: ', JSON.stringify(CONFIG, null, 2)) 7 | if (!event || !event.Records[0] || !event.Records[0].body) { 8 | throw new Error('No event specified') 9 | } 10 | const message = JSON.parse(event.Records[0].body) 11 | const AutoRemediate = 'AutoRemediate' + message.ruleId 12 | if (!CONFIG[`${AutoRemediate}`]) { 13 | console.log( 14 | 'The %s is not supported. Exiting gracefully ...', 15 | AutoRemediate 16 | ) 17 | return 18 | } 19 | if (!CONFIG[`${AutoRemediate}`]['enabled']) { 20 | console.log('The %s is not enabled. Exiting gracefully ...', AutoRemediate) 21 | return 22 | } 23 | const FunctionName = 24 | process.env['AWS_LAMBDA_FUNCTION_NAME'].substring( 25 | 0, 26 | process.env['AWS_LAMBDA_FUNCTION_NAME'].lastIndexOf('-') + 1 27 | ) + AutoRemediate 28 | console.log(`Invoking ${FunctionName} ...`) 29 | const Lambda = new LambdaClient({ 30 | region: process.env['AWS_REGION'], 31 | apiVersion: '2015-03-31' 32 | }) 33 | 34 | try { 35 | const result = await Lambda.send(new InvokeCommand({ 36 | FunctionName, 37 | Payload: JSON.stringify(message, null, 2) } 38 | )) 39 | 40 | return `Successfully invoked ${FunctionName} with result ${result}` 41 | } catch (error) { 42 | console.log(`Error occurred while invoking ${FunctionName}`) 43 | console.log(error) 44 | throw error 45 | } 46 | } 47 | 48 | module.exports = { handler } 49 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-001.js: -------------------------------------------------------------------------------- 1 | 2 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 3 | const allUsersURI = 'http://acs.amazonaws.com/groups/global/AllUsers' 4 | const readPermission = 'READ' 5 | 6 | const CCRuleCode = 'S3-001' 7 | 8 | const aclNew = { 9 | Owner: '', 10 | Grants: [] 11 | } 12 | 13 | function remediateAllUsers (thisGrant, newAcl) { 14 | if (thisGrant.Permission !== readPermission) { // any besides READ are passed through 15 | newAcl['Grants'].push(thisGrant) 16 | } 17 | 18 | return newAcl 19 | } 20 | 21 | // look for and remove S3BucketPublicReadAccess 22 | const handler = async (event) => { 23 | console.log( 24 | 'S3 BucketPublicReadAccess - Received event:', 25 | JSON.stringify(event, null, 2) 26 | ) 27 | 28 | if (!event || !event.resource || event.ruleId !== 'S3-001') { 29 | throw new Error('Invalid event') 30 | } 31 | 32 | var s3 = new S3Client({ apiVersion: '2006-03-01' }) 33 | 34 | var getAclParams = { 35 | Bucket: event.resource 36 | } 37 | 38 | try { 39 | const aclWas = await s3.send(new GetBucketAclCommand(getAclParams)) 40 | 41 | aclNew.Owner = aclWas.Owner // transfer the existing bucket owner 42 | aclWas.Grants.forEach((grant, _) => { 43 | if (grant.Grantee.URI === allUsersURI) { 44 | remediateAllUsers(grant, aclNew) 45 | } else { 46 | const found = aclNew['Grants'].some( 47 | ({ Permission, Grantee: { ID, DisplayName, Type } }) => { 48 | return ( 49 | grant.Permission === Permission && 50 | grant.Grantee.DisplayName === DisplayName && 51 | grant.Grantee.ID === ID && 52 | grant.Grantee.Type === Type 53 | ) 54 | } 55 | ) 56 | if (!found) { 57 | aclNew['Grants'].push(grant) 58 | } 59 | } 60 | }) 61 | 62 | var putAclParams = { 63 | Bucket: event.resource, 64 | AccessControlPolicy: aclNew 65 | } 66 | const result = await s3.send(new PutBucketAclCommand(putAclParams)) 67 | console.log('result>' + JSON.stringify(result)) 68 | 69 | return 'Success' 70 | } catch (err) { 71 | console.log(err, err.stack) 72 | throw new Error(`failed to auto-remediate ${CCRuleCode}: ${err}`) 73 | } 74 | } 75 | 76 | module.exports = { handler } 77 | -------------------------------------------------------------------------------- /functions/AutoRemediateLambda-003.js: -------------------------------------------------------------------------------- 1 | 2 | const { IAMClient, AttachRolePolicyCommand } = require('@aws-sdk/client-iam') 3 | const { LambdaClient, GetFunctionConfigurationCommand, UpdateFunctionConfigurationCommand } = require('@aws-sdk/client-lambda') 4 | const { ConfiguredRetryStrategy } = require('@aws-sdk/util-retry') 5 | 6 | const { setTimeout } = require('timers/promises') 7 | 8 | // retry all requests with a 5 sec delay (if they are retry-able), up to 10 times 9 | const retryStrategy = new ConfiguredRetryStrategy(10, 5000) 10 | 11 | /** 12 | * Enable tracing for your AWS Lambda functions 13 | */ 14 | const handler = async (event) => { 15 | console.log('Lambda Function Tracing Enabled - Received event:', JSON.stringify(event, null, 2)) 16 | 17 | if (!event || !event.region) { 18 | return handleError('Invalid event') 19 | } 20 | 21 | const FunctionName = event.resource 22 | 23 | const Lambda = new LambdaClient() 24 | const IAM = new IAMClient({ retryStrategy }) 25 | 26 | try { 27 | const lambdaConfig = await Lambda.send(new GetFunctionConfigurationCommand({ FunctionName })) 28 | console.log('Role ARN:', lambdaConfig.Role) 29 | 30 | const FunctionRoleName = lambdaConfig.Role.split('/')[1] 31 | 32 | const AttachRolePolicyParams = { 33 | PolicyArn: 'arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess', 34 | RoleName: FunctionRoleName 35 | } 36 | 37 | await IAM.send(new AttachRolePolicyCommand(AttachRolePolicyParams)) 38 | console.log('Successfully attached AWSXray managed policy to the function role') 39 | 40 | // Wait 10 seconds to avoid Error: The provided execution role does not have permissions to call PutTraceSegments on XRAY 41 | await setTimeout(10000) 42 | 43 | const FunctionConfigurationParams = { 44 | FunctionName: FunctionName, 45 | TracingConfig: { 46 | Mode: 'Active' 47 | } 48 | } 49 | 50 | await Lambda.send(new UpdateFunctionConfigurationCommand(FunctionConfigurationParams)) 51 | return 'Successfully updated function configuration' 52 | } catch (err) { 53 | console.log('Error', err) 54 | return handleError(err.message ? err.message : 'Failed to enable tracing for the function') 55 | } 56 | 57 | function handleError (message) { 58 | message = message || 'Failed to process request.' 59 | throw new Error(message) 60 | } 61 | } 62 | 63 | module.exports = { handler } 64 | -------------------------------------------------------------------------------- /functions/AutoRemediateIAM-029.js: -------------------------------------------------------------------------------- 1 | const { 2 | IAMClient, 3 | DeleteUserCommand, 4 | DeleteUserPolicyCommand, 5 | DeleteLoginProfileCommand, 6 | ListAttachedUserPoliciesCommand, 7 | ListUserPoliciesCommand, 8 | DetachUserPolicyCommand, 9 | ListGroupsForUserCommand, 10 | RemoveUserFromGroupCommand 11 | } = require('@aws-sdk/client-iam') 12 | 13 | const handler = async ({ region, resource }) => { 14 | const client = new IAMClient({ region }) 15 | resource = resource.replace('users-', '') 16 | 17 | await removeUserRelatedResources(client, resource) 18 | 19 | const deleteUser = new DeleteUserCommand({ 20 | UserName: resource 21 | }) 22 | await client.send(deleteUser) 23 | } 24 | 25 | // Before deleting an IAM user we must remove the related resources. 26 | // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_remove.html 27 | const removeUserRelatedResources = async (client, username) => { 28 | const deleteLoginProfile = new DeleteLoginProfileCommand({ 29 | UserName: username 30 | }) 31 | await client.send(deleteLoginProfile) 32 | 33 | const listUserPolicies = new ListUserPoliciesCommand({ 34 | UserName: username 35 | }) 36 | const listUserPoliciesResponse = await client.send(listUserPolicies) 37 | 38 | for (const policyName of listUserPoliciesResponse.PolicyNames) { 39 | const deleteUserPolicy = new DeleteUserPolicyCommand({ 40 | UserName: username, 41 | PolicyName: policyName 42 | }) 43 | await client.send(deleteUserPolicy) 44 | } 45 | 46 | const listAttachedPolicies = new ListAttachedUserPoliciesCommand({ 47 | UserName: username 48 | }) 49 | const listAttachedPoliciesResponse = await client.send(listAttachedPolicies) 50 | 51 | for (const policy of listAttachedPoliciesResponse.AttachedPolicies) { 52 | const detachUserPolicy = new DetachUserPolicyCommand({ 53 | UserName: username, 54 | PolicyArn: policy.PolicyArn 55 | }) 56 | await client.send(detachUserPolicy) 57 | } 58 | 59 | const listGroups = new ListGroupsForUserCommand({ 60 | UserName: username 61 | }) 62 | const listGroupsResponse = await client.send(listGroups) 63 | 64 | for (const group of listGroupsResponse.Groups) { 65 | const removeUserFromGroup = new RemoveUserFromGroupCommand({ 66 | UserName: username, 67 | GroupName: group.GroupName 68 | }) 69 | await client.send(removeUserFromGroup) 70 | } 71 | } 72 | 73 | module.exports = { 74 | handler 75 | } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-remediate", 3 | "version": "2.0.5", 4 | "description": "Auto Remediate", 5 | "main": "Orchestrator.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test:watch": "npm test -- --watch", 9 | "coverage": "npm run test -- --coverage --silent", 10 | "lint": "eslint '**/*.js' --ignore-path .gitignore", 11 | "lint:fix": "npm run lint -- --fix", 12 | "sls:deploy": "npm prune --production && serverless deploy --force" 13 | }, 14 | "author": "Mike Rahmati", 15 | "contributors": [ 16 | "Edmund Cong" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git@github.com:cloudconformity/auto-remediate" 21 | }, 22 | "license": "MIT", 23 | "dependencies": { 24 | "@aws-sdk/client-cloudformation": "^3.490.0", 25 | "@aws-sdk/client-cloudtrail": "^3.490.0", 26 | "@aws-sdk/client-config-service": "^3.490.0", 27 | "@aws-sdk/client-ec2": "^3.496.0", 28 | "@aws-sdk/client-guardduty": "^3.496.0", 29 | "@aws-sdk/client-iam": "^3.490.0", 30 | "@aws-sdk/client-kinesis": "^3.496.0", 31 | "@aws-sdk/client-kms": "^3.496.0", 32 | "@aws-sdk/client-lambda": "^3.496.0", 33 | "@aws-sdk/client-organizations": "^3.497.0", 34 | "@aws-sdk/client-rds": "^3.497.0", 35 | "@aws-sdk/client-redshift": "^3.496.0", 36 | "@aws-sdk/client-s3": "^3.490.0", 37 | "@aws-sdk/client-sqs": "^3.496.0", 38 | "@aws-sdk/client-sts": "^3.496.0", 39 | "@aws-sdk/util-retry": "^3.374.0", 40 | "lodash.isequal": "^4.5.0" 41 | }, 42 | "devDependencies": { 43 | "aws-sdk-client-mock": "^3.0.1", 44 | "aws-sdk-client-mock-jest": "^3.0.1", 45 | "dotenv": "^10.0.0", 46 | "eslint": "^7.11.0", 47 | "eslint-config-standard": "^13.0.1", 48 | "eslint-plugin-import": "^2.23.3", 49 | "eslint-plugin-jest": "^22.13.6", 50 | "eslint-plugin-node": "^9.1.0", 51 | "eslint-plugin-promise": "^4.2.1", 52 | "eslint-plugin-standard": "^4.0.0", 53 | "husky": "^4.3.0", 54 | "jest": "^26.6.3", 55 | "lint-staged": "^11.0.0", 56 | "lodash.clonedeep": "^4.5.0", 57 | "serverless": "^3.27.0", 58 | "serverless-plugin-split-stacks": "^1.11.3", 59 | "serverless-stack-termination-protection": "^2.0.2" 60 | }, 61 | "engines": { 62 | "node": "^18", 63 | "npm": "^9" 64 | }, 65 | "lint-staged": { 66 | "**/*.js": [ 67 | "eslint --fix" 68 | ] 69 | }, 70 | "husky": { 71 | "hooks": { 72 | "pre-commit": "lint-staged", 73 | "pre-push": "npm run test" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/utils/S3_utils.test.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('../../utils/S3_utils') 3 | const clonedeep = require('lodash.clonedeep') 4 | 5 | describe('S3_utils', () => { 6 | const grantReadAcpAllUsers = { 7 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 8 | } 9 | const grantReadAllUsers = { 10 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 11 | } 12 | const grantReadAcpAuthenticatedUsers = { 13 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 14 | } 15 | 16 | describe('#filterAclGrants', () => { 17 | let sampleAcl 18 | beforeEach(() => { 19 | sampleAcl = { 20 | Owner: { DisplayName: 'user_name', ID: '2ce976687c4d75ad5a026cfc3c1f0397e39a0df116faf88c1fd90f2faa291c8b' }, 21 | Grants: [grantReadAcpAllUsers, grantReadAllUsers] 22 | } 23 | }) 24 | it('should not mutate the input param', () => { 25 | const original = clonedeep(sampleAcl) 26 | utils.filterAclGrants(sampleAcl, grantReadAcpAllUsers) 27 | expect(sampleAcl).toEqual(original) 28 | }) 29 | 30 | it('should return a copy when grant is undefined', () => { 31 | const result = utils.filterAclGrants(sampleAcl) 32 | expect(result).toEqual(sampleAcl) 33 | }) 34 | 35 | it('should return a copy of the acl when no match exists', () => { 36 | const result = utils.filterAclGrants(sampleAcl, grantReadAcpAuthenticatedUsers) 37 | expect(result).toEqual(sampleAcl) 38 | }) 39 | 40 | it('should remove a single match', () => { 41 | sampleAcl.Grants = clonedeep([grantReadAcpAllUsers, grantReadAllUsers]) 42 | const result = utils.filterAclGrants(sampleAcl, grantReadAcpAllUsers) 43 | expect(result.Grants).toEqual([grantReadAllUsers]) 44 | }) 45 | 46 | it('should remove multiple matches', () => { 47 | sampleAcl.Grants = clonedeep([grantReadAcpAllUsers, grantReadAllUsers, grantReadAcpAllUsers]) 48 | const result = utils.filterAclGrants(sampleAcl, grantReadAcpAllUsers) 49 | expect(result.Grants).toEqual([grantReadAllUsers]) 50 | }) 51 | 52 | it('should fail when acl is undefined', () => { 53 | const invalidCall = () => utils.filterAclGrants(undefined, grantReadAcpAllUsers) 54 | expect(invalidCall).toThrow(expect.anything()) 55 | }) 56 | 57 | it('should not match when Grantee differs', () => { 58 | sampleAcl.Grants = clonedeep([grantReadAcpAllUsers]) 59 | const result = utils.filterAclGrants(sampleAcl, grantReadAcpAuthenticatedUsers) 60 | expect(result.Grants).toEqual([grantReadAcpAllUsers]) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 7 * * 3' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | #- name: Autobuild 57 | # uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-014.js: -------------------------------------------------------------------------------- 1 | 2 | const { S3Client, GetBucketPolicyCommand, PutBucketPolicyCommand } = require('@aws-sdk/client-s3') 3 | const { ConfiguredRetryStrategy } = require('@aws-sdk/util-retry') 4 | 5 | const Utils = require('./Utils.js') 6 | 7 | const retryStrategy = new ConfiguredRetryStrategy(10, 5000) 8 | 9 | /** 10 | * This function Alter Bucket policy to enforce AWS S3 bucket are not publicly accessible 11 | * * Note: Here we assume that the S3 Bucket has a policy with the 12 | * "Effect" : "Allow" and "Principal" : "*" 13 | * Which cause S3-014 rule to fail. 14 | * 15 | * This Function replace Principal in the policy with account root user 16 | */ 17 | const handler = async (event) => { 18 | console.log('S3 Bucket Public Access Via Policy - Received event:', JSON.stringify(event, null, 2)) 19 | 20 | if (!event || !event.region) { 21 | return handleError('Invalid event') 22 | } 23 | 24 | const S3Bucket = event.resource 25 | 26 | try { 27 | const accountId = await Utils.getAccountId() 28 | console.log('AWS Account ID:', accountId) 29 | await AlterBucketPolicyPrincipal(S3Bucket, accountId) 30 | console.log('SES is Enabled for bucket ', event.resource) 31 | return 'Successfully processed event' 32 | } catch (err) { 33 | console.log('Error', err) 34 | return handleError(err.message ? err.message : 'Failed to enable AWS Config') 35 | } 36 | 37 | async function AlterBucketPolicyPrincipal (S3Bucket, accountId) { 38 | const S3 = new S3Client({ retryStrategy }) 39 | 40 | try { 41 | const data = await S3.send(new GetBucketPolicyCommand({ Bucket: S3Bucket })) 42 | console.log('Retrieved Bucket Policy:', JSON.stringify(JSON.parse(data.Policy), undefined, 2)) 43 | const BucketPolicy = JSON.parse(data.Policy) 44 | var statements = BucketPolicy.Statement 45 | 46 | for (var i = 0; i < statements.length; i++) { 47 | var currentStatement = statements[i] 48 | console.log(`Processing statement ${i}...`) 49 | for (var key in currentStatement) { 50 | // eslint-disable-next-line no-prototype-builtins 51 | if (currentStatement.hasOwnProperty(key) && currentStatement[key] === 'Allow') { 52 | if (currentStatement['Principal'] === '*') { 53 | currentStatement['Principal'] = { AWS: 'arn:aws:iam::' + accountId + ':root' } 54 | console.log('Altered Principal to the root user') 55 | console.log(currentStatement) 56 | } 57 | } 58 | } 59 | } 60 | 61 | const PutBucketPolicyParams = { 62 | Policy: JSON.stringify(BucketPolicy), 63 | Bucket: S3Bucket 64 | } 65 | 66 | const putBucketPolicyResult = await S3.send(new PutBucketPolicyCommand(PutBucketPolicyParams)) 67 | console.log('Successfully put bucket policy') 68 | return putBucketPolicyResult.Policy 69 | } catch (err) { 70 | console.log(err.message) 71 | throw err 72 | } 73 | } 74 | 75 | function handleError (message) { 76 | message = message || 'Failed to process request.' 77 | throw new Error(message) 78 | } 79 | } 80 | 81 | module.exports = { handler } 82 | -------------------------------------------------------------------------------- /functions/AutoRemediateS3-016.js: -------------------------------------------------------------------------------- 1 | 2 | const { S3Client, GetBucketPolicyCommand, PutBucketPolicyCommand } = require('@aws-sdk/client-s3') 3 | 4 | const { ConfiguredRetryStrategy } = require('@aws-sdk/util-retry') 5 | 6 | const retryStrategy = new ConfiguredRetryStrategy(10, (attempt) => 5000 + attempt * 1000) 7 | 8 | /** 9 | * Enable Server-Side Encryption for AWS S3 buckets 10 | */ 11 | const handler = async (event) => { 12 | console.log('S3 Server Side Encryption - Received event:', JSON.stringify(event, null, 2)) 13 | 14 | if (!event || !event.region) { 15 | return handleError('Invalid event') 16 | } 17 | 18 | const S3Bucket = event.resource 19 | 20 | await UpdateBucketPolicy(S3Bucket) 21 | 22 | console.log('SES is Enabled for bucket ', event.resource) 23 | return 'Successfully processed event' 24 | 25 | async function UpdateBucketPolicy (S3Bucket) { 26 | const S3 = new S3Client({ retryStrategy }) 27 | 28 | const SseStatement1 = JSON.stringify({ 29 | Sid: 'DenyIncorrectEncryptionHeader', 30 | Effect: 'Deny', 31 | Principal: '*', 32 | Action: 's3:PutObject', 33 | Resource: 'arn:aws:s3:::' + S3Bucket + '/*', 34 | Condition: { 35 | StringNotEquals: { 36 | 's3:x-amz-server-side-encryption': 'AES256' 37 | } 38 | } 39 | }) 40 | 41 | const SseStatement2 = JSON.stringify({ 42 | Sid: 'DenyUnEncryptedObjectUploads', 43 | Effect: 'Deny', 44 | Principal: '*', 45 | Action: 's3:PutObject', 46 | Resource: 'arn:aws:s3:::' + S3Bucket + '/*', 47 | Condition: { 48 | Null: { 49 | 's3:x-amz-server-side-encryption': 'true' 50 | } 51 | } 52 | } 53 | ) 54 | 55 | try { 56 | const currentPolicy = await S3.send(new GetBucketPolicyCommand({ Bucket: S3Bucket })) 57 | console.log('Retrieved Bucket Policy:', JSON.stringify(JSON.parse(currentPolicy.Policy), undefined, 2)) 58 | const BucketPolicy = JSON.parse(currentPolicy.Policy) 59 | 60 | if ((currentPolicy.Policy).includes('s3:x-amz-server-side-encryption')) { 61 | console.log('Bucket already has SSE policy in place') 62 | return BucketPolicy 63 | } else { 64 | BucketPolicy.Statement.push(JSON.parse(SseStatement1)) 65 | BucketPolicy.Statement.push(JSON.parse(SseStatement2)) 66 | 67 | const PutBucketPolicyParams = { 68 | Policy: JSON.stringify(BucketPolicy), 69 | Bucket: S3Bucket 70 | } 71 | 72 | return putBucketPolicy(S3, PutBucketPolicyParams) 73 | } 74 | } catch (err) { 75 | console.log(err.message) 76 | 77 | if (err.code !== 'NoSuchBucketPolicy') { 78 | throw err 79 | } 80 | 81 | const NewSsePolicy = JSON.stringify({ 82 | Version: '2012-10-17', 83 | Id: 'PutSSEObjPolicy', 84 | Statement: [] 85 | }) 86 | 87 | const NewSsePolicyObj = JSON.parse(NewSsePolicy) 88 | 89 | NewSsePolicyObj.Statement.push(JSON.parse(SseStatement1)) 90 | NewSsePolicyObj.Statement.push(JSON.parse(SseStatement2)) 91 | 92 | const PutBucketPolicyParams = { 93 | Policy: JSON.stringify(NewSsePolicyObj), 94 | Bucket: S3Bucket 95 | } 96 | 97 | return putBucketPolicy(S3, PutBucketPolicyParams) 98 | } 99 | } 100 | 101 | async function putBucketPolicy (S3, PutBucketPolicyParams) { 102 | const result = await S3.send(new PutBucketPolicyCommand(PutBucketPolicyParams)) 103 | console.log('Successfully put bucket policy') 104 | return result.Policy 105 | } 106 | 107 | function handleError (message) { 108 | message = message || 'Failed to process request.' 109 | throw new Error(message) 110 | } 111 | } 112 | 113 | module.exports = { handler } 114 | -------------------------------------------------------------------------------- /functions/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "AutoRemediateRDS-008": { 3 | "enabled": false 4 | }, 5 | 6 | "AutoRemediateS3-001": { 7 | "enabled": false 8 | }, 9 | 10 | "AutoRemediateS3-002": { 11 | "enabled": false 12 | }, 13 | 14 | "AutoRemediateS3-003": { 15 | "enabled": false 16 | }, 17 | 18 | "AutoRemediateS3-004": { 19 | "enabled": false 20 | }, 21 | 22 | "AutoRemediateS3-005": { 23 | "enabled": false 24 | }, 25 | 26 | "AutoRemediateS3-006": { 27 | "enabled": false 28 | }, 29 | 30 | "AutoRemediateS3-007": { 31 | "enabled": false 32 | }, 33 | 34 | "AutoRemediateS3-008": { 35 | "enabled": false 36 | }, 37 | 38 | "AutoRemediateS3-009": { 39 | "enabled": false 40 | }, 41 | 42 | "AutoRemediateS3-010": { 43 | "enabled": false 44 | }, 45 | 46 | "AutoRemediateS3-012": { 47 | "enabled": false 48 | }, 49 | 50 | "AutoRemediateConfig-001": { 51 | "enabled": false, 52 | "S3BucketName": "" 53 | }, 54 | 55 | "AutoRemediateCFM-005": { 56 | "enabled": false 57 | }, 58 | 59 | "AutoRemediateCT-001": { 60 | "enabled": false, 61 | "Name": "GlobalTrail", 62 | "S3BucketName": "", 63 | "IncludeGlobalServiceEvents": true, 64 | "IsMultiRegionTrail": true, 65 | "S3KeyPrefix": "" 66 | }, 67 | 68 | "AutoRemediateRS-001": { 69 | "enabled": false 70 | }, 71 | 72 | "AutoRemediateIAM-001": { 73 | "enabled": false 74 | }, 75 | 76 | "AutoRemediateIAM-029": { 77 | "enabled": false 78 | }, 79 | 80 | "AutoRemediateEC2-002": { 81 | "enabled": false 82 | }, 83 | 84 | "AutoRemediateEC2-003": { 85 | "enabled": false 86 | }, 87 | 88 | "AutoRemediateEC2-004": { 89 | "enabled": false 90 | }, 91 | 92 | "AutoRemediateEC2-005": { 93 | "enabled": false 94 | }, 95 | 96 | "AutoRemediateEC2-006": { 97 | "enabled": false 98 | }, 99 | 100 | "AutoRemediateEC2-008": { 101 | "enabled": false 102 | }, 103 | 104 | "AutoRemediateEC2-038": { 105 | "enabled": false 106 | }, 107 | 108 | "AutoRemediateEC2-039": { 109 | "enabled": false 110 | }, 111 | 112 | "AutoRemediateEC2-040": { 113 | "enabled": false 114 | }, 115 | 116 | "AutoRemediateEC2-043": { 117 | "enabled": false 118 | }, 119 | 120 | "AutoRemediateEC2-045": { 121 | "enabled": false 122 | }, 123 | 124 | "AutoRemediateIAM-038": { 125 | "enabled": false 126 | }, 127 | 128 | "AutoRemediateKinesis-001": { 129 | "enabled": false, 130 | "KmsKeyId": "alias/aws/kinesis" 131 | }, 132 | 133 | "AutoRemediateVPC-001": { 134 | "enabled": false 135 | }, 136 | 137 | "AutoRemediateEC2-019": { 138 | "enabled": false 139 | }, 140 | 141 | "AutoRemediateCT-003": { 142 | "enabled": false 143 | }, 144 | 145 | "AutoRemediateRds-006": { 146 | "enabled": false 147 | }, 148 | 149 | "AutoRemediateRDS-006": { 150 | "enabled": false 151 | }, 152 | 153 | "AutoRemediateRDS-023": { 154 | "enabled": false 155 | }, 156 | 157 | "AutoRemediateKMS-004": { 158 | "enabled": false 159 | }, 160 | 161 | "AutoRemediateRS-023": { 162 | "enabled": false 163 | }, 164 | 165 | "AutoRemediateGD-001": { 166 | "enabled": false 167 | }, 168 | 169 | "AutoRemediateOrganizations-002": { 170 | "enabled": false 171 | }, 172 | 173 | "AutoRemediateLambda-003": { 174 | "enabled": false 175 | }, 176 | 177 | "AutoRemediateS3-016": { 178 | "enabled": false 179 | }, 180 | 181 | "AutoRemediateS3-014": { 182 | "enabled": false 183 | }, 184 | 185 | "AutoRemediateTrustedAdvisor-003": { 186 | "enabled": false 187 | }, 188 | 189 | "AutoRemediateKMS-002": { 190 | "enabled": false 191 | }, 192 | 193 | "AutoRemediateRS-019": { 194 | "enabled": false, 195 | "RetentionPeriod": 7 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /functions/AutoRemediateVPC-001.js: -------------------------------------------------------------------------------- 1 | 2 | const { IAMClient, GetRoleCommand, CreateRoleCommand, PutRolePolicyCommand } = require('@aws-sdk/client-iam') 3 | const { EC2Client, DescribeFlowLogsCommand, CreateFlowLogsCommand } = require(`@aws-sdk/client-ec2`) 4 | const { ConfiguredRetryStrategy } = require('@aws-sdk/util-retry') 5 | 6 | // retry all requests with a 5 sec delay (if they are retry-able), up to 10 times 7 | const retryStrategy = new ConfiguredRetryStrategy(10, 5000) 8 | 9 | /** 10 | * Ensure that Flow Logs feature is Enabled for your account 11 | */ 12 | const handler = async (event) => { 13 | console.log('Enable Flow Logs - Received event:', JSON.stringify(event, null, 2)) 14 | 15 | function handleError (message) { 16 | message = message || 'Failed to process request.' 17 | throw new Error(message) 18 | } 19 | 20 | if (!event || !event.resource || !event.region) { 21 | return handleError('Invalid event') 22 | } 23 | console.log(event.resource) 24 | 25 | const roleARN = await CreateVPCFlowLogRole() 26 | console.log('Role ARN:', roleARN) 27 | 28 | const describeParam = { 29 | Filter: [ 30 | { 31 | Name: 'resource-id', 32 | Values: [event.resource] 33 | } 34 | ] 35 | } 36 | try { 37 | const EC2 = new EC2Client({ region: event.region, retryStrategy }) 38 | const { FlowLogs: flowLogs } = await EC2.send(new DescribeFlowLogsCommand(describeParam)) 39 | if (!flowLogs.length) { 40 | const params = { 41 | ResourceIds: [event.resource], 42 | ResourceType: 'VPC', 43 | TrafficType: 'ALL', 44 | DeliverLogsPermissionArn: roleARN, 45 | LogGroupName: 'VPCFlowLogs' 46 | } 47 | const result = await EC2.send(new CreateFlowLogsCommand(params)) 48 | console.log('Result', result) 49 | } 50 | return 'Successfully processed event' 51 | } catch (err) { 52 | console.log('Error', err) 53 | const message = err.message ? err.message : 'Failed to createFlowLogs' 54 | throw new Error(message) 55 | } 56 | 57 | async function putRolePolicy (IAM, roleName) { 58 | const PutRolePolicyParams = { 59 | PolicyDocument: JSON.stringify({ 60 | Version: '2012-10-17', 61 | Statement: [ 62 | { 63 | Action: [ 64 | 'logs:CreateLogGroup', 65 | 'logs:CreateLogStream', 66 | 'logs:PutLogEvents', 67 | 'logs:DescribeLogGroups', 68 | 'logs:DescribeLogStreams' 69 | ], 70 | Effect: 'Allow', 71 | Resource: '*' 72 | } 73 | ] 74 | }), 75 | PolicyName: 'VPCFlowLogRolePolicy', 76 | RoleName: roleName 77 | } 78 | await IAM.send(new PutRolePolicyCommand(PutRolePolicyParams)) 79 | console.log('Successfully put role policy') 80 | 81 | const { Role: role } = IAM.send(new GetRoleCommand({ RoleName: roleName })) 82 | return role.Arn 83 | } 84 | 85 | async function CreateVPCFlowLogRole () { 86 | const VPCFlowLogRole = 'VPCFlowLogRole' 87 | 88 | const IAM = new IAMClient({ retryStrategy }) 89 | 90 | try { 91 | await IAM.send(new GetRoleCommand({ RoleName: VPCFlowLogRole })) 92 | 93 | return putRolePolicy(IAM) 94 | } catch (err) { 95 | console.log(err.message) 96 | if (err.code !== 'NoSuchEntity') { 97 | throw err 98 | } 99 | 100 | const CreateRoleParams = { 101 | AssumeRolePolicyDocument: JSON.stringify({ 102 | Version: '2012-10-17', 103 | Statement: [ 104 | { 105 | Effect: 'Allow', 106 | Principal: { 107 | Service: 'vpc-flow-logs.amazonaws.com' 108 | }, 109 | Action: 'sts:AssumeRole' 110 | } 111 | ] 112 | } 113 | ), 114 | RoleName: VPCFlowLogRole, 115 | Description: 'AWS VPC FlowLog Role Created By Cloud Conformity AutoRemediateVPC-001' 116 | } 117 | 118 | await IAM.send(new CreateRoleCommand(CreateRoleParams)) 119 | 120 | return putRolePolicy(IAM) 121 | } 122 | } 123 | } 124 | 125 | module.exports = { handler } 126 | -------------------------------------------------------------------------------- /exclude-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": "https://cloudconformity.com/external/profile.schema.json", 3 | "version": "1.1", 4 | "name": "exclude list of rules triggered by auto-remediate resources", 5 | "description": "This is a Conformity Profile used to add tag exceptions to rules that triggered by auto-remediate resources. The excluded rules are either low risk rules or rules that require extra resources or configurations", 6 | "ruleSettings": [ 7 | { 8 | "id": "CFM-001", 9 | "enabled": true, 10 | "provider": "aws", 11 | "riskLevel": "MEDIUM", 12 | "exceptions": { 13 | "tags": [ 14 | "service::auto-remediate" 15 | ] 16 | } 17 | }, 18 | { 19 | "id": "CFM-002", 20 | "enabled": true, 21 | "provider": "aws", 22 | "riskLevel": "MEDIUM", 23 | "exceptions": { 24 | "tags": [ 25 | "service::auto-remediate" 26 | ] 27 | } 28 | }, 29 | { 30 | "id": "SNS-002", 31 | "enabled": true, 32 | "provider": "aws", 33 | "riskLevel": "HIGH", 34 | "exceptions": { 35 | "tags": [ 36 | "service::auto-remediate" 37 | ] 38 | } 39 | }, 40 | { 41 | "id": "Lambda-007", 42 | "enabled": true, 43 | "provider": "aws", 44 | "riskLevel": "MEDIUM", 45 | "exceptions": { 46 | "tags": [ 47 | "service::auto-remediate" 48 | ] 49 | } 50 | }, 51 | { 52 | "id": "S3-011", 53 | "enabled": true, 54 | "provider": "aws", 55 | "riskLevel": "MEDIUM", 56 | "exceptions": { 57 | "tags": [ 58 | "service::auto-remediate" 59 | ] 60 | } 61 | }, 62 | { 63 | "id": "S3-018", 64 | "enabled": true, 65 | "provider": "aws", 66 | "riskLevel": "LOW", 67 | "exceptions": { 68 | "tags": [ 69 | "service::auto-remediate" 70 | ] 71 | } 72 | }, 73 | { 74 | "id": "S3-012", 75 | "enabled": true, 76 | "provider": "aws", 77 | "riskLevel": "LOW", 78 | "exceptions": { 79 | "tags": [ 80 | "service::auto-remediate" 81 | ] 82 | } 83 | }, 84 | { 85 | "id": "S3-013", 86 | "enabled": true, 87 | "provider": "aws", 88 | "riskLevel": "LOW", 89 | "exceptions": { 90 | "tags": [ 91 | "service::auto-remediate" 92 | ] 93 | } 94 | }, 95 | { 96 | "id": "S3-020", 97 | "enabled": true, 98 | "provider": "aws", 99 | "riskLevel": "LOW", 100 | "exceptions": { 101 | "tags": [ 102 | "service::auto-remediate" 103 | ] 104 | } 105 | }, 106 | { 107 | "id": "S3-023", 108 | "enabled": true, 109 | "provider": "aws", 110 | "riskLevel": "LOW", 111 | "exceptions": { 112 | "tags": [ 113 | "service::auto-remediate" 114 | ] 115 | } 116 | }, 117 | { 118 | "id": "S3-024", 119 | "enabled": true, 120 | "provider": "aws", 121 | "riskLevel": "LOW", 122 | "exceptions": { 123 | "tags": [ 124 | "service::auto-remediate" 125 | ] 126 | } 127 | }, 128 | { 129 | "id": "SQS-006", 130 | "enabled": true, 131 | "provider": "aws", 132 | "riskLevel": "LOW", 133 | "exceptions": { 134 | "tags": [ 135 | "service::auto-remediate" 136 | ] 137 | } 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /functions/AutoRemediateConfig-001.js: -------------------------------------------------------------------------------- 1 | 2 | const CONFIG = require('./config') 3 | const { IAMClient, GetRoleCommand, CreateRoleCommand, AttachRolePolicyCommand, PutRolePolicyCommand } = require('@aws-sdk/client-iam') 4 | const { ConfiguredRetryStrategy } = require('@aws-sdk/util-retry') 5 | const { ConfigServiceClient } = require('@aws-sdk/client-config-service ') 6 | const { setTimeout } = require('timers/promises') 7 | 8 | // retry all requests with a 5 sec delay (if they are retry-able), up to 10 times 9 | const retryStrategy = new ConfiguredRetryStrategy(10, 5000) 10 | 11 | const Utils = require('./Utils.js') 12 | const { PutConfigurationRecorderCommand, PutDeliveryChannelCommand, StartConfigurationRecorderCommand } = require('@aws-sdk/client-config-service') 13 | 14 | /** 15 | * Ensure AWS Config is enabled in all regions
16 | * 17 | * @param {string} event.region 18 | * 19 | * @param context Lambda Context 20 | * @param callback Lambda Callback 21 | */ 22 | const handler = async (event) => { 23 | console.log('Config AutoRemediateConfig-001 - Received event:', JSON.stringify(event, null, 2)) 24 | 25 | if (!event || !event.region) { 26 | return handleError('Invalid event') 27 | } 28 | 29 | if (!CONFIG['AutoRemediateConfig-001']['S3BucketName']) { 30 | return handleError('Missing CONFIG_S3_BUCKET configuration') 31 | } 32 | 33 | try { 34 | const accountId = await Utils.getAccountId() 35 | console.log('AWS Account ID:', accountId) 36 | 37 | const roleARN = await getRole(accountId) 38 | console.log('Role ARN:', roleARN) 39 | 40 | await subscribe(roleARN) 41 | console.log('Successfully enabled AWS config in', event.region) 42 | 43 | return 'Successfully processed event' 44 | } catch (err) { 45 | console.log('Error', err) 46 | return handleError(err.message ? err.message : 'Failed to enable AWS Config') 47 | } 48 | 49 | function handleError (message) { 50 | message = message || 'Failed to process request.' 51 | throw new Error(message) 52 | } 53 | 54 | async function getRole (accountId) { 55 | const ConfigRoleName = 'AWSConfigRole' 56 | 57 | const IAM = new IAMClient({ retryStrategy }) 58 | 59 | try { 60 | const role = await IAM.send(new GetRoleCommand({ RoleName: ConfigRoleName })) 61 | return role.Role.Arn 62 | } catch (err) { 63 | console.log(err.message) 64 | 65 | if (err.code !== 'NoSuchEntity') { 66 | throw err 67 | } 68 | 69 | const CreateRoleParams = { 70 | AssumeRolePolicyDocument: JSON.stringify({ 71 | Version: '2012-10-17', 72 | Statement: [ 73 | { 74 | Sid: '', 75 | Effect: 'Allow', 76 | Principal: { 77 | Service: 'config.amazonaws.com' 78 | }, 79 | Action: 'sts:AssumeRole' 80 | } 81 | ] 82 | }), 83 | RoleName: ConfigRoleName, 84 | Description: 'AWS Config Role Created By Cloud Conformity AutoRemediateConfig-001' 85 | } 86 | const role = await IAM.send(new CreateRoleCommand(CreateRoleParams)) 87 | console.log('Successfully created the role') 88 | const AttachRolePolicyParams = { 89 | PolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSConfigRole', 90 | RoleName: ConfigRoleName 91 | } 92 | 93 | await IAM.send(new AttachRolePolicyCommand(AttachRolePolicyParams)) 94 | console.log('Successfully attach AWSConfigRole managed policy to role') 95 | 96 | const PutRolePolicyParams = { 97 | PolicyDocument: JSON.stringify({ 98 | Version: '2012-10-17', 99 | Statement: [ 100 | { 101 | Effect: 'Allow', 102 | Action: [ 103 | 's3:PutObject' 104 | ], 105 | Resource: 'arn:aws:s3:::' + CONFIG['AutoRemediateConfig-001']['S3BucketName'] + '/AWSLogs/' + accountId + '/Config/*', 106 | Condition: { 107 | StringLike: { 108 | 's3:x-amz-acl': 'bucket-owner-full-control' 109 | } 110 | } 111 | }, 112 | { 113 | Effect: 'Allow', 114 | Action: [ 115 | 's3:GetBucketAcl' 116 | ], 117 | Resource: 'arn:aws:s3:::' + CONFIG['AutoRemediateConfig-001']['S3BucketName'] 118 | } 119 | ] 120 | }), 121 | PolicyName: 'AWSConfigRolePolicy', 122 | RoleName: ConfigRoleName 123 | } 124 | 125 | await IAM.send(new PutRolePolicyCommand(PutRolePolicyParams)) 126 | console.log('Successfully put role policy') 127 | 128 | // Wait 15 seconds to avoid InsufficientDeliveryPolicyException: Insufficient delivery policy to s3 bucket: 129 | await setTimeout(15000) 130 | return role.Role.Arn 131 | } 132 | } 133 | 134 | async function subscribe (roleARN) { 135 | const ConfigService = new ConfigServiceClient({ region: event.region }) 136 | 137 | const ConfigurationRecorderParams = { 138 | ConfigurationRecorder: { 139 | name: 'default', 140 | recordingGroup: { 141 | allSupported: true, 142 | includeGlobalResourceTypes: (event.region === 'us-east-1') 143 | }, 144 | roleARN: roleARN 145 | } 146 | } 147 | 148 | await ConfigService.send(new PutConfigurationRecorderCommand(ConfigurationRecorderParams)) 149 | console.log('Successfully put configuration recorder') 150 | 151 | const PutDeliveryChannelParams = { 152 | DeliveryChannel: { 153 | name: 'default', 154 | s3BucketName: CONFIG['AutoRemediateConfig-001']['S3BucketName'] 155 | } 156 | } 157 | 158 | await ConfigService.send(new PutDeliveryChannelCommand(PutDeliveryChannelParams)) 159 | console.log('Successfully put delivery channel') 160 | 161 | const StartConfigurationRecorderParams = { 162 | ConfigurationRecorderName: 'default' 163 | } 164 | 165 | const startConfigurationRecorderResult = await ConfigService.send(new StartConfigurationRecorderCommand(StartConfigurationRecorderParams)) 166 | console.log('Successfully start configuration recorder') 167 | return startConfigurationRecorderResult 168 | } 169 | } 170 | 171 | module.exports = { handler } 172 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-003.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-003') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-003:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-003', 13 | ruleTitle: "S3 Bucket Public 'WRITE' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: "Bucket sample-bucket allows public 'WRITE' access.", 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-003 AutoRemediation', () => { 40 | const grantReadAcpAllUsers = { 41 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 42 | } 43 | const grantReadAllUsers = { 44 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 45 | } 46 | const grantWriteAcpAllUsers = { 47 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 48 | } 49 | const grantWriteAllUsers = { 50 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 51 | } 52 | const grantWriteAuthenticatedUsers = { 53 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE' 54 | } 55 | const grantFullControlCanonicalUser = { 56 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 57 | } 58 | 59 | const mockS3 = mockClient(S3Client) 60 | 61 | afterEach(() => { 62 | mockS3.reset() 63 | }) 64 | 65 | describe('valid invocation', () => { 66 | beforeEach(async () => { 67 | mockS3.on(GetBucketAclCommand).resolves({ 68 | Owner: { 69 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 70 | }, 71 | Grants: [ 72 | { 73 | Grantee: { 74 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 75 | }, 76 | Permission: 'FULL_CONTROL' 77 | }, 78 | grantReadAllUsers, 79 | grantReadAcpAllUsers, 80 | grantWriteAllUsers, 81 | grantWriteAcpAllUsers, 82 | grantWriteAuthenticatedUsers, 83 | grantFullControlCanonicalUser 84 | ] 85 | }) 86 | 87 | await source.handler(sampleEvent) 88 | }) 89 | 90 | it('should get the correct bucket ACL from S3', () => { 91 | const expectedParams = { 92 | Bucket: 'sample-bucket' 93 | } 94 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 95 | }) 96 | 97 | it('should set a new ACL on the affected bucket', () => { 98 | const expectedParams = { 99 | Bucket: 'sample-bucket', 100 | AccessControlPolicy: expect.any(Object) 101 | } 102 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 103 | }) 104 | 105 | it('should remove WRITE grants for AllUsers', () => { 106 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantWriteAllUsers) 107 | }) 108 | 109 | it('should keep WRITE grants for other users', () => { 110 | const expectedGrant = { 111 | AccessControlPolicy: { 112 | Grants: expect.arrayContaining([grantWriteAuthenticatedUsers]), 113 | Owner: expect.any(Object) 114 | } 115 | } 116 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 117 | }) 118 | 119 | it('should keep READ grants for All Users', () => { 120 | const expectedGrant = { 121 | AccessControlPolicy: { 122 | Grants: expect.arrayContaining([grantReadAllUsers]), 123 | Owner: expect.any(Object) 124 | } 125 | } 126 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 127 | }) 128 | 129 | it('should keep READ_ACP grants for All Users', () => { 130 | const expectedGrant = { 131 | AccessControlPolicy: { 132 | Grants: expect.arrayContaining([grantReadAcpAllUsers]), 133 | Owner: expect.any(Object) 134 | } 135 | } 136 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 137 | }) 138 | 139 | it('should keep WRITE_ACP grants for All Users', () => { 140 | const expectedGrant = { 141 | AccessControlPolicy: { 142 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 143 | Owner: expect.any(Object) 144 | } 145 | } 146 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 147 | }) 148 | 149 | it('should keep FULL_CONTROL grants for Canonical User', () => { 150 | const expectedGrant = { 151 | AccessControlPolicy: { 152 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 153 | Owner: expect.any(Object) 154 | } 155 | } 156 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 157 | }) 158 | }) 159 | 160 | describe('invalid invocation', () => { 161 | it('should fail when event is undefined', async () => { 162 | await expect(source.handler(undefined)) 163 | .rejects 164 | .toThrow('Invalid event') 165 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 166 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 167 | }) 168 | 169 | it('should fail when "resource" missing from the event', async () => { 170 | const malformedEvent = { 171 | ruleId: 'S3-003' 172 | } 173 | await expect(source.handler(malformedEvent)) 174 | .rejects 175 | .toThrow('Invalid event') 176 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 177 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 178 | }) 179 | 180 | it('should fail when the incorrect rule is received', async () => { 181 | const malformedEvent = { 182 | resource: 'sample-bucket', 183 | ruleId: 'S3-00x' 184 | } 185 | await expect(source.handler(malformedEvent)) 186 | .rejects 187 | .toThrow('Invalid event') 188 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 189 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 190 | }) 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-002.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-002') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-001:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-002', 13 | ruleTitle: "S3 Bucket Public 'READ_ACP' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: "Bucket sample-bucket allows public 'READ_ACP' access.", 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-002 AutoRemediation', () => { 40 | const mockS3 = mockClient(S3Client) 41 | const grantReadAcpAllUsers = { 42 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 43 | } 44 | const grantReadAllUsers = { 45 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 46 | } 47 | const grantWriteAcpAllUsers = { 48 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 49 | } 50 | const grantWriteAllUsers = { 51 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 52 | } 53 | const grantReadAcpAuthenticatedUsers = { 54 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 55 | } 56 | const grantFullControlCanonicalUser = { 57 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 58 | } 59 | 60 | afterEach(() => { 61 | mockS3.reset() 62 | }) 63 | 64 | describe('valid invocation', () => { 65 | beforeEach(async () => { 66 | mockS3.on(GetBucketAclCommand).resolves({ 67 | Owner: { 68 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 69 | }, 70 | Grants: [ 71 | { 72 | Grantee: { 73 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 74 | }, 75 | Permission: 'FULL_CONTROL' 76 | }, 77 | grantReadAllUsers, 78 | grantReadAcpAllUsers, 79 | grantWriteAllUsers, 80 | grantWriteAcpAllUsers, 81 | grantReadAcpAuthenticatedUsers, 82 | grantFullControlCanonicalUser 83 | ] 84 | }) 85 | 86 | await source.handler(sampleEvent) 87 | }) 88 | 89 | it('should get the correct bucket ACL from S3', () => { 90 | const expectedParams = { 91 | Bucket: 'sample-bucket' 92 | } 93 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 94 | }) 95 | 96 | it('should set a new ACL on the affected bucket', () => { 97 | const expectedParams = { 98 | Bucket: 'sample-bucket', 99 | AccessControlPolicy: expect.any(Object) 100 | } 101 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 102 | }) 103 | 104 | it('should remove READ_ACP grants for AllUsers', () => { 105 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantReadAcpAllUsers) 106 | }) 107 | 108 | it('should keep READ_ACP grants for other users', () => { 109 | const expectedGrant = { 110 | AccessControlPolicy: { 111 | Grants: expect.arrayContaining([grantReadAcpAuthenticatedUsers]), 112 | Owner: expect.any(Object) 113 | } 114 | } 115 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 116 | }) 117 | 118 | it('should keep READ grants for All Users', () => { 119 | const expectedGrant = { 120 | AccessControlPolicy: { 121 | Grants: expect.arrayContaining([grantReadAllUsers]), 122 | Owner: expect.any(Object) 123 | } 124 | } 125 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 126 | }) 127 | 128 | it('should keep WRITE grants for All Users', () => { 129 | const expectedGrant = { 130 | AccessControlPolicy: { 131 | Grants: expect.arrayContaining([grantWriteAllUsers]), 132 | Owner: expect.any(Object) 133 | } 134 | } 135 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 136 | }) 137 | 138 | it('should keep WRITE_ACP grants for All Users', () => { 139 | const expectedGrant = { 140 | AccessControlPolicy: { 141 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 142 | Owner: expect.any(Object) 143 | } 144 | } 145 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 146 | }) 147 | 148 | it('should keep FULL_CONTROL grants for Canonical User', () => { 149 | const expectedGrant = { 150 | AccessControlPolicy: { 151 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 152 | Owner: expect.any(Object) 153 | } 154 | } 155 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 156 | }) 157 | }) 158 | 159 | describe('invalid invocation', () => { 160 | it('should fail when event is undefined', async () => { 161 | await expect(source.handler(undefined)) 162 | .rejects 163 | .toThrow('Invalid event') 164 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 165 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 166 | }) 167 | 168 | it('should fail when "resource" missing from the event', async () => { 169 | const malformedEvent = { 170 | ruleId: 'S3-002' 171 | } 172 | await expect(source.handler(malformedEvent)) 173 | .rejects 174 | .toThrow('Invalid event') 175 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 176 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 177 | }) 178 | 179 | it('should fail when the incorrect rule is received', async () => { 180 | const malformedEvent = { 181 | resource: 'sample-bucket', 182 | ruleId: 'S3-001' 183 | } 184 | await expect(source.handler(malformedEvent)) 185 | .rejects 186 | .toThrow('Invalid event') 187 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 188 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 189 | }) 190 | }) 191 | }) 192 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-004.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-004') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-004:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-004', 13 | ruleTitle: "S3 Bucket Public 'WRITE_ACP' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: "Bucket sample-bucket allows public 'WRITE_ACP' access.", 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-004 AutoRemediation', () => { 40 | const grantReadAcpAllUsers = { 41 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 42 | } 43 | const grantReadAllUsers = { 44 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 45 | } 46 | const grantWriteAcpAllUsers = { 47 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 48 | } 49 | const grantWriteAllUsers = { 50 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 51 | } 52 | const grantWriteAcpAuthenticatedUsers = { 53 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE_ACP' 54 | } 55 | const grantFullControlCanonicalUser = { 56 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 57 | } 58 | 59 | const mockS3 = mockClient(S3Client) 60 | 61 | afterEach(() => { 62 | mockS3.reset() 63 | }) 64 | 65 | describe('valid invocation', () => { 66 | beforeEach(async () => { 67 | mockS3.on(GetBucketAclCommand).resolves({ 68 | Owner: { 69 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 70 | }, 71 | Grants: [ 72 | { 73 | Grantee: { 74 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 75 | }, 76 | Permission: 'FULL_CONTROL' 77 | }, 78 | grantReadAllUsers, 79 | grantReadAcpAllUsers, 80 | grantWriteAllUsers, 81 | grantWriteAcpAllUsers, 82 | grantWriteAcpAuthenticatedUsers, 83 | grantFullControlCanonicalUser 84 | ] 85 | }) 86 | 87 | await source.handler(sampleEvent) 88 | }) 89 | 90 | it('should get the correct bucket ACL from S3', () => { 91 | const expectedParams = { 92 | Bucket: 'sample-bucket' 93 | } 94 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 95 | }) 96 | 97 | it('should set a new ACL on the affected bucket', () => { 98 | const expectedParams = { 99 | Bucket: 'sample-bucket', 100 | AccessControlPolicy: expect.any(Object) 101 | } 102 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 103 | }) 104 | 105 | it('should remove WRITE_ACP grants for AllUsers', () => { 106 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantWriteAcpAllUsers) 107 | }) 108 | 109 | it('should keep WRITE_ACP grants for other users', () => { 110 | const expectedGrant = { 111 | AccessControlPolicy: { 112 | Grants: expect.arrayContaining([grantWriteAcpAuthenticatedUsers]), 113 | Owner: expect.any(Object) 114 | } 115 | } 116 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 117 | }) 118 | 119 | it('should keep READ grants for All Users', () => { 120 | const expectedGrant = { 121 | AccessControlPolicy: { 122 | Grants: expect.arrayContaining([grantReadAllUsers]), 123 | Owner: expect.any(Object) 124 | } 125 | } 126 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 127 | }) 128 | 129 | it('should keep READ_ACP grants for All Users', () => { 130 | const expectedGrant = { 131 | AccessControlPolicy: { 132 | Grants: expect.arrayContaining([grantReadAcpAllUsers]), 133 | Owner: expect.any(Object) 134 | } 135 | } 136 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 137 | }) 138 | 139 | it('should keep WRITE grants for All Users', () => { 140 | const expectedGrant = { 141 | AccessControlPolicy: { 142 | Grants: expect.arrayContaining([grantWriteAllUsers]), 143 | Owner: expect.any(Object) 144 | } 145 | } 146 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 147 | }) 148 | 149 | it('should keep FULL_CONTROL grants for Canonical User', () => { 150 | const expectedGrant = { 151 | AccessControlPolicy: { 152 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 153 | Owner: expect.any(Object) 154 | } 155 | } 156 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 157 | }) 158 | }) 159 | 160 | describe('invalid invocation', () => { 161 | it('should fail when event is undefined', async () => { 162 | await expect(source.handler(undefined)) 163 | .rejects 164 | .toThrow('Invalid event') 165 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 166 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 167 | }) 168 | 169 | it('should fail when "resource" missing from the event', async () => { 170 | const malformedEvent = { 171 | ruleId: 'S3-004' 172 | } 173 | await expect(source.handler(malformedEvent)) 174 | .rejects 175 | .toThrow('Invalid event') 176 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 177 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 178 | }) 179 | 180 | it('should fail when the incorrect rule is received', async () => { 181 | const malformedEvent = { 182 | resource: 'sample-bucket', 183 | ruleId: 'S3-00x' 184 | } 185 | await expect(source.handler(malformedEvent)) 186 | .rejects 187 | .toThrow('Invalid event') 188 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 189 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 190 | }) 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-005.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-005') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-005:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-005', 13 | ruleTitle: "S3 Bucket Public 'FULL_CONTROL' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: 'Bucket sample-bucket allows public FULL_CONTROL access.', 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-005 AutoRemediation', () => { 40 | const grantReadAcpAllUsers = { 41 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 42 | } 43 | const grantReadAllUsers = { 44 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 45 | } 46 | const grantWriteAcpAllUsers = { 47 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 48 | } 49 | const grantWriteAllUsers = { 50 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 51 | } 52 | const grantFullControlAlllUser = { 53 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'FULL_CONTROL' 54 | } 55 | const grantReadAcpAuthenticatedUsers = { 56 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 57 | } 58 | const grantFullControlCanonicalUser = { 59 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 60 | } 61 | 62 | const mockS3 = mockClient(S3Client) 63 | 64 | afterEach(() => { 65 | mockS3.reset() 66 | }) 67 | 68 | describe('valid invocation', () => { 69 | beforeEach(async () => { 70 | mockS3.on(GetBucketAclCommand).resolves({ 71 | Owner: { 72 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 73 | }, 74 | Grants: [ 75 | { 76 | Grantee: { 77 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 78 | }, 79 | Permission: 'FULL_CONTROL' 80 | }, 81 | grantReadAllUsers, 82 | grantReadAcpAllUsers, 83 | grantWriteAllUsers, 84 | grantWriteAcpAllUsers, 85 | grantFullControlAlllUser, 86 | grantReadAcpAuthenticatedUsers, 87 | grantFullControlCanonicalUser 88 | ] 89 | }) 90 | 91 | await source.handler(sampleEvent) 92 | }) 93 | 94 | it('should get the correct bucket ACL from S3', () => { 95 | const expectedParams = { 96 | Bucket: 'sample-bucket' 97 | } 98 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 99 | }) 100 | 101 | it('should set a new ACL on the affected bucket', () => { 102 | const expectedParams = { 103 | Bucket: 'sample-bucket', 104 | AccessControlPolicy: expect.any(Object) 105 | } 106 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 107 | }) 108 | 109 | it('should remove READ_ACP grants for AllUsers', () => { 110 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantReadAcpAllUsers) 111 | }) 112 | 113 | it('should keep READ_ACP grants for other users', () => { 114 | const expectedGrant = { 115 | AccessControlPolicy: { 116 | Grants: expect.arrayContaining([grantReadAcpAuthenticatedUsers]), 117 | Owner: expect.any(Object) 118 | } 119 | } 120 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 121 | }) 122 | 123 | it('should keep READ grants for All Users', () => { 124 | const expectedGrant = { 125 | AccessControlPolicy: { 126 | Grants: expect.arrayContaining([grantReadAllUsers]), 127 | Owner: expect.any(Object) 128 | } 129 | } 130 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 131 | }) 132 | 133 | it('should keep WRITE grants for All Users', () => { 134 | const expectedGrant = { 135 | AccessControlPolicy: { 136 | Grants: expect.arrayContaining([grantWriteAllUsers]), 137 | Owner: expect.any(Object) 138 | } 139 | } 140 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 141 | }) 142 | 143 | it('should keep WRITE_ACP grants for All Users', () => { 144 | const expectedGrant = { 145 | AccessControlPolicy: { 146 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 147 | Owner: expect.any(Object) 148 | } 149 | } 150 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 151 | }) 152 | 153 | it('should keep FULL_CONTROL grants for Canonical User', () => { 154 | const expectedGrant = { 155 | AccessControlPolicy: { 156 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 157 | Owner: expect.any(Object) 158 | } 159 | } 160 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 161 | }) 162 | }) 163 | 164 | describe('invalid invocation', () => { 165 | it('should fail when event is undefined', async () => { 166 | await expect(source.handler(undefined)) 167 | .rejects 168 | .toThrow('Invalid event') 169 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 170 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 171 | }) 172 | 173 | it('should fail when "resource" missing from the event', async () => { 174 | const malformedEvent = { 175 | ruleId: 'S3-00x' 176 | } 177 | await expect(source.handler(malformedEvent)) 178 | .rejects 179 | .toThrow('Invalid event') 180 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 181 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 182 | }) 183 | 184 | it('should fail when the incorrect rule is received', async () => { 185 | const malformedEvent = { 186 | resource: 'sample-bucket', 187 | ruleId: 'S3-00x' 188 | } 189 | await expect(source.handler(malformedEvent)) 190 | .rejects 191 | .toThrow('Invalid event') 192 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 193 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 194 | }) 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-006.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-006') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-006:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-006', 13 | ruleTitle: "S3 Bucket Authenticated Users 'READ' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: 'Bucket sample-bucket allows authenticated users READ access.', 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-006 AutoRemediation', () => { 40 | const grantReadAcpAllUsers = { 41 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 42 | } 43 | const grantReadAllUsers = { 44 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 45 | } 46 | const grantWriteAcpAllUsers = { 47 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 48 | } 49 | const grantWriteAllUsers = { 50 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 51 | } 52 | const grantReadAcpAuthenticatedUsers = { 53 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 54 | } 55 | const grantReadAuthenticatedUsers = { 56 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ' 57 | } 58 | const grantFullControlCanonicalUser = { 59 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 60 | } 61 | 62 | const mockS3 = mockClient(S3Client) 63 | 64 | afterEach(() => { 65 | mockS3.reset() 66 | }) 67 | 68 | describe('valid invocation', () => { 69 | beforeEach(async () => { 70 | mockS3.on(GetBucketAclCommand).resolves({ 71 | Owner: { 72 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 73 | }, 74 | Grants: [ 75 | { 76 | Grantee: { 77 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 78 | }, 79 | Permission: 'FULL_CONTROL' 80 | }, 81 | grantReadAllUsers, 82 | grantReadAcpAllUsers, 83 | grantWriteAllUsers, 84 | grantWriteAcpAllUsers, 85 | grantReadAuthenticatedUsers, 86 | grantReadAcpAuthenticatedUsers, 87 | grantFullControlCanonicalUser 88 | ] 89 | }) 90 | 91 | await source.handler(sampleEvent) 92 | }) 93 | 94 | it('should get the correct bucket ACL from S3', () => { 95 | const expectedParams = { 96 | Bucket: 'sample-bucket' 97 | } 98 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 99 | }) 100 | 101 | it('should set a new ACL on the affected bucket', () => { 102 | const expectedParams = { 103 | Bucket: 'sample-bucket', 104 | AccessControlPolicy: expect.any(Object) 105 | } 106 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 107 | }) 108 | 109 | it('should remove READ grants for Authenticated Users', () => { 110 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantReadAuthenticatedUsers) 111 | }) 112 | 113 | it('should keep READ_ACP grants for other users', () => { 114 | const expectedGrant = { 115 | AccessControlPolicy: { 116 | Grants: expect.arrayContaining([grantReadAcpAuthenticatedUsers]), 117 | Owner: expect.any(Object) 118 | } 119 | } 120 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 121 | }) 122 | 123 | it('should keep READ grants for All Users', () => { 124 | const expectedGrant = { 125 | AccessControlPolicy: { 126 | Grants: expect.arrayContaining([grantReadAllUsers]), 127 | Owner: expect.any(Object) 128 | } 129 | } 130 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 131 | }) 132 | 133 | it('should keep READ_ACP grants for All Users', () => { 134 | const expectedGrant = { 135 | AccessControlPolicy: { 136 | Grants: expect.arrayContaining([grantReadAcpAllUsers]), 137 | Owner: expect.any(Object) 138 | } 139 | } 140 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 141 | }) 142 | 143 | it('should keep WRITE grants for All Users', () => { 144 | const expectedGrant = { 145 | AccessControlPolicy: { 146 | Grants: expect.arrayContaining([grantWriteAllUsers]), 147 | Owner: expect.any(Object) 148 | } 149 | } 150 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 151 | }) 152 | 153 | it('should keep WRITE_ACP grants for All Users', () => { 154 | const expectedGrant = { 155 | AccessControlPolicy: { 156 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 157 | Owner: expect.any(Object) 158 | } 159 | } 160 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 161 | }) 162 | 163 | it('should keep FULL_CONTROL grants for Canonical User', () => { 164 | const expectedGrant = { 165 | AccessControlPolicy: { 166 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 167 | Owner: expect.any(Object) 168 | } 169 | } 170 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 171 | }) 172 | }) 173 | 174 | describe('invalid invocation', () => { 175 | it('should fail when event is undefined', async () => { 176 | await expect(source.handler(undefined)) 177 | .rejects 178 | .toThrow('Invalid event') 179 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 180 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 181 | }) 182 | 183 | it('should fail when "resource" missing from the event', async () => { 184 | const malformedEvent = { 185 | ruleId: 'S3-006' 186 | } 187 | await expect(source.handler(malformedEvent)) 188 | .rejects 189 | .toThrow('Invalid event') 190 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 191 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 192 | }) 193 | 194 | it('should fail when the incorrect rule is received', async () => { 195 | const malformedEvent = { 196 | resource: 'sample-bucket', 197 | ruleId: 'S3-00x' 198 | } 199 | await expect(source.handler(malformedEvent)) 200 | .rejects 201 | .toThrow('Invalid event') 202 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 203 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 204 | }) 205 | }) 206 | }) 207 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-008.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-008') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-008:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-008', 13 | ruleTitle: "S3 Bucket Authenticated Users 'WRITE' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: 'Bucket sample-bucket allows authenticated users WRITE access.', 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-008 AutoRemediation', () => { 40 | const grantReadAcpAllUsers = { 41 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 42 | } 43 | const grantReadAllUsers = { 44 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 45 | } 46 | const grantWriteAcpAllUsers = { 47 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 48 | } 49 | const grantWriteAllUsers = { 50 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 51 | } 52 | const grantFullControlAllUsers = { 53 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'FULL_CONTROL' 54 | } 55 | const grantReadAcpAuthenticatedUsers = { 56 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 57 | } 58 | const grantReadAuthenticatedUsers = { 59 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ' 60 | } 61 | const grantWriteAcpAuthenticatedUsers = { 62 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE_ACP' 63 | } 64 | const grantWriteAuthenticatedUsers = { 65 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE' 66 | } 67 | const grantFullControlAuthenticatedUsers = { 68 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'FULL_CONTROL' 69 | } 70 | const grantFullControlCanonicalUser = { 71 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 72 | } 73 | 74 | const mockS3 = mockClient(S3Client) 75 | 76 | afterEach(() => { 77 | mockS3.reset() 78 | }) 79 | 80 | describe('valid invocation', () => { 81 | beforeEach(async () => { 82 | mockS3.on(GetBucketAclCommand).resolves({ 83 | Owner: { 84 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 85 | }, 86 | Grants: [ 87 | { 88 | Grantee: { 89 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 90 | }, 91 | Permission: 'FULL_CONTROL' 92 | }, 93 | grantReadAllUsers, 94 | grantReadAcpAllUsers, 95 | grantWriteAllUsers, 96 | grantWriteAcpAllUsers, 97 | grantFullControlAllUsers, 98 | grantReadAuthenticatedUsers, 99 | grantReadAcpAuthenticatedUsers, 100 | grantWriteAuthenticatedUsers, 101 | grantWriteAcpAuthenticatedUsers, 102 | grantFullControlAuthenticatedUsers, 103 | grantFullControlCanonicalUser 104 | ] 105 | }) 106 | 107 | await source.handler(sampleEvent) 108 | }) 109 | 110 | it('should get the correct bucket ACL from S3', () => { 111 | const expectedParams = { 112 | Bucket: 'sample-bucket' 113 | } 114 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 115 | }) 116 | 117 | it('should set a new ACL on the affected bucket', () => { 118 | const expectedParams = { 119 | Bucket: 'sample-bucket', 120 | AccessControlPolicy: expect.any(Object) 121 | } 122 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 123 | }) 124 | 125 | it('should remove WRITE grants for Authenticated Users', () => { 126 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantWriteAuthenticatedUsers) 127 | }) 128 | 129 | it('should keep READ_ACP grants for Authenticated Users', () => { 130 | const expectedGrant = { 131 | AccessControlPolicy: { 132 | Grants: expect.arrayContaining([grantReadAcpAuthenticatedUsers]), 133 | Owner: expect.any(Object) 134 | } 135 | } 136 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 137 | }) 138 | 139 | it('should keep READ grants for Authenticated Users', () => { 140 | const expectedGrant = { 141 | AccessControlPolicy: { 142 | Grants: expect.arrayContaining([grantReadAuthenticatedUsers]), 143 | Owner: expect.any(Object) 144 | } 145 | } 146 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 147 | }) 148 | 149 | it('should keep WRITE_ACP grants for Authenticated Users', () => { 150 | const expectedGrant = { 151 | AccessControlPolicy: { 152 | Grants: expect.arrayContaining([grantWriteAcpAuthenticatedUsers]), 153 | Owner: expect.any(Object) 154 | } 155 | } 156 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 157 | }) 158 | 159 | it('should keep FULL_CONTROL grants for Authenticated Users', () => { 160 | const expectedGrant = { 161 | AccessControlPolicy: { 162 | Grants: expect.arrayContaining([grantFullControlAuthenticatedUsers]), 163 | Owner: expect.any(Object) 164 | } 165 | } 166 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 167 | }) 168 | 169 | it('should keep READ grants for All Users', () => { 170 | const expectedGrant = { 171 | AccessControlPolicy: { 172 | Grants: expect.arrayContaining([grantReadAllUsers]), 173 | Owner: expect.any(Object) 174 | } 175 | } 176 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 177 | }) 178 | 179 | it('should keep READ_ACP grants for All Users', () => { 180 | const expectedGrant = { 181 | AccessControlPolicy: { 182 | Grants: expect.arrayContaining([grantReadAcpAllUsers]), 183 | Owner: expect.any(Object) 184 | } 185 | } 186 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 187 | }) 188 | 189 | it('should keep WRITE grants for All Users', () => { 190 | const expectedGrant = { 191 | AccessControlPolicy: { 192 | Grants: expect.arrayContaining([grantWriteAllUsers]), 193 | Owner: expect.any(Object) 194 | } 195 | } 196 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 197 | }) 198 | 199 | it('should keep WRITE_ACP grants for All Users', () => { 200 | const expectedGrant = { 201 | AccessControlPolicy: { 202 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 203 | Owner: expect.any(Object) 204 | } 205 | } 206 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 207 | }) 208 | 209 | it('should keep FULL_CONTROL grants for All Users', () => { 210 | const expectedGrant = { 211 | AccessControlPolicy: { 212 | Grants: expect.arrayContaining([grantFullControlAllUsers]), 213 | Owner: expect.any(Object) 214 | } 215 | } 216 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 217 | }) 218 | 219 | it('should keep FULL_CONTROL grants for Canonical User', () => { 220 | const expectedGrant = { 221 | AccessControlPolicy: { 222 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 223 | Owner: expect.any(Object) 224 | } 225 | } 226 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 227 | }) 228 | }) 229 | 230 | describe('invalid invocation', () => { 231 | it('should fail when event is undefined', async () => { 232 | await expect(source.handler(undefined)) 233 | .rejects 234 | .toThrow('Invalid event') 235 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 236 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 237 | }) 238 | 239 | it('should fail when "resource" missing from the event', async () => { 240 | const malformedEvent = { 241 | ruleId: 'S3-008' 242 | } 243 | await expect(source.handler(malformedEvent)) 244 | .rejects 245 | .toThrow('Invalid event') 246 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 247 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 248 | }) 249 | 250 | it('should fail when the incorrect rule is received', async () => { 251 | const malformedEvent = { 252 | resource: 'sample-bucket', 253 | ruleId: 'S3-00x' 254 | } 255 | await expect(source.handler(malformedEvent)) 256 | .rejects 257 | .toThrow('Invalid event') 258 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 259 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 260 | }) 261 | }) 262 | }) 263 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-009.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-009') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-009:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-009', 13 | ruleTitle: "S3 Bucket Authenticated Users 'WRITE_ACP' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: 'Bucket sample-bucket allows authenticated users WRITE_ACP access.', 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-009 AutoRemediation', () => { 40 | const grantReadAcpAllUsers = { 41 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 42 | } 43 | const grantReadAllUsers = { 44 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 45 | } 46 | const grantWriteAcpAllUsers = { 47 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 48 | } 49 | const grantWriteAllUsers = { 50 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 51 | } 52 | const grantFullControlAllUsers = { 53 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'FULL_CONTROL' 54 | } 55 | const grantReadAcpAuthenticatedUsers = { 56 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 57 | } 58 | const grantReadAuthenticatedUsers = { 59 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ' 60 | } 61 | const grantWriteAcpAuthenticatedUsers = { 62 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE_ACP' 63 | } 64 | const grantWriteAuthenticatedUsers = { 65 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE' 66 | } 67 | const grantFullControlAuthenticatedUsers = { 68 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'FULL_CONTROL' 69 | } 70 | const grantFullControlCanonicalUser = { 71 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 72 | } 73 | 74 | const mockS3 = mockClient(S3Client) 75 | 76 | afterEach(() => { 77 | mockS3.reset() 78 | }) 79 | 80 | describe('valid invocation', () => { 81 | beforeEach(async () => { 82 | mockS3.on(GetBucketAclCommand).resolves({ 83 | Owner: { 84 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 85 | }, 86 | Grants: [ 87 | { 88 | Grantee: { 89 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 90 | }, 91 | Permission: 'FULL_CONTROL' 92 | }, 93 | grantReadAllUsers, 94 | grantReadAcpAllUsers, 95 | grantWriteAllUsers, 96 | grantWriteAcpAllUsers, 97 | grantFullControlAllUsers, 98 | grantReadAuthenticatedUsers, 99 | grantReadAcpAuthenticatedUsers, 100 | grantWriteAuthenticatedUsers, 101 | grantWriteAcpAuthenticatedUsers, 102 | grantFullControlAuthenticatedUsers, 103 | grantFullControlCanonicalUser 104 | ] 105 | }) 106 | 107 | await source.handler(sampleEvent) 108 | }) 109 | 110 | it('should get the correct bucket ACL from S3', () => { 111 | const expectedParams = { 112 | Bucket: 'sample-bucket' 113 | } 114 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 115 | }) 116 | 117 | it('should set a new ACL on the affected bucket', () => { 118 | const expectedParams = { 119 | Bucket: 'sample-bucket', 120 | AccessControlPolicy: expect.any(Object) 121 | } 122 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 123 | }) 124 | 125 | it('should remove WRITE_ACP grants for Authenticated Users', () => { 126 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantWriteAcpAuthenticatedUsers) 127 | }) 128 | 129 | it('should keep READ_ACP grants for Authenticated Users', () => { 130 | const expectedGrant = { 131 | AccessControlPolicy: { 132 | Grants: expect.arrayContaining([grantReadAcpAuthenticatedUsers]), 133 | Owner: expect.any(Object) 134 | } 135 | } 136 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 137 | }) 138 | 139 | it('should keep READ grants for Authenticated Users', () => { 140 | const expectedGrant = { 141 | AccessControlPolicy: { 142 | Grants: expect.arrayContaining([grantReadAuthenticatedUsers]), 143 | Owner: expect.any(Object) 144 | } 145 | } 146 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 147 | }) 148 | 149 | it('should keep WRITE grants for Authenticated Users', () => { 150 | const expectedGrant = { 151 | AccessControlPolicy: { 152 | Grants: expect.arrayContaining([grantWriteAuthenticatedUsers]), 153 | Owner: expect.any(Object) 154 | } 155 | } 156 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 157 | }) 158 | 159 | it('should keep FULL_CONTROL grants for Authenticated Users', () => { 160 | const expectedGrant = { 161 | AccessControlPolicy: { 162 | Grants: expect.arrayContaining([grantFullControlAuthenticatedUsers]), 163 | Owner: expect.any(Object) 164 | } 165 | } 166 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 167 | }) 168 | 169 | it('should keep READ grants for All Users', () => { 170 | const expectedGrant = { 171 | AccessControlPolicy: { 172 | Grants: expect.arrayContaining([grantReadAllUsers]), 173 | Owner: expect.any(Object) 174 | } 175 | } 176 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 177 | }) 178 | 179 | it('should keep READ_ACP grants for All Users', () => { 180 | const expectedGrant = { 181 | AccessControlPolicy: { 182 | Grants: expect.arrayContaining([grantReadAcpAllUsers]), 183 | Owner: expect.any(Object) 184 | } 185 | } 186 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 187 | }) 188 | 189 | it('should keep WRITE grants for All Users', () => { 190 | const expectedGrant = { 191 | AccessControlPolicy: { 192 | Grants: expect.arrayContaining([grantWriteAllUsers]), 193 | Owner: expect.any(Object) 194 | } 195 | } 196 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 197 | }) 198 | 199 | it('should keep WRITE_ACP grants for All Users', () => { 200 | const expectedGrant = { 201 | AccessControlPolicy: { 202 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 203 | Owner: expect.any(Object) 204 | } 205 | } 206 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 207 | }) 208 | 209 | it('should keep FULL_CONTROL grants for All Users', () => { 210 | const expectedGrant = { 211 | AccessControlPolicy: { 212 | Grants: expect.arrayContaining([grantFullControlAllUsers]), 213 | Owner: expect.any(Object) 214 | } 215 | } 216 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 217 | }) 218 | 219 | it('should keep FULL_CONTROL grants for Canonical User', () => { 220 | const expectedGrant = { 221 | AccessControlPolicy: { 222 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 223 | Owner: expect.any(Object) 224 | } 225 | } 226 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 227 | }) 228 | }) 229 | 230 | describe('invalid invocation', () => { 231 | it('should fail when event is undefined', async () => { 232 | await expect(source.handler(undefined)) 233 | .rejects 234 | .toThrow('Invalid event') 235 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 236 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 237 | }) 238 | 239 | it('should fail when "resource" missing from the event', async () => { 240 | const malformedEvent = { 241 | ruleId: 'S3-006' 242 | } 243 | await expect(source.handler(malformedEvent)) 244 | .rejects 245 | .toThrow('Invalid event') 246 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 247 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 248 | }) 249 | 250 | it('should fail when the incorrect rule is received', async () => { 251 | const malformedEvent = { 252 | resource: 'sample-bucket', 253 | ruleId: 'S3-00x' 254 | } 255 | await expect(source.handler(malformedEvent)) 256 | .rejects 257 | .toThrow('Invalid event') 258 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 259 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 260 | }) 261 | }) 262 | }) 263 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-010.test.js: -------------------------------------------------------------------------------- 1 | 2 | const source = require('../functions/AutoRemediateS3-010') 3 | const { S3Client, GetBucketAclCommand, PutBucketAclCommand } = require('@aws-sdk/client-s3') 4 | const { mockClient } = require('aws-sdk-client-mock') 5 | // eslint-disable-next-line node/no-extraneous-require 6 | require('aws-sdk-client-mock-jest') 7 | 8 | const sampleEvent = { 9 | id: 'ccc:HJzFMHchx:S3-010:S3:ap-southeast-2:sample-bucket', 10 | organisationId: 'some-organisation', 11 | accountId: 'abcdef', 12 | ruleId: 'S3-010', 13 | ruleTitle: "S3 Bucket Authenticated Users 'FULL_CONTROL' Access", 14 | service: 'S3', 15 | region: 'ap-southeast-2', 16 | riskLevel: 'VERY_HIGH', 17 | categories: [ 18 | 'security' 19 | ], 20 | compliances: [ 21 | 'AWAF' 22 | ], 23 | message: 'Bucket sample-bucket allows authenticated users FULL_CONTROL access.', 24 | resource: 'sample-bucket', 25 | status: 'FAILURE', 26 | statusRiskLevel: 'FAILURE:1', 27 | lastUpdatedDate: null, 28 | lastUpdatedBy: 'SYSTEM', 29 | resolvedBy: 'SYSTEM', 30 | eventId: 'Skzp7ra1WW', 31 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 32 | tags: [], 33 | cost: '0', 34 | waste: '0', 35 | lastModifiedDate: '1511060191925', 36 | lastModifiedBy: 'SYSTEM' 37 | } 38 | 39 | describe('S3-010 AutoRemediation', () => { 40 | const grantReadAcpAllUsers = { 41 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ_ACP' 42 | } 43 | const grantReadAllUsers = { 44 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'READ' 45 | } 46 | const grantWriteAcpAllUsers = { 47 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE_ACP' 48 | } 49 | const grantWriteAllUsers = { 50 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'WRITE' 51 | } 52 | const grantFullControlAllUsers = { 53 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AllUsers' }, Permission: 'FULL_CONTROL' 54 | } 55 | const grantReadAcpAuthenticatedUsers = { 56 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ_ACP' 57 | } 58 | const grantReadAuthenticatedUsers = { 59 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'READ' 60 | } 61 | const grantWriteAcpAuthenticatedUsers = { 62 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE_ACP' 63 | } 64 | const grantWriteAuthenticatedUsers = { 65 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'WRITE' 66 | } 67 | const grantFullControlAuthenticatedUsers = { 68 | Grantee: { Type: 'Group', URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' }, Permission: 'FULL_CONTROL' 69 | } 70 | const grantFullControlCanonicalUser = { 71 | Grantee: { DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' }, Permission: 'FULL_CONTROL' 72 | } 73 | 74 | const mockS3 = mockClient(S3Client) 75 | 76 | afterEach(() => { 77 | mockS3.reset() 78 | }) 79 | 80 | describe('valid invocation', () => { 81 | beforeEach(async () => { 82 | mockS3.on(GetBucketAclCommand).resolves({ 83 | Owner: { 84 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef' 85 | }, 86 | Grants: [ 87 | { 88 | Grantee: { 89 | DisplayName: 'user_name', ID: 'account_user_id123455667890abcdef', Type: 'CanonicalUser' 90 | }, 91 | Permission: 'FULL_CONTROL' 92 | }, 93 | grantReadAllUsers, 94 | grantReadAcpAllUsers, 95 | grantWriteAllUsers, 96 | grantWriteAcpAllUsers, 97 | grantFullControlAllUsers, 98 | grantReadAuthenticatedUsers, 99 | grantReadAcpAuthenticatedUsers, 100 | grantWriteAuthenticatedUsers, 101 | grantWriteAcpAuthenticatedUsers, 102 | grantFullControlAuthenticatedUsers, 103 | grantFullControlCanonicalUser 104 | ] 105 | }) 106 | await source.handler(sampleEvent) 107 | }) 108 | 109 | it('should get the correct bucket ACL from S3', () => { 110 | const expectedParams = { 111 | Bucket: 'sample-bucket' 112 | } 113 | expect(mockS3).toHaveReceivedCommandWith(GetBucketAclCommand, expectedParams) 114 | }) 115 | 116 | it('should set a new ACL on the affected bucket', () => { 117 | const expectedParams = { 118 | Bucket: 'sample-bucket', 119 | AccessControlPolicy: expect.any(Object) 120 | } 121 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedParams) 122 | }) 123 | 124 | it('should remove FULL_CONTROL grants for Authenticated Users', () => { 125 | expect(mockS3).not.toHaveReceivedCommandWith(PutBucketAclCommand, grantFullControlAuthenticatedUsers) 126 | }) 127 | 128 | it('should keep READ_ACP grants for Authenticated Users', () => { 129 | const expectedGrant = { 130 | AccessControlPolicy: { 131 | Grants: expect.arrayContaining([grantReadAcpAuthenticatedUsers]), 132 | Owner: expect.any(Object) 133 | } 134 | } 135 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 136 | }) 137 | 138 | it('should keep READ grants for Authenticated Users', () => { 139 | const expectedGrant = { 140 | AccessControlPolicy: { 141 | Grants: expect.arrayContaining([grantReadAuthenticatedUsers]), 142 | Owner: expect.any(Object) 143 | } 144 | } 145 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 146 | }) 147 | 148 | it('should keep WRITE grants for Authenticated Users', () => { 149 | const expectedGrant = { 150 | AccessControlPolicy: { 151 | Grants: expect.arrayContaining([grantWriteAuthenticatedUsers]), 152 | Owner: expect.any(Object) 153 | } 154 | } 155 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 156 | }) 157 | 158 | it('should keep WRITE_ACP grants for Authenticated Users', () => { 159 | const expectedGrant = { 160 | AccessControlPolicy: { 161 | Grants: expect.arrayContaining([grantWriteAcpAuthenticatedUsers]), 162 | Owner: expect.any(Object) 163 | } 164 | } 165 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 166 | }) 167 | 168 | it('should keep READ grants for All Users', () => { 169 | const expectedGrant = { 170 | AccessControlPolicy: { 171 | Grants: expect.arrayContaining([grantReadAllUsers]), 172 | Owner: expect.any(Object) 173 | } 174 | } 175 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 176 | }) 177 | 178 | it('should keep READ_ACP grants for All Users', () => { 179 | const expectedGrant = { 180 | AccessControlPolicy: { 181 | Grants: expect.arrayContaining([grantReadAcpAllUsers]), 182 | Owner: expect.any(Object) 183 | } 184 | } 185 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 186 | }) 187 | 188 | it('should keep WRITE grants for All Users', () => { 189 | const expectedGrant = { 190 | AccessControlPolicy: { 191 | Grants: expect.arrayContaining([grantWriteAllUsers]), 192 | Owner: expect.any(Object) 193 | } 194 | } 195 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 196 | }) 197 | 198 | it('should keep WRITE_ACP grants for All Users', () => { 199 | const expectedGrant = { 200 | AccessControlPolicy: { 201 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 202 | Owner: expect.any(Object) 203 | } 204 | } 205 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 206 | }) 207 | 208 | it('should keep FULL_CONTROL grants for All Users', () => { 209 | const expectedGrant = { 210 | AccessControlPolicy: { 211 | Grants: expect.arrayContaining([grantFullControlAllUsers]), 212 | Owner: expect.any(Object) 213 | } 214 | } 215 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 216 | }) 217 | 218 | it('should keep FULL_CONTROL grants for Canonical User', () => { 219 | const expectedGrant = { 220 | AccessControlPolicy: { 221 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 222 | Owner: expect.any(Object) 223 | } 224 | } 225 | expect(mockS3).toHaveReceivedCommandWith(PutBucketAclCommand, expectedGrant) 226 | }) 227 | }) 228 | 229 | describe('invalid invocation', () => { 230 | it('should fail when event is undefined', async () => { 231 | await expect(source.handler(undefined)) 232 | .rejects 233 | .toThrow('Invalid event') 234 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 235 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 236 | }) 237 | 238 | it('should fail when "resource" missing from the event', async () => { 239 | const malformedEvent = { 240 | ruleId: 'S3-006' 241 | } 242 | await expect(source.handler(malformedEvent)) 243 | .rejects 244 | .toThrow('Invalid event') 245 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 246 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 247 | }) 248 | 249 | it('should fail when the incorrect rule is received', async () => { 250 | const malformedEvent = { 251 | resource: 'sample-bucket', 252 | ruleId: 'S3-00x' 253 | } 254 | await expect(source.handler(malformedEvent)) 255 | .rejects 256 | .toThrow('Invalid event') 257 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 258 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 259 | }) 260 | }) 261 | }) 262 | -------------------------------------------------------------------------------- /test/AutoRemediateS3-007.test.js: -------------------------------------------------------------------------------- 1 | const source = require('../functions/AutoRemediateS3-007') 2 | const { 3 | S3Client, 4 | GetBucketAclCommand, 5 | PutBucketAclCommand 6 | } = require('@aws-sdk/client-s3') 7 | const { mockClient } = require('aws-sdk-client-mock') 8 | // eslint-disable-next-line node/no-extraneous-require 9 | require('aws-sdk-client-mock-jest') 10 | 11 | const sampleEvent = { 12 | id: 'ccc:HJzFMHchx:S3-007:S3:ap-southeast-2:sample-bucket', 13 | organisationId: 'some-organisation', 14 | accountId: 'abcdef', 15 | ruleId: 'S3-007', 16 | ruleTitle: "S3 Bucket Authenticated Users 'READ_ACP' Access", 17 | service: 'S3', 18 | region: 'ap-southeast-2', 19 | riskLevel: 'VERY_HIGH', 20 | categories: ['security'], 21 | compliances: ['AWAF'], 22 | message: 'Bucket sample-bucket allows authenticated users READ_ACP access.', 23 | resource: 'sample-bucket', 24 | status: 'FAILURE', 25 | statusRiskLevel: 'FAILURE:1', 26 | lastUpdatedDate: null, 27 | lastUpdatedBy: 'SYSTEM', 28 | resolvedBy: 'SYSTEM', 29 | eventId: 'Skzp7ra1WW', 30 | ccrn: 'ccrn:aws:HJzFMHchx:S3:global:sample-bucket', 31 | tags: [], 32 | cost: '0', 33 | waste: '0', 34 | lastModifiedDate: '1511060191925', 35 | lastModifiedBy: 'SYSTEM' 36 | } 37 | 38 | describe('S3-007 AutoRemediation', () => { 39 | const grantReadAcpAllUsers = { 40 | Grantee: { 41 | Type: 'Group', 42 | URI: 'http://acs.amazonaws.com/groups/global/AllUsers' 43 | }, 44 | Permission: 'READ_ACP' 45 | } 46 | const grantReadAllUsers = { 47 | Grantee: { 48 | Type: 'Group', 49 | URI: 'http://acs.amazonaws.com/groups/global/AllUsers' 50 | }, 51 | Permission: 'READ' 52 | } 53 | const grantWriteAcpAllUsers = { 54 | Grantee: { 55 | Type: 'Group', 56 | URI: 'http://acs.amazonaws.com/groups/global/AllUsers' 57 | }, 58 | Permission: 'WRITE_ACP' 59 | } 60 | const grantWriteAllUsers = { 61 | Grantee: { 62 | Type: 'Group', 63 | URI: 'http://acs.amazonaws.com/groups/global/AllUsers' 64 | }, 65 | Permission: 'WRITE' 66 | } 67 | const grantFullControlAllUsers = { 68 | Grantee: { 69 | Type: 'Group', 70 | URI: 'http://acs.amazonaws.com/groups/global/AllUsers' 71 | }, 72 | Permission: 'FULL_CONTROL' 73 | } 74 | const grantReadAcpAuthenticatedUsers = { 75 | Grantee: { 76 | Type: 'Group', 77 | URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' 78 | }, 79 | Permission: 'READ_ACP' 80 | } 81 | const grantReadAuthenticatedUsers = { 82 | Grantee: { 83 | Type: 'Group', 84 | URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' 85 | }, 86 | Permission: 'READ' 87 | } 88 | const grantWriteAcpAuthenticatedUsers = { 89 | Grantee: { 90 | Type: 'Group', 91 | URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' 92 | }, 93 | Permission: 'WRITE_ACP' 94 | } 95 | const grantWriteAuthenticatedUsers = { 96 | Grantee: { 97 | Type: 'Group', 98 | URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' 99 | }, 100 | Permission: 'WRITE' 101 | } 102 | const grantFullControlAuthenticatedUsers = { 103 | Grantee: { 104 | Type: 'Group', 105 | URI: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' 106 | }, 107 | Permission: 'FULL_CONTROL' 108 | } 109 | const grantFullControlCanonicalUser = { 110 | Grantee: { 111 | DisplayName: 'user_name', 112 | ID: 'account_user_id123455667890abcdef', 113 | Type: 'CanonicalUser' 114 | }, 115 | Permission: 'FULL_CONTROL' 116 | } 117 | 118 | const mockS3 = mockClient(S3Client) 119 | 120 | afterEach(() => { 121 | mockS3.reset() 122 | }) 123 | 124 | describe('valid invocation', () => { 125 | beforeEach(async () => { 126 | mockS3.on(GetBucketAclCommand).resolves({ 127 | Owner: { 128 | DisplayName: 'user_name', 129 | ID: 'account_user_id123455667890abcdef' 130 | }, 131 | Grants: [ 132 | { 133 | Grantee: { 134 | DisplayName: 'user_name', 135 | ID: 'account_user_id123455667890abcdef', 136 | Type: 'CanonicalUser' 137 | }, 138 | Permission: 'FULL_CONTROL' 139 | }, 140 | grantReadAllUsers, 141 | grantReadAcpAllUsers, 142 | grantWriteAllUsers, 143 | grantWriteAcpAllUsers, 144 | grantFullControlAllUsers, 145 | grantReadAuthenticatedUsers, 146 | grantReadAcpAuthenticatedUsers, 147 | grantWriteAuthenticatedUsers, 148 | grantWriteAcpAuthenticatedUsers, 149 | grantFullControlAuthenticatedUsers, 150 | grantFullControlCanonicalUser 151 | ] 152 | }) 153 | 154 | await source.handler(sampleEvent) 155 | }) 156 | 157 | it('should get the correct bucket ACL from S3', () => { 158 | const expectedParams = { 159 | Bucket: 'sample-bucket' 160 | } 161 | expect(mockS3).toHaveReceivedCommandWith( 162 | GetBucketAclCommand, 163 | expectedParams 164 | ) 165 | }) 166 | 167 | it('should set a new ACL on the affected bucket', () => { 168 | const expectedParams = { 169 | Bucket: 'sample-bucket', 170 | AccessControlPolicy: expect.any(Object) 171 | } 172 | expect(mockS3).toHaveReceivedCommandWith( 173 | PutBucketAclCommand, 174 | expectedParams 175 | ) 176 | }) 177 | 178 | it('should remove READ_ACP grants for Authenticated Users', () => { 179 | expect(mockS3).not.toHaveReceivedCommandWith( 180 | PutBucketAclCommand, 181 | grantReadAcpAuthenticatedUsers 182 | ) 183 | }) 184 | 185 | it('should keep READ grants for Authenticated Users', () => { 186 | const expectedGrant = { 187 | AccessControlPolicy: { 188 | Grants: expect.arrayContaining([grantReadAuthenticatedUsers]), 189 | Owner: expect.any(Object) 190 | } 191 | } 192 | expect(mockS3).toHaveReceivedCommandWith( 193 | PutBucketAclCommand, 194 | expectedGrant 195 | ) 196 | }) 197 | 198 | it('should keep WRITE grants for Authenticated Users', () => { 199 | const expectedGrant = { 200 | AccessControlPolicy: { 201 | Grants: expect.arrayContaining([grantWriteAuthenticatedUsers]), 202 | Owner: expect.any(Object) 203 | } 204 | } 205 | expect(mockS3).toHaveReceivedCommandWith( 206 | PutBucketAclCommand, 207 | expectedGrant 208 | ) 209 | }) 210 | 211 | it('should keep WRITE_ACP grants for Authenticated Users', () => { 212 | const expectedGrant = { 213 | AccessControlPolicy: { 214 | Grants: expect.arrayContaining([grantWriteAcpAuthenticatedUsers]), 215 | Owner: expect.any(Object) 216 | } 217 | } 218 | expect(mockS3).toHaveReceivedCommandWith( 219 | PutBucketAclCommand, 220 | expectedGrant 221 | ) 222 | }) 223 | 224 | it('should keep FULL_CONTROL grants for Authenticated Users', () => { 225 | const expectedGrant = { 226 | AccessControlPolicy: { 227 | Grants: expect.arrayContaining([grantFullControlAuthenticatedUsers]), 228 | Owner: expect.any(Object) 229 | } 230 | } 231 | expect(mockS3).toHaveReceivedCommandWith( 232 | PutBucketAclCommand, 233 | expectedGrant 234 | ) 235 | }) 236 | 237 | it('should keep READ grants for All Users', () => { 238 | const expectedGrant = { 239 | AccessControlPolicy: { 240 | Grants: expect.arrayContaining([grantReadAllUsers]), 241 | Owner: expect.any(Object) 242 | } 243 | } 244 | expect(mockS3).toHaveReceivedCommandWith( 245 | PutBucketAclCommand, 246 | expectedGrant 247 | ) 248 | }) 249 | 250 | it('should keep READ_ACP grants for All Users', () => { 251 | const expectedGrant = { 252 | AccessControlPolicy: { 253 | Grants: expect.arrayContaining([grantReadAcpAllUsers]), 254 | Owner: expect.any(Object) 255 | } 256 | } 257 | expect(mockS3).toHaveReceivedCommandWith( 258 | PutBucketAclCommand, 259 | expectedGrant 260 | ) 261 | }) 262 | 263 | it('should keep WRITE grants for All Users', () => { 264 | const expectedGrant = { 265 | AccessControlPolicy: { 266 | Grants: expect.arrayContaining([grantWriteAllUsers]), 267 | Owner: expect.any(Object) 268 | } 269 | } 270 | expect(mockS3).toHaveReceivedCommandWith( 271 | PutBucketAclCommand, 272 | expectedGrant 273 | ) 274 | }) 275 | 276 | it('should keep WRITE_ACP grants for All Users', () => { 277 | const expectedGrant = { 278 | AccessControlPolicy: { 279 | Grants: expect.arrayContaining([grantWriteAcpAllUsers]), 280 | Owner: expect.any(Object) 281 | } 282 | } 283 | expect(mockS3).toHaveReceivedCommandWith( 284 | PutBucketAclCommand, 285 | expectedGrant 286 | ) 287 | }) 288 | 289 | it('should keep FULL_CONTROL grants for All Users', () => { 290 | const expectedGrant = { 291 | AccessControlPolicy: { 292 | Grants: expect.arrayContaining([grantFullControlAllUsers]), 293 | Owner: expect.any(Object) 294 | } 295 | } 296 | expect(mockS3).toHaveReceivedCommandWith( 297 | PutBucketAclCommand, 298 | expectedGrant 299 | ) 300 | }) 301 | 302 | it('should keep FULL_CONTROL grants for Canonical User', () => { 303 | const expectedGrant = { 304 | AccessControlPolicy: { 305 | Grants: expect.arrayContaining([grantFullControlCanonicalUser]), 306 | Owner: expect.any(Object) 307 | } 308 | } 309 | expect(mockS3).toHaveReceivedCommandWith( 310 | PutBucketAclCommand, 311 | expectedGrant 312 | ) 313 | }) 314 | }) 315 | 316 | describe('invalid invocation', () => { 317 | it('should fail when event is undefined', async () => { 318 | await expect(source.handler(undefined)) 319 | .rejects 320 | .toThrow('Invalid event') 321 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 322 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 323 | }) 324 | 325 | it('should fail when "resource" missing from the event', async () => { 326 | const malformedEvent = { 327 | ruleId: 'S3-007' 328 | } 329 | await expect(source.handler(malformedEvent)) 330 | .rejects 331 | .toThrow('Invalid event') 332 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 333 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 334 | }) 335 | 336 | it('should fail when the incorrect rule is received', async () => { 337 | const malformedEvent = { 338 | resource: 'sample-bucket', 339 | ruleId: 'S3-00x' 340 | } 341 | await expect(source.handler(malformedEvent)) 342 | .rejects 343 | .toThrow('Invalid event') 344 | expect(mockS3).not.toHaveReceivedCommand(GetBucketAclCommand) 345 | expect(mockS3).not.toHaveReceivedCommand(PutBucketAclCommand) 346 | }) 347 | }) 348 | }) 349 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Cloud Conformity](https://www.cloudconformity.com/?utm_source=github) Auto Remediation 2 | 3 | ## Disclaimer 4 | This early "Auto Remediation" is subject to change. Cloud Conformity will use commercially reasonable efforts to support the previous version of the project. 5 | This project is provided on an ‘AS IS’ and ‘WHEN AVAILABLE’ basis. Cloud Conformity has no liability to user as a result of any changes made to their AWS infrastructure by installing this project. 6 | 7 | Auto Remediation is an MIT open-source project, actively maintained by Cloud Conformity team. 8 | 9 | ## How it works 10 | 11 | The following image shows how Cloud Conformity Auto Remediation works: 12 | 13 | ![Cloud Conformity Auto Remediation](images/how-it-works.png) 14 | 15 | Here's an example: 16 | 17 | 1. A user makes an S3 bucket publicly readable via S3 Access Control Lists (ACLs) 18 | 2. Cloud Conformity identifies the risk in real-time 19 | 3. Cloud Conformity publishes a message to the specified SNS Topic 20 | 3. SNS topic triggers the Orchestrator lambda function which in turns calls S3 bucket auto-remediate function 21 | 4. S3 BucketPublicReadAccess Auto Remediate Function (AutoRemediateS3-001) updates the S3 bucket ACL and closes the security gap 22 | 23 | 24 | ## Prerequisites 25 | 1. Install [Node.js](https://nodejs.org/en/) v18 or later. 26 | 27 | ## Installation 28 | > Note that you need to follow the [Deleting a stack on the AWS CloudFormation console](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-console-delete-stack.html) to delete the current stack if you plan to update to the latest version. 29 | 30 | 31 | 1. Create a working copy of "Cloud Conformity Auto Remediation" repository by running the following command: 32 | ```bash 33 | git clone https://github.com/cloudconformity/auto-remediate.git 34 | ``` 35 | 2. Change directory to auto-remediation: 36 | 37 | ```bash 38 | cd auto-remediate 39 | ``` 40 | 3. `IMPORTANT` Update `functions/config.json` with required configurations. Please note that all the rules in config file are disabled by default to prevent unwanted changes. User needs to enable the ones they need manually. 41 | 4. `IMPORTANT` Make any other necessary adjustments before deployment. 42 | 5. Run `npm install` before deploying so that the node_modules folder would be available to AWS 43 | 44 | ```bash 45 | npm install 46 | ``` 47 | 6. Finally deploy 48 | 49 | ```bash 50 | npx serverless deploy --region us-west-2 51 | ``` 52 | 53 | ## Message Format 54 | 55 | The table below gives more information about SNS Message format: 56 | 57 | | Name | Values | 58 | | ------------- | ------------- | 59 | | organisationId | Your Cloud Conformity Organisation Id | 60 | | accountId | Your Cloud Conformity Account Id | 61 | | accountName | Your Cloud Conformity Account Name | 62 | | service | AutoScaling \| CloudFormation \| CloudFront \| CloudTrail \| CloudWatch \| CloudWatchEvents \|
CloudWatchLogs \| Config \| DynamoDB \| EBS \| EC2 \| ElastiCache \| Elasticsearch \| ELB \| IAM \|
KMS \| RDS \| Redshift \| ResourceGroup \| Route53 \| S3 \| SES \| SNS \| SQS \| VPC \| WAF \|
ACM \| Inspector \| TrustedAdvisor \| Shield \| EMR \| Lambda \| Support \| Organizations \|
Kinesis \| EFS

For more information about services, please refer to [Cloud Conformity Services Endpoint](https://us-west-2.cloudconformity.com/v1/services) | 63 | | region | global \| us-east-2 \| us-east-1 \| us-west-1 \| us-west-2 \| ap-south-1 \| ap-northeast-2 \|
ap-southeast-1 \| ap-southeast-2 \| ap-northeast-1 \| ca-central-1 \| eu-central-1 \| eu-west-1 \|
eu-west-2 \| sa-east-1

For more information about regions, please refer to [Cloud Conformity Region Endpoint](https://us-west-2.cloudconformity.com/v1/regions) | 64 | | id | Check Id | 65 | | resource | AWS Resource | 66 | | ccrn | Cloud Conformity Resource Name | 67 | | ruleId | e.g. S3-001

For more information about rules, please refer to [Cloud Conformity Services Endpoint](https://us-west-2.cloudconformity.com/v1/services) | 68 | | ruleTitle | e.g. BucketPublicReadAccess

For more information about rules, please refer to [Cloud Conformity Services Endpoint](https://us-west-2.cloudconformity.com/v1/services) | 69 | | statuses | SUCCESS \| FAILURE | 70 | | categories | security \| cost-optimisation \| reliability \| performance-efficiency \| operational-excellence

For more information about categories, please refer to [Cloud Conformity Services Endpoint](https://us-west-2.cloudconformity.com/v1/services) | 71 | | riskLevels | LOW\| MEDIUM \| HIGH \| VERY_HIGH \| EXTREME

For more information about risk levels, please refer to [Cloud Conformity Services Endpoint](https://us-west-2.cloudconformity.com/v1/services) | 72 | | message | e.g. Bucket my-bucket-name allows public 'READ' access | 73 | | createdDate | The date when the check was created

The numeric value of the specified date as the number of milliseconds since January 1, 1970, 00:00:00 UTC | 74 | | failureDiscoveryDate | The date when the check the failure was discovered

The numeric value of the specified date as the number of milliseconds since January 1, 1970, 00:00:00 UTC | 75 | | tags | Any assigned metadata tags to your AWS resources | 76 | 77 | 78 | ## Auto Remediate Functions 79 | 80 | The table below lists the supported auto auto-remediate functions: 81 | 82 | | Service | Rule Id | Description | 83 | | ------------- | ------------- | ------------- | 84 | | CloudFormation | [CFM-005](https://www.cloudconformity.com/knowledge-base/aws/CloudFormation/stack-termination-protection.html) | Ensure Termination Protection feature is enabled for your AWS CloudFormation stacks | 85 | | Config | [Config-001](https://www.cloudconformity.com/knowledge-base/aws/Config/aws-config-enabled.html) | Ensure AWS Config is enabled in all regions | 86 | | S3 | [S3-001](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-public-read-access.html) | Ensure S3 buckets do not allow public READ access | 87 | | S3 | [S3-002](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-public-read-acp-access.html) | Ensure S3 buckets do not allow public READ_ACP access | 88 | | S3 | [S3-003](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-public-write-access.html) | Ensure S3 buckets do not allow public WRITE access | 89 | | S3 | [S3-004](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-public-write-acp-access.html) | Ensure S3 buckets do not allow public WRITE_ACP access | 90 | | S3 | [S3-005](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-public-full-control-access.html) | Ensure S3 buckets do not allow public FULL_CONTROL access | 91 | | S3 | [S3-006](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-authenticated-users-read-access.html) | Ensure S3 buckets do not allow authenticated users READ access | 92 | | S3 | [S3-007](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-authenticated-users-read-acp-access.html) | Ensure S3 buckets do not allow authenticated users READ_ACP access | 93 | | S3 | [S3-008](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-authenticated-users-write-access.html) | Ensure S3 buckets do not allow authenticated users WRITE access | 94 | | S3 | [S3-009](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-authenticated-users-write-acp-access.html) | Ensure S3 buckets do not allow authenticated users WRITE_ACP access | 95 | | S3 | [S3-010](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-authenticated-users-full-control-access.html) | Ensure S3 buckets do not allow authenticated users FULL_CONTROL access | 96 | | S3 | [S3-012](https://www.cloudconformity.com/knowledge-base/aws/S3/s3-bucket-versioning-enabled.html) | Enable versioning for AWS S3 buckets | 97 | | RDS | [RDS-008](https://www.cloudconformity.com/knowledge-base/aws/RDS/rds-publicly-accessible.html) | Ensure RDS instances are not public facing to minimise security risks | 98 | | CloudTrail | [CT-001](https://www.cloudconformity.com/knowledge-base/aws/CloudTrail/cloudtrail-enabled.html) | Ensure CloudTrail API logging is activated for all Regions | 99 | | Redshift | [RS-001](https://www.cloudconformity.com/knowledge-base/aws/Redshift/redshift-cluster-publicly-accessible.html) | Ensure Redshift clusters are not publicly accessible to minimise security risks | 100 | | IAM | [IAM-001](https://www.cloudconformity.com/knowledge-base/aws/IAM/access-keys-rotated-30-days.html) | Ensure that all your IAM user access keys are rotated every month | 101 | | IAM | [IAM-029](https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/IAM/unused-iam-user.html) | Identify and remove any unused AWS IAM users | 102 | | EC2 | [EC2-002](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-ssh-access.html) | Ensure that there is no unrestricted access through TCP port 22 from the selected EC2 security group | 103 | | EC2 | [EC2-003](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-rdp-access.html) | Ensure that there is no unrestricted access through TCP port 3389 (which is used for MS Remote Desktop Protocol) from the selected EC2 security group | 104 | | EC2 | [EC2-004](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-oracle-access.html) | Ensure that there is no unrestricted access through TCP port 1521 (which is used by Oracle Database Server) from the selected EC2 security group | 105 | | EC2 | [EC2-005](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-mysql-access.html) | Ensure that there is no unrestricted access through TCP port 3306 (which is used by MYSQL Database Server) from the selected EC2 security group | 106 | | EC2 | [EC2-006](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-postgresql-access.html) | Ensure that there is no unrestricted access through TCP port 5432 (which is used by PostgreSQL Database Server) from the selected EC2 security group | 107 | | EC2 | [EC2-008](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-mssql-access.html) | Ensure that there is no unrestricted access through TCP port 1433 (which is used by Microsoft MYSQL Database Server) from the selected EC2 security group | 108 | | EC2 | [EC2-038](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-telnet-access.html) | Ensure that there is no unrestricted access through TCP port 23 (which is used by Telnet) from the selected EC2 security group | 109 | | EC2 | [EC2-039](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-smtp-access.html) | Ensure that there is no unrestricted access through TCP port 25 (which is used by SMTP) from the selected EC2 security group | 110 | | EC2 | [EC2-040](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-rcp-access.html) | Ensure that there is no unrestricted access through TCP port 135 (which is used client/server communication by Microsoft Message Queuing, as well as other Microsoft Windows/Windows Server software.) from the selected EC2 security group | 111 | | EC2 | [EC2-043](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-cifs-access.html) | Ensure that there is no unrestricted access through TCP port 445 (which is used by CIFS for file/printer sharing and other network communications) from the selected EC2 security group | 112 | | EC2 | [EC2-045](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-mongodb-access.html) | Ensure that there is no unrestricted access through TCP port 27017 (which is used by MongoDB database) from the selected EC2 security group | 113 | | Kinesis | [Kinesis-001](https://www.cloudconformity.com/knowledge-base/aws/Kinesis/server-side-encryption.html) | Ensure that your AWS Kinesis streams are encrypted using Server-Side Encryption (SSE) in order to meet strict regulatory requirements and improve the security of your data at rest. | 114 | | SQS | [SQS-004](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-mongodb-access.html) | Ensure that your Amazon Simple Queue Service (SQS) queues are protecting the contents of their messages using Server-Side Encryption (SSE). | 115 | | EC2 | [EC2-045](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-mongodb-access.html) | Ensure that there is no unrestricted access through TCP port 27017 (which is used by MongoDB database) from the selected EC2 security group | 116 | | IAM | [IAM-038](https://www.cloudconformity.com/knowledge-base/aws/IAM/access-keys-rotated-90-days.html) | Ensure that all your IAM user access keys are rotated every month in order to decrease the likelihood of accidental exposures and protect your AWS resources against unauthorized access | 117 | | VPC | [VPC-001](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-cifs-access.html) | Ensure that Flow Logs feature is Enabled for your account to capture network traffic data to and from your Virtual Private Cloud (VPC) | 118 | | EC2 | [EC2-019](https://www.cloudconformity.com/knowledge-base/aws/EC2/publicly-shared-ami.html) | Ensure that your AWS AMIs are not publicly shared with the other AWS accounts in order to avoid exposing sensitive data. | 119 | | CloudTrail | [CT-003](https://www.cloudconformity.com/knowledge-base/aws/CloudTrail/cloudtrail-bucket-publicly-accessible.html) | Ensure there are not any AWS CloudTrail logging buckets are publicly accessible, in order to determine if your AWS account could be at risk. | 120 | | RDS | [RDS-006](https://www.cloudconformity.com/knowledge-base/aws/RDS/rds-auto-minor-version-upgrade.html) | Ensure that your RDS database instances have the Auto Minor Version Upgrade flag enabled in order to receive automatically minor engine upgrades during the specified maintenance window | 121 | | EBS | [EBS-009](https://www.cloudconformity.com/knowledge-base/aws/EBS/public-snapshots.html) | Ensure that your AWS Elastic Block Store (EBS) volume snapshots are not public (i.e. publicly shared with other AWS accounts) in order to avoid exposing personal and sensitive data. | 122 | | RDS | [RDS-023](https://www.cloudconformity.com/knowledge-base/aws/RDS/public-snapshots.html) | Ensure that your AWS Relational Database Service (RDS) database snapshots are not publicly accessible (i.e. shared with all AWS accounts and users) in order to avoid exposing your private data. | 123 | | KMS | [KMS-004](https://www.cloudconformity.com/knowledge-base/aws/KMS/kms-customer-master-key-pending-deletion.html) | Identify any disabled AWS KMS Customer Master Keys (CMK) that have been accidentally or intentionally scheduled for deletion in order to prevent losing any data encrypted with these keys. | 124 | | RedShift | [RS-023](https://www.cloudconformity.com/knowledge-base/aws/Redshift/user-activity-log.html) |Ensure that user activity logging is enabled for your AWS Redshift clusters in order to log each query before it is performed on the clusters database. | 125 | | GuardDuty | [GD-001](https://www.cloudconformity.com/knowledge-base/aws/GuardDuty/guardduty-enabled.html) | Ensure that Amazon GuardDuty service is currently enabled in order to protect your AWS environment and infrastructure | 126 | | Organizations | [Organizations-002](https://www.cloudconformity.com/knowledge-base/aws/Organizations/all-features.html) | Ensure that All Features is enabled within your Amazon Organizations to achieve full control over the use of AWS services and actions across multiple AWS accounts using Service Control Policies (SCPs). | 127 | | Lambda | [Lambda-003](https://www.cloudconformity.com/knowledge-base/aws/Lambda/tracing.html) | Ensure that tracing is enabled for your AWS Lambda functions in order to gain visibility into the functions execution and performance. | 128 | | S3 | [S3-016](https://www.cloudconformity.com/knowledge-base/aws/S3/server-side-encryption.html) | Ensure that your AWS S3 buckets are protecting their sensitive data at rest by enforcing Server-Side Encryption | 129 | | S3 | [S3-014](https://www.cloudconformity.com/knowledge-base/aws/EC2/unrestricted-mongodb-access.html) | Ensure that your AWS S3 buckets are not publicly accessible via bucket policies in order to protect against unauthorized access. | 130 | | TrustedAdvisor | [TrustedAdvisor-003](https://www.cloudconformity.com/knowledge-base/aws/TrustedAdvisor/exposed-access-keys.html) | Ensure that there are not any exposed Amazon IAM access keys in order to protect your AWS resources against unapproved access | 131 | | KMS | [KMS-002](https://www.cloudconformity.com/knowledge-base/aws/KMS/key-rotation-enabled.html) | Ensure that the KMS Key Rotation is Enabled which allows you to set an yearly rotation schedule for your CMK | 132 | | RedShift | [RS-019](https://www.cloudconformity.com/knowledge-base/aws/Redshift/automated-snapshot-retention-period.html) | Ensure that the automated snapshot retention period set for your AWS Redshift clusters is a positive number, meaning that automated backups are enabled for the clusters | 133 | 134 | 135 | 136 | For more information about `Rule Id`, please refer to [Cloud Conformity Services Endpoint](https://us-west-2.cloudconformity.com/v1/services) 137 | 138 | Note: if you want to exclude auto-remediate resources for certain rules, you can use Conformity Profile, for example, `exclude-rules.json`. The example profile provides a list of rules to exlude low risk rules or rules that require extra resources or configurations. 139 | 140 | ## How to contribute 141 | 142 | You are welcome to contribute to "Cloud Conformity Auto Remediation" 143 | 144 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project 145 | 2. Make a well commented and clean commit to the repository 146 | 3. Create a [pull request](https://help.github.com/articles/about-pull-requests/) 147 | 148 | ## Styleguide 149 | 150 | The styleguide used for this project is 151 | [Standard](https://standardjs.com/) forced by [eslint](https://github.com/eslint/eslint). 152 | To run eslint, you only need to run `npm run lint [filename]` or to 153 | run for the whole project, run `npm run lint '**/*.js'`. 154 | Refer to package.json for more info. 155 | 156 | The main rules are as follows: 157 | * 2 spaces – for indentation 158 | * Single quotes for strings – except to avoid escaping 159 | * No unused variables – this one catches tons of bugs! 160 | * No semicolons – It's fine. Really! 161 | * Never start a line with (, [, or ` 162 | * Space after keywords if (condition) { ... } 163 | * Space after function name function name (arg) { ... } 164 | * Always use === instead of == – but obj == null is allowed to check null || undefined. 165 | * Always handle the node.js err function parameter 166 | * Always prefix browser globals with window – except document and navigator are okay 167 | 168 | ## About 169 | 170 | Protect, Detect, Correct. The most complete solution to avoid critical threats and vulnerabilities in your AWS environments. Awarded both AWS Cloud Management Tools Competency and Security Partner Competency, Cloud Conformity’s security and optimization platform delivers continuous assurance that your infrastructure is risk-free and compliant as your cloud presence grows. 171 | --------------------------------------------------------------------------------