style: add eslint styling

This commit is contained in:
2025-01-16 15:48:55 +09:00
parent eba8fd3b1a
commit ee95323ce2
122 changed files with 6890 additions and 6658 deletions

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "none",
"semi": false,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false
}

37
eslint.config.mjs Normal file
View File

@ -0,0 +1,37 @@
import globals from 'globals'
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettierPlugin from 'eslint-plugin-prettier/recommended'
import prettierConfig from 'eslint-config-prettier'
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ['**/*.{js,mjs,cjs,ts}'] },
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
prettierPlugin,
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/no-explicit-any': [
'error',
{
fixToUnknown: true
}
]
}
}
]

View File

@ -47,12 +47,18 @@
"sha.js": "^2.4.11" "sha.js": "^2.4.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.18.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/sha.js": "^2.4.4", "@types/sha.js": "^2.4.4",
"@yao-pkg/pkg": "^6.2.0", "@yao-pkg/pkg": "^6.2.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"typescript": "^5.7.3" "typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
}, },
"pkg": { "pkg": {
"scripts": "build/**/*", "scripts": "build/**/*",

831
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,15 @@
import { BPSet } from "./types"; import { BPSet } from './types'
import { readdir } from 'node:fs/promises' import { readdir } from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
export class BPManager { export class BPManager {
private static _instance = new BPManager() private static _instance = new BPManager()
public static getInstance = () => public static getInstance = () => this._instance
this._instance
// --- // ---
private readonly bpSets: private readonly bpSets: Record<string, BPSet> = {}
Record<string, BPSet> = {}
private constructor() { private constructor() {
this.loadBPSets() this.loadBPSets()
@ -19,14 +17,14 @@ export class BPManager {
private async loadBPSets() { private async loadBPSets() {
const bpSetFolders = await readdir(path.join(__dirname, 'bpsets')) const bpSetFolders = await readdir(path.join(__dirname, 'bpsets'))
for (const bpSetFolder of bpSetFolders) { for (const bpSetFolder of bpSetFolders) {
const bpSetFiles = await readdir(path.join(__dirname, 'bpsets', bpSetFolder)) const bpSetFiles = await readdir(path.join(__dirname, 'bpsets', bpSetFolder))
for (const bpSetFile of bpSetFiles) { for (const bpSetFile of bpSetFiles) {
const bpSetPath = path.join(__dirname, 'bpsets', bpSetFolder, bpSetFile) const bpSetPath = path.join(__dirname, 'bpsets', bpSetFolder, bpSetFile)
const bpSetClasses = await import(bpSetPath) as Record<string, new () => BPSet> const bpSetClasses = (await import(bpSetPath)) as Record<string, new () => BPSet>
for (const bpSetClass of Object.keys(bpSetClasses)) { for (const bpSetClass of Object.keys(bpSetClasses)) {
this.bpSets[bpSetClass] = new bpSetClasses[bpSetClass]() this.bpSets[bpSetClass] = new bpSetClasses[bpSetClass]()
console.log('BPSet implement,', bpSetClass, 'loaded') console.log('BPSet implement,', bpSetClass, 'loaded')
@ -39,29 +37,18 @@ export class BPManager {
return this.bpSets[name].check() return this.bpSets[name].check()
} }
public runCheckAll(finished = (name: string) => {}) { public runCheckAll(finished = (_: string) => {}) {
const checkJobs: Promise<void>[] = [] const checkJobs: Promise<void>[] = []
for (const bpset of Object.values(this.bpSets)) for (const bpset of Object.values(this.bpSets))
checkJobs.push( checkJobs.push(bpset.check().then(() => finished(bpset.getMetadata().name)))
bpset
.check()
.then(() =>
finished(bpset.getMetadata().name))
)
return Promise.all(checkJobs) return Promise.all(checkJobs)
} }
public runFix(name: string, requiredParametersForFix: { name: string, value: string }[]) { public runFix(name: string, requiredParametersForFix: { name: string; value: string }[]) {
return this return this.bpSets[name].fix(this.bpSets[name].getStats().nonCompliantResources, requiredParametersForFix)
.bpSets[name]
.fix(
this.bpSets[name].getStats().nonCompliantResources,
requiredParametersForFix
)
} }
public readonly getBPSets = () => public readonly getBPSets = () => Object.values(this.bpSets)
Object.values(this.bpSets)
} }

View File

@ -5,24 +5,23 @@ import shajs from 'sha.js'
* Memorize AWS SDK operation results. * Memorize AWS SDK operation results.
* This util class tend to be always re-use AWS SDK Client * This util class tend to be always re-use AWS SDK Client
* which makes operation more faster and optimize memory usage. * which makes operation more faster and optimize memory usage.
* *
* All results will be store as Key-Value hash map. * All results will be store as Key-Value hash map.
* * Key: sha256(serialize([OPERATION_NAME, OPERATION_INPUT_PARAMETER])) * * Key: sha256(serialize([OPERATION_NAME, OPERATION_INPUT_PARAMETER]))
* * Value: OPERATION_OUTPUT * * Value: OPERATION_OUTPUT
* *
* @author Minhyeok Park <pmh_only@pmh.codes> * @author Minhyeok Park <pmh_only@pmh.codes>
*/ */
export class Memorizer { export class Memorizer {
private static memorized = new Map<string, Memorizer>() private static memorized = new Map<string, Memorizer>()
public static memo(client: Client<any, any, any, any>, salt = '') { public static memo(client: Client<unknown, unknown, unknown, unknown>, salt = '') {
const serialized = JSON.stringify([client.constructor.name, salt]) const serialized = JSON.stringify([client.constructor.name, salt])
const hashed = shajs('sha256').update(serialized).digest('hex') const hashed = shajs('sha256').update(serialized).digest('hex')
const memorized = this.memorized.get(hashed) const memorized = this.memorized.get(hashed)
if (memorized !== undefined) if (memorized !== undefined) return memorized
return memorized
const newMemo = new Memorizer(client) const newMemo = new Memorizer(client)
this.memorized.set(hashed, newMemo) this.memorized.set(hashed, newMemo)
@ -31,23 +30,19 @@ export class Memorizer {
} }
public static reset() { public static reset() {
for (const memorized of Memorizer.memorized.values()) for (const memorized of Memorizer.memorized.values()) memorized.reset()
memorized.reset()
} }
private memorized = new Map<string, any>() private memorized = new Map<string, unknown>()
private constructor ( private constructor(private client: Client<unknown, unknown, unknown, unknown>) {}
private client: Client<any, any, any, any>
) {}
public readonly send: typeof this.client.send = async (command) => { public readonly send: typeof this.client.send = async (command) => {
const serialized = JSON.stringify([command.constructor.name, command.input]) const serialized = JSON.stringify([command.constructor.name, command.input])
const hashed = shajs('sha256').update(serialized).digest('hex') const hashed = shajs('sha256').update(serialized).digest('hex')
const memorized = this.memorized.get(hashed) const memorized = this.memorized.get(hashed)
if (memorized !== undefined) if (memorized !== undefined) return memorized
return memorized
console.log(command.constructor.name, 'Executed.') console.log(command.constructor.name, 'Executed.')
@ -57,6 +52,5 @@ export class Memorizer {
return newMemo return newMemo
} }
private readonly reset = () => private readonly reset = () => this.memorized.clear()
this.memorized.clear()
} }

View File

@ -1,20 +1,17 @@
import express, { Request, Response } from 'express' import express, { Request, Response } from 'express'
import { BPManager } from './BPManager' import { BPManager } from './BPManager'
import { BPSet, BPSetMetadata, BPSetStats } from './types' import { BPSetMetadata, BPSetStats } from './types'
import { Memorizer } from './Memorizer' import { Memorizer } from './Memorizer'
import path from 'path' import path from 'path'
export class WebServer { export class WebServer {
private readonly app = express() private readonly app = express()
private readonly bpManager = private readonly bpManager = BPManager.getInstance()
BPManager.getInstance()
constructor ( constructor(private readonly port = 2424) {
private readonly port = 2424
) {
this.app.set('view engine', 'ejs') this.app.set('view engine', 'ejs')
this.app.set('views', path.join(__dirname, '../views')); this.app.set('views', path.join(__dirname, '../views'))
this.app.get('/', this.getMainPage.bind(this)) this.app.get('/', this.getMainPage.bind(this))
this.app.get('/check', this.runCheckOnce.bind(this)) this.app.get('/check', this.runCheckOnce.bind(this))
this.app.get('/check_all', this.runCheckAll.bind(this)) this.app.get('/check_all', this.runCheckAll.bind(this))
@ -23,15 +20,15 @@ export class WebServer {
this.app.post('/fix', this.runFix.bind(this)) this.app.post('/fix', this.runFix.bind(this))
this.app.use(this.error404) this.app.use(this.error404)
this.app.listen(this.port, this.showBanner.bind(this)) this.app.listen(this.port, this.showBanner.bind(this))
} }
private getMainPage(req: Request, res: Response) { private getMainPage(req: Request, res: Response) {
const hidePass = req.query['hidePass'] === 'true' const hidePass = req.query['hidePass'] === 'true'
const bpStatus: { const bpStatus: {
category: string, category: string
metadatas: (BPSetMetadata&BPSetStats)[] metadatas: (BPSetMetadata & BPSetStats)[]
}[] = [] }[] = []
const bpMetadatas = this.bpManager.getBPSets().map((v, idx) => ({ ...v, idx })) const bpMetadatas = this.bpManager.getBPSets().map((v, idx) => ({ ...v, idx }))
@ -40,11 +37,11 @@ export class WebServer {
for (const category of categories) for (const category of categories)
bpStatus.push({ bpStatus.push({
category, category,
metadatas: metadatas: bpMetadatas
bpMetadatas .filter(
.filter((v) => (v) =>
v.getMetadata().awsService === category && v.getMetadata().awsService === category && (!hidePass || v.getStats().nonCompliantResources.length > 0)
(!hidePass || v.getStats().nonCompliantResources.length > 0)) )
.map((v) => ({ ...v.getMetadata(), ...v.getStats(), idx: v.idx })) .map((v) => ({ ...v.getMetadata(), ...v.getStats(), idx: v.idx }))
}) })
@ -93,8 +90,7 @@ export class WebServer {
res.write('<pre>Start Checking....\n') res.write('<pre>Start Checking....\n')
Memorizer.reset() Memorizer.reset()
await this.bpManager.runCheckAll((name) => await this.bpManager.runCheckAll((name) => res.write(`${name} - FINISHED\n`))
res.write(`${name} - FINISHED\n`))
res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page</a>`) res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page</a>`)
res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`) res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`)
@ -109,7 +105,7 @@ export class WebServer {
res.write('<body class="bg-gray-100 text-gray-800">') res.write('<body class="bg-gray-100 text-gray-800">')
res.write('<div class="container mx-auto p-4">') res.write('<div class="container mx-auto p-4">')
res.write('<pre>Start Fixing....\n') res.write('<pre>Start Fixing....\n')
const { name, hidePass } = req.query const { name, hidePass } = req.query
if (typeof name !== 'string' || name.length < 1) { if (typeof name !== 'string' || name.length < 1) {
res.write('<a href="/">Failed. name not found. Return to Report Page') res.write('<a href="/">Failed. name not found. Return to Report Page')
@ -117,27 +113,24 @@ export class WebServer {
return return
} }
const requiredParametersForFix = const requiredParametersForFix = Object.keys(req.body).map((k) => ({ name: k, value: req.body[k] }))
Object
.keys(req.body)
.map((k) => ({ name: k, value: req.body[k] }))
await this.bpManager.runFix(name, requiredParametersForFix) await this.bpManager.runFix(name, requiredParametersForFix).catch((error) => {
.catch((error) => { res.write(error.toString() + '\n')
res.write(error.toString() + '\n') })
})
res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page`) res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page`)
res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`) res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`)
res.end() res.end()
} }
private error404 (_: Request, res: Response) { private error404(_: Request, res: Response) {
res.status(404).send({ success: false, message: 'Page not found' }) res.status(404).send({ success: false, message: 'Page not found' })
} }
private showBanner() { private showBanner() {
console.log(` console.log(
`
_______ _______ _______ _______ _______ _______ _______ _______ _______ _______ _______ _______
| _ || || || || || | | _ || || || || || |
@ -151,9 +144,9 @@ export class WebServer {
Server is now on http://127.0.0.1:${this.port} Server is now on http://127.0.0.1:${this.port}
` `
.split('\n') .split('\n')
.map((v) => v.replace(/ /, '')) .map((v) => v.replace(/ {6}/, ''))
.join('\n') .join('\n')
) )
} }
} }

View File

@ -2,26 +2,26 @@ import {
ElasticLoadBalancingV2Client, ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand, DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand, DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand, ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'; } from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ALBHttpDropInvalidHeaderEnabled implements BPSet { export class ALBHttpDropInvalidHeaderEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({}); private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => { private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []; return response.LoadBalancers || []
}; }
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
); )
return response.Attributes || []; return response.Attributes || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ALBHttpDropInvalidHeaderEnabled', name: 'ALBHttpDropInvalidHeaderEnabled',
@ -36,95 +36,92 @@ export class ALBHttpDropInvalidHeaderEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeLoadBalancerAttributesCommand', name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if invalid headers are dropped for ALBs.', reason: 'Verify if invalid headers are dropped for ALBs.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyLoadBalancerAttributesCommand', name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable the invalid header drop attribute on ALBs.', reason: 'Enable the invalid header drop attribute on ALBs.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure enabling this setting aligns with your application requirements.', adviseBeforeFixFunction: 'Ensure enabling this setting aligns with your application requirements.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers(); const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) { for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some( const isEnabled = attributes.some(
(attr) => (attr) => attr.Key === 'routing.http.drop_invalid_header_fields.enabled' && attr.Value === 'true'
attr.Key === 'routing.http.drop_invalid_header_fields.enabled' && attr.Value === 'true' )
);
if (isEnabled) { if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!); compliantResources.push(lb.LoadBalancerArn!)
} else { } else {
nonCompliantResources.push(lb.LoadBalancerArn!); nonCompliantResources.push(lb.LoadBalancerArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const lbArn of nonCompliantResources) { for (const lbArn of nonCompliantResources) {
await this.client.send( await this.client.send(
new ModifyLoadBalancerAttributesCommand({ new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn, LoadBalancerArn: lbArn,
Attributes: [ Attributes: [{ Key: 'routing.http.drop_invalid_header_fields.enabled', Value: 'true' }]
{ Key: 'routing.http.drop_invalid_header_fields.enabled', Value: 'true' },
],
}) })
); )
} }
}; }
} }

View File

@ -13,37 +13,36 @@ export class ALBWAFEnabled implements BPSet {
return response.LoadBalancers || [] return response.LoadBalancers || []
} }
public readonly getMetadata = () => ( public readonly getMetadata = () => ({
{ name: 'ALBWAFEnabled',
name: 'ALBWAFEnabled', description: 'Ensures that WAF is associated with ALBs.',
description: 'Ensures that WAF is associated with ALBs.', priority: 1,
priority: 1, priorityReason: 'Associating WAF with ALBs protects against common web attacks.',
priorityReason: 'Associating WAF with ALBs protects against common web attacks.', awsService: 'Elastic Load Balancing',
awsService: 'Elastic Load Balancing', awsServiceCategory: 'Application Load Balancer',
awsServiceCategory: 'Application Load Balancer', bestPracticeCategory: 'Security',
bestPracticeCategory: 'Security', requiredParametersForFix: [
requiredParametersForFix: [ {
{ name: 'web-acl-arn',
name: 'web-acl-arn', description: 'The ARN of the WAF ACL to associate with the ALB.',
description: 'The ARN of the WAF ACL to associate with the ALB.', default: '',
default: '', example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example'
example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example' }
} ],
], isFixFunctionUsesDestructiveCommand: false,
isFixFunctionUsesDestructiveCommand: false, commandUsedInCheckFunction: [
commandUsedInCheckFunction: [ {
{ name: 'GetWebAclForResourceCommand',
name: 'GetWebAclForResourceCommand', reason: 'Check if a WAF is associated with the ALB.'
reason: 'Check if a WAF is associated with the ALB.' }
} ],
], commandUsedInFixFunction: [
commandUsedInFixFunction: [ {
{ name: 'AssociateWebAclCommand',
name: 'AssociateWebAclCommand', reason: 'Associate a WAF ACL with the ALB.'
reason: 'Associate a WAF ACL with the ALB.' }
} ],
], adviseBeforeFixFunction: "Ensure the WAF ACL has the appropriate rules for the application's requirements."
adviseBeforeFixFunction: 'Ensure the WAF ACL has the appropriate rules for the application\'s requirements.'
}) })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
@ -53,8 +52,7 @@ export class ALBWAFEnabled implements BPSet {
errorMessage: [] errorMessage: []
} }
public readonly getStats = () => public readonly getStats = () => this.stats
this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = [] this.stats.compliantResources = []
@ -62,21 +60,20 @@ export class ALBWAFEnabled implements BPSet {
this.stats.status = 'LOADED' this.stats.status = 'LOADED'
this.stats.errorMessage = [] this.stats.errorMessage = []
} }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING' this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl().then(
.then( () => (this.stats.status = 'FINISHED'),
() => this.stats.status = 'FINISHED', (err) => {
(err) => { this.stats.status = 'ERROR'
this.stats.status = 'ERROR' this.stats.errorMessage.push({
this.stats.errorMessage.push({ date: new Date(),
date: new Date(), message: err
message: err })
} }
) )
})
} }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
@ -85,9 +82,7 @@ export class ALBWAFEnabled implements BPSet {
const loadBalancers = await this.getLoadBalancers() const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) { for (const lb of loadBalancers) {
const response = await this.wafClient.send( const response = await this.wafClient.send(new GetWebACLForResourceCommand({ ResourceArn: lb.LoadBalancerArn }))
new GetWebACLForResourceCommand({ ResourceArn: lb.LoadBalancerArn })
)
if (response.WebACL) { if (response.WebACL) {
compliantResources.push(lb.LoadBalancerArn!) compliantResources.push(lb.LoadBalancerArn!)
} else { } else {
@ -100,23 +95,21 @@ export class ALBWAFEnabled implements BPSet {
this.stats.status = 'FINISHED' this.stats.status = 'FINISHED'
} }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args) await this.fixImpl(...args).then(
.then( () => (this.stats.status = 'FINISHED'),
() => this.stats.status = 'FINISHED', (err) => {
(err) => { this.stats.status = 'ERROR'
this.stats.status = 'ERROR' this.stats.errorMessage.push({
this.stats.errorMessage.push({ date: new Date(),
date: new Date(), message: err
message: err })
} }
) )
})
} }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value const webAclArn = requiredParametersForFix.find((param) => param.name === 'web-acl-arn')?.value
if (!webAclArn) { if (!webAclArn) {
throw new Error("Required parameter 'web-acl-arn' is missing.") throw new Error("Required parameter 'web-acl-arn' is missing.")

View File

@ -2,26 +2,26 @@ import {
ElasticLoadBalancingV2Client, ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand, DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand, DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand, ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'; } from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ELBCrossZoneLoadBalancingEnabled implements BPSet { export class ELBCrossZoneLoadBalancingEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({}); private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => { private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []; return response.LoadBalancers || []
}; }
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
); )
return response.Attributes || []; return response.Attributes || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ELBCrossZoneLoadBalancingEnabled', name: 'ELBCrossZoneLoadBalancingEnabled',
@ -36,93 +36,92 @@ export class ELBCrossZoneLoadBalancingEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeLoadBalancerAttributesCommand', name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if cross-zone load balancing is enabled for ELBs.', reason: 'Verify if cross-zone load balancing is enabled for ELBs.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyLoadBalancerAttributesCommand', name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable cross-zone load balancing for ELBs.', reason: 'Enable cross-zone load balancing for ELBs.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure this setting aligns with your traffic distribution requirements.', adviseBeforeFixFunction: 'Ensure this setting aligns with your traffic distribution requirements.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers(); const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) { for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some( const isEnabled = attributes.some(
(attr) => (attr) => attr.Key === 'load_balancing.cross_zone.enabled' && attr.Value === 'true'
attr.Key === 'load_balancing.cross_zone.enabled' && attr.Value === 'true' )
);
if (isEnabled) { if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!); compliantResources.push(lb.LoadBalancerArn!)
} else { } else {
nonCompliantResources.push(lb.LoadBalancerArn!); nonCompliantResources.push(lb.LoadBalancerArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const lbArn of nonCompliantResources) { for (const lbArn of nonCompliantResources) {
await this.client.send( await this.client.send(
new ModifyLoadBalancerAttributesCommand({ new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn, LoadBalancerArn: lbArn,
Attributes: [{ Key: 'load_balancing.cross_zone.enabled', Value: 'true' }], Attributes: [{ Key: 'load_balancing.cross_zone.enabled', Value: 'true' }]
}) })
); )
} }
}; }
} }

View File

@ -2,26 +2,26 @@ import {
ElasticLoadBalancingV2Client, ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand, DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand, DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand, ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'; } from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ELBDeletionProtectionEnabled implements BPSet { export class ELBDeletionProtectionEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({}); private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => { private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []; return response.LoadBalancers || []
}; }
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
); )
return response.Attributes || []; return response.Attributes || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ELBDeletionProtectionEnabled', name: 'ELBDeletionProtectionEnabled',
@ -36,93 +36,90 @@ export class ELBDeletionProtectionEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeLoadBalancerAttributesCommand', name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if deletion protection is enabled for ELBs.', reason: 'Verify if deletion protection is enabled for ELBs.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyLoadBalancerAttributesCommand', name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable deletion protection for ELBs.', reason: 'Enable deletion protection for ELBs.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with resource management policies.', adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with resource management policies.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers(); const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) { for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some( const isEnabled = attributes.some((attr) => attr.Key === 'deletion_protection.enabled' && attr.Value === 'true')
(attr) =>
attr.Key === 'deletion_protection.enabled' && attr.Value === 'true'
);
if (isEnabled) { if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!); compliantResources.push(lb.LoadBalancerArn!)
} else { } else {
nonCompliantResources.push(lb.LoadBalancerArn!); nonCompliantResources.push(lb.LoadBalancerArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const lbArn of nonCompliantResources) { for (const lbArn of nonCompliantResources) {
await this.client.send( await this.client.send(
new ModifyLoadBalancerAttributesCommand({ new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn, LoadBalancerArn: lbArn,
Attributes: [{ Key: 'deletion_protection.enabled', Value: 'true' }], Attributes: [{ Key: 'deletion_protection.enabled', Value: 'true' }]
}) })
); )
} }
}; }
} }

View File

@ -2,26 +2,26 @@ import {
ElasticLoadBalancingV2Client, ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand, DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand, DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand, ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'; } from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ELBLoggingEnabled implements BPSet { export class ELBLoggingEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({}); private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getLoadBalancers = async () => { private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []; return response.LoadBalancers || []
}; }
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
); )
return response.Attributes || []; return response.Attributes || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ELBLoggingEnabled', name: 'ELBLoggingEnabled',
@ -32,97 +32,100 @@ export class ELBLoggingEnabled implements BPSet {
awsServiceCategory: 'Classic Load Balancer', awsServiceCategory: 'Classic Load Balancer',
bestPracticeCategory: 'Logging and Monitoring', bestPracticeCategory: 'Logging and Monitoring',
requiredParametersForFix: [ requiredParametersForFix: [
{ name: 's3-bucket-name', description: 'The S3 bucket for storing access logs.', default: '', example: 'my-log-bucket' }, {
{ name: 's3-prefix', description: 'The S3 prefix for the access logs.', default: '', example: 'elb/logs/' }, name: 's3-bucket-name',
description: 'The S3 bucket for storing access logs.',
default: '',
example: 'my-log-bucket'
},
{ name: 's3-prefix', description: 'The S3 prefix for the access logs.', default: '', example: 'elb/logs/' }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeLoadBalancerAttributesCommand', name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if access logging is enabled for ELBs.', reason: 'Verify if access logging is enabled for ELBs.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyLoadBalancerAttributesCommand', name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable access logging for ELBs and set S3 bucket and prefix.', reason: 'Enable access logging for ELBs and set S3 bucket and prefix.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the specified S3 bucket and prefix exist and are accessible.', adviseBeforeFixFunction: 'Ensure the specified S3 bucket and prefix exist and are accessible.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers(); const loadBalancers = await this.getLoadBalancers()
for (const lb of loadBalancers) { for (const lb of loadBalancers) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const isEnabled = attributes.some( const isEnabled = attributes.some((attr) => attr.Key === 'access_logs.s3.enabled' && attr.Value === 'true')
(attr) => attr.Key === 'access_logs.s3.enabled' && attr.Value === 'true'
);
if (isEnabled) { if (isEnabled) {
compliantResources.push(lb.LoadBalancerArn!); compliantResources.push(lb.LoadBalancerArn!)
} else { } else {
nonCompliantResources.push(lb.LoadBalancerArn!); nonCompliantResources.push(lb.LoadBalancerArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const bucketName = requiredParametersForFix.find((param) => param.name === 's3-bucket-name')?.value; const bucketName = requiredParametersForFix.find((param) => param.name === 's3-bucket-name')?.value
const bucketPrefix = requiredParametersForFix.find((param) => param.name === 's3-prefix')?.value; const bucketPrefix = requiredParametersForFix.find((param) => param.name === 's3-prefix')?.value
if (!bucketName || !bucketPrefix) { if (!bucketName || !bucketPrefix) {
throw new Error("Required parameters 's3-bucket-name' and/or 's3-prefix' are missing."); throw new Error("Required parameters 's3-bucket-name' and/or 's3-prefix' are missing.")
} }
for (const lbArn of nonCompliantResources) { for (const lbArn of nonCompliantResources) {
@ -132,10 +135,10 @@ export class ELBLoggingEnabled implements BPSet {
Attributes: [ Attributes: [
{ Key: 'access_logs.s3.enabled', Value: 'true' }, { Key: 'access_logs.s3.enabled', Value: 'true' },
{ Key: 'access_logs.s3.bucket', Value: bucketName }, { Key: 'access_logs.s3.bucket', Value: bucketName },
{ Key: 'access_logs.s3.prefix', Value: bucketPrefix }, { Key: 'access_logs.s3.prefix', Value: bucketPrefix }
], ]
}) })
); )
} }
}; }
} }

View File

@ -1,30 +1,22 @@
import { import { ApiGatewayV2Client, GetApisCommand, GetStagesCommand } from '@aws-sdk/client-apigatewayv2'
ApiGatewayV2Client, import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2'
GetApisCommand, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
GetStagesCommand, import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-apigatewayv2';
import {
WAFV2Client,
GetWebACLForResourceCommand,
AssociateWebACLCommand,
} from '@aws-sdk/client-wafv2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class APIGatewayAssociatedWithWAF implements BPSet { export class APIGatewayAssociatedWithWAF implements BPSet {
private readonly client = new ApiGatewayV2Client({}); private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly wafClient = Memorizer.memo(new WAFV2Client({})); private readonly wafClient = Memorizer.memo(new WAFV2Client({}))
private readonly getHttpApis = async () => { private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({})); const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []; return response.Items || []
}; }
private readonly getStages = async (apiId: string) => { private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })); const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []; return response.Items || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'APIGatewayAssociatedWithWAF', name: 'APIGatewayAssociatedWithWAF',
@ -39,108 +31,106 @@ export class APIGatewayAssociatedWithWAF implements BPSet {
name: 'web-acl-arn', name: 'web-acl-arn',
description: 'The ARN of the WAF ACL to associate with the API Gateway stage.', description: 'The ARN of the WAF ACL to associate with the API Gateway stage.',
default: '', default: '',
example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example', example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetWebACLForResourceCommand', name: 'GetWebACLForResourceCommand',
reason: 'Verify if a WAF is associated with the API Gateway stage.', reason: 'Verify if a WAF is associated with the API Gateway stage.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'AssociateWebACLCommand', name: 'AssociateWebACLCommand',
reason: 'Associate a WAF ACL with the API Gateway stage.', reason: 'Associate a WAF ACL with the API Gateway stage.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the WAF ACL has the appropriate rules for the application\'s requirements.', adviseBeforeFixFunction: "Ensure the WAF ACL has the appropriate rules for the application's requirements."
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const apis = await this.getHttpApis(); const apis = await this.getHttpApis()
for (const api of apis) { for (const api of apis) {
const stages = await this.getStages(api.ApiId!); const stages = await this.getStages(api.ApiId!)
for (const stage of stages) { for (const stage of stages) {
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`; const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`
const response = await this.wafClient.send( const response = await this.wafClient.send(new GetWebACLForResourceCommand({ ResourceArn: stageArn }))
new GetWebACLForResourceCommand({ ResourceArn: stageArn })
);
if (response.WebACL) { if (response.WebACL) {
compliantResources.push(stageArn); compliantResources.push(stageArn)
} else { } else {
nonCompliantResources.push(stageArn); nonCompliantResources.push(stageArn)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const webAclArn = requiredParametersForFix.find((param) => param.name === 'web-acl-arn')?.value; const webAclArn = requiredParametersForFix.find((param) => param.name === 'web-acl-arn')?.value
if (!webAclArn) { if (!webAclArn) {
throw new Error("Required parameter 'web-acl-arn' is missing."); throw new Error("Required parameter 'web-acl-arn' is missing.")
} }
for (const stageArn of nonCompliantResources) { for (const stageArn of nonCompliantResources) {
await this.wafClient.send( await this.wafClient.send(
new AssociateWebACLCommand({ new AssociateWebACLCommand({
ResourceArn: stageArn, ResourceArn: stageArn,
WebACLArn: webAclArn, WebACLArn: webAclArn
}) })
); )
} }
}; }
} }

View File

@ -1,25 +1,20 @@
import { import { ApiGatewayV2Client, GetApisCommand, GetStagesCommand, UpdateStageCommand } from '@aws-sdk/client-apigatewayv2'
ApiGatewayV2Client, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
GetApisCommand, import { Memorizer } from '../../Memorizer'
GetStagesCommand,
UpdateStageCommand,
} from '@aws-sdk/client-apigatewayv2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class APIGatewayExecutionLoggingEnabled implements BPSet { export class APIGatewayExecutionLoggingEnabled implements BPSet {
private readonly client = new ApiGatewayV2Client({}); private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getHttpApis = async () => { private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({})); const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []; return response.Items || []
}; }
private readonly getStages = async (apiId: string) => { private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })); const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []; return response.Items || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'APIGatewayExecutionLoggingEnabled', name: 'APIGatewayExecutionLoggingEnabled',
@ -34,103 +29,101 @@ export class APIGatewayExecutionLoggingEnabled implements BPSet {
name: 'log-destination-arn', name: 'log-destination-arn',
description: 'The ARN of the CloudWatch log group for storing API Gateway logs.', description: 'The ARN of the CloudWatch log group for storing API Gateway logs.',
default: '', default: '',
example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs', example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetStagesCommand', name: 'GetStagesCommand',
reason: 'Verify if execution logging is enabled for API Gateway stages.', reason: 'Verify if execution logging is enabled for API Gateway stages.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateStageCommand', name: 'UpdateStageCommand',
reason: 'Enable execution logging for API Gateway stages and set the destination log group.', reason: 'Enable execution logging for API Gateway stages and set the destination log group.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the CloudWatch log group exists and has the appropriate permissions.', adviseBeforeFixFunction: 'Ensure the CloudWatch log group exists and has the appropriate permissions.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const apis = await this.getHttpApis(); const apis = await this.getHttpApis()
for (const api of apis) { for (const api of apis) {
const stages = await this.getStages(api.ApiId!); const stages = await this.getStages(api.ApiId!)
for (const stage of stages) { for (const stage of stages) {
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`; const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`
const loggingLevel = stage.AccessLogSettings?.Format; const loggingLevel = stage.AccessLogSettings?.Format
if (loggingLevel && loggingLevel !== 'OFF') { if (loggingLevel && loggingLevel !== 'OFF') {
compliantResources.push(stageArn); compliantResources.push(stageArn)
} else { } else {
nonCompliantResources.push(stageArn); nonCompliantResources.push(stageArn)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const logDestinationArn = requiredParametersForFix.find( const logDestinationArn = requiredParametersForFix.find((param) => param.name === 'log-destination-arn')?.value
(param) => param.name === 'log-destination-arn'
)?.value;
if (!logDestinationArn) { if (!logDestinationArn) {
throw new Error("Required parameter 'log-destination-arn' is missing."); throw new Error("Required parameter 'log-destination-arn' is missing.")
} }
for (const stageArn of nonCompliantResources) { for (const stageArn of nonCompliantResources) {
const [apiId, stageName] = stageArn.split('/').slice(-2); const [apiId, stageName] = stageArn.split('/').slice(-2)
await this.client.send( await this.client.send(
new UpdateStageCommand({ new UpdateStageCommand({
@ -138,10 +131,10 @@ export class APIGatewayExecutionLoggingEnabled implements BPSet {
StageName: stageName, StageName: stageName,
AccessLogSettings: { AccessLogSettings: {
DestinationArn: logDestinationArn, DestinationArn: logDestinationArn,
Format: '$context.requestId', Format: '$context.requestId'
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,25 +1,20 @@
import { import { ApiGatewayV2Client, GetApisCommand, GetStagesCommand, UpdateStageCommand } from '@aws-sdk/client-apigatewayv2'
ApiGatewayV2Client, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
GetApisCommand, import { Memorizer } from '../../Memorizer'
GetStagesCommand,
UpdateStageCommand,
} from '@aws-sdk/client-apigatewayv2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class APIGatewayV2AccessLogsEnabled implements BPSet { export class APIGatewayV2AccessLogsEnabled implements BPSet {
private readonly client = new ApiGatewayV2Client({}); private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getHttpApis = async () => { private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({})); const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []; return response.Items || []
}; }
private readonly getStages = async (apiId: string) => { private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })); const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []; return response.Items || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'APIGatewayV2AccessLogsEnabled', name: 'APIGatewayV2AccessLogsEnabled',
@ -34,104 +29,102 @@ export class APIGatewayV2AccessLogsEnabled implements BPSet {
name: 'log-destination-arn', name: 'log-destination-arn',
description: 'The ARN of the CloudWatch log group for storing API Gateway logs.', description: 'The ARN of the CloudWatch log group for storing API Gateway logs.',
default: '', default: '',
example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs', example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetStagesCommand', name: 'GetStagesCommand',
reason: 'Verify if access logging is enabled for API Gateway stages.', reason: 'Verify if access logging is enabled for API Gateway stages.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateStageCommand', name: 'UpdateStageCommand',
reason: 'Enable access logging for API Gateway stages and set the destination log group.', reason: 'Enable access logging for API Gateway stages and set the destination log group.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the CloudWatch log group exists and has the appropriate permissions.', adviseBeforeFixFunction: 'Ensure the CloudWatch log group exists and has the appropriate permissions.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const apis = await this.getHttpApis(); const apis = await this.getHttpApis()
for (const api of apis) { for (const api of apis) {
const stages = await this.getStages(api.ApiId!); const stages = await this.getStages(api.ApiId!)
for (const stage of stages) { for (const stage of stages) {
const stageIdentifier = `${api.Name!} / ${stage.StageName!}`; const stageIdentifier = `${api.Name!} / ${stage.StageName!}`
if (!stage.AccessLogSettings) { if (!stage.AccessLogSettings) {
nonCompliantResources.push(stageIdentifier); nonCompliantResources.push(stageIdentifier)
} else { } else {
compliantResources.push(stageIdentifier); compliantResources.push(stageIdentifier)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const logDestinationArn = requiredParametersForFix.find( const logDestinationArn = requiredParametersForFix.find((param) => param.name === 'log-destination-arn')?.value
(param) => param.name === 'log-destination-arn'
)?.value;
if (!logDestinationArn) { if (!logDestinationArn) {
throw new Error("Required parameter 'log-destination-arn' is missing."); throw new Error("Required parameter 'log-destination-arn' is missing.")
} }
for (const resource of nonCompliantResources) { for (const resource of nonCompliantResources) {
const [apiName, stageName] = resource.split(' / '); const [apiName, stageName] = resource.split(' / ')
const api = (await this.getHttpApis()).find((a) => a.Name === apiName); const api = (await this.getHttpApis()).find((a) => a.Name === apiName)
if (!api) continue; if (!api) continue
await this.client.send( await this.client.send(
new UpdateStageCommand({ new UpdateStageCommand({
@ -139,10 +132,10 @@ export class APIGatewayV2AccessLogsEnabled implements BPSet {
StageName: stageName, StageName: stageName,
AccessLogSettings: { AccessLogSettings: {
DestinationArn: logDestinationArn, DestinationArn: logDestinationArn,
Format: '$context.requestId', Format: '$context.requestId'
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,25 +1,20 @@
import { import { ApiGatewayV2Client, GetApisCommand, GetRoutesCommand, UpdateRouteCommand } from '@aws-sdk/client-apigatewayv2'
ApiGatewayV2Client, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
GetApisCommand, import { Memorizer } from '../../Memorizer'
GetRoutesCommand,
UpdateRouteCommand,
} from '@aws-sdk/client-apigatewayv2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class APIGatewayV2AuthorizationTypeConfigured implements BPSet { export class APIGatewayV2AuthorizationTypeConfigured implements BPSet {
private readonly client = new ApiGatewayV2Client({}); private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getHttpApis = async () => { private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({})); const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []; return response.Items || []
}; }
private readonly getRoutes = async (apiId: string) => { private readonly getRoutes = async (apiId: string) => {
const response = await this.memoClient.send(new GetRoutesCommand({ ApiId: apiId })); const response = await this.memoClient.send(new GetRoutesCommand({ ApiId: apiId }))
return response.Items || []; return response.Items || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'APIGatewayV2AuthorizationTypeConfigured', name: 'APIGatewayV2AuthorizationTypeConfigured',
@ -34,117 +29,115 @@ export class APIGatewayV2AuthorizationTypeConfigured implements BPSet {
name: 'authorization-type', name: 'authorization-type',
description: 'The authorization type to configure for the routes.', description: 'The authorization type to configure for the routes.',
default: '', default: '',
example: 'JWT', example: 'JWT'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetRoutesCommand', name: 'GetRoutesCommand',
reason: 'Verify if authorization type is configured for API Gateway routes.', reason: 'Verify if authorization type is configured for API Gateway routes.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateRouteCommand', name: 'UpdateRouteCommand',
reason: 'Set the authorization type for API Gateway routes.', reason: 'Set the authorization type for API Gateway routes.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the chosen authorization type aligns with application requirements.', adviseBeforeFixFunction: 'Ensure the chosen authorization type aligns with application requirements.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const apis = await this.getHttpApis(); const apis = await this.getHttpApis()
for (const api of apis) { for (const api of apis) {
const routes = await this.getRoutes(api.ApiId!); const routes = await this.getRoutes(api.ApiId!)
for (const route of routes) { for (const route of routes) {
const routeIdentifier = `${api.Name!} / ${route.RouteKey!}`; const routeIdentifier = `${api.Name!} / ${route.RouteKey!}`
if (route.AuthorizationType === 'NONE') { if (route.AuthorizationType === 'NONE') {
nonCompliantResources.push(routeIdentifier); nonCompliantResources.push(routeIdentifier)
} else { } else {
compliantResources.push(routeIdentifier); compliantResources.push(routeIdentifier)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const authorizationType = requiredParametersForFix.find( const authorizationType = requiredParametersForFix.find((param) => param.name === 'authorization-type')?.value
(param) => param.name === 'authorization-type'
)?.value;
if (!authorizationType) { if (!authorizationType) {
throw new Error("Required parameter 'authorization-type' is missing."); throw new Error("Required parameter 'authorization-type' is missing.")
} }
for (const resource of nonCompliantResources) { for (const resource of nonCompliantResources) {
const [apiName, routeKey] = resource.split(' / '); const [apiName, routeKey] = resource.split(' / ')
const api = (await this.getHttpApis()).find((a) => a.Name === apiName); const api = (await this.getHttpApis()).find((a) => a.Name === apiName)
if (!api) continue; if (!api) continue
const routes = await this.getRoutes(api.ApiId!); const routes = await this.getRoutes(api.ApiId!)
const route = routes.find((r) => r.RouteKey === routeKey); const route = routes.find((r) => r.RouteKey === routeKey)
if (!route) continue; if (!route) continue
await this.client.send( await this.client.send(
new UpdateRouteCommand({ new UpdateRouteCommand({
ApiId: api.ApiId!, ApiId: api.ApiId!,
RouteId: route.RouteId!, RouteId: route.RouteId!,
AuthorizationType: authorizationType as any, AuthorizationType: authorizationType as unknown
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,19 @@
import { import {
AutoScalingClient, AutoScalingClient,
DescribeAutoScalingGroupsCommand, DescribeAutoScalingGroupsCommand,
UpdateAutoScalingGroupCommand, UpdateAutoScalingGroupCommand
} from '@aws-sdk/client-auto-scaling'; } from '@aws-sdk/client-auto-scaling'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class AutoScalingGroupELBHealthCheckRequired implements BPSet { export class AutoScalingGroupELBHealthCheckRequired implements BPSet {
private readonly client = new AutoScalingClient({}); private readonly client = new AutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getAutoScalingGroups = async () => { private readonly getAutoScalingGroups = async () => {
const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})); const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({}))
return response.AutoScalingGroups || []; return response.AutoScalingGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'AutoScalingGroupELBHealthCheckRequired', name: 'AutoScalingGroupELBHealthCheckRequired',
@ -28,91 +28,88 @@ export class AutoScalingGroupELBHealthCheckRequired implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeAutoScalingGroupsCommand', name: 'DescribeAutoScalingGroupsCommand',
reason: 'Retrieve Auto Scaling groups to check health check type.', reason: 'Retrieve Auto Scaling groups to check health check type.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateAutoScalingGroupCommand', name: 'UpdateAutoScalingGroupCommand',
reason: 'Set the health check type to ELB for Auto Scaling groups.', reason: 'Set the health check type to ELB for Auto Scaling groups.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that the Auto Scaling group is associated with a functional ELB or Target Group.', adviseBeforeFixFunction: 'Ensure that the Auto Scaling group is associated with a functional ELB or Target Group.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const asgs = await this.getAutoScalingGroups(); const asgs = await this.getAutoScalingGroups()
for (const asg of asgs) { for (const asg of asgs) {
if ( if ((asg.LoadBalancerNames?.length || asg.TargetGroupARNs?.length) && asg.HealthCheckType !== 'ELB') {
(asg.LoadBalancerNames?.length || asg.TargetGroupARNs?.length) && nonCompliantResources.push(asg.AutoScalingGroupARN!)
asg.HealthCheckType !== 'ELB'
) {
nonCompliantResources.push(asg.AutoScalingGroupARN!);
} else { } else {
compliantResources.push(asg.AutoScalingGroupARN!); compliantResources.push(asg.AutoScalingGroupARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const asgArn of nonCompliantResources) { for (const asgArn of nonCompliantResources) {
const asgName = asgArn.split(':').pop()!; const asgName = asgArn.split(':').pop()!
await this.client.send( await this.client.send(
new UpdateAutoScalingGroupCommand({ new UpdateAutoScalingGroupCommand({
AutoScalingGroupName: asgName, AutoScalingGroupName: asgName,
HealthCheckType: 'ELB', HealthCheckType: 'ELB'
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,19 @@
import { import {
AutoScalingClient, AutoScalingClient,
DescribeAutoScalingGroupsCommand, DescribeAutoScalingGroupsCommand,
UpdateAutoScalingGroupCommand, UpdateAutoScalingGroupCommand
} from '@aws-sdk/client-auto-scaling'; } from '@aws-sdk/client-auto-scaling'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class AutoScalingLaunchTemplate implements BPSet { export class AutoScalingLaunchTemplate implements BPSet {
private readonly client = new AutoScalingClient({}); private readonly client = new AutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getAutoScalingGroups = async () => { private readonly getAutoScalingGroups = async () => {
const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})); const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({}))
return response.AutoScalingGroups || []; return response.AutoScalingGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'AutoScalingLaunchTemplate', name: 'AutoScalingLaunchTemplate',
@ -24,107 +24,116 @@ export class AutoScalingLaunchTemplate implements BPSet {
awsServiceCategory: 'Compute', awsServiceCategory: 'Compute',
bestPracticeCategory: 'Management', bestPracticeCategory: 'Management',
requiredParametersForFix: [ requiredParametersForFix: [
{ name: 'launch-template-id', description: 'The ID of the launch template to associate.', default: '', example: 'lt-0abcd1234efgh5678' }, {
{ name: 'version', description: 'The version of the launch template to use.', default: '$Default', example: '$Default' }, name: 'launch-template-id',
description: 'The ID of the launch template to associate.',
default: '',
example: 'lt-0abcd1234efgh5678'
},
{
name: 'version',
description: 'The version of the launch template to use.',
default: '$Default',
example: '$Default'
}
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeAutoScalingGroupsCommand', name: 'DescribeAutoScalingGroupsCommand',
reason: 'Retrieve Auto Scaling groups to check for launch template usage.', reason: 'Retrieve Auto Scaling groups to check for launch template usage.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateAutoScalingGroupCommand', name: 'UpdateAutoScalingGroupCommand',
reason: 'Associate the Auto Scaling group with the specified launch template.', reason: 'Associate the Auto Scaling group with the specified launch template.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the launch template is configured properly before associating it with an Auto Scaling group.', adviseBeforeFixFunction:
}); 'Ensure the launch template is configured properly before associating it with an Auto Scaling group.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const asgs = await this.getAutoScalingGroups(); const asgs = await this.getAutoScalingGroups()
for (const asg of asgs) { for (const asg of asgs) {
if (asg.LaunchConfigurationName) { if (asg.LaunchConfigurationName) {
nonCompliantResources.push(asg.AutoScalingGroupARN!); nonCompliantResources.push(asg.AutoScalingGroupARN!)
} else { } else {
compliantResources.push(asg.AutoScalingGroupARN!); compliantResources.push(asg.AutoScalingGroupARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const launchTemplateId = requiredParametersForFix.find( const launchTemplateId = requiredParametersForFix.find((param) => param.name === 'launch-template-id')?.value
(param) => param.name === 'launch-template-id' const version = requiredParametersForFix.find((param) => param.name === 'version')?.value
)?.value;
const version = requiredParametersForFix.find((param) => param.name === 'version')?.value;
if (!launchTemplateId || !version) { if (!launchTemplateId || !version) {
throw new Error("Required parameters 'launch-template-id' and/or 'version' are missing."); throw new Error("Required parameters 'launch-template-id' and/or 'version' are missing.")
} }
for (const asgArn of nonCompliantResources) { for (const asgArn of nonCompliantResources) {
const asgName = asgArn.split(':').pop()!; const asgName = asgArn.split(':').pop()!
await this.client.send( await this.client.send(
new UpdateAutoScalingGroupCommand({ new UpdateAutoScalingGroupCommand({
AutoScalingGroupName: asgName, AutoScalingGroupName: asgName,
LaunchTemplate: { LaunchTemplate: {
LaunchTemplateId: launchTemplateId, LaunchTemplateId: launchTemplateId,
Version: version, Version: version
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,19 @@
import { import {
AutoScalingClient, AutoScalingClient,
DescribeAutoScalingGroupsCommand, DescribeAutoScalingGroupsCommand,
UpdateAutoScalingGroupCommand, UpdateAutoScalingGroupCommand
} from '@aws-sdk/client-auto-scaling'; } from '@aws-sdk/client-auto-scaling'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class AutoScalingMultipleAZ implements BPSet { export class AutoScalingMultipleAZ implements BPSet {
private readonly client = new AutoScalingClient({}); private readonly client = new AutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getAutoScalingGroups = async () => { private readonly getAutoScalingGroups = async () => {
const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})); const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({}))
return response.AutoScalingGroups || []; return response.AutoScalingGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'AutoScalingMultipleAZ', name: 'AutoScalingMultipleAZ',
@ -28,103 +28,102 @@ export class AutoScalingMultipleAZ implements BPSet {
name: 'availability-zones', name: 'availability-zones',
description: 'Comma-separated list of Availability Zones to assign to the Auto Scaling group.', description: 'Comma-separated list of Availability Zones to assign to the Auto Scaling group.',
default: '', default: '',
example: 'us-east-1a,us-east-1b', example: 'us-east-1a,us-east-1b'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeAutoScalingGroupsCommand', name: 'DescribeAutoScalingGroupsCommand',
reason: 'Retrieve Auto Scaling groups to verify their Availability Zone configuration.', reason: 'Retrieve Auto Scaling groups to verify their Availability Zone configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateAutoScalingGroupCommand', name: 'UpdateAutoScalingGroupCommand',
reason: 'Update the Auto Scaling group to use the specified Availability Zones.', reason: 'Update the Auto Scaling group to use the specified Availability Zones.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that the specified Availability Zones are correctly configured in your infrastructure.', adviseBeforeFixFunction:
}); 'Ensure that the specified Availability Zones are correctly configured in your infrastructure.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const asgs = await this.getAutoScalingGroups(); const asgs = await this.getAutoScalingGroups()
for (const asg of asgs) { for (const asg of asgs) {
if (asg.AvailabilityZones?.length! > 1) { if ((asg.AvailabilityZones?.length ?? 0) > 1) {
compliantResources.push(asg.AutoScalingGroupARN!); compliantResources.push(asg.AutoScalingGroupARN!)
} else { } else {
nonCompliantResources.push(asg.AutoScalingGroupARN!); nonCompliantResources.push(asg.AutoScalingGroupARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const availabilityZones = requiredParametersForFix.find( const availabilityZones = requiredParametersForFix.find((param) => param.name === 'availability-zones')?.value
(param) => param.name === 'availability-zones'
)?.value;
if (!availabilityZones) { if (!availabilityZones) {
throw new Error("Required parameter 'availability-zones' is missing."); throw new Error("Required parameter 'availability-zones' is missing.")
} }
for (const asgArn of nonCompliantResources) { for (const asgArn of nonCompliantResources) {
const asgName = asgArn.split(':').pop()!; const asgName = asgArn.split(':').pop()!
await this.client.send( await this.client.send(
new UpdateAutoScalingGroupCommand({ new UpdateAutoScalingGroupCommand({
AutoScalingGroupName: asgName, AutoScalingGroupName: asgName,
AvailabilityZones: availabilityZones.split(','), AvailabilityZones: availabilityZones.split(',')
}) })
); )
} }
}; }
} }

View File

@ -2,29 +2,27 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand, UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'; } from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CloudFrontAccessLogsEnabled implements BPSet { export class CloudFrontAccessLogsEnabled implements BPSet {
private readonly client = new CloudFrontClient({}); private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => { private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({})); const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []; return response.DistributionList?.Items || []
}; }
private readonly getDistributionDetails = async (distributionId: string) => { private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetDistributionCommand({ Id: distributionId }))
new GetDistributionCommand({ Id: distributionId })
);
return { return {
distribution: response.Distribution!, distribution: response.Distribution!,
etag: response.ETag!, etag: response.ETag!
}; }
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CloudFrontAccessLogsEnabled', name: 'CloudFrontAccessLogsEnabled',
@ -39,103 +37,101 @@ export class CloudFrontAccessLogsEnabled implements BPSet {
name: 'log-bucket-name', name: 'log-bucket-name',
description: 'The S3 bucket name for storing access logs.', description: 'The S3 bucket name for storing access logs.',
default: '', default: '',
example: 'my-cloudfront-logs-bucket.s3.amazonaws.com', example: 'my-cloudfront-logs-bucket.s3.amazonaws.com'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListDistributionsCommand', name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions.', reason: 'List all CloudFront distributions.'
}, },
{ {
name: 'GetDistributionCommand', name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to check logging settings.', reason: 'Retrieve distribution details to check logging settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateDistributionCommand', name: 'UpdateDistributionCommand',
reason: 'Enable logging and update distribution settings.', reason: 'Enable logging and update distribution settings.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the specified S3 bucket exists and has proper permissions for CloudFront logging.', adviseBeforeFixFunction: 'Ensure the specified S3 bucket exists and has proper permissions for CloudFront logging.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const distributions = await this.getDistributions(); const distributions = await this.getDistributions()
for (const distribution of distributions) { for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
if (details.DistributionConfig?.Logging?.Enabled) { if (details.DistributionConfig?.Logging?.Enabled) {
compliantResources.push(details.ARN!); compliantResources.push(details.ARN!)
} else { } else {
nonCompliantResources.push(details.ARN!); nonCompliantResources.push(details.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const logBucketName = requiredParametersForFix.find( const logBucketName = requiredParametersForFix.find((param) => param.name === 'log-bucket-name')?.value
(param) => param.name === 'log-bucket-name'
)?.value;
if (!logBucketName) { if (!logBucketName) {
throw new Error("Required parameter 'log-bucket-name' is missing."); throw new Error("Required parameter 'log-bucket-name' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!; const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId); const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = { const updatedConfig = {
...distribution.DistributionConfig, ...distribution.DistributionConfig,
@ -143,17 +139,17 @@ export class CloudFrontAccessLogsEnabled implements BPSet {
Enabled: true, Enabled: true,
Bucket: logBucketName, Bucket: logBucketName,
IncludeCookies: false, IncludeCookies: false,
Prefix: '', Prefix: ''
}, }
}; }
await this.client.send( await this.client.send(
new UpdateDistributionCommand({ new UpdateDistributionCommand({
Id: distributionId, Id: distributionId,
IfMatch: etag, IfMatch: etag,
DistributionConfig: updatedConfig as any, DistributionConfig: updatedConfig as unknown
}) })
); )
} }
}; }
} }

View File

@ -2,29 +2,27 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand, UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'; } from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CloudFrontAssociatedWithWAF implements BPSet { export class CloudFrontAssociatedWithWAF implements BPSet {
private readonly client = new CloudFrontClient({}); private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => { private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({})); const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []; return response.DistributionList?.Items || []
}; }
private readonly getDistributionDetails = async (distributionId: string) => { private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetDistributionCommand({ Id: distributionId }))
new GetDistributionCommand({ Id: distributionId })
);
return { return {
distribution: response.Distribution!, distribution: response.Distribution!,
etag: response.ETag!, etag: response.ETag!
}; }
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CloudFrontAssociatedWithWAF', name: 'CloudFrontAssociatedWithWAF',
@ -39,111 +37,109 @@ export class CloudFrontAssociatedWithWAF implements BPSet {
name: 'web-acl-id', name: 'web-acl-id',
description: 'The ID of the Web ACL to associate with the CloudFront distribution.', description: 'The ID of the Web ACL to associate with the CloudFront distribution.',
default: '', default: '',
example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example', example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListDistributionsCommand', name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for WAF association.', reason: 'List all CloudFront distributions to check for WAF association.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateDistributionCommand', name: 'UpdateDistributionCommand',
reason: 'Associate the specified WAF with the CloudFront distribution.', reason: 'Associate the specified WAF with the CloudFront distribution.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the Web ACL is configured correctly for the applications security requirements.', adviseBeforeFixFunction: 'Ensure the Web ACL is configured correctly for the applications security requirements.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const distributions = await this.getDistributions(); const distributions = await this.getDistributions()
for (const distribution of distributions) { for (const distribution of distributions) {
if (distribution.WebACLId && distribution.WebACLId !== '') { if (distribution.WebACLId && distribution.WebACLId !== '') {
compliantResources.push(distribution.ARN!); compliantResources.push(distribution.ARN!)
} else { } else {
nonCompliantResources.push(distribution.ARN!); nonCompliantResources.push(distribution.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const webAclId = requiredParametersForFix.find( const webAclId = requiredParametersForFix.find((param) => param.name === 'web-acl-id')?.value
(param) => param.name === 'web-acl-id'
)?.value;
if (!webAclId) { if (!webAclId) {
throw new Error("Required parameter 'web-acl-id' is missing."); throw new Error("Required parameter 'web-acl-id' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!; const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId); const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = { const updatedConfig = {
...distribution.DistributionConfig, ...distribution.DistributionConfig,
WebACLId: webAclId, WebACLId: webAclId
}; }
await this.client.send( await this.client.send(
new UpdateDistributionCommand({ new UpdateDistributionCommand({
Id: distributionId, Id: distributionId,
IfMatch: etag, IfMatch: etag,
DistributionConfig: updatedConfig as any, DistributionConfig: updatedConfig as unknown
}) })
); )
} }
}; }
} }

View File

@ -2,35 +2,34 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand, UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'; } from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CloudFrontDefaultRootObjectConfigured implements BPSet { export class CloudFrontDefaultRootObjectConfigured implements BPSet {
private readonly client = new CloudFrontClient({}); private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => { private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({})); const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []; return response.DistributionList?.Items || []
}; }
private readonly getDistributionDetails = async (distributionId: string) => { private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetDistributionCommand({ Id: distributionId }))
new GetDistributionCommand({ Id: distributionId })
);
return { return {
distribution: response.Distribution!, distribution: response.Distribution!,
etag: response.ETag!, etag: response.ETag!
}; }
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CloudFrontDefaultRootObjectConfigured', name: 'CloudFrontDefaultRootObjectConfigured',
description: 'Ensures that CloudFront distributions have a default root object configured.', description: 'Ensures that CloudFront distributions have a default root object configured.',
priority: 3, priority: 3,
priorityReason: 'A default root object ensures users access the correct content when navigating to the distribution domain.', priorityReason:
'A default root object ensures users access the correct content when navigating to the distribution domain.',
awsService: 'CloudFront', awsService: 'CloudFront',
awsServiceCategory: 'CDN', awsServiceCategory: 'CDN',
bestPracticeCategory: 'Configuration', bestPracticeCategory: 'Configuration',
@ -39,116 +38,114 @@ export class CloudFrontDefaultRootObjectConfigured implements BPSet {
name: 'default-root-object', name: 'default-root-object',
description: 'The default root object for the CloudFront distribution.', description: 'The default root object for the CloudFront distribution.',
default: '', default: '',
example: 'index.html', example: 'index.html'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListDistributionsCommand', name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for a default root object.', reason: 'List all CloudFront distributions to check for a default root object.'
}, },
{ {
name: 'GetDistributionCommand', name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to verify the default root object setting.', reason: 'Retrieve distribution details to verify the default root object setting.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateDistributionCommand', name: 'UpdateDistributionCommand',
reason: 'Set the default root object for the CloudFront distribution.', reason: 'Set the default root object for the CloudFront distribution.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the default root object exists in the origin to avoid 404 errors.', adviseBeforeFixFunction: 'Ensure the default root object exists in the origin to avoid 404 errors.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const distributions = await this.getDistributions(); const distributions = await this.getDistributions()
for (const distribution of distributions) { for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
if (details.DistributionConfig?.DefaultRootObject !== '') { if (details.DistributionConfig?.DefaultRootObject !== '') {
compliantResources.push(details.ARN!); compliantResources.push(details.ARN!)
} else { } else {
nonCompliantResources.push(details.ARN!); nonCompliantResources.push(details.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const defaultRootObject = requiredParametersForFix.find( const defaultRootObject = requiredParametersForFix.find((param) => param.name === 'default-root-object')?.value
(param) => param.name === 'default-root-object'
)?.value;
if (!defaultRootObject) { if (!defaultRootObject) {
throw new Error("Required parameter 'default-root-object' is missing."); throw new Error("Required parameter 'default-root-object' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!; const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId); const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = { const updatedConfig = {
...distribution.DistributionConfig, ...distribution.DistributionConfig,
DefaultRootObject: defaultRootObject, DefaultRootObject: defaultRootObject
}; }
await this.client.send( await this.client.send(
new UpdateDistributionCommand({ new UpdateDistributionCommand({
Id: distributionId, Id: distributionId,
IfMatch: etag, IfMatch: etag,
DistributionConfig: updatedConfig as any, DistributionConfig: updatedConfig as unknown
}) })
); )
} }
}; }
} }

View File

@ -2,29 +2,27 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand, UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'; } from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CloudFrontNoDeprecatedSSLProtocols implements BPSet { export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
private readonly client = new CloudFrontClient({}); private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => { private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({})); const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []; return response.DistributionList?.Items || []
}; }
private readonly getDistributionDetails = async (distributionId: string) => { private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetDistributionCommand({ Id: distributionId }))
new GetDistributionCommand({ Id: distributionId })
);
return { return {
distribution: response.Distribution!, distribution: response.Distribution!,
etag: response.ETag!, etag: response.ETag!
}; }
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CloudFrontNoDeprecatedSSLProtocols', name: 'CloudFrontNoDeprecatedSSLProtocols',
@ -39,94 +37,92 @@ export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListDistributionsCommand', name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for deprecated SSL protocols.', reason: 'List all CloudFront distributions to check for deprecated SSL protocols.'
}, },
{ {
name: 'GetDistributionCommand', name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to identify deprecated SSL protocols in origin configuration.', reason: 'Retrieve distribution details to identify deprecated SSL protocols in origin configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateDistributionCommand', name: 'UpdateDistributionCommand',
reason: 'Remove deprecated SSL protocols from the origin configuration of the distribution.', reason: 'Remove deprecated SSL protocols from the origin configuration of the distribution.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the origins are configured to support only secure and modern SSL protocols.', adviseBeforeFixFunction: 'Ensure the origins are configured to support only secure and modern SSL protocols.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const distributions = await this.getDistributions(); const distributions = await this.getDistributions()
for (const distribution of distributions) { for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
const hasDeprecatedSSL = details.DistributionConfig?.Origins?.Items?.some( const hasDeprecatedSSL = details.DistributionConfig?.Origins?.Items?.some(
(origin) => (origin) => origin.CustomOriginConfig && origin.CustomOriginConfig.OriginSslProtocols?.Items?.includes('SSLv3')
origin.CustomOriginConfig && )
origin.CustomOriginConfig.OriginSslProtocols?.Items?.includes('SSLv3')
);
if (hasDeprecatedSSL) { if (hasDeprecatedSSL) {
nonCompliantResources.push(details.ARN!); nonCompliantResources.push(details.ARN!)
} else { } else {
compliantResources.push(details.ARN!); compliantResources.push(details.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!; const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId); const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = { const updatedConfig = {
...distribution.DistributionConfig, ...distribution.DistributionConfig,
@ -141,23 +137,23 @@ export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
...origin.CustomOriginConfig.OriginSslProtocols, ...origin.CustomOriginConfig.OriginSslProtocols,
Items: origin.CustomOriginConfig.OriginSslProtocols?.Items?.filter( Items: origin.CustomOriginConfig.OriginSslProtocols?.Items?.filter(
(protocol) => protocol !== 'SSLv3' (protocol) => protocol !== 'SSLv3'
), )
}, }
}, }
}; }
} }
return origin; return origin
}), })
}, }
}; }
await this.client.send( await this.client.send(
new UpdateDistributionCommand({ new UpdateDistributionCommand({
Id: distributionId, Id: distributionId,
IfMatch: etag, IfMatch: etag,
DistributionConfig: updatedConfig as any, DistributionConfig: updatedConfig as unknown
}) })
); )
} }
}; }
} }

View File

@ -2,35 +2,34 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand, UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'; } from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CloudFrontS3OriginAccessControlEnabled implements BPSet { export class CloudFrontS3OriginAccessControlEnabled implements BPSet {
private readonly client = new CloudFrontClient({}); private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => { private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({})); const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []; return response.DistributionList?.Items || []
}; }
private readonly getDistributionDetails = async (distributionId: string) => { private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetDistributionCommand({ Id: distributionId }))
new GetDistributionCommand({ Id: distributionId })
);
return { return {
distribution: response.Distribution!, distribution: response.Distribution!,
etag: response.ETag!, etag: response.ETag!
}; }
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CloudFrontS3OriginAccessControlEnabled', name: 'CloudFrontS3OriginAccessControlEnabled',
description: 'Ensures that CloudFront distributions with S3 origins have Origin Access Control (OAC) enabled.', description: 'Ensures that CloudFront distributions with S3 origins have Origin Access Control (OAC) enabled.',
priority: 3, priority: 3,
priorityReason: 'Using Origin Access Control enhances security by ensuring only CloudFront can access the S3 origin.', priorityReason:
'Using Origin Access Control enhances security by ensuring only CloudFront can access the S3 origin.',
awsService: 'CloudFront', awsService: 'CloudFront',
awsServiceCategory: 'CDN', awsServiceCategory: 'CDN',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -39,109 +38,108 @@ export class CloudFrontS3OriginAccessControlEnabled implements BPSet {
name: 'origin-access-control-id', name: 'origin-access-control-id',
description: 'The ID of the Origin Access Control to associate with the S3 origin.', description: 'The ID of the Origin Access Control to associate with the S3 origin.',
default: '', default: '',
example: 'oac-0abcd1234efgh5678', example: 'oac-0abcd1234efgh5678'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListDistributionsCommand', name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for S3 origins.', reason: 'List all CloudFront distributions to check for S3 origins.'
}, },
{ {
name: 'GetDistributionCommand', name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to verify Origin Access Control configuration.', reason: 'Retrieve distribution details to verify Origin Access Control configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateDistributionCommand', name: 'UpdateDistributionCommand',
reason: 'Enable Origin Access Control for S3 origins in the distribution.', reason: 'Enable Origin Access Control for S3 origins in the distribution.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the specified Origin Access Control is correctly configured and applied to the S3 bucket.', adviseBeforeFixFunction:
}); 'Ensure the specified Origin Access Control is correctly configured and applied to the S3 bucket.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const distributions = await this.getDistributions(); const distributions = await this.getDistributions()
for (const distribution of distributions) { for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
const hasNonCompliantOrigin = details.DistributionConfig?.Origins?.Items?.some( const hasNonCompliantOrigin = details.DistributionConfig?.Origins?.Items?.some(
(origin) => (origin) => origin.S3OriginConfig && (!origin.OriginAccessControlId || origin.OriginAccessControlId === '')
origin.S3OriginConfig && )
(!origin.OriginAccessControlId || origin.OriginAccessControlId === '')
);
if (hasNonCompliantOrigin) { if (hasNonCompliantOrigin) {
nonCompliantResources.push(details.ARN!); nonCompliantResources.push(details.ARN!)
} else { } else {
compliantResources.push(details.ARN!); compliantResources.push(details.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const originAccessControlId = requiredParametersForFix.find( const originAccessControlId = requiredParametersForFix.find(
(param) => param.name === 'origin-access-control-id' (param) => param.name === 'origin-access-control-id'
)?.value; )?.value
if (!originAccessControlId) { if (!originAccessControlId) {
throw new Error("Required parameter 'origin-access-control-id' is missing."); throw new Error("Required parameter 'origin-access-control-id' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!; const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId); const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = { const updatedConfig = {
...distribution.DistributionConfig, ...distribution.DistributionConfig,
@ -150,21 +148,21 @@ export class CloudFrontS3OriginAccessControlEnabled implements BPSet {
if (origin.S3OriginConfig) { if (origin.S3OriginConfig) {
return { return {
...origin, ...origin,
OriginAccessControlId: originAccessControlId, OriginAccessControlId: originAccessControlId
}; }
} }
return origin; return origin
}), })
}, }
}; }
await this.client.send( await this.client.send(
new UpdateDistributionCommand({ new UpdateDistributionCommand({
Id: distributionId, Id: distributionId,
IfMatch: etag, IfMatch: etag,
DistributionConfig: updatedConfig as any, DistributionConfig: updatedConfig as unknown
}) })
); )
} }
}; }
} }

View File

@ -2,35 +2,34 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand, UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'; } from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CloudFrontViewerPolicyHTTPS implements BPSet { export class CloudFrontViewerPolicyHTTPS implements BPSet {
private readonly client = new CloudFrontClient({}); private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getDistributions = async () => { private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({})); const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []; return response.DistributionList?.Items || []
}; }
private readonly getDistributionDetails = async (distributionId: string) => { private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetDistributionCommand({ Id: distributionId }))
new GetDistributionCommand({ Id: distributionId })
);
return { return {
distribution: response.Distribution!, distribution: response.Distribution!,
etag: response.ETag!, etag: response.ETag!
}; }
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CloudFrontViewerPolicyHTTPS', name: 'CloudFrontViewerPolicyHTTPS',
description: 'Ensures that CloudFront distributions enforce HTTPS for viewer requests.', description: 'Ensures that CloudFront distributions enforce HTTPS for viewer requests.',
priority: 1, priority: 1,
priorityReason: 'Enforcing HTTPS improves security by ensuring secure communication between viewers and CloudFront.', priorityReason:
'Enforcing HTTPS improves security by ensuring secure communication between viewers and CloudFront.',
awsService: 'CloudFront', awsService: 'CloudFront',
awsServiceCategory: 'CDN', awsServiceCategory: 'CDN',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -39,116 +38,116 @@ export class CloudFrontViewerPolicyHTTPS implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListDistributionsCommand', name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check viewer protocol policies.', reason: 'List all CloudFront distributions to check viewer protocol policies.'
}, },
{ {
name: 'GetDistributionCommand', name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to verify viewer protocol policy settings.', reason: 'Retrieve distribution details to verify viewer protocol policy settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateDistributionCommand', name: 'UpdateDistributionCommand',
reason: 'Update the viewer protocol policy to enforce HTTPS.', reason: 'Update the viewer protocol policy to enforce HTTPS.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure all origins and endpoints support HTTPS to prevent connectivity issues.', adviseBeforeFixFunction: 'Ensure all origins and endpoints support HTTPS to prevent connectivity issues.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const distributions = await this.getDistributions(); const distributions = await this.getDistributions()
for (const distribution of distributions) { for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
const hasNonCompliantViewerPolicy = const hasNonCompliantViewerPolicy =
details.DistributionConfig?.DefaultCacheBehavior?.ViewerProtocolPolicy === 'allow-all' || details.DistributionConfig?.DefaultCacheBehavior?.ViewerProtocolPolicy === 'allow-all' ||
details.DistributionConfig?.CacheBehaviors?.Items?.some( details.DistributionConfig?.CacheBehaviors?.Items?.some(
(behavior) => behavior.ViewerProtocolPolicy === 'allow-all' (behavior) => behavior.ViewerProtocolPolicy === 'allow-all'
); )
if (hasNonCompliantViewerPolicy) { if (hasNonCompliantViewerPolicy) {
nonCompliantResources.push(details.ARN!); nonCompliantResources.push(details.ARN!)
} else { } else {
compliantResources.push(details.ARN!); compliantResources.push(details.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const distributionId = arn.split('/').pop()!; const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId); const { distribution, etag } = await this.getDistributionDetails(distributionId)
const updatedConfig = { const updatedConfig = {
...distribution.DistributionConfig, ...distribution.DistributionConfig,
DefaultCacheBehavior: { DefaultCacheBehavior: {
...distribution.DistributionConfig?.DefaultCacheBehavior, ...distribution.DistributionConfig?.DefaultCacheBehavior,
ViewerProtocolPolicy: 'redirect-to-https', ViewerProtocolPolicy: 'redirect-to-https'
}, },
CacheBehaviors: { CacheBehaviors: {
Items: distribution.DistributionConfig?.CacheBehaviors?.Items?.map((behavior) => ({ Items: distribution.DistributionConfig?.CacheBehaviors?.Items?.map((behavior) => ({
...behavior, ...behavior,
ViewerProtocolPolicy: 'redirect-to-https', ViewerProtocolPolicy: 'redirect-to-https'
})), }))
}, }
}; }
await this.client.send( await this.client.send(
new UpdateDistributionCommand({ new UpdateDistributionCommand({
Id: distributionId, Id: distributionId,
IfMatch: etag, IfMatch: etag,
DistributionConfig: updatedConfig as any, DistributionConfig: updatedConfig as unknown
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,19 @@
import { import {
CloudWatchLogsClient, CloudWatchLogsClient,
DescribeLogGroupsCommand, DescribeLogGroupsCommand,
PutRetentionPolicyCommand, PutRetentionPolicyCommand
} from '@aws-sdk/client-cloudwatch-logs'; } from '@aws-sdk/client-cloudwatch-logs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CWLogGroupRetentionPeriodCheck implements BPSet { export class CWLogGroupRetentionPeriodCheck implements BPSet {
private readonly client = new CloudWatchLogsClient({}); private readonly client = new CloudWatchLogsClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getLogGroups = async () => { private readonly getLogGroups = async () => {
const response = await this.memoClient.send(new DescribeLogGroupsCommand({})); const response = await this.memoClient.send(new DescribeLogGroupsCommand({}))
return response.logGroups || []; return response.logGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CWLogGroupRetentionPeriodCheck', name: 'CWLogGroupRetentionPeriodCheck',
@ -28,103 +28,101 @@ export class CWLogGroupRetentionPeriodCheck implements BPSet {
name: 'retention-period-days', name: 'retention-period-days',
description: 'Retention period in days to apply to log groups.', description: 'Retention period in days to apply to log groups.',
default: '', default: '',
example: '30', example: '30'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeLogGroupsCommand', name: 'DescribeLogGroupsCommand',
reason: 'Retrieve all CloudWatch log groups to verify retention settings.', reason: 'Retrieve all CloudWatch log groups to verify retention settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutRetentionPolicyCommand', name: 'PutRetentionPolicyCommand',
reason: 'Set the retention period for log groups without a defined retention policy.', reason: 'Set the retention period for log groups without a defined retention policy.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the specified retention period meets your organizational compliance requirements.', adviseBeforeFixFunction: 'Ensure the specified retention period meets your organizational compliance requirements.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const logGroups = await this.getLogGroups(); const logGroups = await this.getLogGroups()
for (const logGroup of logGroups) { for (const logGroup of logGroups) {
if (logGroup.retentionInDays) { if (logGroup.retentionInDays) {
compliantResources.push(logGroup.logGroupArn!); compliantResources.push(logGroup.logGroupArn!)
} else { } else {
nonCompliantResources.push(logGroup.logGroupArn!); nonCompliantResources.push(logGroup.logGroupArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find((param) => param.name === 'retention-period-days')?.value
(param) => param.name === 'retention-period-days'
)?.value;
if (!retentionPeriod) { if (!retentionPeriod) {
throw new Error("Required parameter 'retention-period-days' is missing."); throw new Error("Required parameter 'retention-period-days' is missing.")
} }
for (const logGroupArn of nonCompliantResources) { for (const logGroupArn of nonCompliantResources) {
const logGroupName = logGroupArn.split(':').pop()!; const logGroupName = logGroupArn.split(':').pop()!
await this.client.send( await this.client.send(
new PutRetentionPolicyCommand({ new PutRetentionPolicyCommand({
logGroupName, logGroupName,
retentionInDays: parseInt(retentionPeriod, 10), retentionInDays: parseInt(retentionPeriod, 10)
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,15 @@
import { import { CloudWatchClient, DescribeAlarmsCommand, PutMetricAlarmCommand } from '@aws-sdk/client-cloudwatch'
CloudWatchClient, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeAlarmsCommand, import { Memorizer } from '../../Memorizer'
PutMetricAlarmCommand,
} from '@aws-sdk/client-cloudwatch';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CloudWatchAlarmSettingsCheck implements BPSet { export class CloudWatchAlarmSettingsCheck implements BPSet {
private readonly client = new CloudWatchClient({}); private readonly client = new CloudWatchClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getAlarms = async () => { private readonly getAlarms = async () => {
const response = await this.memoClient.send(new DescribeAlarmsCommand({})); const response = await this.memoClient.send(new DescribeAlarmsCommand({}))
return response.MetricAlarms || []; return response.MetricAlarms || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CloudWatchAlarmSettingsCheck', name: 'CloudWatchAlarmSettingsCheck',
@ -26,112 +22,120 @@ export class CloudWatchAlarmSettingsCheck implements BPSet {
requiredParametersForFix: [ requiredParametersForFix: [
{ name: 'metric-name', description: 'The metric name for the alarm.', default: '', example: 'CPUUtilization' }, { name: 'metric-name', description: 'The metric name for the alarm.', default: '', example: 'CPUUtilization' },
{ name: 'threshold', description: 'The threshold for the alarm.', default: '', example: '80' }, { name: 'threshold', description: 'The threshold for the alarm.', default: '', example: '80' },
{ name: 'evaluation-periods', description: 'Number of evaluation periods for the alarm.', default: '', example: '5' }, {
name: 'evaluation-periods',
description: 'Number of evaluation periods for the alarm.',
default: '',
example: '5'
},
{ name: 'period', description: 'The period in seconds for the metric evaluation.', default: '', example: '60' }, { name: 'period', description: 'The period in seconds for the metric evaluation.', default: '', example: '60' },
{ name: 'comparison-operator', description: 'Comparison operator for the threshold.', default: '', example: 'GreaterThanThreshold' }, {
{ name: 'statistic', description: 'Statistic to apply to the metric.', default: '', example: 'Average' }, name: 'comparison-operator',
description: 'Comparison operator for the threshold.',
default: '',
example: 'GreaterThanThreshold'
},
{ name: 'statistic', description: 'Statistic to apply to the metric.', default: '', example: 'Average' }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeAlarmsCommand', name: 'DescribeAlarmsCommand',
reason: 'Retrieve all CloudWatch alarms to verify their settings.', reason: 'Retrieve all CloudWatch alarms to verify their settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutMetricAlarmCommand', name: 'PutMetricAlarmCommand',
reason: 'Update or create alarms with the required settings.', reason: 'Update or create alarms with the required settings.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the required settings are correctly configured for your monitoring needs.', adviseBeforeFixFunction: 'Ensure the required settings are correctly configured for your monitoring needs.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const alarms = await this.getAlarms(); const alarms = await this.getAlarms()
const parameters = { const parameters = {
MetricName: '', // Required MetricName: '', // Required
Threshold: null, Threshold: null,
EvaluationPeriods: null, EvaluationPeriods: null,
Period: null, Period: null,
ComparisonOperator: null, ComparisonOperator: null,
Statistic: null, Statistic: null
}; }
for (const alarm of alarms) { for (const alarm of alarms) {
let isCompliant = true; let isCompliant = true
for (const key of Object.keys(parameters).filter((k) => (parameters as any)[k] !== null)) { for (const key of Object.keys(parameters).filter((k) => (parameters as unknown)[k] !== null)) {
if (alarm[key as keyof typeof alarm] !== parameters[key as keyof typeof parameters]) { if (alarm[key as keyof typeof alarm] !== parameters[key as keyof typeof parameters]) {
isCompliant = false; isCompliant = false
break; break
} }
} }
if (isCompliant) { if (isCompliant) {
compliantResources.push(alarm.AlarmArn!); compliantResources.push(alarm.AlarmArn!)
} else { } else {
nonCompliantResources.push(alarm.AlarmArn!); nonCompliantResources.push(alarm.AlarmArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const requiredSettings = Object.fromEntries( const requiredSettings = Object.fromEntries(requiredParametersForFix.map((param) => [param.name, param.value]))
requiredParametersForFix.map((param) => [param.name, param.value])
);
for (const alarmArn of nonCompliantResources) { for (const alarmArn of nonCompliantResources) {
const alarmName = alarmArn.split(':').pop()!; const alarmName = alarmArn.split(':').pop()!
await this.client.send( await this.client.send(
new PutMetricAlarmCommand({ new PutMetricAlarmCommand({
@ -140,10 +144,10 @@ export class CloudWatchAlarmSettingsCheck implements BPSet {
Threshold: parseFloat(requiredSettings['threshold']), Threshold: parseFloat(requiredSettings['threshold']),
EvaluationPeriods: parseInt(requiredSettings['evaluation-periods'], 10), EvaluationPeriods: parseInt(requiredSettings['evaluation-periods'], 10),
Period: parseInt(requiredSettings['period'], 10), Period: parseInt(requiredSettings['period'], 10),
ComparisonOperator: requiredSettings['comparison-operator'] as any, ComparisonOperator: requiredSettings['comparison-operator'] as unknown,
Statistic: requiredSettings['statistic'] as any, Statistic: requiredSettings['statistic'] as unknown
}) })
); )
} }
}; }
} }

View File

@ -2,25 +2,23 @@ import {
CodeBuildClient, CodeBuildClient,
ListProjectsCommand, ListProjectsCommand,
BatchGetProjectsCommand, BatchGetProjectsCommand,
UpdateProjectCommand, UpdateProjectCommand
} from '@aws-sdk/client-codebuild'; } from '@aws-sdk/client-codebuild'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CodeBuildProjectEnvironmentPrivilegedCheck implements BPSet { export class CodeBuildProjectEnvironmentPrivilegedCheck implements BPSet {
private readonly client = new CodeBuildClient({}); private readonly client = new CodeBuildClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getProjects = async () => { private readonly getProjects = async () => {
const projectNames = await this.memoClient.send(new ListProjectsCommand({})); const projectNames = await this.memoClient.send(new ListProjectsCommand({}))
if (!projectNames.projects?.length) { if (!projectNames.projects?.length) {
return []; return []
} }
const response = await this.memoClient.send( const response = await this.memoClient.send(new BatchGetProjectsCommand({ names: projectNames.projects }))
new BatchGetProjectsCommand({ names: projectNames.projects }) return response.projects || []
); }
return response.projects || [];
};
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CodeBuildProjectEnvironmentPrivilegedCheck', name: 'CodeBuildProjectEnvironmentPrivilegedCheck',
@ -35,92 +33,92 @@ export class CodeBuildProjectEnvironmentPrivilegedCheck implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListProjectsCommand', name: 'ListProjectsCommand',
reason: 'Retrieve all CodeBuild projects to verify environment settings.', reason: 'Retrieve all CodeBuild projects to verify environment settings.'
}, },
{ {
name: 'BatchGetProjectsCommand', name: 'BatchGetProjectsCommand',
reason: 'Fetch detailed configuration for each CodeBuild project.', reason: 'Fetch detailed configuration for each CodeBuild project.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateProjectCommand', name: 'UpdateProjectCommand',
reason: 'Update the project to disable privileged mode in the environment settings.', reason: 'Update the project to disable privileged mode in the environment settings.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the privileged mode is not required for any valid use case before applying fixes.', adviseBeforeFixFunction: 'Ensure the privileged mode is not required for any valid use case before applying fixes.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const projects = await this.getProjects(); const projects = await this.getProjects()
for (const project of projects) { for (const project of projects) {
if (!project.environment?.privilegedMode) { if (!project.environment?.privilegedMode) {
compliantResources.push(project.arn!); compliantResources.push(project.arn!)
} else { } else {
nonCompliantResources.push(project.arn!); nonCompliantResources.push(project.arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const projects = await this.getProjects(); const projects = await this.getProjects()
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const projectName = arn.split(':').pop()!; const projectName = arn.split(':').pop()!
const projectToFix = projects.find((project) => project.arn === arn); const projectToFix = projects.find((project) => project.arn === arn)
if (!projectToFix) { if (!projectToFix) {
continue; continue
} }
await this.client.send( await this.client.send(
@ -128,10 +126,10 @@ export class CodeBuildProjectEnvironmentPrivilegedCheck implements BPSet {
name: projectName, name: projectName,
environment: { environment: {
...projectToFix.environment, ...projectToFix.environment,
privilegedMode: false, privilegedMode: false
} as any, } as unknown
}) })
); )
} }
}; }
} }

View File

@ -2,25 +2,23 @@ import {
CodeBuildClient, CodeBuildClient,
ListProjectsCommand, ListProjectsCommand,
BatchGetProjectsCommand, BatchGetProjectsCommand,
UpdateProjectCommand, UpdateProjectCommand
} from '@aws-sdk/client-codebuild'; } from '@aws-sdk/client-codebuild'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CodeBuildProjectLoggingEnabled implements BPSet { export class CodeBuildProjectLoggingEnabled implements BPSet {
private readonly client = new CodeBuildClient({}); private readonly client = new CodeBuildClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getProjects = async () => { private readonly getProjects = async () => {
const projectNames = await this.memoClient.send(new ListProjectsCommand({})); const projectNames = await this.memoClient.send(new ListProjectsCommand({}))
if (!projectNames.projects?.length) { if (!projectNames.projects?.length) {
return []; return []
} }
const response = await this.memoClient.send( const response = await this.memoClient.send(new BatchGetProjectsCommand({ names: projectNames.projects }))
new BatchGetProjectsCommand({ names: projectNames.projects }) return response.projects || []
); }
return response.projects || [];
};
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CodeBuildProjectLoggingEnabled', name: 'CodeBuildProjectLoggingEnabled',
@ -35,96 +33,93 @@ export class CodeBuildProjectLoggingEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListProjectsCommand', name: 'ListProjectsCommand',
reason: 'Retrieve all CodeBuild projects to verify logging settings.', reason: 'Retrieve all CodeBuild projects to verify logging settings.'
}, },
{ {
name: 'BatchGetProjectsCommand', name: 'BatchGetProjectsCommand',
reason: 'Fetch detailed configuration for each CodeBuild project.', reason: 'Fetch detailed configuration for each CodeBuild project.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateProjectCommand', name: 'UpdateProjectCommand',
reason: 'Enable logging for projects that have it disabled.', reason: 'Enable logging for projects that have it disabled.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the default log group and stream names are suitable for your organization.', adviseBeforeFixFunction: 'Ensure the default log group and stream names are suitable for your organization.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const projects = await this.getProjects(); const projects = await this.getProjects()
for (const project of projects) { for (const project of projects) {
const logsConfig = project.logsConfig; const logsConfig = project.logsConfig
if ( if (logsConfig?.cloudWatchLogs?.status === 'ENABLED' || logsConfig?.s3Logs?.status === 'ENABLED') {
logsConfig?.cloudWatchLogs?.status === 'ENABLED' || compliantResources.push(project.arn!)
logsConfig?.s3Logs?.status === 'ENABLED'
) {
compliantResources.push(project.arn!);
} else { } else {
nonCompliantResources.push(project.arn!); nonCompliantResources.push(project.arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const projects = await this.getProjects(); const projects = await this.getProjects()
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const projectName = arn.split(':').pop()!; const projectName = arn.split(':').pop()!
const projectToFix = projects.find((project) => project.arn === arn); const projectToFix = projects.find((project) => project.arn === arn)
if (!projectToFix) { if (!projectToFix) {
continue; continue
} }
await this.client.send( await this.client.send(
@ -135,11 +130,11 @@ export class CodeBuildProjectLoggingEnabled implements BPSet {
cloudWatchLogs: { cloudWatchLogs: {
status: 'ENABLED', status: 'ENABLED',
groupName: 'default-cloudwatch-group', groupName: 'default-cloudwatch-group',
streamName: 'default-stream', streamName: 'default-stream'
}, }
}, }
}) })
); )
} }
}; }
} }

View File

@ -3,43 +3,44 @@ import {
ListApplicationsCommand, ListApplicationsCommand,
ListDeploymentGroupsCommand, ListDeploymentGroupsCommand,
BatchGetDeploymentGroupsCommand, BatchGetDeploymentGroupsCommand,
UpdateDeploymentGroupCommand, UpdateDeploymentGroupCommand
} from '@aws-sdk/client-codedeploy'; } from '@aws-sdk/client-codedeploy'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class CodeDeployAutoRollbackMonitorEnabled implements BPSet { export class CodeDeployAutoRollbackMonitorEnabled implements BPSet {
private readonly client = new CodeDeployClient({}); private readonly client = new CodeDeployClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getDeploymentGroups = async () => { private readonly getDeploymentGroups = async () => {
const applications = await this.memoClient.send(new ListApplicationsCommand({})); const applications = await this.memoClient.send(new ListApplicationsCommand({}))
const deploymentGroupsInfo = []; const deploymentGroupsInfo = []
for (const application of applications.applications || []) { for (const application of applications.applications || []) {
const deploymentGroups = await this.memoClient.send( const deploymentGroups = await this.memoClient.send(
new ListDeploymentGroupsCommand({ applicationName: application }) new ListDeploymentGroupsCommand({ applicationName: application })
); )
if (!deploymentGroups.deploymentGroups?.length) { if (!deploymentGroups.deploymentGroups?.length) {
continue; continue
} }
const batchResponse = await this.memoClient.send( const batchResponse = await this.memoClient.send(
new BatchGetDeploymentGroupsCommand({ new BatchGetDeploymentGroupsCommand({
applicationName: application, applicationName: application,
deploymentGroupNames: deploymentGroups.deploymentGroups, deploymentGroupNames: deploymentGroups.deploymentGroups
}) })
); )
deploymentGroupsInfo.push(...(batchResponse.deploymentGroupsInfo || [])); deploymentGroupsInfo.push(...(batchResponse.deploymentGroupsInfo || []))
} }
return deploymentGroupsInfo; return deploymentGroupsInfo
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'CodeDeployAutoRollbackMonitorEnabled', name: 'CodeDeployAutoRollbackMonitorEnabled',
description: 'Ensures that auto-rollback and alarm monitoring are enabled for CodeDeploy deployment groups.', description: 'Ensures that auto-rollback and alarm monitoring are enabled for CodeDeploy deployment groups.',
priority: 2, priority: 2,
priorityReason: 'Enabling auto-rollback and alarms helps prevent deployment issues and allows for automated recovery.', priorityReason:
'Enabling auto-rollback and alarms helps prevent deployment issues and allows for automated recovery.',
awsService: 'CodeDeploy', awsService: 'CodeDeploy',
awsServiceCategory: 'Deployment', awsServiceCategory: 'Deployment',
bestPracticeCategory: 'Resilience', bestPracticeCategory: 'Resilience',
@ -48,101 +49,96 @@ export class CodeDeployAutoRollbackMonitorEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListApplicationsCommand', name: 'ListApplicationsCommand',
reason: 'Retrieve all CodeDeploy applications to analyze their deployment groups.', reason: 'Retrieve all CodeDeploy applications to analyze their deployment groups.'
}, },
{ {
name: 'ListDeploymentGroupsCommand', name: 'ListDeploymentGroupsCommand',
reason: 'Fetch deployment groups for each application.', reason: 'Fetch deployment groups for each application.'
}, },
{ {
name: 'BatchGetDeploymentGroupsCommand', name: 'BatchGetDeploymentGroupsCommand',
reason: 'Get detailed information about deployment groups to check configurations.', reason: 'Get detailed information about deployment groups to check configurations.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateDeploymentGroupCommand', name: 'UpdateDeploymentGroupCommand',
reason: 'Enable alarm monitoring and auto-rollback configurations for deployment groups.', reason: 'Enable alarm monitoring and auto-rollback configurations for deployment groups.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure your alarms and configurations align with organizational standards before enabling auto-rollback and monitoring.', 'Ensure your alarms and configurations align with organizational standards before enabling auto-rollback and monitoring.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const deploymentGroups = await this.getDeploymentGroups(); const deploymentGroups = await this.getDeploymentGroups()
for (const deploymentGroup of deploymentGroups) { for (const deploymentGroup of deploymentGroups) {
if ( if (deploymentGroup.alarmConfiguration?.enabled && deploymentGroup.autoRollbackConfiguration?.enabled) {
deploymentGroup.alarmConfiguration?.enabled && compliantResources.push(deploymentGroup.deploymentGroupId!)
deploymentGroup.autoRollbackConfiguration?.enabled
) {
compliantResources.push(deploymentGroup.deploymentGroupId!);
} else { } else {
nonCompliantResources.push(deploymentGroup.deploymentGroupId!); nonCompliantResources.push(deploymentGroup.deploymentGroupId!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const deploymentGroups = await this.getDeploymentGroups(); const deploymentGroups = await this.getDeploymentGroups()
for (const groupId of nonCompliantResources) { for (const groupId of nonCompliantResources) {
const deploymentGroupToFix = deploymentGroups.find( const deploymentGroupToFix = deploymentGroups.find((group) => group.deploymentGroupId === groupId)
(group) => group.deploymentGroupId === groupId
);
if (!deploymentGroupToFix) { if (!deploymentGroupToFix) {
continue; continue
} }
await this.client.send( await this.client.send(
@ -151,14 +147,14 @@ export class CodeDeployAutoRollbackMonitorEnabled implements BPSet {
currentDeploymentGroupName: deploymentGroupToFix.deploymentGroupName!, currentDeploymentGroupName: deploymentGroupToFix.deploymentGroupName!,
alarmConfiguration: { alarmConfiguration: {
...deploymentGroupToFix.alarmConfiguration, ...deploymentGroupToFix.alarmConfiguration,
enabled: true, enabled: true
}, },
autoRollbackConfiguration: { autoRollbackConfiguration: {
...deploymentGroupToFix.autoRollbackConfiguration, ...deploymentGroupToFix.autoRollbackConfiguration,
enabled: true, enabled: true
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,39 +1,34 @@
import { import { DynamoDBClient, ListTablesCommand, DescribeTableCommand } from '@aws-sdk/client-dynamodb'
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand,
} from '@aws-sdk/client-dynamodb';
import { import {
ApplicationAutoScalingClient, ApplicationAutoScalingClient,
RegisterScalableTargetCommand, RegisterScalableTargetCommand,
PutScalingPolicyCommand, PutScalingPolicyCommand,
DescribeScalingPoliciesCommand, DescribeScalingPoliciesCommand
} from '@aws-sdk/client-application-auto-scaling'; } from '@aws-sdk/client-application-auto-scaling'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class DynamoDBAutoscalingEnabled implements BPSet { export class DynamoDBAutoscalingEnabled implements BPSet {
private readonly client = new DynamoDBClient({}); private readonly client = new DynamoDBClient({})
private readonly autoScalingClient = new ApplicationAutoScalingClient({}); private readonly autoScalingClient = new ApplicationAutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTables = async () => { private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({})); const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []; const tables = []
for (const tableName of tableNames.TableNames || []) { for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send( const tableDetails = await this.memoClient.send(new DescribeTableCommand({ TableName: tableName }))
new DescribeTableCommand({ TableName: tableName }) tables.push(tableDetails.Table!)
);
tables.push(tableDetails.Table!);
} }
return tables; return tables
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'DynamoDBAutoscalingEnabled', name: 'DynamoDBAutoscalingEnabled',
description: 'Ensures DynamoDB tables have autoscaling enabled for both read and write capacity.', description: 'Ensures DynamoDB tables have autoscaling enabled for both read and write capacity.',
priority: 2, priority: 2,
priorityReason: 'Autoscaling ensures DynamoDB tables dynamically adjust capacity to meet demand, reducing costs and preventing throttling.', priorityReason:
'Autoscaling ensures DynamoDB tables dynamically adjust capacity to meet demand, reducing costs and preventing throttling.',
awsService: 'DynamoDB', awsService: 'DynamoDB',
awsServiceCategory: 'Database', awsServiceCategory: 'Database',
bestPracticeCategory: 'Scalability', bestPracticeCategory: 'Scalability',
@ -42,113 +37,111 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTablesCommand', name: 'ListTablesCommand',
reason: 'List all DynamoDB tables to check their configurations.', reason: 'List all DynamoDB tables to check their configurations.'
}, },
{ {
name: 'DescribeTableCommand', name: 'DescribeTableCommand',
reason: 'Retrieve details of DynamoDB tables to analyze billing mode and autoscaling.', reason: 'Retrieve details of DynamoDB tables to analyze billing mode and autoscaling.'
}, },
{ {
name: 'DescribeScalingPoliciesCommand', name: 'DescribeScalingPoliciesCommand',
reason: 'Fetch scaling policies for each table to check autoscaling settings.', reason: 'Fetch scaling policies for each table to check autoscaling settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RegisterScalableTargetCommand', name: 'RegisterScalableTargetCommand',
reason: 'Register read and write capacity units for autoscaling.', reason: 'Register read and write capacity units for autoscaling.'
}, },
{ {
name: 'PutScalingPolicyCommand', name: 'PutScalingPolicyCommand',
reason: 'Configure target tracking scaling policies for read and write capacity.', reason: 'Configure target tracking scaling policies for read and write capacity.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure the tables read and write workloads are predictable to configure appropriate scaling limits.', 'Ensure the tables read and write workloads are predictable to configure appropriate scaling limits.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const tables = await this.getTables(); const tables = await this.getTables()
for (const table of tables) { for (const table of tables) {
if (table.BillingModeSummary?.BillingMode === 'PAY_PER_REQUEST') { if (table.BillingModeSummary?.BillingMode === 'PAY_PER_REQUEST') {
compliantResources.push(table.TableArn!); compliantResources.push(table.TableArn!)
continue; continue
} }
const scalingPolicies = await this.autoScalingClient.send( const scalingPolicies = await this.autoScalingClient.send(
new DescribeScalingPoliciesCommand({ new DescribeScalingPoliciesCommand({
ServiceNamespace: 'dynamodb', ServiceNamespace: 'dynamodb',
ResourceId: `table/${table.TableName}`, ResourceId: `table/${table.TableName}`
}) })
); )
const scalingPolicyDimensions = scalingPolicies.ScalingPolicies?.map( const scalingPolicyDimensions = scalingPolicies.ScalingPolicies?.map((policy) => policy.ScalableDimension)
(policy) => policy.ScalableDimension
);
if ( if (
scalingPolicyDimensions?.includes('dynamodb:table:ReadCapacityUnits') && scalingPolicyDimensions?.includes('dynamodb:table:ReadCapacityUnits') &&
scalingPolicyDimensions?.includes('dynamodb:table:WriteCapacityUnits') scalingPolicyDimensions?.includes('dynamodb:table:WriteCapacityUnits')
) { ) {
compliantResources.push(table.TableArn!); compliantResources.push(table.TableArn!)
} else { } else {
nonCompliantResources.push(table.TableArn!); nonCompliantResources.push(table.TableArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!; const tableName = arn.split('/').pop()!
// Register scalable targets for read and write capacity // Register scalable targets for read and write capacity
await this.autoScalingClient.send( await this.autoScalingClient.send(
@ -157,9 +150,9 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ResourceId: `table/${tableName}`, ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:ReadCapacityUnits', ScalableDimension: 'dynamodb:table:ReadCapacityUnits',
MinCapacity: 1, MinCapacity: 1,
MaxCapacity: 100, MaxCapacity: 100
}) })
); )
await this.autoScalingClient.send( await this.autoScalingClient.send(
new RegisterScalableTargetCommand({ new RegisterScalableTargetCommand({
@ -167,9 +160,9 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ResourceId: `table/${tableName}`, ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:WriteCapacityUnits', ScalableDimension: 'dynamodb:table:WriteCapacityUnits',
MinCapacity: 1, MinCapacity: 1,
MaxCapacity: 100, MaxCapacity: 100
}) })
); )
// Put scaling policies for read and write capacity // Put scaling policies for read and write capacity
await this.autoScalingClient.send( await this.autoScalingClient.send(
@ -184,11 +177,11 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ScaleInCooldown: 60, ScaleInCooldown: 60,
ScaleOutCooldown: 60, ScaleOutCooldown: 60,
PredefinedMetricSpecification: { PredefinedMetricSpecification: {
PredefinedMetricType: 'DynamoDBReadCapacityUtilization', PredefinedMetricType: 'DynamoDBReadCapacityUtilization'
}, }
}, }
}) })
); )
await this.autoScalingClient.send( await this.autoScalingClient.send(
new PutScalingPolicyCommand({ new PutScalingPolicyCommand({
@ -202,11 +195,11 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ScaleInCooldown: 60, ScaleInCooldown: 60,
ScaleOutCooldown: 60, ScaleOutCooldown: 60,
PredefinedMetricSpecification: { PredefinedMetricSpecification: {
PredefinedMetricType: 'DynamoDBWriteCapacityUtilization', PredefinedMetricType: 'DynamoDBWriteCapacityUtilization'
}, }
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,44 +1,34 @@
import { import { DynamoDBClient, ListTablesCommand, DescribeTableCommand } from '@aws-sdk/client-dynamodb'
DynamoDBClient, import { BackupClient, ListRecoveryPointsByResourceCommand, StartBackupJobCommand } from '@aws-sdk/client-backup'
ListTablesCommand, import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
DescribeTableCommand, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
} from '@aws-sdk/client-dynamodb'; import { Memorizer } from '../../Memorizer'
import {
BackupClient,
ListRecoveryPointsByResourceCommand,
StartBackupJobCommand,
} from '@aws-sdk/client-backup';
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBLastBackupRecoveryPointCreated implements BPSet { export class DynamoDBLastBackupRecoveryPointCreated implements BPSet {
private readonly client = new DynamoDBClient({}); private readonly client = new DynamoDBClient({})
private readonly backupClient = new BackupClient({}); private readonly backupClient = new BackupClient({})
private readonly stsClient = new STSClient({}); private readonly stsClient = new STSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private accountId: string | undefined; private accountId: string | undefined
private readonly fetchAccountId = async () => { private readonly fetchAccountId = async () => {
if (!this.accountId) { if (!this.accountId) {
const identity = await this.stsClient.send(new GetCallerIdentityCommand({})); const identity = await this.stsClient.send(new GetCallerIdentityCommand({}))
this.accountId = identity.Account!; this.accountId = identity.Account!
} }
return this.accountId; return this.accountId
}; }
private readonly getTables = async () => { private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({})); const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []; const tables = []
for (const tableName of tableNames.TableNames || []) { for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send( const tableDetails = await this.memoClient.send(new DescribeTableCommand({ TableName: tableName }))
new DescribeTableCommand({ TableName: tableName }) tables.push(tableDetails.Table!)
);
tables.push(tableDetails.Table!);
} }
return tables; return tables
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'DynamoDBLastBackupRecoveryPointCreated', name: 'DynamoDBLastBackupRecoveryPointCreated',
@ -53,115 +43,114 @@ export class DynamoDBLastBackupRecoveryPointCreated implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTablesCommand', name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to check for backups.', reason: 'Retrieve the list of DynamoDB tables to check for backups.'
}, },
{ {
name: 'DescribeTableCommand', name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table.', reason: 'Fetch details of each DynamoDB table.'
}, },
{ {
name: 'ListRecoveryPointsByResourceCommand', name: 'ListRecoveryPointsByResourceCommand',
reason: 'Check recovery points for DynamoDB tables in AWS Backup.', reason: 'Check recovery points for DynamoDB tables in AWS Backup.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'StartBackupJobCommand', name: 'StartBackupJobCommand',
reason: 'Initiate a backup job for non-compliant DynamoDB tables.', reason: 'Initiate a backup job for non-compliant DynamoDB tables.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure the backup vault and IAM role are properly configured for your backup strategy.'
'Ensure the backup vault and IAM role are properly configured for your backup strategy.', })
});
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const tables = await this.getTables(); const tables = await this.getTables()
for (const table of tables) { for (const table of tables) {
const recoveryPointsResponse = await this.backupClient.send( const recoveryPointsResponse = await this.backupClient.send(
new ListRecoveryPointsByResourceCommand({ new ListRecoveryPointsByResourceCommand({
ResourceArn: table.TableArn, ResourceArn: table.TableArn
}) })
); )
const recoveryPoints = recoveryPointsResponse.RecoveryPoints || []; const recoveryPoints = recoveryPointsResponse.RecoveryPoints || []
if (recoveryPoints.length === 0) { if (recoveryPoints.length === 0) {
nonCompliantResources.push(table.TableArn!); nonCompliantResources.push(table.TableArn!)
continue; continue
} }
const latestRecoveryPoint = recoveryPoints const latestRecoveryPoint = recoveryPoints
.map((point) => new Date(point.CreationDate!)) .map((point) => new Date(point.CreationDate!))
.sort((a, b) => b.getTime() - a.getTime())[0]; .sort((a, b) => b.getTime() - a.getTime())[0]
if (new Date().getTime() - latestRecoveryPoint.getTime() > 86400000) { if (new Date().getTime() - latestRecoveryPoint.getTime() > 86400000) {
nonCompliantResources.push(table.TableArn!); nonCompliantResources.push(table.TableArn!)
} else { } else {
compliantResources.push(table.TableArn!); compliantResources.push(table.TableArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const accountId = await this.fetchAccountId(); const accountId = await this.fetchAccountId()
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
await this.backupClient.send( await this.backupClient.send(
new StartBackupJobCommand({ new StartBackupJobCommand({
ResourceArn: arn, ResourceArn: arn,
BackupVaultName: 'Default', BackupVaultName: 'Default',
IamRoleArn: `arn:aws:iam::${accountId}:role/service-role/BackupDefaultServiceRole`, IamRoleArn: `arn:aws:iam::${accountId}:role/service-role/BackupDefaultServiceRole`
}) })
); )
} }
}; }
} }

View File

@ -3,32 +3,31 @@ import {
ListTablesCommand, ListTablesCommand,
DescribeTableCommand, DescribeTableCommand,
DescribeContinuousBackupsCommand, DescribeContinuousBackupsCommand,
UpdateContinuousBackupsCommand, UpdateContinuousBackupsCommand
} from '@aws-sdk/client-dynamodb'; } from '@aws-sdk/client-dynamodb'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class DynamoDBPITREnabled implements BPSet { export class DynamoDBPITREnabled implements BPSet {
private readonly client = new DynamoDBClient({}); private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTables = async () => { private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({})); const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []; const tables = []
for (const tableName of tableNames.TableNames || []) { for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send( const tableDetails = await this.memoClient.send(new DescribeTableCommand({ TableName: tableName }))
new DescribeTableCommand({ TableName: tableName }) tables.push(tableDetails.Table!)
);
tables.push(tableDetails.Table!);
} }
return tables; return tables
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'DynamoDBPITREnabled', name: 'DynamoDBPITREnabled',
description: 'Ensures that Point-In-Time Recovery (PITR) is enabled for DynamoDB tables.', description: 'Ensures that Point-In-Time Recovery (PITR) is enabled for DynamoDB tables.',
priority: 1, priority: 1,
priorityReason: 'PITR provides continuous backups of DynamoDB tables, enabling recovery to any second within the last 35 days.', priorityReason:
'PITR provides continuous backups of DynamoDB tables, enabling recovery to any second within the last 35 days.',
awsService: 'DynamoDB', awsService: 'DynamoDB',
awsServiceCategory: 'Database', awsServiceCategory: 'Database',
bestPracticeCategory: 'Backup and Recovery', bestPracticeCategory: 'Backup and Recovery',
@ -37,108 +36,109 @@ export class DynamoDBPITREnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTablesCommand', name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify PITR settings.', reason: 'Retrieve the list of DynamoDB tables to verify PITR settings.'
}, },
{ {
name: 'DescribeTableCommand', name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table.', reason: 'Fetch details of each DynamoDB table.'
}, },
{ {
name: 'DescribeContinuousBackupsCommand', name: 'DescribeContinuousBackupsCommand',
reason: 'Check if PITR is enabled for each table.', reason: 'Check if PITR is enabled for each table.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateContinuousBackupsCommand', name: 'UpdateContinuousBackupsCommand',
reason: 'Enable PITR for non-compliant DynamoDB tables.', reason: 'Enable PITR for non-compliant DynamoDB tables.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure enabling PITR aligns with organizational backup policies and compliance requirements.', adviseBeforeFixFunction:
}); 'Ensure enabling PITR aligns with organizational backup policies and compliance requirements.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const tables = await this.getTables(); const tables = await this.getTables()
for (const table of tables) { for (const table of tables) {
const backupStatus = await this.memoClient.send( const backupStatus = await this.memoClient.send(
new DescribeContinuousBackupsCommand({ new DescribeContinuousBackupsCommand({
TableName: table.TableName!, TableName: table.TableName!
}) })
); )
if ( if (
backupStatus.ContinuousBackupsDescription?.PointInTimeRecoveryDescription backupStatus.ContinuousBackupsDescription?.PointInTimeRecoveryDescription?.PointInTimeRecoveryStatus ===
?.PointInTimeRecoveryStatus === 'ENABLED' 'ENABLED'
) { ) {
compliantResources.push(table.TableArn!); compliantResources.push(table.TableArn!)
} else { } else {
nonCompliantResources.push(table.TableArn!); nonCompliantResources.push(table.TableArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!; const tableName = arn.split('/').pop()!
await this.client.send( await this.client.send(
new UpdateContinuousBackupsCommand({ new UpdateContinuousBackupsCommand({
TableName: tableName, TableName: tableName,
PointInTimeRecoverySpecification: { PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true, PointInTimeRecoveryEnabled: true
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,27 +1,20 @@
import { import { DynamoDBClient, ListTablesCommand, DescribeTableCommand, UpdateTableCommand } from '@aws-sdk/client-dynamodb'
DynamoDBClient, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
ListTablesCommand, import { Memorizer } from '../../Memorizer'
DescribeTableCommand,
UpdateTableCommand,
} from '@aws-sdk/client-dynamodb';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBTableDeletionProtectionEnabled implements BPSet { export class DynamoDBTableDeletionProtectionEnabled implements BPSet {
private readonly client = new DynamoDBClient({}); private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTables = async () => { private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({})); const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []; const tables = []
for (const tableName of tableNames.TableNames || []) { for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send( const tableDetails = await this.memoClient.send(new DescribeTableCommand({ TableName: tableName }))
new DescribeTableCommand({ TableName: tableName }) tables.push(tableDetails.Table!)
);
tables.push(tableDetails.Table!);
} }
return tables; return tables
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'DynamoDBTableDeletionProtectionEnabled', name: 'DynamoDBTableDeletionProtectionEnabled',
@ -36,93 +29,93 @@ export class DynamoDBTableDeletionProtectionEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTablesCommand', name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify deletion protection.', reason: 'Retrieve the list of DynamoDB tables to verify deletion protection.'
}, },
{ {
name: 'DescribeTableCommand', name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table, including deletion protection settings.', reason: 'Fetch details of each DynamoDB table, including deletion protection settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateTableCommand', name: 'UpdateTableCommand',
reason: 'Enable deletion protection for non-compliant DynamoDB tables.', reason: 'Enable deletion protection for non-compliant DynamoDB tables.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with operational and compliance requirements.', adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with operational and compliance requirements.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const tables = await this.getTables(); const tables = await this.getTables()
for (const table of tables) { for (const table of tables) {
if (table.DeletionProtectionEnabled) { if (table.DeletionProtectionEnabled) {
compliantResources.push(table.TableArn!); compliantResources.push(table.TableArn!)
} else { } else {
nonCompliantResources.push(table.TableArn!); nonCompliantResources.push(table.TableArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!; const tableName = arn.split('/').pop()!
await this.client.send( await this.client.send(
new UpdateTableCommand({ new UpdateTableCommand({
TableName: tableName, TableName: tableName,
DeletionProtectionEnabled: true, DeletionProtectionEnabled: true
}) })
); )
} }
}; }
} }

View File

@ -1,27 +1,20 @@
import { import { DynamoDBClient, ListTablesCommand, DescribeTableCommand, UpdateTableCommand } from '@aws-sdk/client-dynamodb'
DynamoDBClient, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
ListTablesCommand, import { Memorizer } from '../../Memorizer'
DescribeTableCommand,
UpdateTableCommand,
} from '@aws-sdk/client-dynamodb';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBTableEncryptedKMS implements BPSet { export class DynamoDBTableEncryptedKMS implements BPSet {
private readonly client = new DynamoDBClient({}); private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTables = async () => { private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({})); const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []; const tables = []
for (const tableName of tableNames.TableNames || []) { for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send( const tableDetails = await this.memoClient.send(new DescribeTableCommand({ TableName: tableName }))
new DescribeTableCommand({ TableName: tableName }) tables.push(tableDetails.Table!)
);
tables.push(tableDetails.Table!);
} }
return tables; return tables
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'DynamoDBTableEncryptedKMS', name: 'DynamoDBTableEncryptedKMS',
@ -36,102 +29,99 @@ export class DynamoDBTableEncryptedKMS implements BPSet {
name: 'kms-key-id', name: 'kms-key-id',
description: 'The ID of the KMS key used to encrypt the DynamoDB table.', description: 'The ID of the KMS key used to encrypt the DynamoDB table.',
default: '', default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv', example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTablesCommand', name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.', reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.'
}, },
{ {
name: 'DescribeTableCommand', name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table, including SSE configuration.', reason: 'Fetch details of each DynamoDB table, including SSE configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateTableCommand', name: 'UpdateTableCommand',
reason: 'Enable KMS encryption for non-compliant DynamoDB tables.', reason: 'Enable KMS encryption for non-compliant DynamoDB tables.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the specified KMS key is accessible and meets your security policies.', adviseBeforeFixFunction: 'Ensure the specified KMS key is accessible and meets your security policies.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const tables = await this.getTables(); const tables = await this.getTables()
for (const table of tables) { for (const table of tables) {
if ( if (table.SSEDescription?.Status === 'ENABLED' && table.SSEDescription?.SSEType === 'KMS') {
table.SSEDescription?.Status === 'ENABLED' && compliantResources.push(table.TableArn!)
table.SSEDescription?.SSEType === 'KMS'
) {
compliantResources.push(table.TableArn!);
} else { } else {
nonCompliantResources.push(table.TableArn!); nonCompliantResources.push(table.TableArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value
if (!kmsKeyId) { if (!kmsKeyId) {
throw new Error("Required parameter 'kms-key-id' is missing."); throw new Error("Required parameter 'kms-key-id' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!; const tableName = arn.split('/').pop()!
await this.client.send( await this.client.send(
new UpdateTableCommand({ new UpdateTableCommand({
@ -139,10 +129,10 @@ export class DynamoDBTableEncryptedKMS implements BPSet {
SSESpecification: { SSESpecification: {
Enabled: true, Enabled: true,
SSEType: 'KMS', SSEType: 'KMS',
KMSMasterKeyId: kmsKeyId, KMSMasterKeyId: kmsKeyId
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,33 +1,27 @@
import { import { DynamoDBClient, ListTablesCommand, DescribeTableCommand, UpdateTableCommand } from '@aws-sdk/client-dynamodb'
DynamoDBClient, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
ListTablesCommand, import { Memorizer } from '../../Memorizer'
DescribeTableCommand,
UpdateTableCommand,
} from '@aws-sdk/client-dynamodb';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBTableEncryptionEnabled implements BPSet { export class DynamoDBTableEncryptionEnabled implements BPSet {
private readonly client = new DynamoDBClient({}); private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTables = async () => { private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({})); const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []; const tables = []
for (const tableName of tableNames.TableNames || []) { for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send( const tableDetails = await this.memoClient.send(new DescribeTableCommand({ TableName: tableName }))
new DescribeTableCommand({ TableName: tableName }) tables.push(tableDetails.Table!)
);
tables.push(tableDetails.Table!);
} }
return tables; return tables
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'DynamoDBTableEncryptionEnabled', name: 'DynamoDBTableEncryptionEnabled',
description: 'Ensures that DynamoDB tables have server-side encryption enabled.', description: 'Ensures that DynamoDB tables have server-side encryption enabled.',
priority: 3, priority: 3,
priorityReason: 'Enabling server-side encryption ensures data security and compliance with organizational policies.', priorityReason:
'Enabling server-side encryption ensures data security and compliance with organizational policies.',
awsService: 'DynamoDB', awsService: 'DynamoDB',
awsServiceCategory: 'Database', awsServiceCategory: 'Database',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -36,95 +30,95 @@ export class DynamoDBTableEncryptionEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTablesCommand', name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.', reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.'
}, },
{ {
name: 'DescribeTableCommand', name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table, including encryption settings.', reason: 'Fetch details of each DynamoDB table, including encryption settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateTableCommand', name: 'UpdateTableCommand',
reason: 'Enable server-side encryption for non-compliant DynamoDB tables.', reason: 'Enable server-side encryption for non-compliant DynamoDB tables.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure enabling encryption aligns with organizational data security policies.', adviseBeforeFixFunction: 'Ensure enabling encryption aligns with organizational data security policies.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const tables = await this.getTables(); const tables = await this.getTables()
for (const table of tables) { for (const table of tables) {
if (table.SSEDescription?.Status === 'ENABLED') { if (table.SSEDescription?.Status === 'ENABLED') {
compliantResources.push(table.TableArn!); compliantResources.push(table.TableArn!)
} else { } else {
nonCompliantResources.push(table.TableArn!); nonCompliantResources.push(table.TableArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!; const tableName = arn.split('/').pop()!
await this.client.send( await this.client.send(
new UpdateTableCommand({ new UpdateTableCommand({
TableName: tableName, TableName: tableName,
SSESpecification: { SSESpecification: {
Enabled: true, Enabled: true
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { EC2Client, GetEbsEncryptionByDefaultCommand, EnableEbsEncryptionByDefaultCommand } from '@aws-sdk/client-ec2'
EC2Client, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeVolumesCommand, import { Memorizer } from '../../Memorizer'
GetEbsEncryptionByDefaultCommand,
EnableEbsEncryptionByDefaultCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2EbsEncryptionByDefault implements BPSet { export class EC2EbsEncryptionByDefault implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2EbsEncryptionByDefault', name: 'EC2EbsEncryptionByDefault',
description: 'Ensures that EBS encryption is enabled by default for all volumes in the AWS account.', description: 'Ensures that EBS encryption is enabled by default for all volumes in the AWS account.',
priority: 1, priority: 1,
priorityReason: 'Enabling EBS encryption by default ensures data at rest is encrypted, enhancing security and compliance.', priorityReason:
'Enabling EBS encryption by default ensures data at rest is encrypted, enhancing security and compliance.',
awsService: 'EC2', awsService: 'EC2',
awsServiceCategory: 'Compute', awsServiceCategory: 'Compute',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -24,79 +20,79 @@ export class EC2EbsEncryptionByDefault implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetEbsEncryptionByDefaultCommand', name: 'GetEbsEncryptionByDefaultCommand',
reason: 'Verify if EBS encryption by default is enabled in the AWS account.', reason: 'Verify if EBS encryption by default is enabled in the AWS account.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'EnableEbsEncryptionByDefaultCommand', name: 'EnableEbsEncryptionByDefaultCommand',
reason: 'Enable EBS encryption by default for the account.', reason: 'Enable EBS encryption by default for the account.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure enabling EBS encryption by default aligns with your organizations security policies.', 'Ensure enabling EBS encryption by default aligns with your organizations security policies.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.client.send(new GetEbsEncryptionByDefaultCommand({})); const response = await this.client.send(new GetEbsEncryptionByDefaultCommand({}))
if (response.EbsEncryptionByDefault) { if (response.EbsEncryptionByDefault) {
compliantResources.push('EBS Encryption By Default'); compliantResources.push('EBS Encryption By Default')
} else { } else {
nonCompliantResources.push('EBS Encryption By Default'); nonCompliantResources.push('EBS Encryption By Default')
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async () => { public readonly fixImpl: BPSetFixFn = async () => {
await this.client.send(new EnableEbsEncryptionByDefaultCommand({})); await this.client.send(new EnableEbsEncryptionByDefaultCommand({}))
}; }
} }

View File

@ -1,14 +1,10 @@
import { import { DescribeInstancesCommand, EC2Client, ModifyInstanceMetadataOptionsCommand } from '@aws-sdk/client-ec2'
DescribeInstancesCommand, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
EC2Client, import { Memorizer } from '../../Memorizer'
ModifyInstanceMetadataOptionsCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2Imdsv2Check implements BPSet { export class EC2Imdsv2Check implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2Imdsv2Check', name: 'EC2Imdsv2Check',
@ -23,92 +19,91 @@ export class EC2Imdsv2Check implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeInstancesCommand', name: 'DescribeInstancesCommand',
reason: 'Retrieve all EC2 instances and check their metadata options.', reason: 'Retrieve all EC2 instances and check their metadata options.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyInstanceMetadataOptionsCommand', name: 'ModifyInstanceMetadataOptionsCommand',
reason: 'Update EC2 instance metadata options to enforce IMDSv2.', reason: 'Update EC2 instance metadata options to enforce IMDSv2.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure modifying metadata options aligns with operational policies.', adviseBeforeFixFunction: 'Ensure modifying metadata options aligns with operational policies.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})); const response = await this.memoClient.send(new DescribeInstancesCommand({}))
for (const reservation of response.Reservations || []) { for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) { for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'terminated') if (instance.State?.Name === 'terminated') continue
continue
if (instance.MetadataOptions?.HttpTokens === 'required') { if (instance.MetadataOptions?.HttpTokens === 'required') {
compliantResources.push(instance.InstanceId!); compliantResources.push(instance.InstanceId!)
} else { } else {
nonCompliantResources.push(instance.InstanceId!); nonCompliantResources.push(instance.InstanceId!)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const instanceId of nonCompliantResources) { for (const instanceId of nonCompliantResources) {
await this.client.send( await this.client.send(
new ModifyInstanceMetadataOptionsCommand({ new ModifyInstanceMetadataOptionsCommand({
InstanceId: instanceId, InstanceId: instanceId,
HttpTokens: 'required', HttpTokens: 'required'
}) })
); )
} }
}; }
} }

View File

@ -1,14 +1,10 @@
import { import { DescribeInstancesCommand, EC2Client, MonitorInstancesCommand } from '@aws-sdk/client-ec2'
DescribeInstancesCommand, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
EC2Client, import { Memorizer } from '../../Memorizer'
MonitorInstancesCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2InstanceDetailedMonitoringEnabled implements BPSet { export class EC2InstanceDetailedMonitoringEnabled implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2InstanceDetailedMonitoringEnabled', name: 'EC2InstanceDetailedMonitoringEnabled',
@ -23,92 +19,91 @@ export class EC2InstanceDetailedMonitoringEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeInstancesCommand', name: 'DescribeInstancesCommand',
reason: 'Retrieve the list of all EC2 instances and their monitoring state.', reason: 'Retrieve the list of all EC2 instances and their monitoring state.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'MonitorInstancesCommand', name: 'MonitorInstancesCommand',
reason: 'Enable detailed monitoring for non-compliant EC2 instances.', reason: 'Enable detailed monitoring for non-compliant EC2 instances.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure enabling detailed monitoring aligns with organizational policies and cost considerations.', 'Ensure enabling detailed monitoring aligns with organizational policies and cost considerations.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})); const response = await this.memoClient.send(new DescribeInstancesCommand({}))
for (const reservation of response.Reservations || []) { for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) { for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'terminated') if (instance.State?.Name === 'terminated') continue
continue
if (instance.Monitoring?.State === 'enabled') { if (instance.Monitoring?.State === 'enabled') {
compliantResources.push(instance.InstanceId!); compliantResources.push(instance.InstanceId!)
} else { } else {
nonCompliantResources.push(instance.InstanceId!); nonCompliantResources.push(instance.InstanceId!)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
if (nonCompliantResources.length > 0) { if (nonCompliantResources.length > 0) {
await this.client.send( await this.client.send(
new MonitorInstancesCommand({ new MonitorInstancesCommand({
InstanceIds: nonCompliantResources, InstanceIds: nonCompliantResources
}) })
); )
} }
}; }
} }

View File

@ -1,26 +1,21 @@
import { import { EC2Client, DescribeInstancesCommand } from '@aws-sdk/client-ec2'
EC2Client, import { SSMClient, DescribeInstanceInformationCommand } from '@aws-sdk/client-ssm'
DescribeInstancesCommand, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
} from '@aws-sdk/client-ec2'; import { Memorizer } from '../../Memorizer'
import {
SSMClient,
DescribeInstanceInformationCommand,
} from '@aws-sdk/client-ssm';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2InstanceManagedBySystemsManager implements BPSet { export class EC2InstanceManagedBySystemsManager implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly ssmClient = new SSMClient({}); private readonly ssmClient = new SSMClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2InstanceManagedBySystemsManager', name: 'EC2InstanceManagedBySystemsManager',
description: 'Ensures that EC2 instances are managed by AWS Systems Manager.', description: 'Ensures that EC2 instances are managed by AWS Systems Manager.',
priority: 2, priority: 2,
priorityReason: 'Management through Systems Manager ensures efficient and secure configuration and operation of EC2 instances.', priorityReason:
'Management through Systems Manager ensures efficient and secure configuration and operation of EC2 instances.',
awsService: 'EC2', awsService: 'EC2',
awsServiceCategory: 'Compute', awsServiceCategory: 'Compute',
bestPracticeCategory: 'Management and Governance', bestPracticeCategory: 'Management and Governance',
requiredParametersForFix: [], requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
@ -28,80 +23,75 @@ export class EC2InstanceManagedBySystemsManager implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeInstancesCommand', name: 'DescribeInstancesCommand',
reason: 'Retrieve the list of all EC2 instances.', reason: 'Retrieve the list of all EC2 instances.'
}, },
{ {
name: 'DescribeInstanceInformationCommand', name: 'DescribeInstanceInformationCommand',
reason: 'Retrieve information about instances managed by Systems Manager.', reason: 'Retrieve information about instances managed by Systems Manager.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure Systems Manager Agent (SSM Agent) is installed and configured properly on non-compliant instances.', 'Ensure Systems Manager Agent (SSM Agent) is installed and configured properly on non-compliant instances.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})); const response = await this.memoClient.send(new DescribeInstancesCommand({}))
const ssmResponse = await this.ssmClient.send( const ssmResponse = await this.ssmClient.send(new DescribeInstanceInformationCommand({}))
new DescribeInstanceInformationCommand({})
);
const managedInstanceIds = ssmResponse.InstanceInformationList?.map( const managedInstanceIds = ssmResponse.InstanceInformationList?.map((info) => info.InstanceId)
(info) => info.InstanceId
);
for (const reservation of response.Reservations || []) { for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) { for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'terminated') if (instance.State?.Name === 'terminated') continue
continue
if (managedInstanceIds?.includes(instance.InstanceId!)) { if (managedInstanceIds?.includes(instance.InstanceId!)) {
compliantResources.push(instance.InstanceId!); compliantResources.push(instance.InstanceId!)
} else { } else {
nonCompliantResources.push(instance.InstanceId!); nonCompliantResources.push(instance.InstanceId!)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async () => { public readonly fix: BPSetFixFn = async () => {
throw new Error( throw new Error(
'Fix logic for EC2InstanceManagedBySystemsManager is not directly applicable. Systems Manager Agent setup requires manual intervention.' 'Fix logic for EC2InstanceManagedBySystemsManager is not directly applicable. Systems Manager Agent setup requires manual intervention.'
); )
}; }
} }

View File

@ -1,14 +1,10 @@
import { import { EC2Client, DescribeInstancesCommand, AssociateIamInstanceProfileCommand } from '@aws-sdk/client-ec2'
EC2Client, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeInstancesCommand, import { Memorizer } from '../../Memorizer'
AssociateIamInstanceProfileCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2InstanceProfileAttached implements BPSet { export class EC2InstanceProfileAttached implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2InstanceProfileAttached', name: 'EC2InstanceProfileAttached',
@ -23,108 +19,105 @@ export class EC2InstanceProfileAttached implements BPSet {
name: 'iam-instance-profile', name: 'iam-instance-profile',
description: 'The name of the IAM instance profile to attach.', description: 'The name of the IAM instance profile to attach.',
default: '', default: '',
example: 'EC2InstanceProfile', example: 'EC2InstanceProfile'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeInstancesCommand', name: 'DescribeInstancesCommand',
reason: 'Retrieve all EC2 instances and their associated IAM instance profiles.', reason: 'Retrieve all EC2 instances and their associated IAM instance profiles.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'AssociateIamInstanceProfileCommand', name: 'AssociateIamInstanceProfileCommand',
reason: 'Attach an IAM instance profile to non-compliant EC2 instances.', reason: 'Attach an IAM instance profile to non-compliant EC2 instances.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure the specified IAM instance profile exists and aligns with your access control policies.', 'Ensure the specified IAM instance profile exists and aligns with your access control policies.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})); const response = await this.memoClient.send(new DescribeInstancesCommand({}))
for (const reservation of response.Reservations || []) { for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) { for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'terminated') if (instance.State?.Name === 'terminated') continue
continue
if (instance.IamInstanceProfile) { if (instance.IamInstanceProfile) {
compliantResources.push(instance.InstanceId!); compliantResources.push(instance.InstanceId!)
} else { } else {
nonCompliantResources.push(instance.InstanceId!); nonCompliantResources.push(instance.InstanceId!)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const iamInstanceProfile = requiredParametersForFix.find( const iamInstanceProfile = requiredParametersForFix.find((param) => param.name === 'iam-instance-profile')?.value
(param) => param.name === 'iam-instance-profile'
)?.value;
if (!iamInstanceProfile) { if (!iamInstanceProfile) {
throw new Error("Required parameter 'iam-instance-profile' is missing."); throw new Error("Required parameter 'iam-instance-profile' is missing.")
} }
for (const instanceId of nonCompliantResources) { for (const instanceId of nonCompliantResources) {
await this.client.send( await this.client.send(
new AssociateIamInstanceProfileCommand({ new AssociateIamInstanceProfileCommand({
InstanceId: instanceId, InstanceId: instanceId,
IamInstanceProfile: { Name: iamInstanceProfile }, IamInstanceProfile: { Name: iamInstanceProfile }
}) })
); )
} }
}; }
} }

View File

@ -1,13 +1,10 @@
import { import { EC2Client, DescribeInstancesCommand } from '@aws-sdk/client-ec2'
EC2Client, import { BPSet, BPSetStats } from '../../types'
DescribeInstancesCommand, import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2NoAmazonKeyPair implements BPSet { export class EC2NoAmazonKeyPair implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2NoAmazonKeyPair', name: 'EC2NoAmazonKeyPair',
@ -22,70 +19,69 @@ export class EC2NoAmazonKeyPair implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeInstancesCommand', name: 'DescribeInstancesCommand',
reason: 'Retrieve all EC2 instances and verify if an Amazon Key Pair is used.', reason: 'Retrieve all EC2 instances and verify if an Amazon Key Pair is used.'
}, }
], ],
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure instances are launched without a Key Pair or configure SSH access using alternative mechanisms like Systems Manager Session Manager.', 'Ensure instances are launched without a Key Pair or configure SSH access using alternative mechanisms like Systems Manager Session Manager.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})); const response = await this.memoClient.send(new DescribeInstancesCommand({}))
for (const reservation of response.Reservations || []) { for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) { for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'terminated') if (instance.State?.Name === 'terminated') continue
continue
if (instance.KeyName) { if (instance.KeyName) {
nonCompliantResources.push(instance.InstanceId!); nonCompliantResources.push(instance.InstanceId!)
} else { } else {
compliantResources.push(instance.InstanceId!); compliantResources.push(instance.InstanceId!)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async () => { public readonly fix = async () => {
throw new Error( throw new Error(
'Fix logic for EC2NoAmazonKeyPair is not applicable. Key pairs must be removed manually or during instance creation.' 'Fix logic for EC2NoAmazonKeyPair is not applicable. Key pairs must be removed manually or during instance creation.'
); )
}; }
} }

View File

@ -1,21 +1,16 @@
import { import { EC2Client, DescribeInstancesCommand, TerminateInstancesCommand } from '@aws-sdk/client-ec2'
EC2Client, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeInstancesCommand, import { Memorizer } from '../../Memorizer'
TerminateInstancesCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2StoppedInstance implements BPSet { export class EC2StoppedInstance implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2StoppedInstance', name: 'EC2StoppedInstance',
description: 'Ensures that stopped EC2 instances are identified and terminated if necessary.', description: 'Ensures that stopped EC2 instances are identified and terminated if necessary.',
priority: 3, priority: 3,
priorityReason: priorityReason: 'Stopped instances can incur costs for storage and IP addresses without being in use.',
'Stopped instances can incur costs for storage and IP addresses without being in use.',
awsService: 'EC2', awsService: 'EC2',
awsServiceCategory: 'Compute', awsServiceCategory: 'Compute',
bestPracticeCategory: 'Cost Optimization', bestPracticeCategory: 'Cost Optimization',
@ -24,91 +19,91 @@ export class EC2StoppedInstance implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeInstancesCommand', name: 'DescribeInstancesCommand',
reason: 'Retrieve EC2 instances to check their state.', reason: 'Retrieve EC2 instances to check their state.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'TerminateInstancesCommand', name: 'TerminateInstancesCommand',
reason: 'Terminate stopped EC2 instances to prevent unnecessary costs.', reason: 'Terminate stopped EC2 instances to prevent unnecessary costs.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure terminated instances do not contain any critical data or configurations before proceeding.', 'Ensure terminated instances do not contain any critical data or configurations before proceeding.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})); const response = await this.memoClient.send(new DescribeInstancesCommand({}))
for (const reservation of response.Reservations || []) { for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) { for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'stopped') { if (instance.State?.Name === 'stopped') {
nonCompliantResources.push(instance.InstanceId!); nonCompliantResources.push(instance.InstanceId!)
} else { } else {
compliantResources.push(instance.InstanceId!); compliantResources.push(instance.InstanceId!)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
if (nonCompliantResources.length === 0) { if (nonCompliantResources.length === 0) {
return; // No stopped instances to terminate return // No stopped instances to terminate
} }
await this.client.send( await this.client.send(
new TerminateInstancesCommand({ new TerminateInstancesCommand({
InstanceIds: nonCompliantResources, InstanceIds: nonCompliantResources
}) })
); )
}; }
} }

View File

@ -1,21 +1,16 @@
import { import { EC2Client, DescribeInstancesCommand, ModifyInstanceMetadataOptionsCommand } from '@aws-sdk/client-ec2'
EC2Client, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeInstancesCommand, import { Memorizer } from '../../Memorizer'
ModifyInstanceMetadataOptionsCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2TokenHopLimitCheck implements BPSet { export class EC2TokenHopLimitCheck implements BPSet {
private readonly client = new EC2Client({}); private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EC2TokenHopLimitCheck', name: 'EC2TokenHopLimitCheck',
description: 'Ensures that EC2 instances have a Metadata Options HttpPutResponseHopLimit of 1.', description: 'Ensures that EC2 instances have a Metadata Options HttpPutResponseHopLimit of 1.',
priority: 3, priority: 3,
priorityReason: priorityReason: 'Setting the HttpPutResponseHopLimit to 1 ensures secure access to the instance metadata.',
'Setting the HttpPutResponseHopLimit to 1 ensures secure access to the instance metadata.',
awsService: 'EC2', awsService: 'EC2',
awsServiceCategory: 'Compute', awsServiceCategory: 'Compute',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -24,96 +19,91 @@ export class EC2TokenHopLimitCheck implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeInstancesCommand', name: 'DescribeInstancesCommand',
reason: 'Retrieve EC2 instances and check the metadata options for HttpPutResponseHopLimit.', reason: 'Retrieve EC2 instances and check the metadata options for HttpPutResponseHopLimit.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyInstanceMetadataOptionsCommand', name: 'ModifyInstanceMetadataOptionsCommand',
reason: 'Update the HttpPutResponseHopLimit to enforce secure metadata access.', reason: 'Update the HttpPutResponseHopLimit to enforce secure metadata access.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure modifying instance metadata options aligns with your operational policies.'
'Ensure modifying instance metadata options aligns with your operational policies.', })
});
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})); const response = await this.memoClient.send(new DescribeInstancesCommand({}))
for (const reservation of response.Reservations || []) { for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) { for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'terminated') if (instance.State?.Name === 'terminated') continue
continue
if ( if (instance.MetadataOptions?.HttpPutResponseHopLimit && instance.MetadataOptions.HttpPutResponseHopLimit < 2) {
instance.MetadataOptions?.HttpPutResponseHopLimit && compliantResources.push(instance.InstanceId!)
instance.MetadataOptions.HttpPutResponseHopLimit < 2
) {
compliantResources.push(instance.InstanceId!);
} else { } else {
nonCompliantResources.push(instance.InstanceId!); nonCompliantResources.push(instance.InstanceId!)
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const instanceId of nonCompliantResources) { for (const instanceId of nonCompliantResources) {
await this.client.send( await this.client.send(
new ModifyInstanceMetadataOptionsCommand({ new ModifyInstanceMetadataOptionsCommand({
InstanceId: instanceId, InstanceId: instanceId,
HttpPutResponseHopLimit: 1, HttpPutResponseHopLimit: 1
}) })
); )
} }
}; }
} }

View File

@ -5,19 +5,19 @@ import {
ListImagesCommand, ListImagesCommand,
BatchGetImageCommand, BatchGetImageCommand,
PutImageCommand, PutImageCommand,
DeleteRepositoryCommand, DeleteRepositoryCommand
} from '@aws-sdk/client-ecr'; } from '@aws-sdk/client-ecr'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECRKmsEncryption1 implements BPSet { export class ECRKmsEncryption1 implements BPSet {
private readonly client = new ECRClient({}); private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getRepositories = async () => { private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []; return response.repositories || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECRKmsEncryption1', name: 'ECRKmsEncryption1',
@ -32,135 +32,131 @@ export class ECRKmsEncryption1 implements BPSet {
name: 'kms-key-id', name: 'kms-key-id',
description: 'The ID of the KMS key used to encrypt the ECR repository.', description: 'The ID of the KMS key used to encrypt the ECR repository.',
default: '', default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv', example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv'
}, }
], ],
isFixFunctionUsesDestructiveCommand: true, isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeRepositoriesCommand', name: 'DescribeRepositoriesCommand',
reason: 'Retrieve the list of ECR repositories to verify encryption settings.', reason: 'Retrieve the list of ECR repositories to verify encryption settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'CreateRepositoryCommand', name: 'CreateRepositoryCommand',
reason: 'Create a new repository with KMS encryption.', reason: 'Create a new repository with KMS encryption.'
}, },
{ {
name: 'ListImagesCommand', name: 'ListImagesCommand',
reason: 'List all images in the existing repository for migration.', reason: 'List all images in the existing repository for migration.'
}, },
{ {
name: 'BatchGetImageCommand', name: 'BatchGetImageCommand',
reason: 'Retrieve image manifests for migration to the new repository.', reason: 'Retrieve image manifests for migration to the new repository.'
}, },
{ {
name: 'PutImageCommand', name: 'PutImageCommand',
reason: 'Push images to the newly created repository.', reason: 'Push images to the newly created repository.'
}, },
{ {
name: 'DeleteRepositoryCommand', name: 'DeleteRepositoryCommand',
reason: 'Delete the old repository after migration.', reason: 'Delete the old repository after migration.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure the specified KMS key is accessible, and deleting the old repository aligns with operational policies.', 'Ensure the specified KMS key is accessible, and deleting the old repository aligns with operational policies.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const repositories = await this.getRepositories(); const repositories = await this.getRepositories()
for (const repository of repositories) { for (const repository of repositories) {
if (repository.encryptionConfiguration?.encryptionType === 'KMS') { if (repository.encryptionConfiguration?.encryptionType === 'KMS') {
compliantResources.push(repository.repositoryArn!); compliantResources.push(repository.repositoryArn!)
} else { } else {
nonCompliantResources.push(repository.repositoryArn!); nonCompliantResources.push(repository.repositoryArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value
if (!kmsKeyId) { if (!kmsKeyId) {
throw new Error("Required parameter 'kms-key-id' is missing."); throw new Error("Required parameter 'kms-key-id' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const repositoryName = arn.split('/').pop()!; const repositoryName = arn.split('/').pop()!
// Create a new repository with KMS encryption // Create a new repository with KMS encryption
const newRepositoryName = `${repositoryName}-kms`; const newRepositoryName = `${repositoryName}-kms`
await this.client.send( await this.client.send(
new CreateRepositoryCommand({ new CreateRepositoryCommand({
repositoryName: newRepositoryName, repositoryName: newRepositoryName,
encryptionConfiguration: { encryptionConfiguration: {
encryptionType: 'KMS', encryptionType: 'KMS',
kmsKey: kmsKeyId, kmsKey: kmsKeyId
}, }
}) })
); )
// Get all images in the existing repository // Get all images in the existing repository
const listImagesResponse = await this.client.send( const listImagesResponse = await this.client.send(new ListImagesCommand({ repositoryName }))
new ListImagesCommand({ repositoryName }) const imageIds = listImagesResponse.imageIds || []
);
const imageIds = listImagesResponse.imageIds || [];
if (imageIds.length > 0) { if (imageIds.length > 0) {
const batchGetImageResponse = await this.client.send( const batchGetImageResponse = await this.client.send(new BatchGetImageCommand({ repositoryName, imageIds }))
new BatchGetImageCommand({ repositoryName, imageIds })
);
// Push images to the new repository // Push images to the new repository
for (const image of batchGetImageResponse.images || []) { for (const image of batchGetImageResponse.images || []) {
@ -168,9 +164,9 @@ export class ECRKmsEncryption1 implements BPSet {
new PutImageCommand({ new PutImageCommand({
repositoryName: newRepositoryName, repositoryName: newRepositoryName,
imageManifest: image.imageManifest, imageManifest: image.imageManifest,
imageTag: image.imageId?.imageTag, imageTag: image.imageId?.imageTag
}) })
); )
} }
} }
@ -178,9 +174,9 @@ export class ECRKmsEncryption1 implements BPSet {
await this.client.send( await this.client.send(
new DeleteRepositoryCommand({ new DeleteRepositoryCommand({
repositoryName, repositoryName,
force: true, force: true
}) })
); )
} }
}; }
} }

View File

@ -1,26 +1,21 @@
import { import { ECRClient, DescribeRepositoriesCommand, PutImageScanningConfigurationCommand } from '@aws-sdk/client-ecr'
ECRClient, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeRepositoriesCommand, import { Memorizer } from '../../Memorizer'
PutImageScanningConfigurationCommand,
} from '@aws-sdk/client-ecr';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECRPrivateImageScanningEnabled implements BPSet { export class ECRPrivateImageScanningEnabled implements BPSet {
private readonly client = new ECRClient({}); private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getRepositories = async () => { private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []; return response.repositories || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECRPrivateImageScanningEnabled', name: 'ECRPrivateImageScanningEnabled',
description: 'Ensures that image scanning on push is enabled for private ECR repositories.', description: 'Ensures that image scanning on push is enabled for private ECR repositories.',
priority: 2, priority: 2,
priorityReason: priorityReason: 'Enabling image scanning on push helps identify vulnerabilities in container images during upload.',
'Enabling image scanning on push helps identify vulnerabilities in container images during upload.',
awsService: 'ECR', awsService: 'ECR',
awsServiceCategory: 'Container', awsServiceCategory: 'Container',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -29,90 +24,90 @@ export class ECRPrivateImageScanningEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeRepositoriesCommand', name: 'DescribeRepositoriesCommand',
reason: 'Retrieve all ECR repositories and check their image scanning configuration.', reason: 'Retrieve all ECR repositories and check their image scanning configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutImageScanningConfigurationCommand', name: 'PutImageScanningConfigurationCommand',
reason: 'Enable image scanning on push for non-compliant repositories.', reason: 'Enable image scanning on push for non-compliant repositories.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure enabling image scanning aligns with organizational policies and does not affect performance.', 'Ensure enabling image scanning aligns with organizational policies and does not affect performance.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const repositories = await this.getRepositories(); const repositories = await this.getRepositories()
for (const repository of repositories) { for (const repository of repositories) {
if (repository.imageScanningConfiguration?.scanOnPush) { if (repository.imageScanningConfiguration?.scanOnPush) {
compliantResources.push(repository.repositoryArn!); compliantResources.push(repository.repositoryArn!)
} else { } else {
nonCompliantResources.push(repository.repositoryArn!); nonCompliantResources.push(repository.repositoryArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const repositoryName = arn.split('/').pop()!; const repositoryName = arn.split('/').pop()!
await this.client.send( await this.client.send(
new PutImageScanningConfigurationCommand({ new PutImageScanningConfigurationCommand({
repositoryName, repositoryName,
imageScanningConfiguration: { scanOnPush: true }, imageScanningConfiguration: { scanOnPush: true }
}) })
); )
} }
}; }
} }

View File

@ -2,19 +2,19 @@ import {
ECRClient, ECRClient,
DescribeRepositoriesCommand, DescribeRepositoriesCommand,
PutLifecyclePolicyCommand, PutLifecyclePolicyCommand,
GetLifecyclePolicyCommand, GetLifecyclePolicyCommand
} from '@aws-sdk/client-ecr'; } from '@aws-sdk/client-ecr'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECRPrivateLifecyclePolicyConfigured implements BPSet { export class ECRPrivateLifecyclePolicyConfigured implements BPSet {
private readonly client = new ECRClient({}); private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getRepositories = async () => { private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []; return response.repositories || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECRPrivateLifecyclePolicyConfigured', name: 'ECRPrivateLifecyclePolicyConfigured',
@ -30,119 +30,118 @@ export class ECRPrivateLifecyclePolicyConfigured implements BPSet {
name: 'lifecycle-policy', name: 'lifecycle-policy',
description: 'The JSON-formatted lifecycle policy text to apply to the repositories.', description: 'The JSON-formatted lifecycle policy text to apply to the repositories.',
default: '', default: '',
example: '{"rules":[{"rulePriority":1,"description":"Expire untagged images older than 30 days","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countNumber":30,"countUnit":"days"},"action":{"type":"expire"}}]}', example:
}, '{"rules":[{"rulePriority":1,"description":"Expire untagged images older than 30 days","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countNumber":30,"countUnit":"days"},"action":{"type":"expire"}}]}'
}
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeRepositoriesCommand', name: 'DescribeRepositoriesCommand',
reason: 'Retrieve all ECR repositories to check their lifecycle policy status.', reason: 'Retrieve all ECR repositories to check their lifecycle policy status.'
}, },
{ {
name: 'GetLifecyclePolicyCommand', name: 'GetLifecyclePolicyCommand',
reason: 'Verify if a lifecycle policy exists for each repository.', reason: 'Verify if a lifecycle policy exists for each repository.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutLifecyclePolicyCommand', name: 'PutLifecyclePolicyCommand',
reason: 'Apply a lifecycle policy to non-compliant ECR repositories.', reason: 'Apply a lifecycle policy to non-compliant ECR repositories.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure the provided lifecycle policy is well-tested and does not inadvertently remove critical images.', 'Ensure the provided lifecycle policy is well-tested and does not inadvertently remove critical images.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const repositories = await this.getRepositories(); const repositories = await this.getRepositories()
for (const repository of repositories) { for (const repository of repositories) {
try { try {
await this.client.send( await this.client.send(
new GetLifecyclePolicyCommand({ new GetLifecyclePolicyCommand({
registryId: repository.registryId, registryId: repository.registryId,
repositoryName: repository.repositoryName, repositoryName: repository.repositoryName
}) })
); )
compliantResources.push(repository.repositoryArn!); compliantResources.push(repository.repositoryArn!)
} catch (error: any) { } catch (error: unknown) {
if (error.name === 'LifecyclePolicyNotFoundException') { if (error.name === 'LifecyclePolicyNotFoundException') {
nonCompliantResources.push(repository.repositoryArn!); nonCompliantResources.push(repository.repositoryArn!)
} else { } else {
throw error; throw error
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const lifecyclePolicy = requiredParametersForFix.find( const lifecyclePolicy = requiredParametersForFix.find((param) => param.name === 'lifecycle-policy')?.value
(param) => param.name === 'lifecycle-policy'
)?.value;
if (!lifecyclePolicy) { if (!lifecyclePolicy) {
throw new Error("Required parameter 'lifecycle-policy' is missing."); throw new Error("Required parameter 'lifecycle-policy' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const repositoryName = arn.split('/').pop()!; const repositoryName = arn.split('/').pop()!
await this.client.send( await this.client.send(
new PutLifecyclePolicyCommand({ new PutLifecyclePolicyCommand({
repositoryName, repositoryName,
lifecyclePolicyText: lifecyclePolicy, lifecyclePolicyText: lifecyclePolicy
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,15 @@
import { import { ECRClient, DescribeRepositoriesCommand, PutImageTagMutabilityCommand } from '@aws-sdk/client-ecr'
ECRClient, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeRepositoriesCommand, import { Memorizer } from '../../Memorizer'
PutImageTagMutabilityCommand,
} from '@aws-sdk/client-ecr';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECRPrivateTagImmutabilityEnabled implements BPSet { export class ECRPrivateTagImmutabilityEnabled implements BPSet {
private readonly client = new ECRClient({}); private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getRepositories = async () => { private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []; return response.repositories || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECRPrivateTagImmutabilityEnabled', name: 'ECRPrivateTagImmutabilityEnabled',
@ -29,91 +25,90 @@ export class ECRPrivateTagImmutabilityEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeRepositoriesCommand', name: 'DescribeRepositoriesCommand',
reason: 'Retrieve all ECR repositories to check their tag mutability setting.', reason: 'Retrieve all ECR repositories to check their tag mutability setting.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutImageTagMutabilityCommand', name: 'PutImageTagMutabilityCommand',
reason: 'Set image tag immutability for non-compliant repositories.', reason: 'Set image tag immutability for non-compliant repositories.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure that enabling tag immutability aligns with your development and deployment processes.', 'Ensure that enabling tag immutability aligns with your development and deployment processes.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const repositories = await this.getRepositories(); const repositories = await this.getRepositories()
for (const repository of repositories) { for (const repository of repositories) {
if (repository.imageTagMutability === 'IMMUTABLE') { if (repository.imageTagMutability === 'IMMUTABLE') {
compliantResources.push(repository.repositoryArn!); compliantResources.push(repository.repositoryArn!)
} else { } else {
nonCompliantResources.push(repository.repositoryArn!); nonCompliantResources.push(repository.repositoryArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const repositoryName = arn.split('/').pop()!; const repositoryName = arn.split('/').pop()!
await this.client.send( await this.client.send(
new PutImageTagMutabilityCommand({ new PutImageTagMutabilityCommand({
repositoryName, repositoryName,
imageTagMutability: 'IMMUTABLE', imageTagMutability: 'IMMUTABLE'
}) })
); )
} }
}; }
} }

View File

@ -2,35 +2,30 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'; } from '@aws-sdk/client-ecs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECSAwsVpcNetworkingEnabled implements BPSet { export class ECSAwsVpcNetworkingEnabled implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTaskDefinitions = async () => { private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send( const taskDefinitionArns = await this.memoClient.send(new ListTaskDefinitionsCommand({ status: 'ACTIVE' }))
new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) { for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) taskDefinitions.push(taskDefinition.taskDefinition!)
);
taskDefinitions.push(taskDefinition.taskDefinition!);
} }
return taskDefinitions; return taskDefinitions
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSAwsVpcNetworkingEnabled', name: 'ECSAwsVpcNetworkingEnabled',
description: 'Ensures that ECS task definitions are configured to use the awsvpc network mode.', description: 'Ensures that ECS task definitions are configured to use the awsvpc network mode.',
priority: 3, priority: 3,
priorityReason: priorityReason: 'Using the awsvpc network mode provides enhanced security and networking capabilities.',
'Using the awsvpc network mode provides enhanced security and networking capabilities.',
awsService: 'ECS', awsService: 'ECS',
awsServiceCategory: 'Container', awsServiceCategory: 'Container',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -39,90 +34,88 @@ export class ECSAwsVpcNetworkingEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTaskDefinitionsCommand', name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.', reason: 'Retrieve all active ECS task definitions.'
}, },
{ {
name: 'DescribeTaskDefinitionCommand', name: 'DescribeTaskDefinitionCommand',
reason: 'Describe details of each ECS task definition.', reason: 'Describe details of each ECS task definition.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RegisterTaskDefinitionCommand', name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with awsvpc network mode.', reason: 'Re-register ECS task definitions with awsvpc network mode.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure that the awsvpc network mode is supported by your ECS setup and compatible with your workloads.', 'Ensure that the awsvpc network mode is supported by your ECS setup and compatible with your workloads.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const taskDefinitions = await this.getTaskDefinitions(); const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) { for (const taskDefinition of taskDefinitions) {
if (taskDefinition.networkMode === 'awsvpc') { if (taskDefinition.networkMode === 'awsvpc') {
compliantResources.push(taskDefinition.taskDefinitionArn!); compliantResources.push(taskDefinition.taskDefinitionArn!)
} else { } else {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!); nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -131,9 +124,9 @@ export class ECSAwsVpcNetworkingEnabled implements BPSet {
networkMode: 'awsvpc', networkMode: 'awsvpc',
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu, cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory, memory: taskDefinition.taskDefinition?.memory
}) })
); )
} }
}; }
} }

View File

@ -1,28 +1,21 @@
import { import { ECSClient, DescribeClustersCommand, UpdateClusterSettingsCommand } from '@aws-sdk/client-ecs'
ECSClient, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeClustersCommand, import { Memorizer } from '../../Memorizer'
UpdateClusterSettingsCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSContainerInsightsEnabled implements BPSet { export class ECSContainerInsightsEnabled implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getClusters = async () => { private readonly getClusters = async () => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new DescribeClustersCommand({ include: ['SETTINGS'] }))
new DescribeClustersCommand({ include: ['SETTINGS'] }) return response.clusters || []
); }
return response.clusters || [];
};
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSContainerInsightsEnabled', name: 'ECSContainerInsightsEnabled',
description: 'Ensures that ECS clusters have Container Insights enabled.', description: 'Ensures that ECS clusters have Container Insights enabled.',
priority: 3, priority: 3,
priorityReason: priorityReason: 'Enabling Container Insights provides enhanced monitoring and diagnostics for ECS clusters.',
'Enabling Container Insights provides enhanced monitoring and diagnostics for ECS clusters.',
awsService: 'ECS', awsService: 'ECS',
awsServiceCategory: 'Container', awsServiceCategory: 'Container',
bestPracticeCategory: 'Monitoring', bestPracticeCategory: 'Monitoring',
@ -31,94 +24,91 @@ export class ECSContainerInsightsEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeClustersCommand', name: 'DescribeClustersCommand',
reason: 'Retrieve ECS clusters and their settings to check if Container Insights is enabled.', reason: 'Retrieve ECS clusters and their settings to check if Container Insights is enabled.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateClusterSettingsCommand', name: 'UpdateClusterSettingsCommand',
reason: 'Enable Container Insights for non-compliant ECS clusters.', reason: 'Enable Container Insights for non-compliant ECS clusters.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure enabling Container Insights aligns with your monitoring strategy and does not incur unexpected costs.', 'Ensure enabling Container Insights aligns with your monitoring strategy and does not incur unexpected costs.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const clusters = await this.getClusters(); const clusters = await this.getClusters()
for (const cluster of clusters) { for (const cluster of clusters) {
if (cluster.clusterName === 'default') if (cluster.clusterName === 'default') continue
continue
const containerInsightsSetting = cluster.settings?.find( const containerInsightsSetting = cluster.settings?.find((setting) => setting.name === 'containerInsights')
(setting) => setting.name === 'containerInsights'
);
if (containerInsightsSetting?.value === 'enabled') { if (containerInsightsSetting?.value === 'enabled') {
compliantResources.push(cluster.clusterArn!); compliantResources.push(cluster.clusterArn!)
} else { } else {
nonCompliantResources.push(cluster.clusterArn!); nonCompliantResources.push(cluster.clusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
await this.client.send( await this.client.send(
new UpdateClusterSettingsCommand({ new UpdateClusterSettingsCommand({
cluster: arn, cluster: arn,
settings: [{ name: 'containerInsights', value: 'enabled' }], settings: [{ name: 'containerInsights', value: 'enabled' }]
}) })
); )
} }
}; }
} }

View File

@ -2,35 +2,30 @@ import {
ECSClient, ECSClient,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand
} from '@aws-sdk/client-ecs'; } from '@aws-sdk/client-ecs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECSContainersNonPrivileged implements BPSet { export class ECSContainersNonPrivileged implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTaskDefinitions = async () => { private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send( const taskDefinitionArns = await this.memoClient.send(new ListTaskDefinitionsCommand({ status: 'ACTIVE' }))
new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) { for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) taskDefinitions.push(taskDefinition.taskDefinition!)
);
taskDefinitions.push(taskDefinition.taskDefinition!);
} }
return taskDefinitions; return taskDefinitions
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSContainersNonPrivileged', name: 'ECSContainersNonPrivileged',
description: 'Ensures that containers in ECS task definitions are not running in privileged mode.', description: 'Ensures that containers in ECS task definitions are not running in privileged mode.',
priority: 1, priority: 1,
priorityReason: priorityReason: 'Running containers in privileged mode poses significant security risks.',
'Running containers in privileged mode poses significant security risks.',
awsService: 'ECS', awsService: 'ECS',
awsServiceCategory: 'Container', awsServiceCategory: 'Container',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -39,100 +34,93 @@ export class ECSContainersNonPrivileged implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTaskDefinitionsCommand', name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.', reason: 'Retrieve all active ECS task definitions.'
}, },
{ {
name: 'DescribeTaskDefinitionCommand', name: 'DescribeTaskDefinitionCommand',
reason: 'Check the container configurations in ECS task definitions.', reason: 'Check the container configurations in ECS task definitions.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RegisterTaskDefinitionCommand', name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with privileged mode disabled.', reason: 'Re-register ECS task definitions with privileged mode disabled.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure that containers do not rely on privileged mode for their functionality.'
'Ensure that containers do not rely on privileged mode for their functionality.', })
});
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const taskDefinitions = await this.getTaskDefinitions(); const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) { for (const taskDefinition of taskDefinitions) {
const privilegedContainers = taskDefinition.containerDefinitions?.filter( const privilegedContainers = taskDefinition.containerDefinitions?.filter((container) => container.privileged)
(container) => container.privileged
);
if (privilegedContainers?.length) { if (privilegedContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!); nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else { } else {
compliantResources.push(taskDefinition.taskDefinitionArn!); compliantResources.push(taskDefinition.taskDefinitionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map((container) => ({
(container) => ({ ...container,
...container, privileged: false
privileged: false, }))
})
);
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -141,9 +129,9 @@ export class ECSContainersNonPrivileged implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode, networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu, cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory, memory: taskDefinition.taskDefinition?.memory
}) })
); )
} }
}; }
} }

View File

@ -2,28 +2,24 @@ import {
ECSClient, ECSClient,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand
} from '@aws-sdk/client-ecs'; } from '@aws-sdk/client-ecs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECSContainersReadonlyAccess implements BPSet { export class ECSContainersReadonlyAccess implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTaskDefinitions = async () => { private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send( const taskDefinitionArns = await this.memoClient.send(new ListTaskDefinitionsCommand({ status: 'ACTIVE' }))
new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) { for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) taskDefinitions.push(taskDefinition.taskDefinition!)
);
taskDefinitions.push(taskDefinition.taskDefinition!);
} }
return taskDefinitions; return taskDefinitions
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSContainersReadonlyAccess', name: 'ECSContainersReadonlyAccess',
@ -39,100 +35,96 @@ export class ECSContainersReadonlyAccess implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTaskDefinitionsCommand', name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.', reason: 'Retrieve all active ECS task definitions.'
}, },
{ {
name: 'DescribeTaskDefinitionCommand', name: 'DescribeTaskDefinitionCommand',
reason: 'Check the container configurations in ECS task definitions.', reason: 'Check the container configurations in ECS task definitions.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RegisterTaskDefinitionCommand', name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with readonly root filesystems enabled.', reason: 'Re-register ECS task definitions with readonly root filesystems enabled.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure enabling readonly root filesystems does not interfere with the functionality of your containers.', 'Ensure enabling readonly root filesystems does not interfere with the functionality of your containers.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const taskDefinitions = await this.getTaskDefinitions(); const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) { for (const taskDefinition of taskDefinitions) {
const notReadonlyContainers = taskDefinition.containerDefinitions?.filter( const notReadonlyContainers = taskDefinition.containerDefinitions?.filter(
(container) => !container.readonlyRootFilesystem (container) => !container.readonlyRootFilesystem
); )
if (notReadonlyContainers?.length) { if (notReadonlyContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!); nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else { } else {
compliantResources.push(taskDefinition.taskDefinitionArn!); compliantResources.push(taskDefinition.taskDefinitionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map((container) => ({
(container) => ({ ...container,
...container, readonlyRootFilesystem: true
readonlyRootFilesystem: true, }))
})
);
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -141,9 +133,9 @@ export class ECSContainersReadonlyAccess implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode, networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu, cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory, memory: taskDefinition.taskDefinition?.memory
}) })
); )
} }
}; }
} }

View File

@ -3,31 +3,29 @@ import {
ListClustersCommand, ListClustersCommand,
ListServicesCommand, ListServicesCommand,
DescribeServicesCommand, DescribeServicesCommand,
UpdateServiceCommand, UpdateServiceCommand
} from '@aws-sdk/client-ecs'; } from '@aws-sdk/client-ecs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECSFargateLatestPlatformVersion implements BPSet { export class ECSFargateLatestPlatformVersion implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getServices = async () => { private readonly getServices = async () => {
const clustersResponse = await this.memoClient.send(new ListClustersCommand({})); const clustersResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterArns = clustersResponse.clusterArns || []; const clusterArns = clustersResponse.clusterArns || []
const services: { clusterArn: string; serviceArn: string }[] = []; const services: { clusterArn: string; serviceArn: string }[] = []
for (const clusterArn of clusterArns) { for (const clusterArn of clusterArns) {
const servicesResponse = await this.memoClient.send( const servicesResponse = await this.memoClient.send(new ListServicesCommand({ cluster: clusterArn }))
new ListServicesCommand({ cluster: clusterArn })
);
for (const serviceArn of servicesResponse.serviceArns || []) { for (const serviceArn of servicesResponse.serviceArns || []) {
services.push({ clusterArn, serviceArn }); services.push({ clusterArn, serviceArn })
} }
} }
return services; return services
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSFargateLatestPlatformVersion', name: 'ECSFargateLatestPlatformVersion',
@ -43,104 +41,106 @@ export class ECSFargateLatestPlatformVersion implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListClustersCommand', name: 'ListClustersCommand',
reason: 'Retrieve ECS clusters to identify associated services.', reason: 'Retrieve ECS clusters to identify associated services.'
}, },
{ {
name: 'ListServicesCommand', name: 'ListServicesCommand',
reason: 'Retrieve services associated with each ECS cluster.', reason: 'Retrieve services associated with each ECS cluster.'
}, },
{ {
name: 'DescribeServicesCommand', name: 'DescribeServicesCommand',
reason: 'Check the platform version of each ECS service.', reason: 'Check the platform version of each ECS service.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateServiceCommand', name: 'UpdateServiceCommand',
reason: 'Update ECS services to use the latest platform version.', reason: 'Update ECS services to use the latest platform version.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure that updating to the latest platform version aligns with your workload requirements.', 'Ensure that updating to the latest platform version aligns with your workload requirements.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const services = await this.getServices(); const services = await this.getServices()
for (const { clusterArn, serviceArn } of services) { for (const { clusterArn, serviceArn } of services) {
const serviceResponse = await this.memoClient.send( const serviceResponse = await this.memoClient.send(
new DescribeServicesCommand({ cluster: clusterArn, services: [serviceArn] }) new DescribeServicesCommand({ cluster: clusterArn, services: [serviceArn] })
); )
const service = serviceResponse.services?.[0]; const service = serviceResponse.services?.[0]
if (service?.platformVersion === 'LATEST') { if (service?.platformVersion === 'LATEST') {
compliantResources.push(service.serviceArn!); compliantResources.push(service.serviceArn!)
} else { } else {
nonCompliantResources.push(service?.serviceArn!); if (service?.serviceArn) {
nonCompliantResources.push(service.serviceArn)
}
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
for (const serviceArn of nonCompliantResources) { for (const serviceArn of nonCompliantResources) {
const clusterArn = serviceArn.split(':cluster/')[1].split(':service/')[0]; const clusterArn = serviceArn.split(':cluster/')[1].split(':service/')[0]
await this.client.send( await this.client.send(
new UpdateServiceCommand({ new UpdateServiceCommand({
cluster: clusterArn, cluster: clusterArn,
service: serviceArn, service: serviceArn,
platformVersion: 'LATEST', platformVersion: 'LATEST'
}) })
); )
} }
}; }
} }

View File

@ -2,35 +2,30 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'; } from '@aws-sdk/client-ecs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECSTaskDefinitionLogConfiguration implements BPSet { export class ECSTaskDefinitionLogConfiguration implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTaskDefinitions = async () => { private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send( const taskDefinitionArns = await this.memoClient.send(new ListTaskDefinitionsCommand({ status: 'ACTIVE' }))
new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) { for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) taskDefinitions.push(taskDefinition.taskDefinition!)
);
taskDefinitions.push(taskDefinition.taskDefinition!);
} }
return taskDefinitions; return taskDefinitions
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSTaskDefinitionLogConfiguration', name: 'ECSTaskDefinitionLogConfiguration',
description: 'Ensures that ECS task definitions have log configuration enabled.', description: 'Ensures that ECS task definitions have log configuration enabled.',
priority: 1, priority: 1,
priorityReason: priorityReason: 'Enabling log configuration is critical for monitoring and debugging container workloads.',
'Enabling log configuration is critical for monitoring and debugging container workloads.',
awsService: 'ECS', awsService: 'ECS',
awsServiceCategory: 'Container', awsServiceCategory: 'Container',
bestPracticeCategory: 'Monitoring', bestPracticeCategory: 'Monitoring',
@ -39,118 +34,109 @@ export class ECSTaskDefinitionLogConfiguration implements BPSet {
name: 'log-configuration', name: 'log-configuration',
description: 'The log configuration to apply to non-compliant task definitions.', description: 'The log configuration to apply to non-compliant task definitions.',
default: '{}', default: '{}',
example: '{"logDriver": "awslogs", "options": {"awslogs-group": "/ecs/logs"}}', example: '{"logDriver": "awslogs", "options": {"awslogs-group": "/ecs/logs"}}'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTaskDefinitionsCommand', name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.', reason: 'Retrieve all active ECS task definitions.'
}, },
{ {
name: 'DescribeTaskDefinitionCommand', name: 'DescribeTaskDefinitionCommand',
reason: 'Check the container configurations in ECS task definitions.', reason: 'Check the container configurations in ECS task definitions.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RegisterTaskDefinitionCommand', name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with updated log configuration.', reason: 'Re-register ECS task definitions with updated log configuration.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure the provided log configuration aligns with your logging strategy and infrastructure.', 'Ensure the provided log configuration aligns with your logging strategy and infrastructure.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const taskDefinitions = await this.getTaskDefinitions(); const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) { for (const taskDefinition of taskDefinitions) {
const logDisabledContainers = taskDefinition.containerDefinitions?.filter( const logDisabledContainers = taskDefinition.containerDefinitions?.filter(
(container) => !container.logConfiguration (container) => !container.logConfiguration
); )
if (logDisabledContainers?.length) { if (logDisabledContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!); nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else { } else {
compliantResources.push(taskDefinition.taskDefinitionArn!); compliantResources.push(taskDefinition.taskDefinitionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async ( public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
nonCompliantResources, const logConfiguration = requiredParametersForFix.find((param) => param.name === 'log-configuration')?.value
requiredParametersForFix
) => {
const logConfiguration = requiredParametersForFix.find(
(param) => param.name === 'log-configuration'
)?.value;
if (!logConfiguration) { if (!logConfiguration) {
throw new Error("Required parameter 'log-configuration' is missing."); throw new Error("Required parameter 'log-configuration' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map((container) => ({
(container) => ({ ...container,
...container, logConfiguration: JSON.parse(logConfiguration)
logConfiguration: JSON.parse(logConfiguration), }))
})
);
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -159,9 +145,9 @@ export class ECSTaskDefinitionLogConfiguration implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode, networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu, cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory, memory: taskDefinition.taskDefinition?.memory
}) })
); )
} }
}; }
} }

View File

@ -2,28 +2,24 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'; } from '@aws-sdk/client-ecs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECSTaskDefinitionMemoryHardLimit implements BPSet { export class ECSTaskDefinitionMemoryHardLimit implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTaskDefinitions = async () => { private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send( const taskDefinitionArns = await this.memoClient.send(new ListTaskDefinitionsCommand({ status: 'ACTIVE' }))
new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) { for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) taskDefinitions.push(taskDefinition.taskDefinition!)
);
taskDefinitions.push(taskDefinition.taskDefinition!);
} }
return taskDefinitions; return taskDefinitions
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSTaskDefinitionMemoryHardLimit', name: 'ECSTaskDefinitionMemoryHardLimit',
@ -39,115 +35,105 @@ export class ECSTaskDefinitionMemoryHardLimit implements BPSet {
name: 'default-memory-limit', name: 'default-memory-limit',
description: 'The default memory hard limit to set for containers without a memory limit.', description: 'The default memory hard limit to set for containers without a memory limit.',
default: '512', default: '512',
example: '1024', example: '1024'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTaskDefinitionsCommand', name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.', reason: 'Retrieve all active ECS task definitions.'
}, },
{ {
name: 'DescribeTaskDefinitionCommand', name: 'DescribeTaskDefinitionCommand',
reason: 'Check container configurations in ECS task definitions for memory limits.', reason: 'Check container configurations in ECS task definitions for memory limits.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RegisterTaskDefinitionCommand', name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with updated memory limits.', reason: 'Re-register ECS task definitions with updated memory limits.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure the default memory limit aligns with your container resource requirements.'
'Ensure the default memory limit aligns with your container resource requirements.', })
});
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const taskDefinitions = await this.getTaskDefinitions(); const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) { for (const taskDefinition of taskDefinitions) {
const containersWithoutMemoryLimit = taskDefinition.containerDefinitions?.filter( const containersWithoutMemoryLimit = taskDefinition.containerDefinitions?.filter((container) => !container.memory)
(container) => !container.memory
);
if (containersWithoutMemoryLimit?.length) { if (containersWithoutMemoryLimit?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!); nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else { } else {
compliantResources.push(taskDefinition.taskDefinitionArn!); compliantResources.push(taskDefinition.taskDefinitionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async ( public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
nonCompliantResources,
requiredParametersForFix
) => {
const defaultMemoryLimit = parseInt( const defaultMemoryLimit = parseInt(
requiredParametersForFix.find((param) => param.name === 'default-memory-limit')?.value || '512', requiredParametersForFix.find((param) => param.name === 'default-memory-limit')?.value || '512',
10 10
); )
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map((container) => ({
(container) => ({ ...container,
...container, memory: container.memory || defaultMemoryLimit
memory: container.memory || defaultMemoryLimit, }))
})
);
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -156,9 +142,9 @@ export class ECSTaskDefinitionMemoryHardLimit implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode, networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu, cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory, memory: taskDefinition.taskDefinition?.memory
}) })
); )
} }
}; }
} }

View File

@ -2,28 +2,24 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'; } from '@aws-sdk/client-ecs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ECSTaskDefinitionNonRootUser implements BPSet { export class ECSTaskDefinitionNonRootUser implements BPSet {
private readonly client = new ECSClient({}); private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getTaskDefinitions = async () => { private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send( const taskDefinitionArns = await this.memoClient.send(new ListTaskDefinitionsCommand({ status: 'ACTIVE' }))
new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) { for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) taskDefinitions.push(taskDefinition.taskDefinition!)
);
taskDefinitions.push(taskDefinition.taskDefinition!);
} }
return taskDefinitions; return taskDefinitions
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ECSTaskDefinitionNonRootUser', name: 'ECSTaskDefinitionNonRootUser',
@ -39,114 +35,106 @@ export class ECSTaskDefinitionNonRootUser implements BPSet {
name: 'default-non-root-user', name: 'default-non-root-user',
description: 'The default non-root user to assign for containers without a specified user.', description: 'The default non-root user to assign for containers without a specified user.',
default: 'ecs-user', default: 'ecs-user',
example: 'app-user', example: 'app-user'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListTaskDefinitionsCommand', name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.', reason: 'Retrieve all active ECS task definitions.'
}, },
{ {
name: 'DescribeTaskDefinitionCommand', name: 'DescribeTaskDefinitionCommand',
reason: 'Check container configurations in ECS task definitions for user settings.', reason: 'Check container configurations in ECS task definitions for user settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RegisterTaskDefinitionCommand', name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with non-root user configurations.', reason: 'Re-register ECS task definitions with non-root user configurations.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure the default non-root user has sufficient permissions to execute the container workload.', 'Ensure the default non-root user has sufficient permissions to execute the container workload.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const taskDefinitions = await this.getTaskDefinitions(); const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) { for (const taskDefinition of taskDefinitions) {
const privilegedContainers = taskDefinition.containerDefinitions?.filter( const privilegedContainers = taskDefinition.containerDefinitions?.filter(
(container) => !container.user || container.user === 'root' (container) => !container.user || container.user === 'root'
); )
if (privilegedContainers?.length) { if (privilegedContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!); nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else { } else {
compliantResources.push(taskDefinition.taskDefinitionArn!); compliantResources.push(taskDefinition.taskDefinitionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async ( public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
nonCompliantResources, const defaultNonRootUser =
requiredParametersForFix requiredParametersForFix.find((param) => param.name === 'default-non-root-user')?.value || 'ecs-user'
) => {
const defaultNonRootUser = requiredParametersForFix.find(
(param) => param.name === 'default-non-root-user'
)?.value || 'ecs-user';
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const taskDefinition = await this.memoClient.send( const taskDefinition = await this.memoClient.send(new DescribeTaskDefinitionCommand({ taskDefinition: arn }))
new DescribeTaskDefinitionCommand({ taskDefinition: arn }) const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map((container) => ({
(container) => ({ ...container,
...container, user: container.user || defaultNonRootUser
user: container.user || defaultNonRootUser, }))
})
);
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -155,9 +143,9 @@ export class ECSTaskDefinitionNonRootUser implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode, networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu, cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory, memory: taskDefinition.taskDefinition?.memory
}) })
); )
} }
}; }
} }

View File

@ -2,19 +2,19 @@ import {
EFSClient, EFSClient,
DescribeAccessPointsCommand, DescribeAccessPointsCommand,
DeleteAccessPointCommand, DeleteAccessPointCommand,
CreateAccessPointCommand, CreateAccessPointCommand
} from '@aws-sdk/client-efs'; } from '@aws-sdk/client-efs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class EFSAccessPointEnforceRootDirectory implements BPSet { export class EFSAccessPointEnforceRootDirectory implements BPSet {
private readonly client = new EFSClient({}); private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getAccessPoints = async () => { private readonly getAccessPoints = async () => {
const response = await this.memoClient.send(new DescribeAccessPointsCommand({})); const response = await this.memoClient.send(new DescribeAccessPointsCommand({}))
return response.AccessPoints || []; return response.AccessPoints || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EFSAccessPointEnforceRootDirectory', name: 'EFSAccessPointEnforceRootDirectory',
@ -30,121 +30,116 @@ export class EFSAccessPointEnforceRootDirectory implements BPSet {
name: 'root-directory-path', name: 'root-directory-path',
description: 'The root directory path to enforce for EFS Access Points.', description: 'The root directory path to enforce for EFS Access Points.',
default: '/', default: '/',
example: '/data', example: '/data'
}, }
], ],
isFixFunctionUsesDestructiveCommand: true, isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeAccessPointsCommand', name: 'DescribeAccessPointsCommand',
reason: 'Retrieve all existing EFS Access Points and their configurations.', reason: 'Retrieve all existing EFS Access Points and their configurations.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'DeleteAccessPointCommand', name: 'DeleteAccessPointCommand',
reason: 'Delete non-compliant EFS Access Points.', reason: 'Delete non-compliant EFS Access Points.'
}, },
{ {
name: 'CreateAccessPointCommand', name: 'CreateAccessPointCommand',
reason: 'Recreate EFS Access Points with the enforced root directory.', reason: 'Recreate EFS Access Points with the enforced root directory.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.', 'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const accessPoints = await this.getAccessPoints(); const accessPoints = await this.getAccessPoints()
for (const accessPoint of accessPoints) { for (const accessPoint of accessPoints) {
if (accessPoint.RootDirectory?.Path === '/') { if (accessPoint.RootDirectory?.Path === '/') {
compliantResources.push(accessPoint.AccessPointArn!); compliantResources.push(accessPoint.AccessPointArn!)
} else { } else {
nonCompliantResources.push(accessPoint.AccessPointArn!); nonCompliantResources.push(accessPoint.AccessPointArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async ( public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
nonCompliantResources, const rootDirectoryPath = requiredParametersForFix.find((param) => param.name === 'root-directory-path')?.value
requiredParametersForFix
) => {
const rootDirectoryPath = requiredParametersForFix.find(
(param) => param.name === 'root-directory-path'
)?.value;
if (!rootDirectoryPath) { if (!rootDirectoryPath) {
throw new Error("Required parameter 'root-directory-path' is missing."); throw new Error("Required parameter 'root-directory-path' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const accessPointId = arn.split('/').pop()!; const accessPointId = arn.split('/').pop()!
const fileSystemId = arn.split(':file-system/')[1].split('/')[0]; const fileSystemId = arn.split(':file-system/')[1].split('/')[0]
// Delete the existing access point // Delete the existing access point
await this.client.send( await this.client.send(
new DeleteAccessPointCommand({ new DeleteAccessPointCommand({
AccessPointId: accessPointId, AccessPointId: accessPointId
}) })
); )
// Recreate the access point with the desired root directory // Recreate the access point with the desired root directory
await this.client.send( await this.client.send(
new CreateAccessPointCommand({ new CreateAccessPointCommand({
FileSystemId: fileSystemId, FileSystemId: fileSystemId,
RootDirectory: { Path: rootDirectoryPath }, RootDirectory: { Path: rootDirectoryPath }
}) })
); )
} }
}; }
} }

View File

@ -2,19 +2,19 @@ import {
EFSClient, EFSClient,
DescribeAccessPointsCommand, DescribeAccessPointsCommand,
DeleteAccessPointCommand, DeleteAccessPointCommand,
CreateAccessPointCommand, CreateAccessPointCommand
} from '@aws-sdk/client-efs'; } from '@aws-sdk/client-efs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class EFSAccessPointEnforceUserIdentity implements BPSet { export class EFSAccessPointEnforceUserIdentity implements BPSet {
private readonly client = new EFSClient({}); private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getAccessPoints = async () => { private readonly getAccessPoints = async () => {
const response = await this.memoClient.send(new DescribeAccessPointsCommand({})); const response = await this.memoClient.send(new DescribeAccessPointsCommand({}))
return response.AccessPoints || []; return response.AccessPoints || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EFSAccessPointEnforceUserIdentity', name: 'EFSAccessPointEnforceUserIdentity',
@ -30,119 +30,116 @@ export class EFSAccessPointEnforceUserIdentity implements BPSet {
name: 'posix-user', name: 'posix-user',
description: 'The PosixUser configuration to enforce for EFS Access Points.', description: 'The PosixUser configuration to enforce for EFS Access Points.',
default: '{"Uid": "1000", "Gid": "1000"}', default: '{"Uid": "1000", "Gid": "1000"}',
example: '{"Uid": "1234", "Gid": "1234"}', example: '{"Uid": "1234", "Gid": "1234"}'
}, }
], ],
isFixFunctionUsesDestructiveCommand: true, isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeAccessPointsCommand', name: 'DescribeAccessPointsCommand',
reason: 'Retrieve all existing EFS Access Points and their configurations.', reason: 'Retrieve all existing EFS Access Points and their configurations.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'DeleteAccessPointCommand', name: 'DeleteAccessPointCommand',
reason: 'Delete non-compliant EFS Access Points.', reason: 'Delete non-compliant EFS Access Points.'
}, },
{ {
name: 'CreateAccessPointCommand', name: 'CreateAccessPointCommand',
reason: 'Recreate EFS Access Points with the enforced PosixUser.', reason: 'Recreate EFS Access Points with the enforced PosixUser.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.', 'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const accessPoints = await this.getAccessPoints(); const accessPoints = await this.getAccessPoints()
for (const accessPoint of accessPoints) { for (const accessPoint of accessPoints) {
if (accessPoint.PosixUser) { if (accessPoint.PosixUser) {
compliantResources.push(accessPoint.AccessPointArn!); compliantResources.push(accessPoint.AccessPointArn!)
} else { } else {
nonCompliantResources.push(accessPoint.AccessPointArn!); nonCompliantResources.push(accessPoint.AccessPointArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async ( public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
nonCompliantResources, const posixUser = requiredParametersForFix.find((param) => param.name === 'posix-user')?.value
requiredParametersForFix
) => {
const posixUser = requiredParametersForFix.find((param) => param.name === 'posix-user')?.value;
if (!posixUser) { if (!posixUser) {
throw new Error("Required parameter 'posix-user' is missing."); throw new Error("Required parameter 'posix-user' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const accessPointId = arn.split('/').pop()!; const accessPointId = arn.split('/').pop()!
const fileSystemId = arn.split(':file-system/')[1].split('/')[0]; const fileSystemId = arn.split(':file-system/')[1].split('/')[0]
// Delete the existing access point // Delete the existing access point
await this.client.send( await this.client.send(
new DeleteAccessPointCommand({ new DeleteAccessPointCommand({
AccessPointId: accessPointId, AccessPointId: accessPointId
}) })
); )
// Recreate the access point with the desired PosixUser // Recreate the access point with the desired PosixUser
await this.client.send( await this.client.send(
new CreateAccessPointCommand({ new CreateAccessPointCommand({
FileSystemId: fileSystemId, FileSystemId: fileSystemId,
PosixUser: JSON.parse(posixUser), PosixUser: JSON.parse(posixUser)
}) })
); )
} }
}; }
} }

View File

@ -2,19 +2,19 @@ import {
EFSClient, EFSClient,
DescribeFileSystemsCommand, DescribeFileSystemsCommand,
PutBackupPolicyCommand, PutBackupPolicyCommand,
DescribeBackupPolicyCommand, DescribeBackupPolicyCommand
} from '@aws-sdk/client-efs'; } from '@aws-sdk/client-efs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class EFSAutomaticBackupsEnabled implements BPSet { export class EFSAutomaticBackupsEnabled implements BPSet {
private readonly client = new EFSClient({}); private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getFileSystems = async () => { private readonly getFileSystems = async () => {
const response = await this.memoClient.send(new DescribeFileSystemsCommand({})); const response = await this.memoClient.send(new DescribeFileSystemsCommand({}))
return response.FileSystems || []; return response.FileSystems || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EFSAutomaticBackupsEnabled', name: 'EFSAutomaticBackupsEnabled',
@ -30,101 +30,97 @@ export class EFSAutomaticBackupsEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeFileSystemsCommand', name: 'DescribeFileSystemsCommand',
reason: 'Retrieve the list of EFS file systems.', reason: 'Retrieve the list of EFS file systems.'
}, },
{ {
name: 'DescribeBackupPolicyCommand', name: 'DescribeBackupPolicyCommand',
reason: 'Check if a backup policy is enabled for the file system.', reason: 'Check if a backup policy is enabled for the file system.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutBackupPolicyCommand', name: 'PutBackupPolicyCommand',
reason: 'Enable automatic backups for the file system.', reason: 'Enable automatic backups for the file system.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure that enabling backups aligns with the organizations cost and recovery objectives.'
'Ensure that enabling backups aligns with the organizations cost and recovery objectives.', })
});
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const fileSystems = await this.getFileSystems(); const fileSystems = await this.getFileSystems()
for (const fileSystem of fileSystems) { for (const fileSystem of fileSystems) {
const response = await this.client.send( const response = await this.client.send(
new DescribeBackupPolicyCommand({ FileSystemId: fileSystem.FileSystemId! }) new DescribeBackupPolicyCommand({ FileSystemId: fileSystem.FileSystemId! })
); )
if (response.BackupPolicy?.Status === 'ENABLED') { if (response.BackupPolicy?.Status === 'ENABLED') {
compliantResources.push(fileSystem.FileSystemArn!); compliantResources.push(fileSystem.FileSystemArn!)
} else { } else {
nonCompliantResources.push(fileSystem.FileSystemArn!); nonCompliantResources.push(fileSystem.FileSystemArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async ( public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
nonCompliantResources,
requiredParametersForFix
) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const fileSystemId = arn.split('/').pop()!; const fileSystemId = arn.split('/').pop()!
await this.client.send( await this.client.send(
new PutBackupPolicyCommand({ new PutBackupPolicyCommand({
FileSystemId: fileSystemId, FileSystemId: fileSystemId,
BackupPolicy: { Status: 'ENABLED' }, BackupPolicy: { Status: 'ENABLED' }
}) })
); )
} }
}; }
} }

View File

@ -2,19 +2,19 @@ import {
EFSClient, EFSClient,
DescribeFileSystemsCommand, DescribeFileSystemsCommand,
CreateFileSystemCommand, CreateFileSystemCommand,
DeleteFileSystemCommand, DeleteFileSystemCommand
} from '@aws-sdk/client-efs'; } from '@aws-sdk/client-efs'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class EFSEncryptedCheck implements BPSet { export class EFSEncryptedCheck implements BPSet {
private readonly client = new EFSClient({}); private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getFileSystems = async () => { private readonly getFileSystems = async () => {
const response = await this.memoClient.send(new DescribeFileSystemsCommand({})); const response = await this.memoClient.send(new DescribeFileSystemsCommand({}))
return response.FileSystems || []; return response.FileSystems || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EFSEncryptedCheck', name: 'EFSEncryptedCheck',
@ -30,100 +30,95 @@ export class EFSEncryptedCheck implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeFileSystemsCommand', name: 'DescribeFileSystemsCommand',
reason: 'Retrieve all existing EFS file systems and their encryption status.', reason: 'Retrieve all existing EFS file systems and their encryption status.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'DeleteFileSystemCommand', name: 'DeleteFileSystemCommand',
reason: 'Delete non-compliant EFS file systems.', reason: 'Delete non-compliant EFS file systems.'
}, },
{ {
name: 'CreateFileSystemCommand', name: 'CreateFileSystemCommand',
reason: 'Recreate EFS file systems with encryption enabled.', reason: 'Recreate EFS file systems with encryption enabled.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure that backups are taken and data migration plans are in place, as the fix involves deletion and recreation of file systems.', 'Ensure that backups are taken and data migration plans are in place, as the fix involves deletion and recreation of file systems.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const fileSystems = await this.getFileSystems(); const fileSystems = await this.getFileSystems()
for (const fileSystem of fileSystems) { for (const fileSystem of fileSystems) {
if (fileSystem.Encrypted) { if (fileSystem.Encrypted) {
compliantResources.push(fileSystem.FileSystemArn!); compliantResources.push(fileSystem.FileSystemArn!)
} else { } else {
nonCompliantResources.push(fileSystem.FileSystemArn!); nonCompliantResources.push(fileSystem.FileSystemArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (...args) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then( await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
public readonly fixImpl: BPSetFixFn = async ( public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
nonCompliantResources,
requiredParametersForFix
) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const fileSystemId = arn.split('/').pop()!; const fileSystemId = arn.split('/').pop()!
const fileSystem = await this.memoClient.send( const fileSystem = await this.memoClient.send(new DescribeFileSystemsCommand({ FileSystemId: fileSystemId }))
new DescribeFileSystemsCommand({ FileSystemId: fileSystemId })
);
// Delete the non-compliant file system // Delete the non-compliant file system
await this.client.send( await this.client.send(
new DeleteFileSystemCommand({ new DeleteFileSystemCommand({
FileSystemId: fileSystemId, FileSystemId: fileSystemId
}) })
); )
// Recreate the file system with encryption enabled // Recreate the file system with encryption enabled
await this.client.send( await this.client.send(
@ -131,9 +126,9 @@ export class EFSEncryptedCheck implements BPSet {
Encrypted: true, Encrypted: true,
PerformanceMode: fileSystem.FileSystems?.[0]?.PerformanceMode, PerformanceMode: fileSystem.FileSystems?.[0]?.PerformanceMode,
ThroughputMode: fileSystem.FileSystems?.[0]?.ThroughputMode, ThroughputMode: fileSystem.FileSystems?.[0]?.ThroughputMode,
ProvisionedThroughputInMibps: fileSystem.FileSystems?.[0]?.ProvisionedThroughputInMibps, ProvisionedThroughputInMibps: fileSystem.FileSystems?.[0]?.ProvisionedThroughputInMibps
}) })
); )
} }
}; }
} }

View File

@ -1,31 +1,27 @@
import { import { EFSClient, DescribeFileSystemsCommand, DescribeMountTargetsCommand } from '@aws-sdk/client-efs'
EFSClient, import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2'
DescribeFileSystemsCommand, import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
DescribeMountTargetsCommand, import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-efs';
import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EFSMountTargetPublicAccessible implements BPSet { export class EFSMountTargetPublicAccessible implements BPSet {
private readonly efsClient = new EFSClient({}); private readonly efsClient = new EFSClient({})
private readonly ec2Client = new EC2Client({}); private readonly ec2Client = new EC2Client({})
private readonly memoEFSClient = Memorizer.memo(this.efsClient); private readonly memoEFSClient = Memorizer.memo(this.efsClient)
private readonly memoEC2Client = Memorizer.memo(this.ec2Client); private readonly memoEC2Client = Memorizer.memo(this.ec2Client)
private readonly getFileSystems = async () => { private readonly getFileSystems = async () => {
const response = await this.memoEFSClient.send(new DescribeFileSystemsCommand({})); const response = await this.memoEFSClient.send(new DescribeFileSystemsCommand({}))
return response.FileSystems || []; return response.FileSystems || []
}; }
private readonly getRoutesForSubnet = async (subnetId: string) => { private readonly getRoutesForSubnet = async (subnetId: string) => {
const response = await this.memoEC2Client.send( const response = await this.memoEC2Client.send(
new DescribeRouteTablesCommand({ new DescribeRouteTablesCommand({
Filters: [{ Name: 'association.subnet-id', Values: [subnetId] }], Filters: [{ Name: 'association.subnet-id', Values: [subnetId] }]
}) })
); )
return response.RouteTables?.[0]?.Routes || []; return response.RouteTables?.[0]?.Routes || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EFSMountTargetPublicAccessible', name: 'EFSMountTargetPublicAccessible',
@ -41,92 +37,85 @@ export class EFSMountTargetPublicAccessible implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeFileSystemsCommand', name: 'DescribeFileSystemsCommand',
reason: 'Retrieve the list of EFS file systems.', reason: 'Retrieve the list of EFS file systems.'
}, },
{ {
name: 'DescribeMountTargetsCommand', name: 'DescribeMountTargetsCommand',
reason: 'Retrieve the list of mount targets for a file system.', reason: 'Retrieve the list of mount targets for a file system.'
}, },
{ {
name: 'DescribeRouteTablesCommand', name: 'DescribeRouteTablesCommand',
reason: 'Check route tables associated with the mount target subnets.', reason: 'Check route tables associated with the mount target subnets.'
}, }
], ],
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure that the network configurations are reviewed carefully to avoid breaking application connectivity.', 'Ensure that the network configurations are reviewed carefully to avoid breaking application connectivity.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
nonCompliantResources: [], nonCompliantResources: [],
compliantResources: [], compliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const fileSystems = await this.getFileSystems(); const fileSystems = await this.getFileSystems()
for (const fileSystem of fileSystems) { for (const fileSystem of fileSystems) {
const mountTargets = await this.memoEFSClient.send( const mountTargets = await this.memoEFSClient.send(
new DescribeMountTargetsCommand({ FileSystemId: fileSystem.FileSystemId! }) new DescribeMountTargetsCommand({ FileSystemId: fileSystem.FileSystemId! })
); )
let isNonCompliant = false; let isNonCompliant = false
for (const mountTarget of mountTargets.MountTargets || []) { for (const mountTarget of mountTargets.MountTargets || []) {
const routes = await this.getRoutesForSubnet(mountTarget.SubnetId!); const routes = await this.getRoutesForSubnet(mountTarget.SubnetId!)
if ( if (routes.some((route) => route.DestinationCidrBlock === '0.0.0.0/0' && route.GatewayId?.startsWith('igw-'))) {
routes.some( nonCompliantResources.push(fileSystem.FileSystemArn!)
(route) => isNonCompliant = true
route.DestinationCidrBlock === '0.0.0.0/0' && route.GatewayId?.startsWith('igw-') break
)
) {
nonCompliantResources.push(fileSystem.FileSystemArn!);
isNonCompliant = true;
break;
} }
} }
if (!isNonCompliant) { if (!isNonCompliant) {
compliantResources.push(fileSystem.FileSystemArn!); compliantResources.push(fileSystem.FileSystemArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async () => { public readonly fix: BPSetFixFn = async () => {
throw new Error( throw new Error('Fixing public accessibility for mount targets requires manual network reconfiguration.')
'Fixing public accessibility for mount targets requires manual network reconfiguration.' }
);
};
} }

View File

@ -1,35 +1,27 @@
import { import { EKSClient, ListClustersCommand, DescribeClusterCommand, UpdateClusterConfigCommand } from '@aws-sdk/client-eks'
EKSClient, import { BPSet, BPSetStats, BPSetFixFn } from '../../types'
ListClustersCommand, import { Memorizer } from '../../Memorizer'
DescribeClusterCommand,
UpdateClusterConfigCommand,
} from '@aws-sdk/client-eks';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EKSClusterLoggingEnabled implements BPSet { export class EKSClusterLoggingEnabled implements BPSet {
private readonly client = new EKSClient({}); private readonly client = new EKSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getClusters = async () => { private readonly getClusters = async () => {
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})); const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterNames = clusterNamesResponse.clusters || []; const clusterNames = clusterNamesResponse.clusters || []
const clusters = []; const clusters = []
for (const clusterName of clusterNames) { for (const clusterName of clusterNames) {
const cluster = await this.memoClient.send( const cluster = await this.memoClient.send(new DescribeClusterCommand({ name: clusterName }))
new DescribeClusterCommand({ name: clusterName }) clusters.push(cluster.cluster!)
);
clusters.push(cluster.cluster!);
} }
return clusters; return clusters
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EKSClusterLoggingEnabled', name: 'EKSClusterLoggingEnabled',
description: 'Ensures that all EKS clusters have full logging enabled.', description: 'Ensures that all EKS clusters have full logging enabled.',
priority: 1, priority: 1,
priorityReason: priorityReason: 'Cluster logging is essential for monitoring, debugging, and auditing purposes.',
'Cluster logging is essential for monitoring, debugging, and auditing purposes.',
awsService: 'EKS', awsService: 'EKS',
awsServiceCategory: 'Kubernetes Service', awsServiceCategory: 'Kubernetes Service',
bestPracticeCategory: 'Observability', bestPracticeCategory: 'Observability',
@ -38,75 +30,74 @@ export class EKSClusterLoggingEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListClustersCommand', name: 'ListClustersCommand',
reason: 'Retrieve the list of EKS clusters.', reason: 'Retrieve the list of EKS clusters.'
}, },
{ {
name: 'DescribeClusterCommand', name: 'DescribeClusterCommand',
reason: 'Fetch details about the EKS cluster, including logging configuration.', reason: 'Fetch details about the EKS cluster, including logging configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateClusterConfigCommand', name: 'UpdateClusterConfigCommand',
reason: 'Enable all logging types for the EKS cluster.', reason: 'Enable all logging types for the EKS cluster.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure that enabling full logging does not generate excessive costs or logs.'
'Ensure that enabling full logging does not generate excessive costs or logs.', })
});
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const clusters = await this.getClusters(); const clusters = await this.getClusters()
for (const cluster of clusters) { for (const cluster of clusters) {
const clusterLogging = cluster.logging?.clusterLogging?.[0]; const clusterLogging = cluster.logging?.clusterLogging?.[0]
if (clusterLogging?.enabled && clusterLogging.types?.length === 5) { if (clusterLogging?.enabled && clusterLogging.types?.length === 5) {
compliantResources.push(cluster.arn!); compliantResources.push(cluster.arn!)
} else { } else {
nonCompliantResources.push(cluster.arn!); nonCompliantResources.push(cluster.arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (nonCompliantResources) => { public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterName = arn.split(':cluster/')[1]; const clusterName = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new UpdateClusterConfigCommand({ new UpdateClusterConfigCommand({
@ -115,12 +106,12 @@ export class EKSClusterLoggingEnabled implements BPSet {
clusterLogging: [ clusterLogging: [
{ {
enabled: true, enabled: true,
types: ['api', 'audit', 'authenticator', 'controllerManager', 'scheduler'], types: ['api', 'audit', 'authenticator', 'controllerManager', 'scheduler']
}, }
], ]
}, }
}) })
); )
} }
}; }
} }

View File

@ -2,34 +2,31 @@ import {
EKSClient, EKSClient,
ListClustersCommand, ListClustersCommand,
DescribeClusterCommand, DescribeClusterCommand,
AssociateEncryptionConfigCommand, AssociateEncryptionConfigCommand
} from '@aws-sdk/client-eks'; } from '@aws-sdk/client-eks'
import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; import { BPSet, BPSetStats, BPSetFixFn } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class EKSClusterSecretsEncrypted implements BPSet { export class EKSClusterSecretsEncrypted implements BPSet {
private readonly client = new EKSClient({}); private readonly client = new EKSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getClusters = async () => { private readonly getClusters = async () => {
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})); const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterNames = clusterNamesResponse.clusters || []; const clusterNames = clusterNamesResponse.clusters || []
const clusters = []; const clusters = []
for (const clusterName of clusterNames) { for (const clusterName of clusterNames) {
const cluster = await this.memoClient.send( const cluster = await this.memoClient.send(new DescribeClusterCommand({ name: clusterName }))
new DescribeClusterCommand({ name: clusterName }) clusters.push(cluster.cluster!)
);
clusters.push(cluster.cluster!);
} }
return clusters; return clusters
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EKSClusterSecretsEncrypted', name: 'EKSClusterSecretsEncrypted',
description: 'Ensures that all EKS clusters have secrets encrypted with a KMS key.', description: 'Ensures that all EKS clusters have secrets encrypted with a KMS key.',
priority: 1, priority: 1,
priorityReason: priorityReason: 'Encrypting secrets ensures the security and compliance of sensitive data in EKS clusters.',
'Encrypting secrets ensures the security and compliance of sensitive data in EKS clusters.',
awsService: 'EKS', awsService: 'EKS',
awsServiceCategory: 'Kubernetes Service', awsServiceCategory: 'Kubernetes Service',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -38,88 +35,87 @@ export class EKSClusterSecretsEncrypted implements BPSet {
name: 'kms-key-id', name: 'kms-key-id',
description: 'The KMS key ARN to enable encryption for EKS secrets.', description: 'The KMS key ARN to enable encryption for EKS secrets.',
default: '', default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/example-key-id', example: 'arn:aws:kms:us-east-1:123456789012:key/example-key-id'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListClustersCommand', name: 'ListClustersCommand',
reason: 'Retrieve the list of EKS clusters.', reason: 'Retrieve the list of EKS clusters.'
}, },
{ {
name: 'DescribeClusterCommand', name: 'DescribeClusterCommand',
reason: 'Fetch details about the EKS cluster, including encryption configuration.', reason: 'Fetch details about the EKS cluster, including encryption configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'AssociateEncryptionConfigCommand', name: 'AssociateEncryptionConfigCommand',
reason: 'Enable encryption for EKS secrets using the provided KMS key.', reason: 'Enable encryption for EKS secrets using the provided KMS key.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure that the specified KMS key is accessible to the EKS service and cluster.'
'Ensure that the specified KMS key is accessible to the EKS service and cluster.', })
});
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const clusters = await this.getClusters(); const clusters = await this.getClusters()
for (const cluster of clusters) { for (const cluster of clusters) {
const encryptionConfig = cluster.encryptionConfig?.[0]; const encryptionConfig = cluster.encryptionConfig?.[0]
if (encryptionConfig?.resources?.includes('secrets')) { if (encryptionConfig?.resources?.includes('secrets')) {
compliantResources.push(cluster.arn!); compliantResources.push(cluster.arn!)
} else { } else {
nonCompliantResources.push(cluster.arn!); nonCompliantResources.push(cluster.arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value
if (!kmsKeyId) { if (!kmsKeyId) {
throw new Error("Required parameter 'kms-key-id' is missing."); throw new Error("Required parameter 'kms-key-id' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterName = arn.split(':cluster/')[1]; const clusterName = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new AssociateEncryptionConfigCommand({ new AssociateEncryptionConfigCommand({
@ -127,11 +123,11 @@ export class EKSClusterSecretsEncrypted implements BPSet {
encryptionConfig: [ encryptionConfig: [
{ {
resources: ['secrets'], resources: ['secrets'],
provider: { keyArn: kmsKeyId }, provider: { keyArn: kmsKeyId }
}, }
], ]
}) })
); )
} }
}; }
} }

View File

@ -1,34 +1,28 @@
import { import { EKSClient, ListClustersCommand, DescribeClusterCommand, UpdateClusterConfigCommand } from '@aws-sdk/client-eks'
EKSClient, import { BPSet, BPSetStats, BPSetFixFn } from '../../types'
ListClustersCommand, import { Memorizer } from '../../Memorizer'
DescribeClusterCommand,
UpdateClusterConfigCommand,
} from '@aws-sdk/client-eks';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EKSEndpointNoPublicAccess implements BPSet { export class EKSEndpointNoPublicAccess implements BPSet {
private readonly client = new EKSClient({}); private readonly client = new EKSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getClusters = async () => { private readonly getClusters = async () => {
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})); const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterNames = clusterNamesResponse.clusters || []; const clusterNames = clusterNamesResponse.clusters || []
const clusters = []; const clusters = []
for (const clusterName of clusterNames) { for (const clusterName of clusterNames) {
const cluster = await this.memoClient.send( const cluster = await this.memoClient.send(new DescribeClusterCommand({ name: clusterName }))
new DescribeClusterCommand({ name: clusterName }) clusters.push(cluster.cluster!)
);
clusters.push(cluster.cluster!);
} }
return clusters; return clusters
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'EKSEndpointNoPublicAccess', name: 'EKSEndpointNoPublicAccess',
description: 'Ensures EKS cluster endpoint does not have public access enabled.', description: 'Ensures EKS cluster endpoint does not have public access enabled.',
priority: 1, priority: 1,
priorityReason: 'Disabling public access to the cluster endpoint enhances security by limiting exposure to public networks.', priorityReason:
'Disabling public access to the cluster endpoint enhances security by limiting exposure to public networks.',
awsService: 'EKS', awsService: 'EKS',
awsServiceCategory: 'Kubernetes Service', awsServiceCategory: 'Kubernetes Service',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -37,83 +31,84 @@ export class EKSEndpointNoPublicAccess implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListClustersCommand', name: 'ListClustersCommand',
reason: 'Retrieves the list of EKS clusters.', reason: 'Retrieves the list of EKS clusters.'
}, },
{ {
name: 'DescribeClusterCommand', name: 'DescribeClusterCommand',
reason: 'Fetches detailed configuration of each EKS cluster.', reason: 'Fetches detailed configuration of each EKS cluster.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateClusterConfigCommand', name: 'UpdateClusterConfigCommand',
reason: 'Updates the EKS cluster configuration to disable public endpoint access.', reason: 'Updates the EKS cluster configuration to disable public endpoint access.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the private endpoint is properly configured and accessible before disabling public access.', adviseBeforeFixFunction:
}); 'Ensure the private endpoint is properly configured and accessible before disabling public access.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const clusters = await this.getClusters(); const clusters = await this.getClusters()
for (const cluster of clusters) { for (const cluster of clusters) {
const endpointPublicAccess = cluster.resourcesVpcConfig?.endpointPublicAccess; const endpointPublicAccess = cluster.resourcesVpcConfig?.endpointPublicAccess
if (endpointPublicAccess) { if (endpointPublicAccess) {
nonCompliantResources.push(cluster.arn!); nonCompliantResources.push(cluster.arn!)
} else { } else {
compliantResources.push(cluster.arn!); compliantResources.push(cluster.arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (nonCompliantResources) => { public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterName = arn.split(':cluster/')[1]; const clusterName = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new UpdateClusterConfigCommand({ new UpdateClusterConfigCommand({
name: clusterName, name: clusterName,
resourcesVpcConfig: { resourcesVpcConfig: {
endpointPublicAccess: false, endpointPublicAccess: false
}, }
}) })
); )
} }
}; }
} }

View File

@ -1,25 +1,22 @@
import { import { ElastiCacheClient, DescribeCacheClustersCommand, ModifyCacheClusterCommand } from '@aws-sdk/client-elasticache'
ElastiCacheClient, import { BPSet, BPSetStats, BPSetFixFn } from '../../types'
DescribeCacheClustersCommand, import { Memorizer } from '../../Memorizer'
ModifyCacheClusterCommand,
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheAutoMinorVersionUpgradeCheck implements BPSet { export class ElastiCacheAutoMinorVersionUpgradeCheck implements BPSet {
private readonly client = new ElastiCacheClient({}); private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getClusters = async () => { private readonly getClusters = async () => {
const response = await this.memoClient.send(new DescribeCacheClustersCommand({})); const response = await this.memoClient.send(new DescribeCacheClustersCommand({}))
return response.CacheClusters || []; return response.CacheClusters || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ElastiCacheAutoMinorVersionUpgradeCheck', name: 'ElastiCacheAutoMinorVersionUpgradeCheck',
description: 'Ensures that ElastiCache clusters have auto minor version upgrade enabled.', description: 'Ensures that ElastiCache clusters have auto minor version upgrade enabled.',
priority: 2, priority: 2,
priorityReason: 'Auto minor version upgrades help ensure clusters stay up-to-date with the latest security and bug fixes.', priorityReason:
'Auto minor version upgrades help ensure clusters stay up-to-date with the latest security and bug fixes.',
awsService: 'ElastiCache', awsService: 'ElastiCache',
awsServiceCategory: 'Cache Service', awsServiceCategory: 'Cache Service',
bestPracticeCategory: 'Reliability', bestPracticeCategory: 'Reliability',
@ -28,75 +25,76 @@ export class ElastiCacheAutoMinorVersionUpgradeCheck implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeCacheClustersCommand', name: 'DescribeCacheClustersCommand',
reason: 'Fetches the list and configurations of ElastiCache clusters.', reason: 'Fetches the list and configurations of ElastiCache clusters.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyCacheClusterCommand', name: 'ModifyCacheClusterCommand',
reason: 'Enables auto minor version upgrade on ElastiCache clusters.', reason: 'Enables auto minor version upgrade on ElastiCache clusters.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure application compatibility with updated ElastiCache versions before enabling this setting.', adviseBeforeFixFunction:
}); 'Ensure application compatibility with updated ElastiCache versions before enabling this setting.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const clusters = await this.getClusters(); const clusters = await this.getClusters()
for (const cluster of clusters) { for (const cluster of clusters) {
if (cluster.AutoMinorVersionUpgrade) { if (cluster.AutoMinorVersionUpgrade) {
compliantResources.push(cluster.ARN!); compliantResources.push(cluster.ARN!)
} else { } else {
nonCompliantResources.push(cluster.ARN!); nonCompliantResources.push(cluster.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (nonCompliantResources) => { public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster:')[1]; const clusterId = arn.split(':cluster:')[1]
await this.client.send( await this.client.send(
new ModifyCacheClusterCommand({ new ModifyCacheClusterCommand({
CacheClusterId: clusterId, CacheClusterId: clusterId,
AutoMinorVersionUpgrade: true, AutoMinorVersionUpgrade: true
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,19 @@
import { import {
ElastiCacheClient, ElastiCacheClient,
DescribeReplicationGroupsCommand, DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand, ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'; } from '@aws-sdk/client-elasticache'
import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; import { BPSet, BPSetStats, BPSetFixFn } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ElastiCacheRedisClusterAutomaticBackupCheck implements BPSet { export class ElastiCacheRedisClusterAutomaticBackupCheck implements BPSet {
private readonly client = new ElastiCacheClient({}); private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getReplicationGroups = async () => { private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []; return response.ReplicationGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ElastiCacheRedisClusterAutomaticBackupCheck', name: 'ElastiCacheRedisClusterAutomaticBackupCheck',
@ -28,90 +28,89 @@ export class ElastiCacheRedisClusterAutomaticBackupCheck implements BPSet {
name: 'snapshot-retention-period', name: 'snapshot-retention-period',
description: 'Number of days to retain automatic snapshots.', description: 'Number of days to retain automatic snapshots.',
default: '7', default: '7',
example: '7', example: '7'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeReplicationGroupsCommand', name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches details of replication groups to verify backup settings.', reason: 'Fetches details of replication groups to verify backup settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyReplicationGroupCommand', name: 'ModifyReplicationGroupCommand',
reason: 'Enables automatic snapshots and sets the retention period for Redis clusters.', reason: 'Enables automatic snapshots and sets the retention period for Redis clusters.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that enabling snapshots does not conflict with operational or compliance requirements.', adviseBeforeFixFunction:
}); 'Ensure that enabling snapshots does not conflict with operational or compliance requirements.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const replicationGroups = await this.getReplicationGroups(); const replicationGroups = await this.getReplicationGroups()
for (const group of replicationGroups) { for (const group of replicationGroups) {
if (group.SnapshottingClusterId) { if (group.SnapshottingClusterId) {
compliantResources.push(group.ARN!); compliantResources.push(group.ARN!)
} else { } else {
nonCompliantResources.push(group.ARN!); nonCompliantResources.push(group.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find((param) => param.name === 'snapshot-retention-period')?.value
(param) => param.name === 'snapshot-retention-period'
)?.value;
if (!retentionPeriod) { if (!retentionPeriod) {
throw new Error("Required parameter 'snapshot-retention-period' is missing."); throw new Error("Required parameter 'snapshot-retention-period' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const groupId = arn.split(':replication-group:')[1]; const groupId = arn.split(':replication-group:')[1]
await this.client.send( await this.client.send(
new ModifyReplicationGroupCommand({ new ModifyReplicationGroupCommand({
ReplicationGroupId: groupId, ReplicationGroupId: groupId,
SnapshotRetentionLimit: parseInt(retentionPeriod, 10), SnapshotRetentionLimit: parseInt(retentionPeriod, 10)
}) })
); )
} }
}; }
} }

View File

@ -1,19 +1,19 @@
import { import {
ElastiCacheClient, ElastiCacheClient,
DescribeReplicationGroupsCommand, DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand, ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'; } from '@aws-sdk/client-elasticache'
import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; import { BPSet, BPSetStats, BPSetFixFn } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ElastiCacheReplGrpAutoFailoverEnabled implements BPSet { export class ElastiCacheReplGrpAutoFailoverEnabled implements BPSet {
private readonly client = new ElastiCacheClient({}); private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getReplicationGroups = async () => { private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []; return response.ReplicationGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ElastiCacheReplGrpAutoFailoverEnabled', name: 'ElastiCacheReplGrpAutoFailoverEnabled',
@ -28,75 +28,76 @@ export class ElastiCacheReplGrpAutoFailoverEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeReplicationGroupsCommand', name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches replication group details to verify automatic failover settings.', reason: 'Fetches replication group details to verify automatic failover settings.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyReplicationGroupCommand', name: 'ModifyReplicationGroupCommand',
reason: 'Enables automatic failover for replication groups.', reason: 'Enables automatic failover for replication groups.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the environment supports multi-AZ configurations before enabling automatic failover.', adviseBeforeFixFunction:
}); 'Ensure the environment supports multi-AZ configurations before enabling automatic failover.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const replicationGroups = await this.getReplicationGroups(); const replicationGroups = await this.getReplicationGroups()
for (const group of replicationGroups) { for (const group of replicationGroups) {
if (group.AutomaticFailover === 'enabled') { if (group.AutomaticFailover === 'enabled') {
compliantResources.push(group.ARN!); compliantResources.push(group.ARN!)
} else { } else {
nonCompliantResources.push(group.ARN!); nonCompliantResources.push(group.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix: BPSetFixFn = async (nonCompliantResources) => { public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const groupId = arn.split(':replication-group:')[1]; const groupId = arn.split(':replication-group:')[1]
await this.client.send( await this.client.send(
new ModifyReplicationGroupCommand({ new ModifyReplicationGroupCommand({
ReplicationGroupId: groupId, ReplicationGroupId: groupId,
AutomaticFailoverEnabled: true, AutomaticFailoverEnabled: true
}) })
); )
} }
}; }
} }

View File

@ -1,18 +1,15 @@
import { import { ElastiCacheClient, DescribeReplicationGroupsCommand } from '@aws-sdk/client-elasticache'
ElastiCacheClient, import { BPSet, BPSetStats } from '../../types'
DescribeReplicationGroupsCommand, import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheReplGrpEncryptedAtRest implements BPSet { export class ElastiCacheReplGrpEncryptedAtRest implements BPSet {
private readonly client = new ElastiCacheClient({}); private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getReplicationGroups = async () => { private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []; return response.ReplicationGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ElastiCacheReplGrpEncryptedAtRest', name: 'ElastiCacheReplGrpEncryptedAtRest',
@ -27,64 +24,65 @@ export class ElastiCacheReplGrpEncryptedAtRest implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeReplicationGroupsCommand', name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches replication group details to verify encryption settings.', reason: 'Fetches replication group details to verify encryption settings.'
}, }
], ],
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Recreation of the replication group is required for encryption. Ensure data backups are available.', adviseBeforeFixFunction:
}); 'Recreation of the replication group is required for encryption. Ensure data backups are available.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const replicationGroups = await this.getReplicationGroups(); const replicationGroups = await this.getReplicationGroups()
for (const group of replicationGroups) { for (const group of replicationGroups) {
if (group.AtRestEncryptionEnabled) { if (group.AtRestEncryptionEnabled) {
compliantResources.push(group.ARN!); compliantResources.push(group.ARN!)
} else { } else {
nonCompliantResources.push(group.ARN!); nonCompliantResources.push(group.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async () => { public readonly fix = async () => {
throw new Error( throw new Error(
'Fixing encryption at rest for replication groups requires recreation. Please create a new replication group with AtRestEncryptionEnabled set to true.' 'Fixing encryption at rest for replication groups requires recreation. Please create a new replication group with AtRestEncryptionEnabled set to true.'
); )
}; }
} }

View File

@ -1,18 +1,15 @@
import { import { ElastiCacheClient, DescribeReplicationGroupsCommand } from '@aws-sdk/client-elasticache'
ElastiCacheClient, import { BPSet, BPSetStats } from '../../types'
DescribeReplicationGroupsCommand, import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheReplGrpEncryptedInTransit implements BPSet { export class ElastiCacheReplGrpEncryptedInTransit implements BPSet {
private readonly client = new ElastiCacheClient({}); private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getReplicationGroups = async () => { private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []; return response.ReplicationGroups || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ElastiCacheReplGrpEncryptedInTransit', name: 'ElastiCacheReplGrpEncryptedInTransit',
@ -27,64 +24,65 @@ export class ElastiCacheReplGrpEncryptedInTransit implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeReplicationGroupsCommand', name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches replication group details to verify in-transit encryption settings.', reason: 'Fetches replication group details to verify in-transit encryption settings.'
}, }
], ],
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Recreation of the replication group is required for enabling in-transit encryption. Ensure data backups are available.', adviseBeforeFixFunction:
}); 'Recreation of the replication group is required for enabling in-transit encryption. Ensure data backups are available.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const replicationGroups = await this.getReplicationGroups(); const replicationGroups = await this.getReplicationGroups()
for (const group of replicationGroups) { for (const group of replicationGroups) {
if (group.TransitEncryptionEnabled) { if (group.TransitEncryptionEnabled) {
compliantResources.push(group.ARN!); compliantResources.push(group.ARN!)
} else { } else {
nonCompliantResources.push(group.ARN!); nonCompliantResources.push(group.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async () => { public readonly fix = async () => {
throw new Error( throw new Error(
'Fixing in-transit encryption for replication groups requires recreation. Please create a new replication group with TransitEncryptionEnabled set to true.' 'Fixing in-transit encryption for replication groups requires recreation. Please create a new replication group with TransitEncryptionEnabled set to true.'
); )
}; }
} }

View File

@ -3,18 +3,18 @@ import {
DescribeCacheClustersCommand, DescribeCacheClustersCommand,
DeleteCacheClusterCommand, DeleteCacheClusterCommand,
CreateCacheClusterCommand CreateCacheClusterCommand
} from '@aws-sdk/client-elasticache'; } from '@aws-sdk/client-elasticache'
import { BPSet, BPSetStats } from '../../types'; import { BPSet, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class ElastiCacheSubnetGroupCheck implements BPSet { export class ElastiCacheSubnetGroupCheck implements BPSet {
private readonly client = new ElastiCacheClient({}); private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getClusters = async () => { private readonly getClusters = async () => {
const response = await this.memoClient.send(new DescribeCacheClustersCommand({})); const response = await this.memoClient.send(new DescribeCacheClustersCommand({}))
return response.CacheClusters || []; return response.CacheClusters || []
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'ElastiCacheSubnetGroupCheck', name: 'ElastiCacheSubnetGroupCheck',
@ -29,106 +29,103 @@ export class ElastiCacheSubnetGroupCheck implements BPSet {
name: 'subnet-group-name', name: 'subnet-group-name',
description: 'The name of the desired subnet group to associate with the cluster.', description: 'The name of the desired subnet group to associate with the cluster.',
default: '', default: '',
example: 'custom-subnet-group', example: 'custom-subnet-group'
} }
], ],
isFixFunctionUsesDestructiveCommand: true, isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeCacheClustersCommand', name: 'DescribeCacheClustersCommand',
reason: 'Fetches the details of all ElastiCache clusters to check their subnet group.', reason: 'Fetches the details of all ElastiCache clusters to check their subnet group.'
} }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'DeleteCacheClusterCommand', name: 'DeleteCacheClusterCommand',
reason: 'Deletes non-compliant ElastiCache clusters.', reason: 'Deletes non-compliant ElastiCache clusters.'
}, },
{ {
name: 'CreateCacheClusterCommand', name: 'CreateCacheClusterCommand',
reason: 'Recreates ElastiCache clusters with the desired subnet group.', reason: 'Recreates ElastiCache clusters with the desired subnet group.'
} }
], ],
adviseBeforeFixFunction: 'Ensure data backups are available before fixing as clusters will be deleted and recreated.', adviseBeforeFixFunction:
}); 'Ensure data backups are available before fixing as clusters will be deleted and recreated.'
})
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const clusters = await this.getClusters(); const clusters = await this.getClusters()
for (const cluster of clusters) { for (const cluster of clusters) {
if (cluster.CacheSubnetGroupName !== 'default') { if (cluster.CacheSubnetGroupName !== 'default') {
compliantResources.push(cluster.ARN!); compliantResources.push(cluster.ARN!)
} else { } else {
nonCompliantResources.push(cluster.ARN!); nonCompliantResources.push(cluster.ARN!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const subnetGroupName = requiredParametersForFix.find( const subnetGroupName = requiredParametersForFix.find((param) => param.name === 'subnet-group-name')?.value
(param) => param.name === 'subnet-group-name'
)?.value;
if (!subnetGroupName) { if (!subnetGroupName) {
throw new Error("Required parameter 'subnet-group-name' is missing."); throw new Error("Required parameter 'subnet-group-name' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster:')[1]; const clusterId = arn.split(':cluster:')[1]
const cluster = await this.memoClient.send( const cluster = await this.memoClient.send(new DescribeCacheClustersCommand({ CacheClusterId: clusterId }))
new DescribeCacheClustersCommand({ CacheClusterId: clusterId }) const clusterDetails = cluster.CacheClusters?.[0]
);
const clusterDetails = cluster.CacheClusters?.[0];
if (!clusterDetails) { if (!clusterDetails) {
continue; continue
} }
// Delete the non-compliant cluster // Delete the non-compliant cluster
await this.client.send( await this.client.send(
new DeleteCacheClusterCommand({ new DeleteCacheClusterCommand({
CacheClusterId: clusterId, CacheClusterId: clusterId
}) })
); )
// Recreate the cluster with the desired subnet group // Recreate the cluster with the desired subnet group
await this.client.send( await this.client.send(
@ -138,13 +135,11 @@ export class ElastiCacheSubnetGroupCheck implements BPSet {
CacheNodeType: clusterDetails.CacheNodeType!, CacheNodeType: clusterDetails.CacheNodeType!,
NumCacheNodes: clusterDetails.NumCacheNodes!, NumCacheNodes: clusterDetails.NumCacheNodes!,
CacheSubnetGroupName: subnetGroupName, CacheSubnetGroupName: subnetGroupName,
SecurityGroupIds: clusterDetails.SecurityGroups?.map( SecurityGroupIds: clusterDetails.SecurityGroups?.map((group) => group.SecurityGroupId) as string[],
(group) => group.SecurityGroupId
) as string[],
PreferredMaintenanceWindow: clusterDetails.PreferredMaintenanceWindow, PreferredMaintenanceWindow: clusterDetails.PreferredMaintenanceWindow,
EngineVersion: clusterDetails.EngineVersion, EngineVersion: clusterDetails.EngineVersion
}) })
); )
} }
}; }
} }

View File

@ -1,27 +1,22 @@
import { import { IAMClient, ListPoliciesCommand, GetPolicyVersionCommand, DeletePolicyCommand } from '@aws-sdk/client-iam'
IAMClient, import { BPSet, BPSetStats } from '../../types'
ListPoliciesCommand, import { Memorizer } from '../../Memorizer'
GetPolicyVersionCommand,
DeletePolicyCommand
} from '@aws-sdk/client-iam';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class IAMPolicyNoStatementsWithAdminAccess implements BPSet { export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
private readonly client = new IAMClient({}); private readonly client = new IAMClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly getPolicies = async () => { private readonly getPolicies = async () => {
const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' })); const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' }))
return response.Policies || []; return response.Policies || []
}; }
private readonly getPolicyDefaultVersions = async (policyArn: string, versionId: string) => { private readonly getPolicyDefaultVersions = async (policyArn: string, versionId: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(
new GetPolicyVersionCommand({ PolicyArn: policyArn, VersionId: versionId }) new GetPolicyVersionCommand({ PolicyArn: policyArn, VersionId: versionId })
); )
return response.PolicyVersion!; return response.PolicyVersion!
}; }
public readonly getMetadata = () => ({ public readonly getMetadata = () => ({
name: 'IAMPolicyNoStatementsWithAdminAccess', name: 'IAMPolicyNoStatementsWithAdminAccess',
@ -36,89 +31,83 @@ export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListPoliciesCommand', name: 'ListPoliciesCommand',
reason: 'Fetches all local IAM policies.', reason: 'Fetches all local IAM policies.'
}, },
{ {
name: 'GetPolicyVersionCommand', name: 'GetPolicyVersionCommand',
reason: 'Retrieves the default version of each policy.', reason: 'Retrieves the default version of each policy.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'DeletePolicyCommand', name: 'DeletePolicyCommand',
reason: 'Deletes non-compliant IAM policies.', reason: 'Deletes non-compliant IAM policies.'
}, }
], ],
adviseBeforeFixFunction: 'Deleting policies is irreversible. Verify policies before applying fixes.', adviseBeforeFixFunction: 'Deleting policies is irreversible. Verify policies before applying fixes.'
}); })
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl().then( await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'), () => (this.stats.status = 'FINISHED'),
(err) => { (err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
} }
); )
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const policies = await this.getPolicies(); const policies = await this.getPolicies()
for (const policy of policies) { for (const policy of policies) {
const policyVersion = await this.getPolicyDefaultVersions(policy.Arn!, policy.DefaultVersionId!); const policyVersion = await this.getPolicyDefaultVersions(policy.Arn!, policy.DefaultVersionId!)
const policyDocument = JSON.parse(JSON.stringify(policyVersion.Document)); // Parse Document JSON string const policyDocument = JSON.parse(JSON.stringify(policyVersion.Document)) // Parse Document JSON string
const statements = Array.isArray(policyDocument.Statement) const statements = Array.isArray(policyDocument.Statement) ? policyDocument.Statement : [policyDocument.Statement]
? policyDocument.Statement
: [policyDocument.Statement];
for (const statement of statements) { for (const statement of statements) {
if ( if (statement?.Action === '*' && statement?.Resource === '*' && statement?.Effect === 'Allow') {
statement?.Action === '*' && nonCompliantResources.push(policy.Arn!)
statement?.Resource === '*' && break
statement?.Effect === 'Allow'
) {
nonCompliantResources.push(policy.Arn!);
break;
} }
} }
if (!nonCompliantResources.includes(policy.Arn!)) { if (!nonCompliantResources.includes(policy.Arn!)) {
compliantResources.push(policy.Arn!); compliantResources.push(policy.Arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
await this.client.send(new DeletePolicyCommand({ PolicyArn: arn })); await this.client.send(new DeletePolicyCommand({ PolicyArn: arn }))
} }
}; }
} }

View File

@ -3,185 +3,170 @@ import {
ListPoliciesCommand, ListPoliciesCommand,
GetPolicyVersionCommand, GetPolicyVersionCommand,
CreatePolicyVersionCommand, CreatePolicyVersionCommand,
DeletePolicyVersionCommand, DeletePolicyVersionCommand
} from "@aws-sdk/client-iam"; } from '@aws-sdk/client-iam'
import { BPSet, BPSetMetadata, BPSetStats } from "../../types"; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from "../../Memorizer"; import { Memorizer } from '../../Memorizer'
export class IAMPolicyNoStatementsWithFullAccess implements BPSet { export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
private readonly client = new IAMClient({}); private readonly client = new IAMClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: "LOADED", status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: "IAMPolicyNoStatementsWithFullAccess", name: 'IAMPolicyNoStatementsWithFullAccess',
description: "Ensures IAM policies do not have statements granting full access.", description: 'Ensures IAM policies do not have statements granting full access.',
priority: 2, priority: 2,
priorityReason: "Granting full access poses a significant security risk.", priorityReason: 'Granting full access poses a significant security risk.',
awsService: "IAM", awsService: 'IAM',
awsServiceCategory: "Access Management", awsServiceCategory: 'Access Management',
bestPracticeCategory: "Security", bestPracticeCategory: 'Security',
requiredParametersForFix: [ requiredParametersForFix: [
{ {
name: "policy-revision-strategy", name: 'policy-revision-strategy',
description: "Strategy to revise policies (e.g., remove, restrict actions)", description: 'Strategy to revise policies (e.g., remove, restrict actions)',
default: "remove", default: 'remove',
example: "remove", example: 'remove'
}, }
], ],
isFixFunctionUsesDestructiveCommand: true, isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ name: "ListPoliciesCommand", reason: "Fetch all customer-managed policies." }, { name: 'ListPoliciesCommand', reason: 'Fetch all customer-managed policies.' },
{ name: "GetPolicyVersionCommand", reason: "Retrieve the default version of the policy." }, { name: 'GetPolicyVersionCommand', reason: 'Retrieve the default version of the policy.' }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ name: "CreatePolicyVersionCommand", reason: "Create a new policy version." }, { name: 'CreatePolicyVersionCommand', reason: 'Create a new policy version.' },
{ name: "DeletePolicyVersionCommand", reason: "Delete outdated policy versions." }, { name: 'DeletePolicyVersionCommand', reason: 'Delete outdated policy versions.' }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: "Ensure revised policies meet the organization's security and access requirements."
"Ensure revised policies meet the organization's security and access requirements.", })
});
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = "LOADED"; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = "CHECKING"; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl().then(
.then( () => {
() => { this.stats.status = 'FINISHED'
this.stats.status = "FINISHED"; },
}, (err) => {
(err) => { this.stats.status = 'ERROR'
this.stats.status = "ERROR"; this.stats.errorMessage.push({
this.stats.errorMessage.push({ date: new Date(),
date: new Date(), message: err.message
message: err.message, })
}); }
} )
); }
};
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const policiesResponse = await this.memoClient.send( const policiesResponse = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' }))
new ListPoliciesCommand({ Scope: "Local" }) const policies = policiesResponse.Policies || []
);
const policies = policiesResponse.Policies || [];
for (const policy of policies) { for (const policy of policies) {
const policyVersionResponse = await this.memoClient.send( const policyVersionResponse = await this.memoClient.send(
new GetPolicyVersionCommand({ new GetPolicyVersionCommand({
PolicyArn: policy.Arn!, PolicyArn: policy.Arn!,
VersionId: policy.DefaultVersionId!, VersionId: policy.DefaultVersionId!
}) })
); )
const policyDocument = JSON.parse( const policyDocument = JSON.parse(decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string))
decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
);
const hasFullAccess = policyDocument.Statement.some((statement: any) => { const hasFullAccess = policyDocument.Statement.some((statement: unknown) => {
if (statement.Effect === "Deny") return false; if (statement.Effect === 'Deny') return false
const actions = Array.isArray(statement.Action) const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action]
? statement.Action return actions.some((action: string) => action.endsWith(':*'))
: [statement.Action]; })
return actions.some((action: string) => action.endsWith(":*"));
});
if (hasFullAccess) { if (hasFullAccess) {
nonCompliantResources.push(policy.Arn!); nonCompliantResources.push(policy.Arn!)
} else { } else {
compliantResources.push(policy.Arn!); compliantResources.push(policy.Arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix).then(
.then( () => {
() => { this.stats.status = 'FINISHED'
this.stats.status = "FINISHED"; },
}, (err) => {
(err) => { this.stats.status = 'ERROR'
this.stats.status = "ERROR"; this.stats.errorMessage.push({
this.stats.errorMessage.push({ date: new Date(),
date: new Date(), message: err.message
message: err.message, })
}); }
} )
); }
};
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const strategy = requiredParametersForFix.find( const strategy = requiredParametersForFix.find((param) => param.name === 'policy-revision-strategy')?.value
(param) => param.name === "policy-revision-strategy"
)?.value;
if (!strategy) { if (!strategy) {
throw new Error("Required parameter 'policy-revision-strategy' is missing."); throw new Error("Required parameter 'policy-revision-strategy' is missing.")
} }
for (const policyArn of nonCompliantResources) { for (const policyArn of nonCompliantResources) {
const policyVersionResponse = await this.memoClient.send( const policyVersionResponse = await this.memoClient.send(
new GetPolicyVersionCommand({ new GetPolicyVersionCommand({
PolicyArn: policyArn, PolicyArn: policyArn,
VersionId: "v1", VersionId: 'v1'
}) })
); )
const policyDocument = JSON.parse( const policyDocument = JSON.parse(decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string))
decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
);
policyDocument.Statement = policyDocument.Statement.filter((statement: any) => { policyDocument.Statement = policyDocument.Statement.filter((statement: unknown) => {
if (statement.Effect === "Deny") return true; if (statement.Effect === 'Deny') return true
const actions = Array.isArray(statement.Action) const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action]
? statement.Action return !actions.some((action: string) => action.endsWith(':*'))
: [statement.Action]; })
return !actions.some((action: string) => action.endsWith(":*"));
});
const createVersionResponse = await this.client.send( const createVersionResponse = await this.client.send(
new CreatePolicyVersionCommand({ new CreatePolicyVersionCommand({
PolicyArn: policyArn, PolicyArn: policyArn,
PolicyDocument: JSON.stringify(policyDocument), PolicyDocument: JSON.stringify(policyDocument),
SetAsDefault: true, SetAsDefault: true
}) })
); )
if (createVersionResponse.PolicyVersion?.VersionId) { if (createVersionResponse.PolicyVersion?.VersionId) {
await this.client.send( await this.client.send(
new DeletePolicyVersionCommand({ new DeletePolicyVersionCommand({
PolicyArn: policyArn, PolicyArn: policyArn,
VersionId: policyVersionResponse.PolicyVersion!.VersionId, VersionId: policyVersionResponse.PolicyVersion!.VersionId
}) })
); )
} }
} }
}; }
} }

View File

@ -1,27 +1,24 @@
import { import { IAMClient, ListPoliciesCommand, ListEntitiesForPolicyCommand } from '@aws-sdk/client-iam'
IAMClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ListPoliciesCommand, import { Memorizer } from '../../Memorizer'
ListEntitiesForPolicyCommand,
} from '@aws-sdk/client-iam';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class IAMRoleManagedPolicyCheck implements BPSet { export class IAMRoleManagedPolicyCheck implements BPSet {
private readonly client = new IAMClient({}); private readonly client = new IAMClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'IAMRoleManagedPolicyCheck', name: 'IAMRoleManagedPolicyCheck',
description: 'Checks whether managed IAM policies are attached to any entities (roles, users, or groups).', description: 'Checks whether managed IAM policies are attached to any entities (roles, users, or groups).',
priority: 3, priority: 3,
priorityReason: 'Orphaned managed policies may indicate unused resources that can be removed for better security and resource management.', priorityReason:
'Orphaned managed policies may indicate unused resources that can be removed for better security and resource management.',
awsService: 'IAM', awsService: 'IAM',
awsServiceCategory: 'Access Management', awsServiceCategory: 'Access Management',
bestPracticeCategory: 'Security', bestPracticeCategory: 'Security',
@ -30,86 +27,77 @@ export class IAMRoleManagedPolicyCheck implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListPoliciesCommand', name: 'ListPoliciesCommand',
reason: 'Retrieve all customer-managed IAM policies.', reason: 'Retrieve all customer-managed IAM policies.'
}, },
{ {
name: 'ListEntitiesForPolicyCommand', name: 'ListEntitiesForPolicyCommand',
reason: 'Check if policies are attached to any users, roles, or groups.', reason: 'Check if policies are attached to any users, roles, or groups.'
}, }
], ],
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Ensure orphaned policies are no longer required before removing them.', adviseBeforeFixFunction: 'Ensure orphaned policies are no longer required before removing them.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl().then(
.then( () => {
() => { this.stats.status = 'FINISHED'
this.stats.status = 'FINISHED'; },
}, (err) => {
(err) => { this.stats.status = 'ERROR'
this.stats.status = 'ERROR'; this.stats.errorMessage.push({
this.stats.errorMessage.push({ date: new Date(),
date: new Date(), message: err.message
message: err.message, })
}); }
} )
); }
};
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const policies = await this.getPolicies(); const policies = await this.getPolicies()
for (const policy of policies) { for (const policy of policies) {
const { attached } = await this.checkEntitiesForPolicy(policy.Arn!); const { attached } = await this.checkEntitiesForPolicy(policy.Arn!)
if (attached) { if (attached) {
compliantResources.push(policy.Arn!); compliantResources.push(policy.Arn!)
} else { } else {
nonCompliantResources.push(policy.Arn!); nonCompliantResources.push(policy.Arn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async () => { public readonly fix = async () => {
throw new Error( throw new Error(
'Fixing orphaned managed policies requires manual review and removal. Ensure these policies are no longer needed.' 'Fixing orphaned managed policies requires manual review and removal. Ensure these policies are no longer needed.'
); )
}; }
private readonly getPolicies = async () => { private readonly getPolicies = async () => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' }))
new ListPoliciesCommand({ Scope: 'Local' }) return response.Policies || []
); }
return response.Policies || [];
};
private readonly checkEntitiesForPolicy = async (policyArn: string) => { private readonly checkEntitiesForPolicy = async (policyArn: string) => {
const response = await this.memoClient.send( const response = await this.memoClient.send(new ListEntitiesForPolicyCommand({ PolicyArn: policyArn }))
new ListEntitiesForPolicyCommand({ PolicyArn: policyArn })
);
return { return {
attached: Boolean( attached: Boolean(response.PolicyGroups?.length || response.PolicyUsers?.length || response.PolicyRoles?.length)
response.PolicyGroups?.length || }
response.PolicyUsers?.length || }
response.PolicyRoles?.length
),
};
};
} }

View File

@ -1,17 +1,17 @@
import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'; import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class LambdaDLQCheck implements BPSet { export class LambdaDLQCheck implements BPSet {
private readonly client = new LambdaClient({}); private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaDLQCheck', name: 'LambdaDLQCheck',
@ -26,111 +26,109 @@ export class LambdaDLQCheck implements BPSet {
name: 'dlq-arn', name: 'dlq-arn',
description: 'The ARN of the Dead Letter Queue to associate with the Lambda function.', description: 'The ARN of the Dead Letter Queue to associate with the Lambda function.',
default: '', default: '',
example: 'arn:aws:sqs:us-east-1:123456789012:example-queue', example: 'arn:aws:sqs:us-east-1:123456789012:example-queue'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListFunctionsCommand', name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.', reason: 'Retrieve all Lambda functions in the account.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateFunctionConfigurationCommand', name: 'UpdateFunctionConfigurationCommand',
reason: 'Update the DLQ configuration for Lambda functions.', reason: 'Update the DLQ configuration for Lambda functions.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that the specified DLQ exists and is correctly configured to handle failed events.', adviseBeforeFixFunction: 'Ensure that the specified DLQ exists and is correctly configured to handle failed events.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl().then(
.then( () => {
() => { this.stats.status = 'FINISHED'
this.stats.status = 'FINISHED'; },
}, (err) => {
(err) => { this.stats.status = 'ERROR'
this.stats.status = 'ERROR'; this.stats.errorMessage.push({
this.stats.errorMessage.push({ date: new Date(),
date: new Date(), message: err.message
message: err.message, })
}); }
} )
); }
};
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const functions = await this.getFunctions(); const functions = await this.getFunctions()
for (const func of functions) { for (const func of functions) {
if (func.DeadLetterConfig) { if (func.DeadLetterConfig) {
compliantResources.push(func.FunctionArn!); compliantResources.push(func.FunctionArn!)
} else { } else {
nonCompliantResources.push(func.FunctionArn!); nonCompliantResources.push(func.FunctionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix).then(
.then( () => {
() => { this.stats.status = 'FINISHED'
this.stats.status = 'FINISHED'; },
}, (err) => {
(err) => { this.stats.status = 'ERROR'
this.stats.status = 'ERROR'; this.stats.errorMessage.push({
this.stats.errorMessage.push({ date: new Date(),
date: new Date(), message: err.message
message: err.message, })
}); }
} )
); }
};
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const dlqArn = requiredParametersForFix.find((param) => param.name === 'dlq-arn')?.value; const dlqArn = requiredParametersForFix.find((param) => param.name === 'dlq-arn')?.value
if (!dlqArn) { if (!dlqArn) {
throw new Error("Required parameter 'dlq-arn' is missing."); throw new Error("Required parameter 'dlq-arn' is missing.")
} }
for (const functionArn of nonCompliantResources) { for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!; const functionName = functionArn.split(':').pop()!
await this.client.send( await this.client.send(
new UpdateFunctionConfigurationCommand({ new UpdateFunctionConfigurationCommand({
FunctionName: functionName, FunctionName: functionName,
DeadLetterConfig: { TargetArn: dlqArn }, DeadLetterConfig: { TargetArn: dlqArn }
}) })
); )
} }
}; }
private readonly getFunctions = async () => { private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({})); const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []; return response.Functions || []
}; }
} }

View File

@ -1,22 +1,17 @@
import { import { LambdaClient, ListFunctionsCommand, GetPolicyCommand, RemovePermissionCommand } from '@aws-sdk/client-lambda'
LambdaClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ListFunctionsCommand, import { Memorizer } from '../../Memorizer'
GetPolicyCommand,
RemovePermissionCommand,
} from '@aws-sdk/client-lambda';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class LambdaFunctionPublicAccessProhibited implements BPSet { export class LambdaFunctionPublicAccessProhibited implements BPSet {
private readonly client = new LambdaClient({}); private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaFunctionPublicAccessProhibited', name: 'LambdaFunctionPublicAccessProhibited',
@ -31,124 +26,121 @@ export class LambdaFunctionPublicAccessProhibited implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListFunctionsCommand', name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.', reason: 'Retrieve all Lambda functions in the account.'
}, },
{ {
name: 'GetPolicyCommand', name: 'GetPolicyCommand',
reason: 'Fetch the resource-based policy of a Lambda function.', reason: 'Fetch the resource-based policy of a Lambda function.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'RemovePermissionCommand', name: 'RemovePermissionCommand',
reason: 'Remove public access permissions from a Lambda function.', reason: 'Remove public access permissions from a Lambda function.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that removing permissions does not disrupt legitimate use of the Lambda function.', adviseBeforeFixFunction: 'Ensure that removing permissions does not disrupt legitimate use of the Lambda function.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const functions = await this.getFunctions(); const functions = await this.getFunctions()
for (const func of functions) { for (const func of functions) {
try { try {
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! })); const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! }))
const policy = JSON.parse(response.Policy!); const policy = JSON.parse(response.Policy!)
const hasPublicAccess = policy.Statement.some( const hasPublicAccess = policy.Statement.some(
(statement: any) => statement.Principal === '*' || statement.Principal?.AWS === '*' (statement: unknown) => statement.Principal === '*' || statement.Principal?.AWS === '*'
); )
if (hasPublicAccess) { if (hasPublicAccess) {
nonCompliantResources.push(func.FunctionArn!); nonCompliantResources.push(func.FunctionArn!)
} else { } else {
compliantResources.push(func.FunctionArn!); compliantResources.push(func.FunctionArn!)
} }
} catch (error) { } catch (error) {
if ((error as any).name === 'ResourceNotFoundException') { if ((error as unknown).name === 'ResourceNotFoundException') {
compliantResources.push(func.FunctionArn!); compliantResources.push(func.FunctionArn!)
} else { } else {
throw error; throw error
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
await this.fixImpl(nonCompliantResources) await this.fixImpl(nonCompliantResources)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async (nonCompliantResources: string[]) => { private readonly fixImpl = async (nonCompliantResources: string[]) => {
for (const functionArn of nonCompliantResources) { for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!; const functionName = functionArn.split(':').pop()!
try { try {
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: functionName })); const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: functionName }))
const policy = JSON.parse(response.Policy!); const policy = JSON.parse(response.Policy!)
for (const statement of policy.Statement) { for (const statement of policy.Statement) {
if (statement.Principal === '*' || statement.Principal?.AWS === '*') { if (statement.Principal === '*' || statement.Principal?.AWS === '*') {
await this.client.send( await this.client.send(
new RemovePermissionCommand({ new RemovePermissionCommand({
FunctionName: functionName, FunctionName: functionName,
StatementId: statement.Sid, // Use the actual StatementId from the policy StatementId: statement.Sid // Use the actual StatementId from the policy
}) })
); )
} }
} }
} catch (error) { } catch (error) {
if ((error as any).name !== 'ResourceNotFoundException') { if ((error as unknown).name !== 'ResourceNotFoundException') {
throw error; throw error
} }
} }
} }
}; }
private readonly getFunctions = async () => { private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({})); const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []; return response.Functions || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'
LambdaClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ListFunctionsCommand, import { Memorizer } from '../../Memorizer'
UpdateFunctionConfigurationCommand
} from '@aws-sdk/client-lambda';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class LambdaFunctionSettingsCheck implements BPSet { export class LambdaFunctionSettingsCheck implements BPSet {
private readonly client = new LambdaClient({}); private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaFunctionSettingsCheck', name: 'LambdaFunctionSettingsCheck',
@ -30,75 +26,76 @@ export class LambdaFunctionSettingsCheck implements BPSet {
name: 'timeout', name: 'timeout',
description: 'Timeout value in seconds for the Lambda function.', description: 'Timeout value in seconds for the Lambda function.',
default: '3', default: '3',
example: '30', example: '30'
}, },
{ {
name: 'memory-size', name: 'memory-size',
description: 'Memory size in MB for the Lambda function.', description: 'Memory size in MB for the Lambda function.',
default: '128', default: '128',
example: '256', example: '256'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListFunctionsCommand', name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.', reason: 'Retrieve all Lambda functions in the account.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateFunctionConfigurationCommand', name: 'UpdateFunctionConfigurationCommand',
reason: 'Update the timeout and memory size settings for non-compliant Lambda functions.', reason: 'Update the timeout and memory size settings for non-compliant Lambda functions.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that the timeout and memory size changes are suitable for the function\'s requirements.', adviseBeforeFixFunction:
}); "Ensure that the timeout and memory size changes are suitable for the function's requirements."
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const defaultTimeout = 3; const defaultTimeout = 3
const defaultMemorySize = 128; const defaultMemorySize = 128
const functions = await this.getFunctions(); const functions = await this.getFunctions()
for (const func of functions) { for (const func of functions) {
if (func.Timeout === defaultTimeout || func.MemorySize === defaultMemorySize) { if (func.Timeout === defaultTimeout || func.MemorySize === defaultMemorySize) {
nonCompliantResources.push(func.FunctionArn!); nonCompliantResources.push(func.FunctionArn!)
} else { } else {
compliantResources.push(func.FunctionArn!); compliantResources.push(func.FunctionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
@ -106,42 +103,42 @@ export class LambdaFunctionSettingsCheck implements BPSet {
) => { ) => {
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.value; const timeout = requiredParametersForFix.find((param) => param.name === 'timeout')?.value
const memorySize = requiredParametersForFix.find(param => param.name === 'memory-size')?.value; const memorySize = requiredParametersForFix.find((param) => param.name === 'memory-size')?.value
if (!timeout || !memorySize) { if (!timeout || !memorySize) {
throw new Error("Required parameters 'timeout' and/or 'memory-size' are missing."); throw new Error("Required parameters 'timeout' and/or 'memory-size' are missing.")
} }
for (const functionArn of nonCompliantResources) { for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!; const functionName = functionArn.split(':').pop()!
await this.client.send( await this.client.send(
new UpdateFunctionConfigurationCommand({ new UpdateFunctionConfigurationCommand({
FunctionName: functionName, FunctionName: functionName,
Timeout: parseInt(timeout, 10), Timeout: parseInt(timeout, 10),
MemorySize: parseInt(memorySize, 10), MemorySize: parseInt(memorySize, 10)
}) })
); )
} }
}; }
private readonly getFunctions = async () => { private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({})); const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []; return response.Functions || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'
LambdaClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ListFunctionsCommand, import { Memorizer } from '../../Memorizer'
UpdateFunctionConfigurationCommand
} from '@aws-sdk/client-lambda';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class LambdaInsideVPC implements BPSet { export class LambdaInsideVPC implements BPSet {
private readonly client = new LambdaClient({}); private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaInsideVPC', name: 'LambdaInsideVPC',
@ -30,72 +26,73 @@ export class LambdaInsideVPC implements BPSet {
name: 'subnet-ids', name: 'subnet-ids',
description: 'Comma-separated list of VPC subnet IDs.', description: 'Comma-separated list of VPC subnet IDs.',
default: '', default: '',
example: 'subnet-abc123,subnet-def456', example: 'subnet-abc123,subnet-def456'
}, },
{ {
name: 'security-group-ids', name: 'security-group-ids',
description: 'Comma-separated list of VPC security group IDs.', description: 'Comma-separated list of VPC security group IDs.',
default: '', default: '',
example: 'sg-abc123,sg-def456', example: 'sg-abc123,sg-def456'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListFunctionsCommand', name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.', reason: 'Retrieve all Lambda functions in the account.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'UpdateFunctionConfigurationCommand', name: 'UpdateFunctionConfigurationCommand',
reason: 'Update the VPC configuration for non-compliant Lambda functions.', reason: 'Update the VPC configuration for non-compliant Lambda functions.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the provided subnet and security group IDs are correct and appropriate for the Lambda function\'s requirements.', adviseBeforeFixFunction:
}); "Ensure the provided subnet and security group IDs are correct and appropriate for the Lambda function's requirements."
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const functions = await this.getFunctions(); const functions = await this.getFunctions()
for (const func of functions) { for (const func of functions) {
if (func.VpcConfig && Object.keys(func.VpcConfig).length > 0) { if (func.VpcConfig && Object.keys(func.VpcConfig).length > 0) {
compliantResources.push(func.FunctionArn!); compliantResources.push(func.FunctionArn!)
} else { } else {
nonCompliantResources.push(func.FunctionArn!); nonCompliantResources.push(func.FunctionArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
@ -103,30 +100,30 @@ export class LambdaInsideVPC implements BPSet {
) => { ) => {
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-ids')?.value; const subnetIds = requiredParametersForFix.find((param) => param.name === 'subnet-ids')?.value
const securityGroupIds = requiredParametersForFix.find(param => param.name === 'security-group-ids')?.value; const securityGroupIds = requiredParametersForFix.find((param) => param.name === 'security-group-ids')?.value
if (!subnetIds || !securityGroupIds) { if (!subnetIds || !securityGroupIds) {
throw new Error("Required parameters 'subnet-ids' and/or 'security-group-ids' are missing."); throw new Error("Required parameters 'subnet-ids' and/or 'security-group-ids' are missing.")
} }
for (const functionArn of nonCompliantResources) { for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!; const functionName = functionArn.split(':').pop()!
await this.client.send( await this.client.send(
new UpdateFunctionConfigurationCommand({ new UpdateFunctionConfigurationCommand({
FunctionName: functionName, FunctionName: functionName,
@ -135,12 +132,12 @@ export class LambdaInsideVPC implements BPSet {
SecurityGroupIds: securityGroupIds.split(',') SecurityGroupIds: securityGroupIds.split(',')
} }
}) })
); )
} }
}; }
private readonly getFunctions = async () => { private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({})); const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []; return response.Functions || []
}; }
} }

View File

@ -1,26 +1,19 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup'
DescribeDBClustersCommand, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ModifyDBClusterCommand import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import {
BackupClient,
ListRecoveryPointsByResourceCommand
} from '@aws-sdk/client-backup';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class AuroraLastBackupRecoveryPointCreated implements BPSet { export class AuroraLastBackupRecoveryPointCreated implements BPSet {
private readonly rdsClient = new RDSClient({}); private readonly rdsClient = new RDSClient({})
private readonly backupClient = new BackupClient({}); private readonly backupClient = new BackupClient({})
private readonly memoRdsClient = Memorizer.memo(this.rdsClient); private readonly memoRdsClient = Memorizer.memo(this.rdsClient)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'AuroraLastBackupRecoveryPointCreated', name: 'AuroraLastBackupRecoveryPointCreated',
@ -35,77 +28,75 @@ export class AuroraLastBackupRecoveryPointCreated implements BPSet {
name: 'backup-retention-period', name: 'backup-retention-period',
description: 'The number of days to retain backups for the DB cluster.', description: 'The number of days to retain backups for the DB cluster.',
default: '7', default: '7',
example: '7', example: '7'
} }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeDBClustersCommand', name: 'DescribeDBClustersCommand',
reason: 'Retrieve information about Aurora DB clusters.', reason: 'Retrieve information about Aurora DB clusters.'
}, },
{ {
name: 'ListRecoveryPointsByResourceCommand', name: 'ListRecoveryPointsByResourceCommand',
reason: 'Check the recovery points associated with the DB cluster.', reason: 'Check the recovery points associated with the DB cluster.'
} }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyDBClusterCommand', name: 'ModifyDBClusterCommand',
reason: 'Update the backup retention period for the DB cluster.', reason: 'Update the backup retention period for the DB cluster.'
} }
], ],
adviseBeforeFixFunction: 'Ensure that extending the backup retention period aligns with your data protection policies.', adviseBeforeFixFunction:
}); 'Ensure that extending the backup retention period aligns with your data protection policies.'
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
const recoveryPoints = await this.getRecoveryPoints(cluster.DBClusterArn!); const recoveryPoints = await this.getRecoveryPoints(cluster.DBClusterArn!)
const recoveryDates = recoveryPoints.map(rp => new Date(rp.CreationDate!)); const recoveryDates = recoveryPoints.map((rp) => new Date(rp.CreationDate!))
recoveryDates.sort((a, b) => b.getTime() - a.getTime()); recoveryDates.sort((a, b) => b.getTime() - a.getTime())
if ( if (recoveryDates.length > 0 && new Date().getTime() - recoveryDates[0].getTime() < 24 * 60 * 60 * 1000) {
recoveryDates.length > 0 && compliantResources.push(cluster.DBClusterArn!)
new Date().getTime() - recoveryDates[0].getTime() < 24 * 60 * 60 * 1000
) {
compliantResources.push(cluster.DBClusterArn!);
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
@ -113,50 +104,46 @@ export class AuroraLastBackupRecoveryPointCreated implements BPSet {
) => { ) => {
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find((param) => param.name === 'backup-retention-period')?.value
param => param.name === 'backup-retention-period'
)?.value;
if (!retentionPeriod) { if (!retentionPeriod) {
throw new Error("Required parameter 'backup-retention-period' is missing."); throw new Error("Required parameter 'backup-retention-period' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
await this.rdsClient.send( await this.rdsClient.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
BackupRetentionPeriod: parseInt(retentionPeriod, 10) BackupRetentionPeriod: parseInt(retentionPeriod, 10)
}) })
); )
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({})); const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
private readonly getRecoveryPoints = async (resourceArn: string) => { private readonly getRecoveryPoints = async (resourceArn: string) => {
const response = await this.backupClient.send( const response = await this.backupClient.send(new ListRecoveryPointsByResourceCommand({ ResourceArn: resourceArn }))
new ListRecoveryPointsByResourceCommand({ ResourceArn: resourceArn }) return response.RecoveryPoints || []
); }
return response.RecoveryPoints || [];
};
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand, import { Memorizer } from '../../Memorizer'
ModifyDBClusterCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class AuroraMySQLBacktrackingEnabled implements BPSet { export class AuroraMySQLBacktrackingEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'AuroraMySQLBacktrackingEnabled', name: 'AuroraMySQLBacktrackingEnabled',
@ -30,94 +26,91 @@ export class AuroraMySQLBacktrackingEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'DescribeDBClustersCommand', name: 'DescribeDBClustersCommand',
reason: 'Fetch Aurora MySQL DB clusters and check backtracking configuration.', reason: 'Fetch Aurora MySQL DB clusters and check backtracking configuration.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'ModifyDBClusterCommand', name: 'ModifyDBClusterCommand',
reason: 'Enable backtracking for non-compliant Aurora MySQL DB clusters.', reason: 'Enable backtracking for non-compliant Aurora MySQL DB clusters.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that enabling backtracking aligns with your application requirements.', adviseBeforeFixFunction: 'Ensure that enabling backtracking aligns with your application requirements.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if ( if (
cluster.Engine === 'aurora-mysql' && cluster.Engine === 'aurora-mysql' &&
(!cluster.EarliestBacktrackTime || cluster.EarliestBacktrackTime === null) (!cluster.EarliestBacktrackTime || cluster.EarliestBacktrackTime === null)
) { ) {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} else { } else {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
await this.fixImpl(nonCompliantResources) await this.fixImpl(nonCompliantResources)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async (nonCompliantResources: string[]) => { private readonly fixImpl = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
BacktrackWindow: 3600, // Set backtracking window to 1 hour BacktrackWindow: 3600 // Set backtracking window to 1 hour
}) })
); )
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBInstancesCommand, ModifyDBInstanceCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBInstancesCommand, import { Memorizer } from '../../Memorizer'
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DBInstanceBackupEnabled implements BPSet { export class DBInstanceBackupEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'DBInstanceBackupEnabled', name: 'DBInstanceBackupEnabled',
@ -47,49 +43,49 @@ export class DBInstanceBackupEnabled implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure the retention period aligns with your organizations backup policy.' adviseBeforeFixFunction: 'Ensure the retention period aligns with your organizations backup policy.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbInstances = await this.getDBInstances(); const dbInstances = await this.getDBInstances()
for (const instance of dbInstances) { for (const instance of dbInstances) {
if (instance.BackupRetentionPeriod && instance.BackupRetentionPeriod > 0) { if (instance.BackupRetentionPeriod && instance.BackupRetentionPeriod > 0) {
compliantResources.push(instance.DBInstanceArn!); compliantResources.push(instance.DBInstanceArn!)
} else { } else {
nonCompliantResources.push(instance.DBInstanceArn!); nonCompliantResources.push(instance.DBInstanceArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
@ -97,42 +93,40 @@ export class DBInstanceBackupEnabled implements BPSet {
) => { ) => {
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find((param) => param.name === 'retention-period')?.value
(param) => param.name === 'retention-period'
)?.value;
if (!retentionPeriod) { if (!retentionPeriod) {
throw new Error("Required parameter 'retention-period' is missing."); throw new Error("Required parameter 'retention-period' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const instanceId = arn.split(':instance/')[1]; const instanceId = arn.split(':instance/')[1]
await this.client.send( await this.client.send(
new ModifyDBInstanceCommand({ new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId, DBInstanceIdentifier: instanceId,
BackupRetentionPeriod: parseInt(retentionPeriod, 10) BackupRetentionPeriod: parseInt(retentionPeriod, 10)
}) })
); )
} }
}; }
private readonly getDBInstances = async () => { private readonly getDBInstances = async () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({})); const response = await this.memoClient.send(new DescribeDBInstancesCommand({}))
return response.DBInstances || []; return response.DBInstances || []
}; }
} }

View File

@ -1,27 +1,24 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand, import { Memorizer } from '../../Memorizer'
ModifyDBClusterCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterAutoMinorVersionUpgradeEnabled implements BPSet { export class RDSClusterAutoMinorVersionUpgradeEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterAutoMinorVersionUpgradeEnabled', name: 'RDSClusterAutoMinorVersionUpgradeEnabled',
description: 'Ensures Auto Minor Version Upgrade is enabled for RDS clusters.', description: 'Ensures Auto Minor Version Upgrade is enabled for RDS clusters.',
priority: 1, priority: 1,
priorityReason: 'Auto minor version upgrades help keep the database engine updated with minimal effort, ensuring security and performance improvements.', priorityReason:
'Auto minor version upgrades help keep the database engine updated with minimal effort, ensuring security and performance improvements.',
awsService: 'RDS', awsService: 'RDS',
awsServiceCategory: 'Database', awsServiceCategory: 'Database',
bestPracticeCategory: 'Configuration Management', bestPracticeCategory: 'Configuration Management',
@ -39,83 +36,81 @@ export class RDSClusterAutoMinorVersionUpgradeEnabled implements BPSet {
reason: 'Enable auto minor version upgrade for non-compliant RDS clusters.' reason: 'Enable auto minor version upgrade for non-compliant RDS clusters.'
} }
], ],
adviseBeforeFixFunction: 'Ensure that enabling auto minor version upgrades aligns with your organizations change management policies.' adviseBeforeFixFunction:
}); 'Ensure that enabling auto minor version upgrades aligns with your organizations change management policies.'
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if (cluster.Engine === 'docdb' || cluster.AutoMinorVersionUpgrade) { if (cluster.Engine === 'docdb' || cluster.AutoMinorVersionUpgrade) {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
await this.fixImpl(nonCompliantResources) await this.fixImpl(nonCompliantResources)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async (nonCompliantResources: string[]) => { private readonly fixImpl = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
AutoMinorVersionUpgrade: true AutoMinorVersionUpgrade: true
}) })
); )
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand, import { Memorizer } from '../../Memorizer'
ModifyDBClusterCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterDefaultAdminCheck implements BPSet { export class RDSClusterDefaultAdminCheck implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterDefaultAdminCheck', name: 'RDSClusterDefaultAdminCheck',
@ -53,49 +49,49 @@ export class RDSClusterDefaultAdminCheck implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure that the new master username and password comply with your security policies.' adviseBeforeFixFunction: 'Ensure that the new master username and password comply with your security policies.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if (!['admin', 'postgres'].includes(cluster.MasterUsername!)) { if (!['admin', 'postgres'].includes(cluster.MasterUsername!)) {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
@ -103,46 +99,42 @@ export class RDSClusterDefaultAdminCheck implements BPSet {
) => { ) => {
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const newMasterUsername = requiredParametersForFix.find( const newMasterUsername = requiredParametersForFix.find((param) => param.name === 'new-master-username')?.value
(param) => param.name === 'new-master-username' const newMasterPassword = requiredParametersForFix.find((param) => param.name === 'new-master-password')?.value
)?.value;
const newMasterPassword = requiredParametersForFix.find(
(param) => param.name === 'new-master-password'
)?.value;
if (!newMasterUsername || !newMasterPassword) { if (!newMasterUsername || !newMasterPassword) {
throw new Error("Required parameters 'new-master-username' and 'new-master-password' are missing."); throw new Error("Required parameters 'new-master-username' and 'new-master-password' are missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
MasterUserPassword: newMasterPassword MasterUserPassword: newMasterPassword
}) })
); )
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand, import { Memorizer } from '../../Memorizer'
ModifyDBClusterCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterDeletionProtectionEnabled implements BPSet { export class RDSClusterDeletionProtectionEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterDeletionProtectionEnabled', name: 'RDSClusterDeletionProtectionEnabled',
@ -40,82 +36,79 @@ export class RDSClusterDeletionProtectionEnabled implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure that enabling deletion protection aligns with your operational policies.' adviseBeforeFixFunction: 'Ensure that enabling deletion protection aligns with your operational policies.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if (cluster.DeletionProtection) { if (cluster.DeletionProtection) {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
await this.fixImpl(nonCompliantResources) await this.fixImpl(nonCompliantResources)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async (nonCompliantResources: string[]) => { private readonly fixImpl = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
DeletionProtection: true DeletionProtection: true
}) })
); )
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,20 +1,17 @@
import { import { RDSClient, DescribeDBClustersCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterEncryptedAtRest implements BPSet { export class RDSClusterEncryptedAtRest implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterEncryptedAtRest', name: 'RDSClusterEncryptedAtRest',
@ -33,67 +30,66 @@ export class RDSClusterEncryptedAtRest implements BPSet {
} }
], ],
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Manually recreate the RDS cluster with encryption at rest enabled, as fixing this requires destructive operations.' adviseBeforeFixFunction:
}); 'Manually recreate the RDS cluster with encryption at rest enabled, as fixing this requires destructive operations.'
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if (cluster.StorageEncrypted) { if (cluster.StorageEncrypted) {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async () => {
nonCompliantResources: string[], this.stats.status = 'ERROR'
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: 'Fixing encryption at rest requires recreating the cluster. Please manually recreate the cluster with encryption enabled.' message:
}); 'Fixing encryption at rest requires recreating the cluster. Please manually recreate the cluster with encryption enabled.'
})
throw new Error( throw new Error(
'Fixing encryption at rest requires recreating the cluster. Please manually recreate the cluster with encryption enabled.' 'Fixing encryption at rest requires recreating the cluster. Please manually recreate the cluster with encryption enabled.'
); )
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand, import { Memorizer } from '../../Memorizer'
ModifyDBClusterCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterIAMAuthenticationEnabled implements BPSet { export class RDSClusterIAMAuthenticationEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterIAMAuthenticationEnabled', name: 'RDSClusterIAMAuthenticationEnabled',
@ -39,91 +35,86 @@ export class RDSClusterIAMAuthenticationEnabled implements BPSet {
reason: 'To enable IAM Database Authentication for non-compliant clusters.' reason: 'To enable IAM Database Authentication for non-compliant clusters.'
} }
], ],
adviseBeforeFixFunction: 'Ensure that enabling IAM Database Authentication aligns with your security and application needs.' adviseBeforeFixFunction:
}); 'Ensure that enabling IAM Database Authentication aligns with your security and application needs.'
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if ( if (cluster.Engine === 'docdb' || cluster.IAMDatabaseAuthenticationEnabled) {
cluster.Engine === 'docdb' || compliantResources.push(cluster.DBClusterArn!)
cluster.IAMDatabaseAuthenticationEnabled
) {
compliantResources.push(cluster.DBClusterArn!);
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
await this.client.send( await this.client.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
EnableIAMDatabaseAuthentication: true EnableIAMDatabaseAuthentication: true
}) })
); )
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,20 +1,17 @@
import { import { RDSClient, DescribeDBClustersCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterMultiAZEnabled implements BPSet { export class RDSClusterMultiAZEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterMultiAZEnabled', name: 'RDSClusterMultiAZEnabled',
@ -42,80 +39,75 @@ export class RDSClusterMultiAZEnabled implements BPSet {
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Enabling Multi-AZ for an existing cluster may require significant reconfiguration and potential downtime. Proceed with caution.' 'Enabling Multi-AZ for an existing cluster may require significant reconfiguration and potential downtime. Proceed with caution.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if ((cluster.AvailabilityZones || []).length > 1) { if ((cluster.AvailabilityZones || []).length > 1) {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async () => {
nonCompliantResources: string[], throw new Error('Enabling Multi-AZ requires cluster reconfiguration. This must be performed manually.')
requiredParametersForFix: { name: string; value: string }[] }
) => {
throw new Error(
'Enabling Multi-AZ requires cluster reconfiguration. This must be performed manually.'
);
};
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,24 +1,20 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2'
DescribeDBClustersCommand, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ModifyDBClusterCommand import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSDBSecurityGroupNotAllowed implements BPSet { export class RDSDBSecurityGroupNotAllowed implements BPSet {
private readonly rdsClient = new RDSClient({}); private readonly rdsClient = new RDSClient({})
private readonly ec2Client = new EC2Client({}); private readonly ec2Client = new EC2Client({})
private readonly memoRdsClient = Memorizer.memo(this.rdsClient); private readonly memoRdsClient = Memorizer.memo(this.rdsClient)
private readonly memoEc2Client = Memorizer.memo(this.ec2Client); private readonly memoEc2Client = Memorizer.memo(this.ec2Client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSDBSecurityGroupNotAllowed', name: 'RDSDBSecurityGroupNotAllowed',
@ -48,94 +44,91 @@ export class RDSDBSecurityGroupNotAllowed implements BPSet {
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction:
'Ensure valid non-default security groups are associated with the clusters before applying the fix.' 'Ensure valid non-default security groups are associated with the clusters before applying the fix.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
const defaultSecurityGroupIds = (await this.getDefaultSecurityGroups()).map(sg => sg.GroupId!); const defaultSecurityGroupIds = (await this.getDefaultSecurityGroups()).map((sg) => sg.GroupId!)
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
const activeSecurityGroups = cluster.VpcSecurityGroups?.filter(sg => sg.Status === 'active') || []; const activeSecurityGroups = cluster.VpcSecurityGroups?.filter((sg) => sg.Status === 'active') || []
if (activeSecurityGroups.some(sg => defaultSecurityGroupIds.includes(sg.VpcSecurityGroupId!))) { if (activeSecurityGroups.some((sg) => defaultSecurityGroupIds.includes(sg.VpcSecurityGroupId!))) {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} else { } else {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[], this.stats.status = 'CHECKING'
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources) await this.fixImpl(nonCompliantResources)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async (nonCompliantResources: string[]) => { private readonly fixImpl = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
await this.rdsClient.send( await this.rdsClient.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
VpcSecurityGroupIds: [] // Ensure valid non-default security groups are used here VpcSecurityGroupIds: [] // Ensure valid non-default security groups are used here
}) })
); )
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({})); const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
private readonly getDefaultSecurityGroups = async () => { private readonly getDefaultSecurityGroups = async () => {
const response = await this.memoEc2Client.send( const response = await this.memoEc2Client.send(
new DescribeSecurityGroupsCommand({ Filters: [{ Name: 'group-name', Values: ['default'] }] }) new DescribeSecurityGroupsCommand({ Filters: [{ Name: 'group-name', Values: ['default'] }] })
); )
return response.SecurityGroups || []; return response.SecurityGroups || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBInstancesCommand, ModifyDBInstanceCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBInstancesCommand, import { Memorizer } from '../../Memorizer'
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSEnhancedMonitoringEnabled implements BPSet { export class RDSEnhancedMonitoringEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSEnhancedMonitoringEnabled', name: 'RDSEnhancedMonitoringEnabled',
@ -47,95 +43,93 @@ export class RDSEnhancedMonitoringEnabled implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure that enabling Enhanced Monitoring does not conflict with existing configurations.' adviseBeforeFixFunction: 'Ensure that enabling Enhanced Monitoring does not conflict with existing configurations.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbInstances = await this.getDBInstances(); const dbInstances = await this.getDBInstances()
for (const instance of dbInstances) { for (const instance of dbInstances) {
if (instance.MonitoringInterval && instance.MonitoringInterval > 0) { if (instance.MonitoringInterval && instance.MonitoringInterval > 0) {
compliantResources.push(instance.DBInstanceArn!); compliantResources.push(instance.DBInstanceArn!)
} else { } else {
nonCompliantResources.push(instance.DBInstanceArn!); nonCompliantResources.push(instance.DBInstanceArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const monitoringInterval = requiredParametersForFix.find( const monitoringInterval = requiredParametersForFix.find((param) => param.name === 'monitoring-interval')?.value
(param) => param.name === 'monitoring-interval'
)?.value;
if (!monitoringInterval) { if (!monitoringInterval) {
throw new Error("Required parameter 'monitoring-interval' is missing."); throw new Error("Required parameter 'monitoring-interval' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const instanceId = arn.split(':instance/')[1]; const instanceId = arn.split(':instance/')[1]
await this.client.send( await this.client.send(
new ModifyDBInstanceCommand({ new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId, DBInstanceIdentifier: instanceId,
MonitoringInterval: parseInt(monitoringInterval, 10) MonitoringInterval: parseInt(monitoringInterval, 10)
}) })
); )
} }
}; }
private readonly getDBInstances = async () => { private readonly getDBInstances = async () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({})); const response = await this.memoClient.send(new DescribeDBInstancesCommand({}))
return response.DBInstances || []; return response.DBInstances || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBInstancesCommand, ModifyDBInstanceCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBInstancesCommand, import { Memorizer } from '../../Memorizer'
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSInstancePublicAccessCheck implements BPSet { export class RDSInstancePublicAccessCheck implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSInstancePublicAccessCheck', name: 'RDSInstancePublicAccessCheck',
@ -39,88 +35,86 @@ export class RDSInstancePublicAccessCheck implements BPSet {
reason: 'Disables public access for non-compliant RDS instances.' reason: 'Disables public access for non-compliant RDS instances.'
} }
], ],
adviseBeforeFixFunction: 'Ensure there are valid private network configurations in place before disabling public access.' adviseBeforeFixFunction:
}); 'Ensure there are valid private network configurations in place before disabling public access.'
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const dbInstances = await this.getDBInstances(); const dbInstances = await this.getDBInstances()
for (const instance of dbInstances) { for (const instance of dbInstances) {
if (instance.PubliclyAccessible) { if (instance.PubliclyAccessible) {
nonCompliantResources.push(instance.DBInstanceArn!); nonCompliantResources.push(instance.DBInstanceArn!)
} else { } else {
compliantResources.push(instance.DBInstanceArn!); compliantResources.push(instance.DBInstanceArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const instanceId = arn.split(':instance/')[1]; const instanceId = arn.split(':instance/')[1]
await this.client.send( await this.client.send(
new ModifyDBInstanceCommand({ new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId, DBInstanceIdentifier: instanceId,
PubliclyAccessible: false PubliclyAccessible: false
}) })
); )
} }
}; }
private readonly getDBInstances = async () => { private readonly getDBInstances = async () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({})); const response = await this.memoClient.send(new DescribeDBInstancesCommand({}))
return response.DBInstances || []; return response.DBInstances || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClustersCommand, import { Memorizer } from '../../Memorizer'
ModifyDBClusterCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSLoggingEnabled implements BPSet { export class RDSLoggingEnabled implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSLoggingEnabled', name: 'RDSLoggingEnabled',
@ -40,111 +36,108 @@ export class RDSLoggingEnabled implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure that enabling logs does not negatively impact application performance.' adviseBeforeFixFunction: 'Ensure that enabling logs does not negatively impact application performance.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const logsForEngine: Record<string, string[]> = { const logsForEngine: Record<string, string[]> = {
'aurora-mysql': ['audit', 'error', 'general', 'slowquery'], 'aurora-mysql': ['audit', 'error', 'general', 'slowquery'],
'aurora-postgresql': ['postgresql'], 'aurora-postgresql': ['postgresql'],
'docdb': ['audit', 'profiler'], docdb: ['audit', 'profiler'],
'mysql': ['audit', 'error', 'general', 'slowquery'], mysql: ['audit', 'error', 'general', 'slowquery'],
'postgresql': ['postgresql', 'upgrade'], postgresql: ['postgresql', 'upgrade']
}; }
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
const requiredLogs = logsForEngine[cluster.Engine!] || []; const requiredLogs = logsForEngine[cluster.Engine!] || []
const enabledLogs = cluster.EnabledCloudwatchLogsExports || []; const enabledLogs = cluster.EnabledCloudwatchLogsExports || []
if (JSON.stringify(enabledLogs) === JSON.stringify(requiredLogs)) { if (JSON.stringify(enabledLogs) === JSON.stringify(requiredLogs)) {
compliantResources.push(cluster.DBClusterArn!); compliantResources.push(cluster.DBClusterArn!)
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!); nonCompliantResources.push(cluster.DBClusterArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const logsForEngine: Record<string, string[]> = { const logsForEngine: Record<string, string[]> = {
'aurora-mysql': ['audit', 'error', 'general', 'slowquery'], 'aurora-mysql': ['audit', 'error', 'general', 'slowquery'],
'aurora-postgresql': ['postgresql'], 'aurora-postgresql': ['postgresql'],
'docdb': ['audit', 'profiler'] docdb: ['audit', 'profiler']
}; }
const dbClusters = await this.getDBClusters(); const dbClusters = await this.getDBClusters()
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]; const clusterId = arn.split(':cluster/')[1]
const cluster = dbClusters.find((c) => c.DBClusterArn === arn); const cluster = dbClusters.find((c) => c.DBClusterArn === arn)
if (cluster) { if (cluster) {
const logsToEnable = logsForEngine[cluster.Engine!] || []; const logsToEnable = logsForEngine[cluster.Engine!] || []
await this.client.send( await this.client.send(
new ModifyDBClusterCommand({ new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId, DBClusterIdentifier: clusterId,
CloudwatchLogsExportConfiguration: { EnableLogTypes: logsToEnable } CloudwatchLogsExportConfiguration: { EnableLogTypes: logsToEnable }
}) })
); )
} }
} }
}; }
private readonly getDBClusters = async () => { private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})); const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []; return response.DBClusters || []
}; }
} }

View File

@ -1,21 +1,17 @@
import { import { RDSClient, DescribeDBClusterSnapshotsCommand, CopyDBClusterSnapshotCommand } from '@aws-sdk/client-rds'
RDSClient, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
DescribeDBClusterSnapshotsCommand, import { Memorizer } from '../../Memorizer'
CopyDBClusterSnapshotCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSSnapshotEncrypted implements BPSet { export class RDSSnapshotEncrypted implements BPSet {
private readonly client = new RDSClient({}); private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSSnapshotEncrypted', name: 'RDSSnapshotEncrypted',
@ -47,83 +43,81 @@ export class RDSSnapshotEncrypted implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure that the KMS key is properly configured and accessible.' adviseBeforeFixFunction: 'Ensure that the KMS key is properly configured and accessible.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const snapshots = await this.getDBClusterSnapshots(); const snapshots = await this.getDBClusterSnapshots()
for (const snapshot of snapshots) { for (const snapshot of snapshots) {
if (snapshot.StorageEncrypted) { if (snapshot.StorageEncrypted) {
compliantResources.push(snapshot.DBClusterSnapshotArn!); compliantResources.push(snapshot.DBClusterSnapshotArn!)
} else { } else {
nonCompliantResources.push(snapshot.DBClusterSnapshotArn!); nonCompliantResources.push(snapshot.DBClusterSnapshotArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const kmsKeyId = requiredParametersForFix.find( const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value
(param) => param.name === 'kms-key-id'
)?.value;
if (!kmsKeyId) { if (!kmsKeyId) {
throw new Error("Required parameter 'kms-key-id' is missing."); throw new Error("Required parameter 'kms-key-id' is missing.")
} }
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const snapshotId = arn.split(':snapshot:')[1]; const snapshotId = arn.split(':snapshot:')[1]
await this.client.send( await this.client.send(
new CopyDBClusterSnapshotCommand({ new CopyDBClusterSnapshotCommand({
@ -131,12 +125,12 @@ export class RDSSnapshotEncrypted implements BPSet {
TargetDBClusterSnapshotIdentifier: `${snapshotId}-encrypted`, TargetDBClusterSnapshotIdentifier: `${snapshotId}-encrypted`,
KmsKeyId: kmsKeyId KmsKeyId: kmsKeyId
}) })
); )
} }
}; }
private readonly getDBClusterSnapshots = async () => { private readonly getDBClusterSnapshots = async () => {
const response = await this.memoClient.send(new DescribeDBClusterSnapshotsCommand({})); const response = await this.memoClient.send(new DescribeDBClusterSnapshotsCommand({}))
return response.DBClusterSnapshots || []; return response.DBClusterSnapshots || []
}; }
} }

View File

@ -3,22 +3,22 @@ import {
ListAccessPointsCommand, ListAccessPointsCommand,
DeleteAccessPointCommand, DeleteAccessPointCommand,
CreateAccessPointCommand CreateAccessPointCommand
} from '@aws-sdk/client-s3-control'; } from '@aws-sdk/client-s3-control'
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class S3AccessPointInVpcOnly implements BPSet { export class S3AccessPointInVpcOnly implements BPSet {
private readonly client = new S3ControlClient({}); private readonly client = new S3ControlClient({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stsClient = Memorizer.memo(new STSClient({})); private readonly stsClient = Memorizer.memo(new STSClient({}))
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3AccessPointInVpcOnly', name: 'S3AccessPointInVpcOnly',
@ -54,94 +54,92 @@ export class S3AccessPointInVpcOnly implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure that the specified VPC ID is correct and accessible.' adviseBeforeFixFunction: 'Ensure that the specified VPC ID is correct and accessible.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const accountId = await this.getAccountId(); const accountId = await this.getAccountId()
const response = await this.memoClient.send( const response = await this.memoClient.send(new ListAccessPointsCommand({ AccountId: accountId }))
new ListAccessPointsCommand({ AccountId: accountId })
);
for (const accessPoint of response.AccessPointList || []) { for (const accessPoint of response.AccessPointList || []) {
if (accessPoint.NetworkOrigin === 'VPC') { if (accessPoint.NetworkOrigin === 'VPC') {
compliantResources.push(accessPoint.AccessPointArn!); compliantResources.push(accessPoint.AccessPointArn!)
} else { } else {
nonCompliantResources.push(accessPoint.AccessPointArn!); nonCompliantResources.push(accessPoint.AccessPointArn!)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const accountId = await this.getAccountId(); const accountId = await this.getAccountId()
const vpcId = requiredParametersForFix.find(param => param.name === 'your-vpc-id')?.value; const vpcId = requiredParametersForFix.find((param) => param.name === 'your-vpc-id')?.value
if (!vpcId) { if (!vpcId) {
throw new Error("Required parameter 'your-vpc-id' is missing."); throw new Error("Required parameter 'your-vpc-id' is missing.")
} }
for (const accessPointArn of nonCompliantResources) { for (const accessPointArn of nonCompliantResources) {
const accessPointName = accessPointArn.split(':').pop()!; const accessPointName = accessPointArn.split(':').pop()!
const bucketName = accessPointArn.split('/')[1]!; const bucketName = accessPointArn.split('/')[1]!
await this.client.send( await this.client.send(
new DeleteAccessPointCommand({ new DeleteAccessPointCommand({
AccountId: accountId, AccountId: accountId,
Name: accessPointName Name: accessPointName
}) })
); )
await this.client.send( await this.client.send(
new CreateAccessPointCommand({ new CreateAccessPointCommand({
@ -152,12 +150,12 @@ export class S3AccessPointInVpcOnly implements BPSet {
VpcId: vpcId VpcId: vpcId
} }
}) })
); )
} }
}; }
private readonly getAccountId = async (): Promise<string> => { private readonly getAccountId = async (): Promise<string> => {
const response = await this.stsClient.send(new GetCallerIdentityCommand({})); const response = await this.stsClient.send(new GetCallerIdentityCommand({}))
return response.Account!; return response.Account!
}; }
} }

View File

@ -3,26 +3,27 @@ import {
ListBucketsCommand, ListBucketsCommand,
GetObjectLockConfigurationCommand, GetObjectLockConfigurationCommand,
PutObjectLockConfigurationCommand PutObjectLockConfigurationCommand
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class S3BucketDefaultLockEnabled implements BPSet { export class S3BucketDefaultLockEnabled implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketDefaultLockEnabled', name: 'S3BucketDefaultLockEnabled',
description: 'Ensures that all S3 buckets have default object lock configuration enabled.', description: 'Ensures that all S3 buckets have default object lock configuration enabled.',
priority: 2, priority: 2,
priorityReason: 'Object lock configuration ensures immutability of bucket objects, protecting them from unintended deletions or modifications.', priorityReason:
'Object lock configuration ensures immutability of bucket objects, protecting them from unintended deletions or modifications.',
awsService: 'S3', awsService: 'S3',
awsServiceCategory: 'Buckets', awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Data Protection', bestPracticeCategory: 'Data Protection',
@ -40,83 +41,79 @@ export class S3BucketDefaultLockEnabled implements BPSet {
reason: 'Enables object lock configuration with a default retention rule.' reason: 'Enables object lock configuration with a default retention rule.'
} }
], ],
adviseBeforeFixFunction: 'Ensure that the S3 bucket has object lock enabled before running the fix function, as this operation cannot be undone.' adviseBeforeFixFunction:
}); 'Ensure that the S3 bucket has object lock enabled before running the fix function, as this operation cannot be undone.'
})
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
try { try {
await this.memoClient.send( await this.memoClient.send(new GetObjectLockConfigurationCommand({ Bucket: bucket.Name! }))
new GetObjectLockConfigurationCommand({ Bucket: bucket.Name! }) compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
);
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} catch (error) { } catch (error) {
if ((error as any).name === 'ObjectLockConfigurationNotFoundError') { if ((error as unknown).name === 'ObjectLockConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
throw error; throw error
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const bucketArn of nonCompliantResources) { for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!; const bucketName = bucketArn.split(':::')[1]!
await this.client.send( await this.client.send(
new PutObjectLockConfigurationCommand({ new PutObjectLockConfigurationCommand({
Bucket: bucketName, Bucket: bucketName,
@ -130,12 +127,12 @@ export class S3BucketDefaultLockEnabled implements BPSet {
} }
} }
}) })
); )
} }
}; }
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

View File

@ -3,20 +3,20 @@ import {
ListBucketsCommand, ListBucketsCommand,
GetPublicAccessBlockCommand, GetPublicAccessBlockCommand,
PutPublicAccessBlockCommand PutPublicAccessBlockCommand
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class S3BucketLevelPublicAccessProhibited implements BPSet { export class S3BucketLevelPublicAccessProhibited implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [] errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketLevelPublicAccessProhibited', name: 'S3BucketLevelPublicAccessProhibited',
@ -41,88 +41,83 @@ export class S3BucketLevelPublicAccessProhibited implements BPSet {
} }
], ],
adviseBeforeFixFunction: 'Ensure no legitimate use cases require public access before applying this fix.' adviseBeforeFixFunction: 'Ensure no legitimate use cases require public access before applying this fix.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
try { try {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetPublicAccessBlockCommand({ Bucket: bucket.Name! }))
new GetPublicAccessBlockCommand({ Bucket: bucket.Name! }) const config = response.PublicAccessBlockConfiguration
);
const config = response.PublicAccessBlockConfiguration;
if ( if (
config?.BlockPublicAcls && config?.BlockPublicAcls &&
config?.IgnorePublicAcls && config?.IgnorePublicAcls &&
config?.BlockPublicPolicy && config?.BlockPublicPolicy &&
config?.RestrictPublicBuckets config?.RestrictPublicBuckets
) { ) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} catch { } catch {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const bucketArn of nonCompliantResources) { for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!; const bucketName = bucketArn.split(':::')[1]!
await this.client.send( await this.client.send(
new PutPublicAccessBlockCommand({ new PutPublicAccessBlockCommand({
Bucket: bucketName, Bucket: bucketName,
@ -133,12 +128,12 @@ export class S3BucketLevelPublicAccessProhibited implements BPSet {
RestrictPublicBuckets: true RestrictPublicBuckets: true
} }
}) })
); )
} }
}; }
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

View File

@ -1,29 +1,23 @@
import { import { S3Client, ListBucketsCommand, GetBucketLoggingCommand, PutBucketLoggingCommand } from '@aws-sdk/client-s3'
S3Client, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ListBucketsCommand, import { Memorizer } from '../../Memorizer'
GetBucketLoggingCommand,
PutBucketLoggingCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3BucketLoggingEnabled implements BPSet { export class S3BucketLoggingEnabled implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketLoggingEnabled', name: 'S3BucketLoggingEnabled',
description: 'Ensures that S3 buckets have logging enabled.', description: 'Ensures that S3 buckets have logging enabled.',
priority: 2, priority: 2,
priorityReason: priorityReason: 'Enabling logging on S3 buckets provides audit and security capabilities.',
'Enabling logging on S3 buckets provides audit and security capabilities.',
awsService: 'S3', awsService: 'S3',
awsServiceCategory: 'Buckets', awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Logging', bestPracticeCategory: 'Logging',
@ -32,93 +26,90 @@ export class S3BucketLoggingEnabled implements BPSet {
name: 'log-destination-bucket', name: 'log-destination-bucket',
description: 'The bucket where access logs should be stored.', description: 'The bucket where access logs should be stored.',
default: '', default: '',
example: 'my-log-bucket', example: 'my-log-bucket'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetBucketLoggingCommand', name: 'GetBucketLoggingCommand',
reason: 'Retrieves the logging configuration for the bucket.', reason: 'Retrieves the logging configuration for the bucket.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutBucketLoggingCommand', name: 'PutBucketLoggingCommand',
reason: 'Enables logging on the bucket.', reason: 'Enables logging on the bucket.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure the destination bucket for logs exists and has proper permissions for logging.'
'Ensure the destination bucket for logs exists and has proper permissions for logging.', })
});
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
try { try {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetBucketLoggingCommand({ Bucket: bucket.Name! }))
new GetBucketLoggingCommand({ Bucket: bucket.Name! })
);
if (response.LoggingEnabled) { if (response.LoggingEnabled) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} catch (error) { } catch {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
@ -126,30 +117,30 @@ export class S3BucketLoggingEnabled implements BPSet {
) => { ) => {
const logDestinationBucket = requiredParametersForFix.find( const logDestinationBucket = requiredParametersForFix.find(
(param) => param.name === 'log-destination-bucket' (param) => param.name === 'log-destination-bucket'
)?.value; )?.value
if (!logDestinationBucket) { if (!logDestinationBucket) {
throw new Error("Required parameter 'log-destination-bucket' is missing."); throw new Error("Required parameter 'log-destination-bucket' is missing.")
} }
for (const bucketArn of nonCompliantResources) { for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!; const bucketName = bucketArn.split(':::')[1]!
await this.client.send( await this.client.send(
new PutBucketLoggingCommand({ new PutBucketLoggingCommand({
Bucket: bucketName, Bucket: bucketName,
BucketLoggingStatus: { BucketLoggingStatus: {
LoggingEnabled: { LoggingEnabled: {
TargetBucket: logDestinationBucket, TargetBucket: logDestinationBucket,
TargetPrefix: `${bucketName}/logs/`, TargetPrefix: `${bucketName}/logs/`
}, }
}, }
}) })
); )
} }
}; }
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

View File

@ -1,22 +1,17 @@
import { import { S3Client, ListBucketsCommand, GetBucketPolicyCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'
S3Client, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
ListBucketsCommand, import { Memorizer } from '../../Memorizer'
GetBucketPolicyCommand,
PutBucketPolicyCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3BucketSSLRequestsOnly implements BPSet { export class S3BucketSSLRequestsOnly implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketSSLRequestsOnly', name: 'S3BucketSSLRequestsOnly',
@ -31,115 +26,105 @@ export class S3BucketSSLRequestsOnly implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetBucketPolicyCommand', name: 'GetBucketPolicyCommand',
reason: 'Retrieves the bucket policy to check for SSL conditions.', reason: 'Retrieves the bucket policy to check for SSL conditions.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutBucketPolicyCommand', name: 'PutBucketPolicyCommand',
reason: 'Updates the bucket policy to enforce SSL requests.', reason: 'Updates the bucket policy to enforce SSL requests.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure existing bucket policies will not conflict with the SSL-only policy.'
'Ensure existing bucket policies will not conflict with the SSL-only policy.', })
});
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
try { try {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetBucketPolicyCommand({ Bucket: bucket.Name! }))
new GetBucketPolicyCommand({ Bucket: bucket.Name! }) const policy = JSON.parse(response.Policy!)
);
const policy = JSON.parse(response.Policy!);
const hasSSLCondition = policy.Statement.some( const hasSSLCondition = policy.Statement.some(
(stmt: any) => (stmt: unknown) =>
stmt.Condition && stmt.Condition && stmt.Condition.Bool && stmt.Condition.Bool['aws:SecureTransport'] === 'false'
stmt.Condition.Bool && )
stmt.Condition.Bool['aws:SecureTransport'] === 'false'
);
if (hasSSLCondition) { if (hasSSLCondition) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} catch (error) { } catch (error) {
if ((error as any).name === 'NoSuchBucketPolicy') { if ((error as unknown).name === 'NoSuchBucketPolicy') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
throw error; throw error
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const bucketArn of nonCompliantResources) { for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!; const bucketName = bucketArn.split(':::')[1]!
let existingPolicy: any; let existingPolicy: unknown
try { try {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetBucketPolicyCommand({ Bucket: bucketName }))
new GetBucketPolicyCommand({ Bucket: bucketName }) existingPolicy = JSON.parse(response.Policy!)
);
existingPolicy = JSON.parse(response.Policy!);
} catch (error) { } catch (error) {
if ((error as any).name !== 'NoSuchBucketPolicy') { if ((error as unknown).name !== 'NoSuchBucketPolicy') {
throw error; throw error
} }
} }
@ -151,27 +136,27 @@ export class S3BucketSSLRequestsOnly implements BPSet {
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`], Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: { Condition: {
Bool: { Bool: {
'aws:SecureTransport': 'false', 'aws:SecureTransport': 'false'
}, }
}, }
}; }
let updatedPolicy; let updatedPolicy
if (existingPolicy) { if (existingPolicy) {
existingPolicy.Statement.push(sslPolicyStatement); existingPolicy.Statement.push(sslPolicyStatement)
updatedPolicy = JSON.stringify(existingPolicy); updatedPolicy = JSON.stringify(existingPolicy)
} else { } else {
updatedPolicy = this.createSSLOnlyPolicy(bucketName); updatedPolicy = this.createSSLOnlyPolicy(bucketName)
} }
await this.client.send( await this.client.send(
new PutBucketPolicyCommand({ new PutBucketPolicyCommand({
Bucket: bucketName, Bucket: bucketName,
Policy: updatedPolicy, Policy: updatedPolicy
}) })
); )
} }
}; }
private readonly createSSLOnlyPolicy = (bucketName: string): string => { private readonly createSSLOnlyPolicy = (bucketName: string): string => {
return JSON.stringify({ return JSON.stringify({
@ -185,16 +170,16 @@ export class S3BucketSSLRequestsOnly implements BPSet {
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`], Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: { Condition: {
Bool: { Bool: {
'aws:SecureTransport': 'false', 'aws:SecureTransport': 'false'
}, }
}, }
}, }
], ]
}); })
}; }
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

View File

@ -2,27 +2,28 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketVersioningCommand, GetBucketVersioningCommand,
PutBucketVersioningCommand, PutBucketVersioningCommand
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class S3BucketVersioningEnabled implements BPSet { export class S3BucketVersioningEnabled implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketVersioningEnabled', name: 'S3BucketVersioningEnabled',
description: 'Ensures that versioning is enabled on all S3 buckets.', description: 'Ensures that versioning is enabled on all S3 buckets.',
priority: 1, priority: 1,
priorityReason: 'Enabling versioning protects against accidental data loss and allows recovery of previous versions.', priorityReason:
'Enabling versioning protects against accidental data loss and allows recovery of previous versions.',
awsService: 'S3', awsService: 'S3',
awsServiceCategory: 'Buckets', awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Data Protection', bestPracticeCategory: 'Data Protection',
@ -31,101 +32,96 @@ export class S3BucketVersioningEnabled implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetBucketVersioningCommand', name: 'GetBucketVersioningCommand',
reason: 'Retrieve the current versioning status of the bucket.', reason: 'Retrieve the current versioning status of the bucket.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutBucketVersioningCommand', name: 'PutBucketVersioningCommand',
reason: 'Enable versioning on the bucket.', reason: 'Enable versioning on the bucket.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure that enabling versioning aligns with your data lifecycle and cost considerations.', adviseBeforeFixFunction: 'Ensure that enabling versioning aligns with your data lifecycle and cost considerations.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetBucketVersioningCommand({ Bucket: bucket.Name! }))
new GetBucketVersioningCommand({ Bucket: bucket.Name! })
);
if (response.Status === 'Enabled') { if (response.Status === 'Enabled') {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (nonCompliantResources: string[]) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const bucketArn of nonCompliantResources) { for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!; const bucketName = bucketArn.split(':::')[1]!
await this.client.send( await this.client.send(
new PutBucketVersioningCommand({ new PutBucketVersioningCommand({
Bucket: bucketName, Bucket: bucketName,
VersioningConfiguration: { VersioningConfiguration: {
Status: 'Enabled', Status: 'Enabled'
}, }
}) })
); )
} }
}; }
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

View File

@ -2,21 +2,21 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketEncryptionCommand, GetBucketEncryptionCommand,
PutBucketEncryptionCommand, PutBucketEncryptionCommand
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class S3DefaultEncryptionKMS implements BPSet { export class S3DefaultEncryptionKMS implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3DefaultEncryptionKMS', name: 'S3DefaultEncryptionKMS',
@ -31,117 +31,114 @@ export class S3DefaultEncryptionKMS implements BPSet {
name: 'kms-key-id', name: 'kms-key-id',
description: 'The KMS Key ID used for bucket encryption.', description: 'The KMS Key ID used for bucket encryption.',
default: '', default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ab-cdef-EXAMPLE12345', example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ab-cdef-EXAMPLE12345'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetBucketEncryptionCommand', name: 'GetBucketEncryptionCommand',
reason: 'Retrieve the encryption configuration for a bucket.', reason: 'Retrieve the encryption configuration for a bucket.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutBucketEncryptionCommand', name: 'PutBucketEncryptionCommand',
reason: 'Enable KMS encryption for the bucket.', reason: 'Enable KMS encryption for the bucket.'
}, }
], ],
adviseBeforeFixFunction: adviseBeforeFixFunction: 'Ensure the KMS key is properly configured with necessary permissions for S3 operations.'
'Ensure the KMS key is properly configured with necessary permissions for S3 operations.', })
});
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
try { try {
const response = await this.memoClient.send( const response = await this.memoClient.send(new GetBucketEncryptionCommand({ Bucket: bucket.Name! }))
new GetBucketEncryptionCommand({ Bucket: bucket.Name! }) const encryption = response.ServerSideEncryptionConfiguration!
);
const encryption = response.ServerSideEncryptionConfiguration!;
const isKmsEnabled = encryption.Rules?.some( const isKmsEnabled = encryption.Rules?.some(
(rule) => (rule) =>
rule.ApplyServerSideEncryptionByDefault && rule.ApplyServerSideEncryptionByDefault &&
rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm === 'aws:kms' rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm === 'aws:kms'
); )
if (isKmsEnabled) { if (isKmsEnabled) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} catch (error) { } catch (error) {
if ((error as any).name === 'ServerSideEncryptionConfigurationNotFoundError') { if ((error as unknown).name === 'ServerSideEncryptionConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else { } else {
throw error; throw error
} }
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value
if (!kmsKeyId) { if (!kmsKeyId) {
throw new Error("Required parameter 'kms-key-id' is missing."); throw new Error("Required parameter 'kms-key-id' is missing.")
} }
for (const bucketArn of nonCompliantResources) { for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!; const bucketName = bucketArn.split(':::')[1]!
await this.client.send( await this.client.send(
new PutBucketEncryptionCommand({ new PutBucketEncryptionCommand({
Bucket: bucketName, Bucket: bucketName,
@ -150,18 +147,18 @@ export class S3DefaultEncryptionKMS implements BPSet {
{ {
ApplyServerSideEncryptionByDefault: { ApplyServerSideEncryptionByDefault: {
SSEAlgorithm: 'aws:kms', SSEAlgorithm: 'aws:kms',
KMSMasterKeyID: kmsKeyId, KMSMasterKeyID: kmsKeyId
}, }
}, }
], ]
}, }
}) })
); )
} }
}; }
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

View File

@ -2,27 +2,28 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketNotificationConfigurationCommand, GetBucketNotificationConfigurationCommand,
PutBucketNotificationConfigurationCommand, PutBucketNotificationConfigurationCommand
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'; import { Memorizer } from '../../Memorizer'
export class S3EventNotificationsEnabled implements BPSet { export class S3EventNotificationsEnabled implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3EventNotificationsEnabled', name: 'S3EventNotificationsEnabled',
description: 'Ensures that S3 buckets have event notifications configured.', description: 'Ensures that S3 buckets have event notifications configured.',
priority: 2, priority: 2,
priorityReason: 'Event notifications facilitate automated responses to S3 events, enhancing automation and security.', priorityReason:
'Event notifications facilitate automated responses to S3 events, enhancing automation and security.',
awsService: 'S3', awsService: 'S3',
awsServiceCategory: 'Buckets', awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Monitoring & Automation', bestPracticeCategory: 'Monitoring & Automation',
@ -31,118 +32,108 @@ export class S3EventNotificationsEnabled implements BPSet {
name: 'lambda-function-arn', name: 'lambda-function-arn',
description: 'ARN of the Lambda function to invoke for bucket events.', description: 'ARN of the Lambda function to invoke for bucket events.',
default: '', default: '',
example: 'arn:aws:lambda:us-east-1:123456789012:function:example-function', example: 'arn:aws:lambda:us-east-1:123456789012:function:example-function'
}, },
{ {
name: 'event-type', name: 'event-type',
description: 'S3 event type to trigger the notification.', description: 'S3 event type to trigger the notification.',
default: 's3:ObjectCreated:*', default: 's3:ObjectCreated:*',
example: 's3:ObjectCreated:Put', example: 's3:ObjectCreated:Put'
}, }
], ],
isFixFunctionUsesDestructiveCommand: false, isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'GetBucketNotificationConfigurationCommand', name: 'GetBucketNotificationConfigurationCommand',
reason: 'Retrieve the current notification configuration for a bucket.', reason: 'Retrieve the current notification configuration for a bucket.'
}, }
], ],
commandUsedInFixFunction: [ commandUsedInFixFunction: [
{ {
name: 'PutBucketNotificationConfigurationCommand', name: 'PutBucketNotificationConfigurationCommand',
reason: 'Add or update event notifications for the bucket.', reason: 'Add or update event notifications for the bucket.'
}, }
], ],
adviseBeforeFixFunction: 'Ensure the Lambda function has necessary permissions to handle the S3 events.', adviseBeforeFixFunction: 'Ensure the Lambda function has necessary permissions to handle the S3 events.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
const response = await this.memoClient.send( const response = await this.memoClient.send(
new GetBucketNotificationConfigurationCommand({ Bucket: bucket.Name! }) new GetBucketNotificationConfigurationCommand({ Bucket: bucket.Name! })
); )
if ( if (response.LambdaFunctionConfigurations || response.QueueConfigurations || response.TopicConfigurations) {
response.LambdaFunctionConfigurations || compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
response.QueueConfigurations ||
response.TopicConfigurations
) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else { } else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix) await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly fixImpl = async ( private readonly fixImpl = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const lambdaArn = requiredParametersForFix.find( const lambdaArn = requiredParametersForFix.find((param) => param.name === 'lambda-function-arn')?.value
(param) => param.name === 'lambda-function-arn' const eventType = requiredParametersForFix.find((param) => param.name === 'event-type')?.value
)?.value;
const eventType = requiredParametersForFix.find(
(param) => param.name === 'event-type'
)?.value;
if (!lambdaArn || !eventType) { if (!lambdaArn || !eventType) {
throw new Error( throw new Error("Required parameters 'lambda-function-arn' and/or 'event-type' are missing.")
"Required parameters 'lambda-function-arn' and/or 'event-type' are missing."
);
} }
for (const bucketArn of nonCompliantResources) { for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!; const bucketName = bucketArn.split(':::')[1]!
await this.client.send( await this.client.send(
new PutBucketNotificationConfigurationCommand({ new PutBucketNotificationConfigurationCommand({
Bucket: bucketName, Bucket: bucketName,
@ -150,17 +141,17 @@ export class S3EventNotificationsEnabled implements BPSet {
LambdaFunctionConfigurations: [ LambdaFunctionConfigurations: [
{ {
LambdaFunctionArn: lambdaArn, LambdaFunctionArn: lambdaArn,
Events: [eventType as any], Events: [eventType as unknown]
}, }
], ]
}, }
}) })
); )
} }
}; }
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

View File

@ -1,22 +1,19 @@
import { import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3'
S3Client, import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup'
ListBucketsCommand, import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
} from '@aws-sdk/client-s3'; import { Memorizer } from '../../Memorizer'
import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3LastBackupRecoveryPointCreated implements BPSet { export class S3LastBackupRecoveryPointCreated implements BPSet {
private readonly client = new S3Client({}); private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client); private readonly memoClient = Memorizer.memo(this.client)
private readonly backupClient = Memorizer.memo(new BackupClient({})); private readonly backupClient = Memorizer.memo(new BackupClient({}))
private readonly stats: BPSetStats = { private readonly stats: BPSetStats = {
compliantResources: [], compliantResources: [],
nonCompliantResources: [], nonCompliantResources: [],
status: 'LOADED', status: 'LOADED',
errorMessage: [], errorMessage: []
}; }
public readonly getMetadata = (): BPSetMetadata => ({ public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3LastBackupRecoveryPointCreated', name: 'S3LastBackupRecoveryPointCreated',
@ -31,77 +28,72 @@ export class S3LastBackupRecoveryPointCreated implements BPSet {
commandUsedInCheckFunction: [ commandUsedInCheckFunction: [
{ {
name: 'ListRecoveryPointsByResourceCommand', name: 'ListRecoveryPointsByResourceCommand',
reason: 'Checks for recent recovery points for the S3 bucket.', reason: 'Checks for recent recovery points for the S3 bucket.'
}, }
], ],
commandUsedInFixFunction: [], commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Ensure the backup plan for S3 buckets is appropriately configured before proceeding.', adviseBeforeFixFunction: 'Ensure the backup plan for S3 buckets is appropriately configured before proceeding.'
}); })
public readonly getStats = () => this.stats; public readonly getStats = () => this.stats
public readonly clearStats = () => { public readonly clearStats = () => {
this.stats.compliantResources = []; this.stats.compliantResources = []
this.stats.nonCompliantResources = []; this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'; this.stats.status = 'LOADED'
this.stats.errorMessage = []; this.stats.errorMessage = []
}; }
public readonly check = async () => { public readonly check = async () => {
this.stats.status = 'CHECKING'; this.stats.status = 'CHECKING'
await this.checkImpl() await this.checkImpl()
.then(() => { .then(() => {
this.stats.status = 'FINISHED'; this.stats.status = 'FINISHED'
}) })
.catch((err) => { .catch((err) => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: err.message, message: err.message
}); })
}); })
}; }
private readonly checkImpl = async () => { private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = []
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = []
const buckets = await this.getBuckets(); const buckets = await this.getBuckets()
for (const bucket of buckets) { for (const bucket of buckets) {
const recoveryPoints = await this.backupClient.send( const recoveryPoints = await this.backupClient.send(
new ListRecoveryPointsByResourceCommand({ new ListRecoveryPointsByResourceCommand({
ResourceArn: `arn:aws:s3:::${bucket.Name!}`, ResourceArn: `arn:aws:s3:::${bucket.Name!}`
}) })
); )
if ( if (recoveryPoints.RecoveryPoints && recoveryPoints.RecoveryPoints.length > 0) {
recoveryPoints.RecoveryPoints && compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
recoveryPoints.RecoveryPoints.length > 0
) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else { } else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} }
} }
this.stats.compliantResources = compliantResources; this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources; this.stats.nonCompliantResources = nonCompliantResources
}; }
public readonly fix = async () => { public readonly fix = async () => {
this.stats.status = 'ERROR'; this.stats.status = 'ERROR'
this.stats.errorMessage.push({ this.stats.errorMessage.push({
date: new Date(), date: new Date(),
message: 'Fixing recovery points requires custom implementation for backup setup.', message: 'Fixing recovery points requires custom implementation for backup setup.'
}); })
throw new Error( throw new Error('Fixing recovery points requires custom implementation for backup setup.')
'Fixing recovery points requires custom implementation for backup setup.' }
);
};
private readonly getBuckets = async () => { private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({})); const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []; return response.Buckets || []
}; }
} }

Some files were not shown because too many files have changed in this diff Show More