diff --git a/package.json b/package.json index 9c80924..5c86b59 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "@aws-sdk/client-rds": "^3.716.0", "@aws-sdk/client-s3": "^3.717.0", "@aws-sdk/client-s3-control": "^3.716.0", + "@aws-sdk/client-secrets-manager": "^3.716.0", + "@aws-sdk/client-securityhub": "^3.716.0", + "@aws-sdk/client-sns": "^3.716.0", "@aws-sdk/client-ssm": "^3.716.0", "@aws-sdk/client-sts": "^3.716.0", "@aws-sdk/client-wafv2": "^3.716.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c56390f..994f0bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,15 @@ importers: '@aws-sdk/client-s3-control': specifier: ^3.716.0 version: 3.716.0 + '@aws-sdk/client-secrets-manager': + specifier: ^3.716.0 + version: 3.716.0 + '@aws-sdk/client-securityhub': + specifier: ^3.716.0 + version: 3.716.0 + '@aws-sdk/client-sns': + specifier: ^3.716.0 + version: 3.716.0 '@aws-sdk/client-ssm': specifier: ^3.716.0 version: 3.716.0 @@ -222,6 +231,18 @@ packages: resolution: {integrity: sha512-jzaH8IskAXVnqlZ3/H/ROwrB2HCnq/atlN7Hi7FIfjWvMPf5nfcJKfzJ1MXFX0EQR5qO6X4TbK7rgi7Bjw9NjQ==} engines: {node: '>=16.0.0'} + '@aws-sdk/client-secrets-manager@3.716.0': + resolution: {integrity: sha512-j2JboOSR3PMoT5msr4uIMwkIm1owzkqgWI8i40IPDa1oeJXmZIx/xkCQq6Hxu5Ve1b2xtrw/8k1LN+TMCvuIfA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/client-securityhub@3.716.0': + resolution: {integrity: sha512-/xGd2NQd7CtUDquRZvXbIDcMoBKaJmvwnjujVnmwth/6yC0gAVsBp6yeBSHOszI6L4mMXmSQ2iBglgnexpVlDQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/client-sns@3.716.0': + resolution: {integrity: sha512-Qg7EqlS83yiHRpfbhlyanRQ5aKmj1M8K7OJcDYgjI8FNf4YlS/YKJkv02SQVdW0lFLT2adceUXBjdAQXlbXC7g==} + engines: {node: '>=16.0.0'} + '@aws-sdk/client-ssm@3.716.0': resolution: {integrity: sha512-da2wTUBCLGRoQf5Ahm/LuUIR/OkQ09kaX7yYRC2Vw+TOcMXbozJSzXbm99SXsOL4u8a8PRq+Vwfptc36e18Feg==} engines: {node: '>=16.0.0'} @@ -2237,6 +2258,146 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-secrets-manager@3.716.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/client-sts': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/credential-provider-node': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/middleware-host-header': 3.714.0 + '@aws-sdk/middleware-logger': 3.714.0 + '@aws-sdk/middleware-recursion-detection': 3.714.0 + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/region-config-resolver': 3.714.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@aws-sdk/util-user-agent-browser': 3.714.0 + '@aws-sdk/util-user-agent-node': 3.716.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.6 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.6 + '@smithy/middleware-retry': 3.0.31 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.1 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.31 + '@smithy/util-defaults-mode-node': 3.0.31 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + '@types/uuid': 9.0.8 + tslib: 2.8.1 + uuid: 9.0.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-securityhub@3.716.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/client-sts': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/credential-provider-node': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/middleware-host-header': 3.714.0 + '@aws-sdk/middleware-logger': 3.714.0 + '@aws-sdk/middleware-recursion-detection': 3.714.0 + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/region-config-resolver': 3.714.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@aws-sdk/util-user-agent-browser': 3.714.0 + '@aws-sdk/util-user-agent-node': 3.716.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.6 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.6 + '@smithy/middleware-retry': 3.0.31 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.1 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.31 + '@smithy/util-defaults-mode-node': 3.0.31 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sns@3.716.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/client-sts': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/credential-provider-node': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/middleware-host-header': 3.714.0 + '@aws-sdk/middleware-logger': 3.714.0 + '@aws-sdk/middleware-recursion-detection': 3.714.0 + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/region-config-resolver': 3.714.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@aws-sdk/util-user-agent-browser': 3.714.0 + '@aws-sdk/util-user-agent-node': 3.716.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.6 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.6 + '@smithy/middleware-retry': 3.0.31 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.1 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.31 + '@smithy/util-defaults-mode-node': 3.0.31 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/client-ssm@3.716.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -2703,7 +2864,7 @@ snapshots: '@aws-sdk/core': 3.716.0 '@aws-sdk/types': 3.714.0 '@aws-sdk/util-endpoints': 3.714.0 - '@smithy/core': 2.5.5 + '@smithy/core': 2.5.6 '@smithy/protocol-http': 4.1.8 '@smithy/types': 3.7.2 tslib: 2.8.1 diff --git a/src/bpsets/BPSet.ts b/src/bpsets/BPSet.ts index 77c8665..0e1cb8c 100644 --- a/src/bpsets/BPSet.ts +++ b/src/bpsets/BPSet.ts @@ -2,7 +2,15 @@ export interface BPSet { check: () => Promise<{ compliantResources: string[] nonCompliantResources: string[] - requiredParametersForFix: {name: string}[] + requiredParametersForFix: { + name: string + }[] }>, - fix: (nonCompliantResources: string[], requiredParametersForFix: {name: string, value: string}[]) => Promise + fix: ( + nonCompliantResources: string[], + requiredParametersForFix: { + name: string, + value: string + }[] + ) => Promise } diff --git a/src/bpsets/secretsmanager/SecretsManagerRotationEnabledCheck.ts b/src/bpsets/secretsmanager/SecretsManagerRotationEnabledCheck.ts new file mode 100644 index 0000000..54ed7a7 --- /dev/null +++ b/src/bpsets/secretsmanager/SecretsManagerRotationEnabledCheck.ts @@ -0,0 +1,48 @@ +import { + SecretsManagerClient, + ListSecretsCommand, + RotateSecretCommand, + UpdateSecretCommand +} from '@aws-sdk/client-secrets-manager' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class SecretsManagerRotationEnabledCheck implements BPSet { + private readonly client = new SecretsManagerClient({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getSecrets = async () => { + const response = await this.memoClient.send(new ListSecretsCommand({})) + return response.SecretList || [] + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + const secrets = await this.getSecrets() + + for (const secret of secrets) { + if (secret.RotationEnabled) { + compliantResources.push(secret.ARN!) + } else { + nonCompliantResources.push(secret.ARN!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const arn of nonCompliantResources) { + await this.client.send( + new RotateSecretCommand({ + SecretId: arn + }) + ) + } + } +} diff --git a/src/bpsets/secretsmanager/SecretsManagerScheduledRotationSuccessCheck.ts b/src/bpsets/secretsmanager/SecretsManagerScheduledRotationSuccessCheck.ts new file mode 100644 index 0000000..8052656 --- /dev/null +++ b/src/bpsets/secretsmanager/SecretsManagerScheduledRotationSuccessCheck.ts @@ -0,0 +1,55 @@ +import { + SecretsManagerClient, + ListSecretsCommand, + RotateSecretCommand +} from '@aws-sdk/client-secrets-manager' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class SecretsManagerScheduledRotationSuccessCheck implements BPSet { + private readonly client = new SecretsManagerClient({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getSecrets = async () => { + const response = await this.memoClient.send(new ListSecretsCommand({})) + return response.SecretList || [] + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + const secrets = await this.getSecrets() + + for (const secret of secrets) { + if (secret.RotationEnabled) { + const now = new Date() + const lastRotated = secret.LastRotatedDate ? new Date(secret.LastRotatedDate) : undefined + const rotationPeriod = secret.RotationRules?.AutomaticallyAfterDays + ? secret.RotationRules.AutomaticallyAfterDays + 2 + : undefined + + if (!lastRotated || !rotationPeriod || now.getTime() - lastRotated.getTime() > rotationPeriod * 24 * 60 * 60 * 1000) { + nonCompliantResources.push(secret.ARN!) + } else { + compliantResources.push(secret.ARN!) + } + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const arn of nonCompliantResources) { + await this.client.send( + new RotateSecretCommand({ + SecretId: arn + }) + ) + } + } +} diff --git a/src/bpsets/secretsmanager/SecretsManagerSecretPeriodicRotation.ts b/src/bpsets/secretsmanager/SecretsManagerSecretPeriodicRotation.ts new file mode 100644 index 0000000..7f8faa1 --- /dev/null +++ b/src/bpsets/secretsmanager/SecretsManagerSecretPeriodicRotation.ts @@ -0,0 +1,52 @@ +import { + SecretsManagerClient, + ListSecretsCommand, + RotateSecretCommand +} from '@aws-sdk/client-secrets-manager' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class SecretsManagerSecretPeriodicRotation implements BPSet { + private readonly client = new SecretsManagerClient({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getSecrets = async () => { + const response = await this.memoClient.send(new ListSecretsCommand({})) + return response.SecretList || [] + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + const secrets = await this.getSecrets() + + for (const secret of secrets) { + if (secret.RotationEnabled) { + const now = new Date() + const lastRotated = secret.LastRotatedDate ? new Date(secret.LastRotatedDate) : undefined + + if (!lastRotated || now.getTime() - lastRotated.getTime() > 90 * 24 * 60 * 60 * 1000) { + nonCompliantResources.push(secret.ARN!) + } else { + compliantResources.push(secret.ARN!) + } + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const arn of nonCompliantResources) { + await this.client.send( + new RotateSecretCommand({ + SecretId: arn + }) + ) + } + } +} diff --git a/src/bpsets/securityhub/SecurityHubEnabled.ts b/src/bpsets/securityhub/SecurityHubEnabled.ts new file mode 100644 index 0000000..2d2e044 --- /dev/null +++ b/src/bpsets/securityhub/SecurityHubEnabled.ts @@ -0,0 +1,51 @@ +import { + SecurityHubClient, + DescribeHubCommand, + EnableSecurityHubCommand +} from '@aws-sdk/client-securityhub' +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class SecurityHubEnabled implements BPSet { + private readonly securityHubClient = new SecurityHubClient({}) + private readonly stsClient = new STSClient({}) + private readonly memoSecurityHubClient = Memorizer.memo(this.securityHubClient) + private readonly memoStsClient = Memorizer.memo(this.stsClient) + + private readonly getAWSAccountId = async () => { + const response = await this.memoStsClient.send(new GetCallerIdentityCommand({})) + return response.Account! + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + const awsAccountId = await this.getAWSAccountId() + + try { + await this.memoSecurityHubClient.send(new DescribeHubCommand({})) + compliantResources.push(awsAccountId) + } catch (error: any) { + if (error.name === 'InvalidAccessException') { + nonCompliantResources.push(awsAccountId) + } else { + throw error + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const accountId of nonCompliantResources) { + if (accountId) { + await this.securityHubClient.send(new EnableSecurityHubCommand({})) + } + } + } +} diff --git a/src/bpsets/sns/SNSEncryptedKMS.ts b/src/bpsets/sns/SNSEncryptedKMS.ts new file mode 100644 index 0000000..dcc3000 --- /dev/null +++ b/src/bpsets/sns/SNSEncryptedKMS.ts @@ -0,0 +1,69 @@ +import { + SNSClient, + ListTopicsCommand, + GetTopicAttributesCommand, + SetTopicAttributesCommand +} from '@aws-sdk/client-sns' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class SNSEncryptedKMS implements BPSet { + private readonly client = new SNSClient({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getTopics = async () => { + const topicsResponse = await this.memoClient.send(new ListTopicsCommand({})) + const topics = topicsResponse.Topics || [] + + const topicDetails = [] + for (const topic of topics) { + const attributes = await this.memoClient.send( + new GetTopicAttributesCommand({ TopicArn: topic.TopicArn! }) + ) + topicDetails.push({ ...attributes.Attributes, TopicArn: topic.TopicArn! }) + } + + return topicDetails + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + const topics = await this.getTopics() as any + + for (const topic of topics) { + if (topic.KmsMasterKeyId) { + compliantResources.push(topic.TopicArn!) + } else { + nonCompliantResources.push(topic.TopicArn!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [{ name: 'kms-key-id', value: '' }] + } + } + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value + + if (!kmsKeyId) { + throw new Error("Required parameter 'kms-key-id' is missing.") + } + + for (const arn of nonCompliantResources) { + await this.client.send( + new SetTopicAttributesCommand({ + TopicArn: arn, + AttributeName: 'KmsMasterKeyId', + AttributeValue: kmsKeyId + }) + ) + } + } +} diff --git a/src/bpsets/sns/SNSTopicMessageDeliveryNotificationEnabled.ts b/src/bpsets/sns/SNSTopicMessageDeliveryNotificationEnabled.ts new file mode 100644 index 0000000..073405f --- /dev/null +++ b/src/bpsets/sns/SNSTopicMessageDeliveryNotificationEnabled.ts @@ -0,0 +1,79 @@ +import { + SNSClient, + ListTopicsCommand, + GetTopicAttributesCommand, + SetTopicAttributesCommand +} from '@aws-sdk/client-sns' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class SNSTopicMessageDeliveryNotificationEnabled implements BPSet { + private readonly client = new SNSClient({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getTopics = async () => { + const topicsResponse = await this.memoClient.send(new ListTopicsCommand({})) + const topics = topicsResponse.Topics || [] + + const topicDetails = [] + for (const topic of topics) { + const attributes = await this.memoClient.send( + new GetTopicAttributesCommand({ TopicArn: topic.TopicArn! }) + ) + topicDetails.push({ ...attributes.Attributes, TopicArn: topic.TopicArn! }) + } + + return topicDetails + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + const topics = await this.getTopics() + + for (const topic of topics) { + const feedbackRoles = Object.keys(topic).filter(key => key.endsWith('FeedbackRoleArn')) + + if (feedbackRoles.length > 0) { + compliantResources.push(topic.TopicArn!) + } else { + nonCompliantResources.push(topic.TopicArn!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [ + { name: 'sns-feedback-role-arn', value: '' } + ] + } + } + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const feedbackRoleArn = requiredParametersForFix.find( + param => param.name === 'sns-feedback-role-arn' + )?.value + + if (!feedbackRoleArn) { + throw new Error("Required parameter 'sns-feedback-role-arn' is missing.") + } + + for (const arn of nonCompliantResources) { + await this.client.send( + new SetTopicAttributesCommand({ + TopicArn: arn, + AttributeName: 'DeliveryPolicy', + AttributeValue: JSON.stringify({ + http: { + DefaultFeedbackRoleArn: feedbackRoleArn + } + }) + }) + ) + } + } +} diff --git a/src/bpsets/vpc/EC2TransitGatewayAutoVPCAttachDisabled.ts b/src/bpsets/vpc/EC2TransitGatewayAutoVPCAttachDisabled.ts new file mode 100644 index 0000000..a1068d6 --- /dev/null +++ b/src/bpsets/vpc/EC2TransitGatewayAutoVPCAttachDisabled.ts @@ -0,0 +1,49 @@ +import { + EC2Client, + DescribeTransitGatewaysCommand, + ModifyTransitGatewayCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class EC2TransitGatewayAutoVPCAttachDisabled implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const response = await this.memoClient.send(new DescribeTransitGatewaysCommand({})) + const transitGateways = response.TransitGateways || [] + + for (const gateway of transitGateways) { + if (gateway.Options?.AutoAcceptSharedAttachments === 'enable') { + nonCompliantResources.push(gateway.TransitGatewayArn!) + } else { + compliantResources.push(gateway.TransitGatewayArn!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const arn of nonCompliantResources) { + const transitGatewayId = arn.split(':transit-gateway/')[1] + + await this.client.send( + new ModifyTransitGatewayCommand({ + TransitGatewayId: transitGatewayId, + Options: { + AutoAcceptSharedAttachments: 'disable' + } + }) + ) + } + } +} diff --git a/src/bpsets/vpc/RestrictedCommonPorts.ts b/src/bpsets/vpc/RestrictedCommonPorts.ts new file mode 100644 index 0000000..c68002b --- /dev/null +++ b/src/bpsets/vpc/RestrictedCommonPorts.ts @@ -0,0 +1,57 @@ +import { + EC2Client, + DescribeSecurityGroupRulesCommand, + RevokeSecurityGroupIngressCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class RestrictedCommonPorts implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getSecurityGroupRules = async () => { + const response = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})) + return response.SecurityGroupRules || [] + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const commonPorts = [-1, 22, 80, 3306, 3389, 5432, 6379, 11211] + const rules = await this.getSecurityGroupRules() + + for (const rule of rules) { + if ( + !rule.IsEgress && + commonPorts.includes(rule.FromPort!) && + commonPorts.includes(rule.ToPort!) && + !rule.PrefixListId + ) { + nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + } else { + compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const resource of nonCompliantResources) { + const [groupId, ruleId] = resource.split(' / ') + + await this.client.send( + new RevokeSecurityGroupIngressCommand({ + GroupId: groupId, + SecurityGroupRuleIds: [ruleId] + }) + ) + } + } +} diff --git a/src/bpsets/vpc/RestrictedSSH.ts b/src/bpsets/vpc/RestrictedSSH.ts new file mode 100644 index 0000000..c0264e7 --- /dev/null +++ b/src/bpsets/vpc/RestrictedSSH.ts @@ -0,0 +1,55 @@ +import { + EC2Client, + DescribeSecurityGroupRulesCommand, + RevokeSecurityGroupIngressCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class RestrictedSSH implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getSecurityGroupRules = async () => { + const response = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})) + return response.SecurityGroupRules || [] + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const rules = await this.getSecurityGroupRules() + for (const rule of rules) { + if ( + !rule.IsEgress && + rule.FromPort! <= 22 && + rule.ToPort! >= 22 && + rule.CidrIpv4 === '0.0.0.0/0' + ) { + nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + } else { + compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const resource of nonCompliantResources) { + const [groupId, ruleId] = resource.split(' / ') + + await this.client.send( + new RevokeSecurityGroupIngressCommand({ + GroupId: groupId, + SecurityGroupRuleIds: [ruleId] + }) + ) + } + } +} diff --git a/src/bpsets/vpc/SubnetAutoAssignPublicIPDisabled.ts b/src/bpsets/vpc/SubnetAutoAssignPublicIPDisabled.ts new file mode 100644 index 0000000..c42426f --- /dev/null +++ b/src/bpsets/vpc/SubnetAutoAssignPublicIPDisabled.ts @@ -0,0 +1,45 @@ +import { + EC2Client, + DescribeSubnetsCommand, + ModifySubnetAttributeCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class SubnetAutoAssignPublicIPDisabled implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const response = await this.memoClient.send(new DescribeSubnetsCommand({})) + const subnets = response.Subnets || [] + + for (const subnet of subnets) { + if (subnet.MapPublicIpOnLaunch) { + nonCompliantResources.push(subnet.SubnetId!) + } else { + compliantResources.push(subnet.SubnetId!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const subnetId of nonCompliantResources) { + await this.client.send( + new ModifySubnetAttributeCommand({ + SubnetId: subnetId, + MapPublicIpOnLaunch: { Value: false } + }) + ) + } + } +} diff --git a/src/bpsets/vpc/VPCDefaultSecurityGroupClosed.ts b/src/bpsets/vpc/VPCDefaultSecurityGroupClosed.ts new file mode 100644 index 0000000..49e9fb3 --- /dev/null +++ b/src/bpsets/vpc/VPCDefaultSecurityGroupClosed.ts @@ -0,0 +1,56 @@ +import { + EC2Client, + DescribeSecurityGroupsCommand, + RevokeSecurityGroupIngressCommand, + RevokeSecurityGroupEgressCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class VPCDefaultSecurityGroupClosed implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const response = await this.memoClient.send( + new DescribeSecurityGroupsCommand({ + Filters: [{ Name: 'group-name', Values: ['default'] }] + }) + ) + const securityGroups = response.SecurityGroups || [] + + for (const group of securityGroups) { + if (group.IpPermissions?.length || group.IpPermissionsEgress?.length) { + nonCompliantResources.push(group.GroupId!) + } else { + compliantResources.push(group.GroupId!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const groupId of nonCompliantResources) { + await this.client.send( + new RevokeSecurityGroupIngressCommand({ + GroupId: groupId, + IpPermissions: [] + }) + ) + await this.client.send( + new RevokeSecurityGroupEgressCommand({ + GroupId: groupId, + IpPermissions: [] + }) + ) + } + } +} diff --git a/src/bpsets/vpc/VPCFlowLogsEnabled.ts b/src/bpsets/vpc/VPCFlowLogsEnabled.ts new file mode 100644 index 0000000..62bf533 --- /dev/null +++ b/src/bpsets/vpc/VPCFlowLogsEnabled.ts @@ -0,0 +1,66 @@ +import { + EC2Client, + DescribeVpcsCommand, + DescribeFlowLogsCommand, + CreateFlowLogsCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class VPCFlowLogsEnabled implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const flowLogsResponse = await this.memoClient.send(new DescribeFlowLogsCommand({})) + const flowLogs = flowLogsResponse.FlowLogs || [] + const flowLogEnabledVpcs = flowLogs.map(log => log.ResourceId!) + + const vpcsResponse = await this.memoClient.send(new DescribeVpcsCommand({})) + const vpcs = vpcsResponse.Vpcs || [] + + for (const vpc of vpcs) { + if (flowLogEnabledVpcs.includes(vpc.VpcId!)) { + compliantResources.push(vpc.VpcId!) + } else { + nonCompliantResources.push(vpc.VpcId!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [ + { name: 'log-group-name', value: '' }, + { name: 'iam-role-arn', value: '' } + ] + } + } + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const logGroupName = requiredParametersForFix.find(param => param.name === 'log-group-name')?.value + const iamRoleArn = requiredParametersForFix.find(param => param.name === 'iam-role-arn')?.value + + if (!logGroupName || !iamRoleArn) { + throw new Error("Required parameters 'log-group-name' and 'iam-role-arn' are missing.") + } + + for (const vpcId of nonCompliantResources) { + await this.client.send( + new CreateFlowLogsCommand({ + ResourceIds: [vpcId], + ResourceType: 'VPC', + LogGroupName: logGroupName, + DeliverLogsPermissionArn: iamRoleArn, + TrafficType: 'ALL' + }) + ) + } + } +} diff --git a/src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts b/src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts new file mode 100644 index 0000000..f2382c0 --- /dev/null +++ b/src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts @@ -0,0 +1,44 @@ +import { + EC2Client, + DescribeNetworkAclsCommand, + DeleteNetworkAclCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class VPCNetworkACLUnusedCheck implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const response = await this.memoClient.send(new DescribeNetworkAclsCommand({})) + const networkAcls = response.NetworkAcls || [] + + for (const acl of networkAcls) { + if (!acl.Associations || acl.Associations.length === 0) { + nonCompliantResources.push(acl.NetworkAclId!) + } else { + compliantResources.push(acl.NetworkAclId!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const aclId of nonCompliantResources) { + await this.client.send( + new DeleteNetworkAclCommand({ + NetworkAclId: aclId + }) + ) + } + } +} diff --git a/src/bpsets/vpc/VPCPeeringDNSResolutionCheck.ts b/src/bpsets/vpc/VPCPeeringDNSResolutionCheck.ts new file mode 100644 index 0000000..dd8afc3 --- /dev/null +++ b/src/bpsets/vpc/VPCPeeringDNSResolutionCheck.ts @@ -0,0 +1,56 @@ +import { + EC2Client, + DescribeVpcPeeringConnectionsCommand, + ModifyVpcPeeringConnectionOptionsCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class VPCPeeringDNSResolutionCheck implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + + const response = await this.memoClient.send(new DescribeVpcPeeringConnectionsCommand({})) + const vpcPeeringConnections = response.VpcPeeringConnections || [] + + for (const connection of vpcPeeringConnections) { + const accepterOptions = connection.AccepterVpcInfo?.PeeringOptions + const requesterOptions = connection.RequesterVpcInfo?.PeeringOptions + + if ( + !accepterOptions?.AllowDnsResolutionFromRemoteVpc || + !requesterOptions?.AllowDnsResolutionFromRemoteVpc + ) { + nonCompliantResources.push(connection.VpcPeeringConnectionId!) + } else { + compliantResources.push(connection.VpcPeeringConnectionId!) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const connectionId of nonCompliantResources) { + await this.client.send( + new ModifyVpcPeeringConnectionOptionsCommand({ + VpcPeeringConnectionId: connectionId, + AccepterPeeringConnectionOptions: { + AllowDnsResolutionFromRemoteVpc: true + }, + RequesterPeeringConnectionOptions: { + AllowDnsResolutionFromRemoteVpc: true + } + }) + ) + } + } +} diff --git a/src/bpsets/vpc/VPCSGOpenOnlyToAuthorizedPorts.ts b/src/bpsets/vpc/VPCSGOpenOnlyToAuthorizedPorts.ts new file mode 100644 index 0000000..dafbc40 --- /dev/null +++ b/src/bpsets/vpc/VPCSGOpenOnlyToAuthorizedPorts.ts @@ -0,0 +1,56 @@ +import { + EC2Client, + DescribeSecurityGroupRulesCommand, + RevokeSecurityGroupIngressCommand +} from '@aws-sdk/client-ec2' +import { BPSet } from '../BPSet' +import { Memorizer } from '../../Memorizer' + +export class VPCSGOpenOnlyToAuthorizedPorts implements BPSet { + private readonly client = new EC2Client({}) + private readonly memoClient = Memorizer.memo(this.client) + + private readonly getSecurityGroupRules = async () => { + const response = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})) + return response.SecurityGroupRules || [] + } + + public readonly check = async () => { + const compliantResources: string[] = [] + const nonCompliantResources: string[] = [] + const authorizedPorts = [80, 443] // Example authorized ports + + const rules = await this.getSecurityGroupRules() + for (const rule of rules) { + if ( + !rule.IsEgress && + (rule.CidrIpv4 === '0.0.0.0/0' || rule.CidrIpv6 === '::/0') && + !authorizedPorts.includes(rule.FromPort!) && + !authorizedPorts.includes(rule.ToPort!) + ) { + nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + } else { + compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [] + } + } + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const resource of nonCompliantResources) { + const [groupId, ruleId] = resource.split(' / ') + + await this.client.send( + new RevokeSecurityGroupIngressCommand({ + GroupId: groupId, + SecurityGroupRuleIds: [ruleId] + }) + ) + } + } +} diff --git a/src/bpsets/waf/WAFv2LoggingEnabled.ts b/src/bpsets/waf/WAFv2LoggingEnabled.ts new file mode 100644 index 0000000..02db714 --- /dev/null +++ b/src/bpsets/waf/WAFv2LoggingEnabled.ts @@ -0,0 +1,74 @@ +import { + WAFV2Client, + ListWebACLsCommand, + GetLoggingConfigurationCommand, + PutLoggingConfigurationCommand, +} from '@aws-sdk/client-wafv2'; +import { BPSet } from '../BPSet'; +import { Memorizer } from '../../Memorizer'; + +export class WAFv2LoggingEnabled implements BPSet { + private readonly regionalClient = new WAFV2Client({}); + private readonly globalClient = new WAFV2Client({ region: 'us-east-1' }); + private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); + private readonly memoGlobalClient = Memorizer.memo(this.globalClient); + + private readonly getWebACLs = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListWebACLsCommand({ Scope: scope })); + return response.WebACLs || []; + }; + + public readonly check = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + for (const scope of ['REGIONAL', 'CLOUDFRONT'] as const) { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const webACLs = await this.getWebACLs(scope); + + for (const webACL of webACLs) { + try { + await client.send(new GetLoggingConfigurationCommand({ ResourceArn: webACL.ARN })); + compliantResources.push(webACL.ARN!); + } catch (error: any) { + if (error.name === 'WAFNonexistentItemException') { + nonCompliantResources.push(webACL.ARN!); + } else { + throw error; + } + } + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [{ name: 'log-group-arn', value: '' }], + }; + }; + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const logGroupArn = requiredParametersForFix.find(param => param.name === 'log-group-arn')?.value; + + if (!logGroupArn) { + throw new Error("Required parameter 'log-group-arn' is missing."); + } + + for (const arn of nonCompliantResources) { + const client = arn.includes('global') ? this.globalClient : this.regionalClient; + + await client.send( + new PutLoggingConfigurationCommand({ + LoggingConfiguration: { + ResourceArn: arn, + LogDestinationConfigs: [logGroupArn], + }, + }) + ); + } + }; +} diff --git a/src/bpsets/waf/WAFv2RuleGroupLoggingEnabled.ts b/src/bpsets/waf/WAFv2RuleGroupLoggingEnabled.ts new file mode 100644 index 0000000..9d12e80 --- /dev/null +++ b/src/bpsets/waf/WAFv2RuleGroupLoggingEnabled.ts @@ -0,0 +1,71 @@ +import { + WAFV2Client, + ListRuleGroupsCommand, + GetRuleGroupCommand, + UpdateRuleGroupCommand, +} from '@aws-sdk/client-wafv2'; +import { BPSet } from '../BPSet'; +import { Memorizer } from '../../Memorizer'; + +export class WAFv2RuleGroupLoggingEnabled implements BPSet { + private readonly regionalClient = new WAFV2Client({}); + private readonly globalClient = new WAFV2Client({ region: 'us-east-1' }); + private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); + private readonly memoGlobalClient = Memorizer.memo(this.globalClient); + + private readonly getRuleGroups = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListRuleGroupsCommand({ Scope: scope })); + return response.RuleGroups || []; + }; + + public readonly check = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + for (const scope of ['REGIONAL', 'CLOUDFRONT'] as const) { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const ruleGroups = await this.getRuleGroups(scope); + + for (const ruleGroup of ruleGroups) { + const details = await client.send( + new GetRuleGroupCommand({ Name: ruleGroup.Name!, Id: ruleGroup.Id!, Scope: scope }) + ); + + if (details.RuleGroup?.VisibilityConfig?.CloudWatchMetricsEnabled) { + compliantResources.push(ruleGroup.ARN!); + } else { + nonCompliantResources.push(ruleGroup.ARN!); + } + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [], + }; + }; + + public readonly fix = async (nonCompliantResources: string[]) => { + for (const arn of nonCompliantResources) { + const client = arn.includes('global') ? this.globalClient : this.regionalClient; + + const [name, id] = arn.split('/')[1].split(':'); + + await client.send( + new UpdateRuleGroupCommand({ + Name: name, + Id: id, + Scope: arn.includes('global') ? 'CLOUDFRONT' : 'REGIONAL', + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: `WAFRuleGroup-${name}`, + SampledRequestsEnabled: true, + }, + LockToken: undefined + }) + ); + } + }; +} diff --git a/src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts b/src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts new file mode 100644 index 0000000..1b553ed --- /dev/null +++ b/src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts @@ -0,0 +1,93 @@ +import { + WAFV2Client, + ListRuleGroupsCommand, + GetRuleGroupCommand, + UpdateRuleGroupCommand +} from '@aws-sdk/client-wafv2'; +import { BPSet } from '../BPSet'; +import { Memorizer } from '../../Memorizer'; + +export class WAFv2RuleGroupNotEmpty implements BPSet { + private readonly regionalClient = new WAFV2Client({}); + private readonly globalClient = new WAFV2Client({ region: 'us-east-1' }); + private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); + private readonly memoGlobalClient = Memorizer.memo(this.globalClient); + + private readonly getRuleGroups = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListRuleGroupsCommand({ Scope: scope })); + return response.RuleGroups || []; + }; + + public readonly check = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + for (const scope of ['REGIONAL', 'CLOUDFRONT'] as const) { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const ruleGroups = await this.getRuleGroups(scope); + + for (const ruleGroup of ruleGroups) { + const details = await client.send( + new GetRuleGroupCommand({ Name: ruleGroup.Name!, Id: ruleGroup.Id!, Scope: scope }) + ); + + if (details.RuleGroup?.Rules?.length! > 0) { + compliantResources.push(ruleGroup.ARN!); + } else { + nonCompliantResources.push(ruleGroup.ARN!); + } + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [{ name: 'default-rule', value: '' }] + }; + }; + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const defaultRule = requiredParametersForFix.find(param => param.name === 'default-rule')?.value; + + if (!defaultRule) { + throw new Error("Required parameter 'default-rule' is missing."); + } + + for (const arn of nonCompliantResources) { + const client = arn.includes('global') ? this.globalClient : this.regionalClient; + + const [name, id] = arn.split('/')[1].split(':'); + + await client.send( + new UpdateRuleGroupCommand({ + Name: name, + Id: id, + Scope: arn.includes('global') ? 'CLOUDFRONT' : 'REGIONAL', + LockToken: undefined, + Rules: [ + { + Name: 'DefaultRule', + Priority: 1, + Action: { Allow: {} }, + Statement: JSON.parse(defaultRule), + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: `DefaultRule-${name}`, + SampledRequestsEnabled: true + } + } + ], + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: `RuleGroup-${name}`, + SampledRequestsEnabled: true + } + }) + ); + } + }; +} diff --git a/src/bpsets/waf/WAFv2WebACLNotEmpty.ts b/src/bpsets/waf/WAFv2WebACLNotEmpty.ts new file mode 100644 index 0000000..da105f7 --- /dev/null +++ b/src/bpsets/waf/WAFv2WebACLNotEmpty.ts @@ -0,0 +1,94 @@ +import { + WAFV2Client, + ListWebACLsCommand, + GetWebACLCommand, + UpdateWebACLCommand +} from '@aws-sdk/client-wafv2'; +import { BPSet } from '../BPSet'; +import { Memorizer } from '../../Memorizer'; + +export class WAFv2WebACLNotEmpty implements BPSet { + private readonly regionalClient = new WAFV2Client({}); + private readonly globalClient = new WAFV2Client({ region: 'us-east-1' }); + private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); + private readonly memoGlobalClient = Memorizer.memo(this.globalClient); + + private readonly getWebACLs = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListWebACLsCommand({ Scope: scope })); + return response.WebACLs || []; + }; + + public readonly check = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + for (const scope of ['REGIONAL', 'CLOUDFRONT'] as const) { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const webACLs = await this.getWebACLs(scope); + + for (const webACL of webACLs) { + const details = await client.send( + new GetWebACLCommand({ Name: webACL.Name!, Id: webACL.Id!, Scope: scope }) + ); + + if (details.WebACL?.Rules?.length! > 0) { + compliantResources.push(webACL.ARN!); + } else { + nonCompliantResources.push(webACL.ARN!); + } + } + } + + return { + compliantResources, + nonCompliantResources, + requiredParametersForFix: [{ name: 'default-rule', value: '' }] + }; + }; + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const defaultRule = requiredParametersForFix.find(param => param.name === 'default-rule')?.value; + + if (!defaultRule) { + throw new Error("Required parameter 'default-rule' is missing."); + } + + for (const arn of nonCompliantResources) { + const client = arn.includes('global') ? this.globalClient : this.regionalClient; + + const [name, id] = arn.split('/')[1].split(':'); + + await client.send( + new UpdateWebACLCommand({ + Name: name, + Id: id, + Scope: arn.includes('global') ? 'CLOUDFRONT' : 'REGIONAL', + LockToken: undefined, + Rules: [ + { + Name: 'DefaultRule', + Priority: 1, + Action: { Allow: {} }, + Statement: JSON.parse(defaultRule), + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: `DefaultRule-${name}`, + SampledRequestsEnabled: true + } + } + ], + DefaultAction: { Allow: {} }, + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: `WebACL-${name}`, + SampledRequestsEnabled: true + } + }) + ); + } + }; +}