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