feat: add many bps
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
6
build.sh
Executable file
6
build.sh
Executable 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
33
package.json
Normal 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
2678
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
script/build.js
Normal file
9
script/build.js
Normal 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
5
sea-config.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"main": "dist/main.js",
|
||||
"output": "dist/sea-prep.blob",
|
||||
"disableExperimentalSEAWarning": true
|
||||
}
|
38
src/Memorizer.ts
Normal file
38
src/Memorizer.ts
Normal 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
17
src/WebServer.ts
Normal 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
8
src/bpsets/BPSet.ts
Normal 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>
|
||||
}
|
72
src/bpsets/alb/ALBHttpDropInvalidHeaderEnabled.ts
Normal file
72
src/bpsets/alb/ALBHttpDropInvalidHeaderEnabled.ts
Normal 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' }
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
62
src/bpsets/alb/ALBWAFEnabled.ts
Normal file
62
src/bpsets/alb/ALBWAFEnabled.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
64
src/bpsets/alb/ELBCrossZoneLoadBalancingEnabled.ts
Normal file
64
src/bpsets/alb/ELBCrossZoneLoadBalancingEnabled.ts
Normal 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' }]
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
64
src/bpsets/alb/ELBDeletionProtectionEnabled.ts
Normal file
64
src/bpsets/alb/ELBDeletionProtectionEnabled.ts
Normal 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' }]
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
75
src/bpsets/alb/ELBLoggingEnabled.ts
Normal file
75
src/bpsets/alb/ELBLoggingEnabled.ts
Normal 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 }
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
70
src/bpsets/apigw/APIGatewayAssociatedWithWAF.ts
Normal file
70
src/bpsets/apigw/APIGatewayAssociatedWithWAF.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
75
src/bpsets/apigw/APIGatewayExecutionLoggingEnabled.ts
Normal file
75
src/bpsets/apigw/APIGatewayExecutionLoggingEnabled.ts
Normal 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'
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
76
src/bpsets/apigw/APIGatewayV2AccessLogsEnabled.ts
Normal file
76
src/bpsets/apigw/APIGatewayV2AccessLogsEnabled.ts
Normal 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'
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
78
src/bpsets/apigw/APIGatewayV2AuthorizationTypeConfigured.ts
Normal file
78
src/bpsets/apigw/APIGatewayV2AuthorizationTypeConfigured.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
48
src/bpsets/asg/AutoScalingGroupELBHealthCheckRequired.ts
Normal file
48
src/bpsets/asg/AutoScalingGroupELBHealthCheckRequired.ts
Normal 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'
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
58
src/bpsets/asg/AutoScalingLaunchTemplate.ts
Normal file
58
src/bpsets/asg/AutoScalingLaunchTemplate.ts
Normal 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
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
54
src/bpsets/asg/AutoScalingMultipleAZ.ts
Normal file
54
src/bpsets/asg/AutoScalingMultipleAZ.ts
Normal 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(',')
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
85
src/bpsets/cloudfront/CloudFrontAccessLogsEnabled.ts
Normal file
85
src/bpsets/cloudfront/CloudFrontAccessLogsEnabled.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
77
src/bpsets/cloudfront/CloudFrontAssociatedWithWAF.ts
Normal file
77
src/bpsets/cloudfront/CloudFrontAssociatedWithWAF.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
93
src/bpsets/cloudfront/CloudFrontNoDeprecatedSSLProtocols.ts
Normal file
93
src/bpsets/cloudfront/CloudFrontNoDeprecatedSSLProtocols.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
84
src/bpsets/cloudfront/CloudFrontViewerPolicyHTTPS.ts
Normal file
84
src/bpsets/cloudfront/CloudFrontViewerPolicyHTTPS.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
58
src/bpsets/lambda/LambdaDLQCheck.ts
Normal file
58
src/bpsets/lambda/LambdaDLQCheck.ts
Normal 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 }
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
86
src/bpsets/lambda/LambdaFunctionPublicAccessProhibited.ts
Normal file
86
src/bpsets/lambda/LambdaFunctionPublicAccessProhibited.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
src/bpsets/lambda/LambdaFunctionSettingsCheck.ts
Normal file
65
src/bpsets/lambda/LambdaFunctionSettingsCheck.ts
Normal 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)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
69
src/bpsets/lambda/LambdaInsideVPC.ts
Normal file
69
src/bpsets/lambda/LambdaInsideVPC.ts
Normal 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(',')
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
81
src/bpsets/s3/S3AccessPointInVpcOnly.ts
Normal file
81
src/bpsets/s3/S3AccessPointInVpcOnly.ts
Normal 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
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
72
src/bpsets/s3/S3BucketDefaultLockEnabled.ts
Normal file
72
src/bpsets/s3/S3BucketDefaultLockEnabled.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
75
src/bpsets/s3/S3BucketLevelPublicAccessProhibited.ts
Normal file
75
src/bpsets/s3/S3BucketLevelPublicAccessProhibited.ts
Normal 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
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
73
src/bpsets/s3/S3BucketLoggingEnabled.ts
Normal file
73
src/bpsets/s3/S3BucketLoggingEnabled.ts
Normal 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/`
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
130
src/bpsets/s3/S3BucketSSLRequestsOnly.ts
Normal file
130
src/bpsets/s3/S3BucketSSLRequestsOnly.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
62
src/bpsets/s3/S3BucketVersioningEnabled.ts
Normal file
62
src/bpsets/s3/S3BucketVersioningEnabled.ts
Normal 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'
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
90
src/bpsets/s3/S3DefaultEncryptionKMS.ts
Normal file
90
src/bpsets/s3/S3DefaultEncryptionKMS.ts
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
87
src/bpsets/s3/S3EventNotificationsEnabled.ts
Normal file
87
src/bpsets/s3/S3EventNotificationsEnabled.ts
Normal 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]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
52
src/bpsets/s3/S3LastBackupRecoveryPointCreated.ts
Normal file
52
src/bpsets/s3/S3LastBackupRecoveryPointCreated.ts
Normal 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.')
|
||||
}
|
||||
}
|
90
src/bpsets/s3/S3LifecyclePolicyCheck.ts
Normal file
90
src/bpsets/s3/S3LifecyclePolicyCheck.ts
Normal 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
9
src/main.ts
Normal 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
14
tsconfig.json
Normal 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/**/*"]
|
||||
}
|
Reference in New Issue
Block a user