feat: add many bps

This commit is contained in:
2024-12-23 21:52:08 +09:00
commit a450604c13
43 changed files with 5218 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
dist/

6
build.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
cp $(which node) dist/bpsets
node script/build.js
node --experimental-sea-config sea-config.json
pnpx postject dist/bpsets NODE_SEA_BLOB dist/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "bpsets",
"version": "0.1.0",
"main": "build/main.js",
"scripts": {
"build": "./build.sh",
"start": "./build.sh && ./dist/bpsets"
},
"author": "Minhyeok Park<pmh_only@pmh.codes>",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-apigatewayv2": "^3.716.0",
"@aws-sdk/client-auto-scaling": "^3.716.0",
"@aws-sdk/client-backup": "^3.716.0",
"@aws-sdk/client-cloudfront": "^3.716.0",
"@aws-sdk/client-elastic-load-balancing-v2": "^3.716.0",
"@aws-sdk/client-lambda": "^3.716.0",
"@aws-sdk/client-s3": "^3.717.0",
"@aws-sdk/client-s3-control": "^3.716.0",
"@aws-sdk/client-sts": "^3.716.0",
"@aws-sdk/client-wafv2": "^3.716.0",
"@smithy/smithy-client": "^3.5.1",
"express": "^4.21.2",
"sha.js": "^2.4.11"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^22.10.2",
"@types/sha.js": "^2.4.4",
"esbuild": "^0.24.2",
"typescript": "^5.7.2"
}
}

2678
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

9
script/build.js Normal file
View File

@ -0,0 +1,9 @@
const { build } = require("esbuild")
build({
entryPoints: ["src/main.ts"],
outfile: "dist/main.js",
platform: 'node',
bundle: true,
minify: true
})

5
sea-config.json Normal file
View File

@ -0,0 +1,5 @@
{
"main": "dist/main.js",
"output": "dist/sea-prep.blob",
"disableExperimentalSEAWarning": true
}

38
src/Memorizer.ts Normal file
View File

@ -0,0 +1,38 @@
import { Client } from '@smithy/smithy-client'
import shajs from 'sha.js'
export class Memorizer {
private static memorized = new Map<string, Memorizer>()
public static memo (client: Client<any, any, any, any>) {
const memorized = this.memorized.get(client.constructor.name)
if (memorized !== undefined)
return memorized
const newMemo = new Memorizer(client)
this.memorized.set(client.constructor.name, newMemo)
return newMemo
}
private memorized = new Map<string, any>()
private constructor (
private client: Client<any, any, any, any>
) {}
public readonly send: typeof this.client.send = async (command) => {
const serialized = JSON.stringify([command.constructor.name, command.input])
const hashed = shajs('sha256').update(serialized).digest('hex')
const memorized = this.memorized.get(hashed)
if (memorized !== undefined)
return memorized
const newMemo = await this.client.send(command)
this.memorized.set(hashed, newMemo)
return newMemo
}
}

17
src/WebServer.ts Normal file
View File

@ -0,0 +1,17 @@
import express from 'express'
export class WebServer {
private readonly app = express()
public WebServer () {
}
private initRoutes () {
}
public listen() {
this.app.listen()
}
}

8
src/bpsets/BPSet.ts Normal file
View File

@ -0,0 +1,8 @@
export interface BPSet {
check: () => Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: {name: string}[]
}>,
fix: (nonCompliantResources: string[], requiredParametersForFix: {name: string, value: string}[]) => Promise<void>
}

View File

@ -0,0 +1,72 @@
import {
ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ALBHttpDropInvalidHeaderEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []
}
private readonly getLoadBalancerAttributes = async (
loadBalancerArn: string
) => {
const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
)
return response.Attributes || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some(
attr => attr.Key === 'routing.http.drop_invalid_header_fields.enabled' && attr.Value === 'true'
)
if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!)
} else {
nonCompliantResources.push(lb.LoadBalancerArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const lbArn of nonCompliantResources) {
await this.client.send(
new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn,
Attributes: [
{ Key: 'routing.http.drop_invalid_header_fields.enabled', Value: 'true' }
]
})
)
}
}
}

View File

@ -0,0 +1,62 @@
import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand } from '@aws-sdk/client-elastic-load-balancing-v2'
import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ALBWAFEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly wafClient = Memorizer.memo(new WAFV2Client({}))
private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) {
const response = await this.wafClient.send(
new GetWebACLForResourceCommand({ ResourceArn: lb.LoadBalancerArn })
)
if (response.WebACL) {
compliantResources.push(lb.LoadBalancerArn!)
} else {
nonCompliantResources.push(lb.LoadBalancerArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'web-acl-arn' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value
if (!webAclArn) {
throw new Error("Required parameter 'web-acl-arn' is missing.")
}
for (const lbArn of nonCompliantResources) {
await this.wafClient.send(
new AssociateWebACLCommand({
ResourceArn: lbArn,
WebACLArn: webAclArn
})
)
}
}
}

View File

@ -0,0 +1,64 @@
import {
ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ELBCrossZoneLoadBalancingEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []
}
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
)
return response.Attributes || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some(
attr => attr.Key === 'load_balancing.cross_zone.enabled' && attr.Value === 'true'
)
if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!)
} else {
nonCompliantResources.push(lb.LoadBalancerArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const lbArn of nonCompliantResources) {
await this.client.send(
new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn,
Attributes: [{ Key: 'load_balancing.cross_zone.enabled', Value: 'true' }]
})
)
}
}
}

View File

@ -0,0 +1,64 @@
import {
ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ELBDeletionProtectionEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []
}
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
)
return response.Attributes || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some(
attr => attr.Key === 'deletion_protection.enabled' && attr.Value === 'true'
)
if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!)
} else {
nonCompliantResources.push(lb.LoadBalancerArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const lbArn of nonCompliantResources) {
await this.client.send(
new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn,
Attributes: [{ Key: 'deletion_protection.enabled', Value: 'true' }]
})
)
}
}
}

View File

@ -0,0 +1,75 @@
import {
ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ELBLoggingEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []
}
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
)
return response.Attributes || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some(
attr => attr.Key === 'access_logs.s3.enabled' && attr.Value === 'true'
)
if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!)
} else {
nonCompliantResources.push(lb.LoadBalancerArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 's3-bucket-name' }, { name: 's3-prefix' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const bucketName = requiredParametersForFix.find(param => param.name === 's3-bucket-name')?.value
const bucketPrefix = requiredParametersForFix.find(param => param.name === 's3-prefix')?.value
if (!bucketName || !bucketPrefix) {
throw new Error("Required parameters 's3-bucket-name' and/or 's3-prefix' are missing.")
}
for (const lbArn of nonCompliantResources) {
await this.client.send(
new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn,
Attributes: [
{ Key: 'access_logs.s3.enabled', Value: 'true' },
{ Key: 'access_logs.s3.bucket', Value: bucketName },
{ Key: 'access_logs.s3.prefix', Value: bucketPrefix }
]
})
)
}
}
}

View File

@ -0,0 +1,70 @@
import {
ApiGatewayV2Client,
GetApisCommand,
GetStagesCommand
} from '@aws-sdk/client-apigatewayv2'
import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class APIGatewayAssociatedWithWAF implements BPSet {
private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly wafClient = Memorizer.memo(new WAFV2Client({}))
private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []
}
private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const apis = await this.getHttpApis()
for (const api of apis) {
const stages = await this.getStages(api.ApiId!)
for (const stage of stages) {
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`
const response = await this.wafClient.send(new GetWebACLForResourceCommand({ ResourceArn: stageArn }))
if (response.WebACL) {
compliantResources.push(stageArn)
} else {
nonCompliantResources.push(stageArn)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'web-acl-arn' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value
if (!webAclArn) {
throw new Error("Required parameter 'web-acl-arn' is missing.")
}
for (const stageArn of nonCompliantResources) {
await this.wafClient.send(
new AssociateWebACLCommand({
ResourceArn: stageArn,
WebACLArn: webAclArn
})
)
}
}
}

View File

@ -0,0 +1,75 @@
import {
ApiGatewayV2Client,
GetApisCommand,
GetStagesCommand,
UpdateStageCommand
} from '@aws-sdk/client-apigatewayv2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class APIGatewayExecutionLoggingEnabled implements BPSet {
private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []
}
private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const apis = await this.getHttpApis()
for (const api of apis) {
const stages = await this.getStages(api.ApiId!)
for (const stage of stages) {
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`
const loggingLevel = stage.AccessLogSettings?.Format
if (loggingLevel && loggingLevel !== 'OFF') {
compliantResources.push(stageArn)
} else {
nonCompliantResources.push(stageArn)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-destination-arn' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value
if (!logDestinationArn) {
throw new Error("Required parameter 'log-destination-arn' is missing.")
}
for (const stageArn of nonCompliantResources) {
const [apiId, stageName] = stageArn.split('/').slice(-2)
await this.client.send(
new UpdateStageCommand({
ApiId: apiId,
StageName: stageName,
AccessLogSettings: {
DestinationArn: logDestinationArn,
Format: '$context.requestId'
}
})
)
}
}
}

View File

@ -0,0 +1,76 @@
import {
ApiGatewayV2Client,
GetApisCommand,
GetStagesCommand,
UpdateStageCommand
} from '@aws-sdk/client-apigatewayv2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class APIGatewayV2AccessLogsEnabled implements BPSet {
private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []
}
private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const apis = await this.getHttpApis()
for (const api of apis) {
const stages = await this.getStages(api.ApiId!)
for (const stage of stages) {
const stageIdentifier = `${api.Name!} / ${stage.StageName!}`
if (!stage.AccessLogSettings) {
nonCompliantResources.push(stageIdentifier)
} else {
compliantResources.push(stageIdentifier)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-destination-arn' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value
if (!logDestinationArn) {
throw new Error("Required parameter 'log-destination-arn' is missing.")
}
for (const resource of nonCompliantResources) {
const [apiName, stageName] = resource.split(' / ')
const api = (await this.getHttpApis()).find(a => a.Name === apiName)
if (!api) continue
await this.client.send(
new UpdateStageCommand({
ApiId: api.ApiId!,
StageName: stageName,
AccessLogSettings: {
DestinationArn: logDestinationArn,
Format: '$context.requestId'
}
})
)
}
}
}

View File

@ -0,0 +1,78 @@
import {
ApiGatewayV2Client,
GetApisCommand,
GetRoutesCommand,
UpdateRouteCommand
} from '@aws-sdk/client-apigatewayv2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class APIGatewayV2AuthorizationTypeConfigured implements BPSet {
private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []
}
private readonly getRoutes = async (apiId: string) => {
const response = await this.memoClient.send(new GetRoutesCommand({ ApiId: apiId }))
return response.Items || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const apis = await this.getHttpApis()
for (const api of apis) {
const routes = await this.getRoutes(api.ApiId!)
for (const route of routes) {
const routeIdentifier = `${api.Name!} / ${route.RouteKey!}`
if (route.AuthorizationType === 'NONE') {
nonCompliantResources.push(routeIdentifier)
} else {
compliantResources.push(routeIdentifier)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'authorization-type' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const authorizationType = requiredParametersForFix.find(param => param.name === 'authorization-type')?.value
if (!authorizationType) {
throw new Error("Required parameter 'authorization-type' is missing.")
}
for (const resource of nonCompliantResources) {
const [apiName, routeKey] = resource.split(' / ')
const api = (await this.getHttpApis()).find(a => a.Name === apiName)
if (!api) continue
const routes = await this.getRoutes(api.ApiId!)
const route = routes.find(r => r.RouteKey === routeKey)
if (!route) continue
await this.client.send(
new UpdateRouteCommand({
ApiId: api.ApiId!,
RouteId: route.RouteId!, // Use RouteId instead of RouteKey
AuthorizationType: authorizationType as any
})
)
}
}
}

View File

@ -0,0 +1,48 @@
import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class AutoScalingGroupELBHealthCheckRequired implements BPSet {
private readonly client = new AutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getAutoScalingGroups = async () => {
const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({}))
return response.AutoScalingGroups || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const asgs = await this.getAutoScalingGroups()
for (const asg of asgs) {
if (
(asg.LoadBalancerNames?.length || asg.TargetGroupARNs?.length) &&
asg.HealthCheckType !== 'ELB'
) {
nonCompliantResources.push(asg.AutoScalingGroupARN!)
} else {
compliantResources.push(asg.AutoScalingGroupARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const asgArn of nonCompliantResources) {
const asgName = asgArn.split(':').pop()!
await this.client.send(
new UpdateAutoScalingGroupCommand({
AutoScalingGroupName: asgName,
HealthCheckType: 'ELB'
})
)
}
}
}

View File

@ -0,0 +1,58 @@
import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class AutoScalingLaunchTemplate implements BPSet {
private readonly client = new AutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getAutoScalingGroups = async () => {
const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({}))
return response.AutoScalingGroups || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const asgs = await this.getAutoScalingGroups()
for (const asg of asgs) {
if (asg.LaunchConfigurationName) {
nonCompliantResources.push(asg.AutoScalingGroupARN!)
} else {
compliantResources.push(asg.AutoScalingGroupARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'launch-template-id' }, { name: 'version' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const launchTemplateId = requiredParametersForFix.find(param => param.name === 'launch-template-id')?.value
const version = requiredParametersForFix.find(param => param.name === 'version')?.value
if (!launchTemplateId || !version) {
throw new Error("Required parameters 'launch-template-id' and/or 'version' are missing.")
}
for (const asgArn of nonCompliantResources) {
const asgName = asgArn.split(':').pop()!
await this.client.send(
new UpdateAutoScalingGroupCommand({
AutoScalingGroupName: asgName,
LaunchTemplate: {
LaunchTemplateId: launchTemplateId,
Version: version
}
})
)
}
}
}

View File

@ -0,0 +1,54 @@
import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class AutoScalingMultipleAZ implements BPSet {
private readonly client = new AutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getAutoScalingGroups = async () => {
const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({}))
return response.AutoScalingGroups || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const asgs = await this.getAutoScalingGroups()
for (const asg of asgs) {
if (asg.AvailabilityZones?.length! > 1) {
compliantResources.push(asg.AutoScalingGroupARN!)
} else {
nonCompliantResources.push(asg.AutoScalingGroupARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'availability-zones' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const availabilityZones = requiredParametersForFix.find(param => param.name === 'availability-zones')?.value
if (!availabilityZones) {
throw new Error("Required parameter 'availability-zones' is missing.")
}
for (const asgArn of nonCompliantResources) {
const asgName = asgArn.split(':').pop()!
await this.client.send(
new UpdateAutoScalingGroupCommand({
AutoScalingGroupName: asgName,
AvailabilityZones: availabilityZones.split(',')
})
)
}
}
}

View File

@ -0,0 +1,85 @@
import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CloudFrontAccessLogsEnabled implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
if (
details.DistributionConfig?.Logging?.Enabled
) {
compliantResources.push(details.ARN!)
} else {
nonCompliantResources.push(details.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-bucket-name' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const logBucketName = requiredParametersForFix.find(param => param.name === 'log-bucket-name')?.value
if (!logBucketName) {
throw new Error("Required parameter 'log-bucket-name' is missing.")
}
for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = {
...distribution.DistributionConfig,
Logging: {
Enabled: true,
Bucket: logBucketName,
IncludeCookies: false,
Prefix: ''
}
}
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any // Include all required properties
})
)
}
}
}

View File

@ -0,0 +1,77 @@
import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CloudFrontAssociatedWithWAF implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
for (const distribution of distributions) {
if (distribution.WebACLId && distribution.WebACLId !== '') {
compliantResources.push(distribution.ARN!)
} else {
nonCompliantResources.push(distribution.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'web-acl-id' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const webAclId = requiredParametersForFix.find(param => param.name === 'web-acl-id')?.value
if (!webAclId) {
throw new Error("Required parameter 'web-acl-id' is missing.")
}
for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = {
...distribution.DistributionConfig,
WebACLId: webAclId
}
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any // Include all required properties
})
)
}
}
}

View File

@ -0,0 +1,78 @@
import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CloudFrontDefaultRootObjectConfigured implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
if (details.DistributionConfig?.DefaultRootObject !== '') {
compliantResources.push(details.ARN!)
} else {
nonCompliantResources.push(details.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'default-root-object' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const defaultRootObject = requiredParametersForFix.find(param => param.name === 'default-root-object')?.value
if (!defaultRootObject) {
throw new Error("Required parameter 'default-root-object' is missing.")
}
for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = {
...distribution.DistributionConfig,
DefaultRootObject: defaultRootObject
}
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any
})
)
}
}
}

View File

@ -0,0 +1,93 @@
import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
const hasDeprecatedSSL = details.DistributionConfig?.Origins?.Items?.some(
origin =>
origin.CustomOriginConfig &&
origin.CustomOriginConfig.OriginSslProtocols?.Items?.includes('SSLv3')
)
if (hasDeprecatedSSL) {
nonCompliantResources.push(details.ARN!)
} else {
compliantResources.push(details.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = {
...distribution.DistributionConfig,
Origins: {
Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => {
if (origin.CustomOriginConfig) {
return {
...origin,
CustomOriginConfig: {
...origin.CustomOriginConfig,
OriginSslProtocols: {
...origin.CustomOriginConfig.OriginSslProtocols,
Items: origin.CustomOriginConfig.OriginSslProtocols?.Items?.filter(
protocol => protocol !== 'SSLv3'
)
}
}
}
}
return origin
})
}
}
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any
})
)
}
}
}

View File

@ -0,0 +1,96 @@
import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CloudFrontS3OriginAccessControlEnabled implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
const hasNonCompliantOrigin = details.DistributionConfig?.Origins?.Items?.some(
origin =>
origin.S3OriginConfig &&
(!origin.OriginAccessControlId || origin.OriginAccessControlId === '')
)
if (hasNonCompliantOrigin) {
nonCompliantResources.push(details.ARN!)
} else {
compliantResources.push(details.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'origin-access-control-id' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const originAccessControlId = requiredParametersForFix.find(
param => param.name === 'origin-access-control-id'
)?.value
if (!originAccessControlId) {
throw new Error("Required parameter 'origin-access-control-id' is missing.")
}
for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = {
...distribution.DistributionConfig,
Origins: {
Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => {
if (origin.S3OriginConfig) {
return {
...origin,
OriginAccessControlId: originAccessControlId
}
}
return origin
})
}
}
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any
})
)
}
}
}

View File

@ -0,0 +1,84 @@
import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CloudFrontViewerPolicyHTTPS implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
const hasNonCompliantViewerPolicy =
details.DistributionConfig?.DefaultCacheBehavior?.ViewerProtocolPolicy === 'allow-all' ||
details.DistributionConfig?.CacheBehaviors?.Items?.some(
behavior => behavior.ViewerProtocolPolicy === 'allow-all'
)
if (hasNonCompliantViewerPolicy) {
nonCompliantResources.push(details.ARN!)
} else {
compliantResources.push(details.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = {
...distribution.DistributionConfig,
DefaultCacheBehavior: {
...distribution.DistributionConfig?.DefaultCacheBehavior,
ViewerProtocolPolicy: 'redirect-to-https'
},
CacheBehaviors: {
Items: distribution.DistributionConfig?.CacheBehaviors?.Items?.map(behavior => ({
...behavior,
ViewerProtocolPolicy: 'redirect-to-https'
}))
}
}
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any
})
)
}
}
}

View File

@ -0,0 +1,58 @@
import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class LambdaDLQCheck implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const functions = await this.getFunctions()
for (const func of functions) {
if (func.DeadLetterConfig) {
compliantResources.push(func.FunctionArn!)
} else {
nonCompliantResources.push(func.FunctionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'dlq-arn' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const dlqArn = requiredParametersForFix.find(param => param.name === 'dlq-arn')?.value
if (!dlqArn) {
throw new Error("Required parameter 'dlq-arn' is missing.")
}
for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!
await this.client.send(
new UpdateFunctionConfigurationCommand({
FunctionName: functionName,
DeadLetterConfig: { TargetArn: dlqArn }
})
)
}
}
}

View File

@ -0,0 +1,86 @@
import {
LambdaClient,
ListFunctionsCommand,
GetPolicyCommand,
RemovePermissionCommand
} from '@aws-sdk/client-lambda'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class LambdaFunctionPublicAccessProhibited implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const functions = await this.getFunctions()
for (const func of functions) {
try {
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! }))
const policy = JSON.parse(response.Policy!)
const hasPublicAccess = policy.Statement.some(
(statement: any) => statement.Principal === '*' || statement.Principal?.AWS === '*'
)
if (hasPublicAccess) {
nonCompliantResources.push(func.FunctionArn!)
} else {
compliantResources.push(func.FunctionArn!)
}
} catch (error) {
if ((error as any).name === 'ResourceNotFoundException') {
nonCompliantResources.push(func.FunctionArn!)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!
try {
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: functionName }))
const policy = JSON.parse(response.Policy!)
for (const statement of policy.Statement) {
if (statement.Principal === '*' || statement.Principal?.AWS === '*') {
await this.client.send(
new RemovePermissionCommand({
FunctionName: functionName,
StatementId: statement.Sid // Use the actual StatementId from the policy
})
)
}
}
} catch (error) {
if ((error as any).name !== 'ResourceNotFoundException') {
throw error
}
}
}
}
}

View File

@ -0,0 +1,65 @@
import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class LambdaFunctionSettingsCheck implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const defaultTimeout = 3
const defaultMemorySize = 128
const functions = await this.getFunctions()
for (const func of functions) {
if (func.Timeout === defaultTimeout || func.MemorySize === defaultMemorySize) {
nonCompliantResources.push(func.FunctionArn!)
} else {
compliantResources.push(func.FunctionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'timeout' },
{ name: 'memory-size' }
]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.value
const memorySize = requiredParametersForFix.find(param => param.name === 'memory-size')?.value
if (!timeout || !memorySize) {
throw new Error("Required parameters 'timeout' and/or 'memory-size' are missing.")
}
for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!
await this.client.send(
new UpdateFunctionConfigurationCommand({
FunctionName: functionName,
Timeout: parseInt(timeout, 10),
MemorySize: parseInt(memorySize, 10)
})
)
}
}
}

View File

@ -0,0 +1,69 @@
import {
LambdaClient,
ListFunctionsCommand,
UpdateFunctionConfigurationCommand
} from '@aws-sdk/client-lambda'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class LambdaInsideVPC implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const functions = await this.getFunctions()
for (const func of functions) {
if (func.VpcConfig && Object.keys(func.VpcConfig).length > 0) {
compliantResources.push(func.FunctionArn!)
} else {
nonCompliantResources.push(func.FunctionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'subnet-ids' },
{ name: 'security-group-ids' }
]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-ids')?.value
const securityGroupIds = requiredParametersForFix.find(param => param.name === 'security-group-ids')?.value
if (!subnetIds || !securityGroupIds) {
throw new Error("Required parameters 'subnet-ids' and/or 'security-group-ids' are missing.")
}
for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!
await this.client.send(
new UpdateFunctionConfigurationCommand({
FunctionName: functionName,
VpcConfig: {
SubnetIds: subnetIds.split(','),
SecurityGroupIds: securityGroupIds.split(',')
}
})
)
}
}
}

View File

@ -0,0 +1,81 @@
import {
S3ControlClient,
ListAccessPointsCommand,
DeleteAccessPointCommand,
CreateAccessPointCommand
} from '@aws-sdk/client-s3-control'
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3AccessPointInVpcOnly implements BPSet {
private readonly client = new S3ControlClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly stsClient = Memorizer.memo(new STSClient({}))
private readonly getAccountId = async (): Promise<string> => {
const response = await this.stsClient.send(new GetCallerIdentityCommand({}))
return response.Account!
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const requiredParametersForFix = [{ name: 'your-vpc-id' }]
const accountId = await this.getAccountId()
const response = await this.memoClient.send(
new ListAccessPointsCommand({ AccountId: accountId })
)
for (const accessPoint of response.AccessPointList || []) {
if (accessPoint.NetworkOrigin === 'VPC') {
compliantResources.push(accessPoint.AccessPointArn!)
} else {
nonCompliantResources.push(accessPoint.AccessPointArn!)
}
}
return { compliantResources, nonCompliantResources, requiredParametersForFix }
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const accountId = await this.getAccountId()
const vpcId = requiredParametersForFix.find(param => param.name === 'your-vpc-id')?.value
if (!vpcId) {
throw new Error("Required parameter 'your-vpc-id' is missing.")
}
for (const accessPointArn of nonCompliantResources) {
const accessPointName = accessPointArn.split(':').pop()!
const bucketName = accessPointArn.split('/')[1]!
await this.client.send(
new DeleteAccessPointCommand({
AccountId: accountId,
Name: accessPointName
})
)
await this.client.send(
new CreateAccessPointCommand({
AccountId: accountId,
Name: accessPointName,
Bucket: bucketName,
VpcConfiguration: {
VpcId: vpcId
}
})
)
}
}
}

View File

@ -0,0 +1,72 @@
import {
S3Client,
ListBucketsCommand,
GetObjectLockConfigurationCommand,
PutObjectLockConfigurationCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3BucketDefaultLockEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
try {
await this.memoClient.send(
new GetObjectLockConfigurationCommand({ Bucket: bucket.Name! })
)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} catch (error) {
if ((error as any).name === 'ObjectLockConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
await this.client.send(
new PutObjectLockConfigurationCommand({
Bucket: bucketName,
ObjectLockConfiguration: {
ObjectLockEnabled: 'Enabled',
Rule: {
DefaultRetention: {
Mode: 'GOVERNANCE',
Days: 365
}
}
}
})
)
}
}
}

View File

@ -0,0 +1,75 @@
import {
S3Client,
ListBucketsCommand,
GetPublicAccessBlockCommand,
PutPublicAccessBlockCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3BucketLevelPublicAccessProhibited implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
try {
const response = await this.memoClient.send(
new GetPublicAccessBlockCommand({ Bucket: bucket.Name! })
)
const config = response.PublicAccessBlockConfiguration
if (
config?.BlockPublicAcls &&
config?.IgnorePublicAcls &&
config?.BlockPublicPolicy &&
config?.RestrictPublicBuckets
) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
} catch (error) {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
await this.client.send(
new PutPublicAccessBlockCommand({
Bucket: bucketName,
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
IgnorePublicAcls: true,
BlockPublicPolicy: true,
RestrictPublicBuckets: true
}
})
)
}
}
}

View File

@ -0,0 +1,73 @@
import {
S3Client,
ListBucketsCommand,
GetBucketLoggingCommand,
PutBucketLoggingCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3BucketLoggingEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
const response = await this.memoClient.send(
new GetBucketLoggingCommand({ Bucket: bucket.Name! })
)
if (response.LoggingEnabled) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-destination-bucket' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const logDestinationBucket = requiredParametersForFix.find(
param => param.name === 'log-destination-bucket'
)?.value
if (!logDestinationBucket) {
throw new Error("Required parameter 'log-destination-bucket' is missing.")
}
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
await this.client.send(
new PutBucketLoggingCommand({
Bucket: bucketName,
BucketLoggingStatus: {
LoggingEnabled: {
TargetBucket: logDestinationBucket,
TargetPrefix: `${bucketName}/logs/`
}
}
})
)
}
}
}

View File

@ -0,0 +1,130 @@
import {
S3Client,
ListBucketsCommand,
GetBucketPolicyCommand,
PutBucketPolicyCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3BucketSSLRequestsOnly implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly createSSLOnlyPolicy = (bucketName: string): string => {
return JSON.stringify({
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyNonSSLRequests',
Effect: 'Deny',
Principal: '*',
Action: 's3:*',
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: {
Bool: {
'aws:SecureTransport': 'false'
}
}
}
]
})
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucket.Name! })
)
const policy = JSON.parse(response.Policy!)
const hasSSLCondition = policy.Statement.some(
(stmt: any) =>
stmt.Condition &&
stmt.Condition.Bool &&
stmt.Condition.Bool['aws:SecureTransport'] === 'false'
)
if (hasSSLCondition) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
} catch (error) {
if ((error as any).name === 'NoSuchBucketPolicy') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
let existingPolicy: any
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucketName })
)
existingPolicy = JSON.parse(response.Policy!)
} catch (error) {
if ((error as any).name !== 'NoSuchBucketPolicy') {
throw error
}
}
const sslPolicyStatement = {
Sid: 'DenyNonSSLRequests',
Effect: 'Deny',
Principal: '*',
Action: 's3:*',
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: {
Bool: {
'aws:SecureTransport': 'false'
}
}
}
let updatedPolicy
if (existingPolicy) {
existingPolicy.Statement.push(sslPolicyStatement)
updatedPolicy = JSON.stringify(existingPolicy)
} else {
updatedPolicy = this.createSSLOnlyPolicy(bucketName)
}
await this.client.send(
new PutBucketPolicyCommand({
Bucket: bucketName,
Policy: updatedPolicy
})
)
}
}
}

View File

@ -0,0 +1,62 @@
import {
S3Client,
ListBucketsCommand,
GetBucketVersioningCommand,
PutBucketVersioningCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3BucketVersioningEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
const response = await this.memoClient.send(
new GetBucketVersioningCommand({ Bucket: bucket.Name! })
)
if (response.Status === 'Enabled') {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
await this.client.send(
new PutBucketVersioningCommand({
Bucket: bucketName,
VersioningConfiguration: {
Status: 'Enabled'
}
})
)
}
}
}

View File

@ -0,0 +1,90 @@
import {
S3Client,
ListBucketsCommand,
GetBucketEncryptionCommand,
PutBucketEncryptionCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3DefaultEncryptionKMS implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
try {
const response = await this.memoClient.send(
new GetBucketEncryptionCommand({ Bucket: bucket.Name! })
)
const encryption = response.ServerSideEncryptionConfiguration!
const isKmsEnabled = encryption.Rules?.some(
rule =>
rule.ApplyServerSideEncryptionByDefault &&
rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm === 'aws:kms'
)
if (isKmsEnabled) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
} catch (error) {
if ((error as any).name === 'ServerSideEncryptionConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
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 bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
await this.client.send(
new PutBucketEncryptionCommand({
Bucket: bucketName,
ServerSideEncryptionConfiguration: {
Rules: [
{
ApplyServerSideEncryptionByDefault: {
SSEAlgorithm: 'aws:kms',
KMSMasterKeyID: kmsKeyId
}
}
]
}
})
)
}
}
}

View File

@ -0,0 +1,87 @@
import {
S3Client,
ListBucketsCommand,
GetBucketNotificationConfigurationCommand,
PutBucketNotificationConfigurationCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3EventNotificationsEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
const response = await this.memoClient.send(
new GetBucketNotificationConfigurationCommand({ Bucket: bucket.Name! })
)
if (
response.LambdaFunctionConfigurations ||
response.QueueConfigurations ||
response.TopicConfigurations
) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'lambda-function-arn' },
{ name: 'event-type' }
]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const lambdaArn = requiredParametersForFix.find(
param => param.name === 'lambda-function-arn'
)?.value
const eventType = requiredParametersForFix.find(
param => param.name === 'event-type'
)?.value
if (!lambdaArn || !eventType) {
throw new Error(
"Required parameters 'lambda-function-arn' and/or 'event-type' are missing."
)
}
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
await this.client.send(
new PutBucketNotificationConfigurationCommand({
Bucket: bucketName,
NotificationConfiguration: {
LambdaFunctionConfigurations: [
{
LambdaFunctionArn: lambdaArn,
Events: [eventType as any]
}
]
}
})
)
}
}
}

View File

@ -0,0 +1,52 @@
import {
S3Client,
ListBucketsCommand
} from '@aws-sdk/client-s3'
import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3LastBackupRecoveryPointCreated implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly backupClient = Memorizer.memo(new BackupClient({}))
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
const recoveryPoints = await this.memoClient.send(
new ListRecoveryPointsByResourceCommand({
ResourceArn: `arn:aws:s3:::${bucket.Name!}`
})
)
if (recoveryPoints.RecoveryPoints && recoveryPoints.RecoveryPoints.length > 0) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (): Promise<void> => {
throw new Error('Fixing recovery points requires custom implementation for backup setup.')
}
}

View File

@ -0,0 +1,90 @@
import {
S3Client,
ListBucketsCommand,
GetBucketLifecycleConfigurationCommand,
PutBucketLifecycleConfigurationCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class S3LifecyclePolicyCheck implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
try {
await this.memoClient.send(
new GetBucketLifecycleConfigurationCommand({ Bucket: bucket.Name! })
)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} catch (error) {
if ((error as any).name === 'NoSuchLifecycleConfiguration') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'lifecycle-policy-rule-id' },
{ name: 'expiration-days' }
]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const ruleId = requiredParametersForFix.find(
param => param.name === 'lifecycle-policy-rule-id'
)?.value
const expirationDays = requiredParametersForFix.find(
param => param.name === 'expiration-days'
)?.value
if (!ruleId || !expirationDays) {
throw new Error(
"Required parameters 'lifecycle-policy-rule-id' and/or 'expiration-days' are missing."
)
}
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
await this.client.send(
new PutBucketLifecycleConfigurationCommand({
Bucket: bucketName,
LifecycleConfiguration: {
Rules: [
{
ID: ruleId,
Status: 'Enabled',
Expiration: {
Days: parseInt(expirationDays, 10)
}
}
]
}
})
)
}
}
}

9
src/main.ts Normal file
View File

@ -0,0 +1,9 @@
import { S3BucketVersioningEnabled } from "./bpsets/s3/S3BucketVersioningEnabled";
new S3BucketVersioningEnabled()
.check()
.then(({ nonCompliantResources }) => {
new S3BucketVersioningEnabled()
.fix(nonCompliantResources, [])
.then(() => console.log('Done'))
})

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2016",
"module": "CommonJS",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"experimentalDecorators": true
},
"include": ["src/**/*"],
"exclude": ["node_modules/**/*"]
}