feat: move external metadata to embeded
All checks were successful
/ deploy_site (push) Successful in 2m24s

This commit is contained in:
2025-01-02 20:11:14 +09:00
parent 6eb667a539
commit 2b0b862345
118 changed files with 11743 additions and 6820 deletions

File diff suppressed because it is too large Load Diff

View File

@ -56,10 +56,7 @@
}, },
"pkg": { "pkg": {
"scripts": "build/**/*", "scripts": "build/**/*",
"assets": [ "assets": "views/**/*",
"views/**/*",
"bpset_metadata.json"
],
"targets": [ "targets": [
"node22-linuxstatic-x86_64", "node22-linuxstatic-x86_64",
"node22-linuxstatic-arm64" "node22-linuxstatic-arm64"

View File

@ -1,5 +1,5 @@
import { BPSet, BPSetMetadata } from "./types"; import { BPSet } from "./types";
import { readdir, readFile } 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 {
@ -13,12 +13,8 @@ export class BPManager {
private readonly bpSets: private readonly bpSets:
Record<string, BPSet> = {} Record<string, BPSet> = {}
private readonly bpSetMetadatas:
Record<string, BPSetMetadata> = {}
private constructor() { private constructor() {
this.loadBPSets() this.loadBPSets()
this.loadBPSetMetadatas()
} }
private async loadBPSets() { private async loadBPSets() {
@ -39,72 +35,20 @@ export class BPManager {
} }
} }
private async loadBPSetMetadatas() {
const bpSetMetadatasRaw = await readFile(path.join(__dirname, '../bpset_metadata.json'))
const bpSetMetadatas = JSON.parse(bpSetMetadatasRaw.toString('utf-8')) as BPSetMetadata[]
for (const [idx, bpSetMetadata] of bpSetMetadatas.entries()) {
this.bpSetMetadatas[bpSetMetadata.name] = {
...bpSetMetadata,
nonCompliantResources: [],
compliantResources: [],
status:'LOADED',
errorMessage: [],
idx
}
}
}
public runCheckOnce(name: string) { public runCheckOnce(name: string) {
return this return this.bpSets[name].check()
.bpSets[name].check()
.catch((err) => {
this.bpSetMetadatas[name].status = 'ERROR'
this.bpSetMetadatas[name].errorMessage.push({
date: new Date(),
message: err
})
return undefined
})
.then((result) => {
if (result === undefined)
return
this.bpSetMetadatas[name].compliantResources = result.compliantResources
this.bpSetMetadatas[name].nonCompliantResources = result.nonCompliantResources
this.bpSetMetadatas[name].status = 'FINISHED'
})
} }
public runCheckAll(finished = (name: string) => {}) { public runCheckAll(finished = (name: string) => {}) {
const checkJobs = const checkJobs: Promise<void>[] = []
Object
.values(this.bpSetMetadatas)
.map(({ name }) => {
this.bpSetMetadatas[name].status = 'CHECKING'
return this for (const bpset of Object.values(this.bpSets))
.bpSets[name].check() checkJobs.push(
.catch((err) => { bpset
this.bpSetMetadatas[name].status = 'ERROR' .check()
this.bpSetMetadatas[name].errorMessage.push({ .then(() =>
date: new Date(), finished(bpset.getMetadata().name))
message: err )
})
return undefined
})
.then((result) => {
if (result === undefined)
return
this.bpSetMetadatas[name].compliantResources = result.compliantResources
this.bpSetMetadatas[name].nonCompliantResources = result.nonCompliantResources
this.bpSetMetadatas[name].status = 'FINISHED'
finished(name)
})
})
return Promise.all(checkJobs) return Promise.all(checkJobs)
} }
@ -113,20 +57,11 @@ export class BPManager {
return this return this
.bpSets[name] .bpSets[name]
.fix( .fix(
this.bpSetMetadatas[name].nonCompliantResources, this.bpSets[name].getStats().nonCompliantResources,
requiredParametersForFix requiredParametersForFix
) )
} }
public readonly getBPSet = (name: string) =>
this.bpSets[name]
public readonly getBPSetMetadata = (name: string) =>
this.bpSetMetadatas[name]
public readonly getBPSets = () => public readonly getBPSets = () =>
Object.values(this.bpSets) Object.values(this.bpSets)
public readonly getBPSetMetadatas = () =>
Object.values(this.bpSetMetadatas)
} }

View File

@ -1,6 +1,6 @@
import express, { Request, Response } from 'express' import express, { Request, Response } from 'express'
import { BPManager } from './BPManager' import { BPManager } from './BPManager'
import { BPSetMetadata } from './types' import { BPSet, BPSetMetadata, BPSetStats } from './types'
import { Memorizer } from './Memorizer' import { Memorizer } from './Memorizer'
import path from 'path' import path from 'path'
@ -28,21 +28,24 @@ export class WebServer {
} }
private getMainPage(req: Request, res: Response) { private getMainPage(req: Request, res: Response) {
const hidePass = req.query['hidePass'] === 'true'
const bpStatus: { const bpStatus: {
category: string, category: string,
metadatas: BPSetMetadata[] metadatas: (BPSetMetadata&BPSetStats)[]
}[] = [] }[] = []
const bpMetadatas = this.bpManager.getBPSetMetadatas() const bpMetadatas = this.bpManager.getBPSets().map((v, idx) => ({ ...v, idx }))
const categories = new Set(bpMetadatas.map((v) => v?.awsService)) const categories = new Set(bpMetadatas.map((v) => v.getMetadata().awsService))
const hidePass = req.query['hidePass'] === 'true'
for (const category of categories) for (const category of categories)
bpStatus.push({ bpStatus.push({
category, category,
metadatas: bpMetadatas.filter((v) => metadatas:
v.awsService === category && bpMetadatas
(!hidePass || v.nonCompliantResources.length > 0)) .filter((v) =>
v.getMetadata().awsService === category &&
(!hidePass || v.getStats().nonCompliantResources.length > 0))
.map((v) => ({ ...v.getMetadata(), ...v.getStats(), idx: v.idx }))
}) })
res.render('index', { res.render('index', {

View File

@ -2,71 +2,129 @@ 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 } 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 ( private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
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 check = async (): Promise<{ public readonly getMetadata = () => ({
compliantResources: string[] name: 'ALBHttpDropInvalidHeaderEnabled',
nonCompliantResources: string[] description: 'Ensures that ALBs have invalid HTTP headers dropped.',
requiredParametersForFix: { name: string }[] priority: 1,
}> => { priorityReason: 'Dropping invalid headers enhances security and avoids unexpected behavior.',
const compliantResources: string[] = [] awsService: 'Elastic Load Balancing',
const nonCompliantResources: string[] = [] awsServiceCategory: 'Application Load Balancer',
const loadBalancers = await this.getLoadBalancers() bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if invalid headers are dropped for ALBs.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable the invalid header drop attribute on ALBs.',
},
],
adviseBeforeFixFunction: 'Ensure enabling this setting aligns with your application requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 === 'routing.http.drop_invalid_header_fields.enabled' && attr.Value === 'true' (attr) =>
) 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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
): Promise<void> => { (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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

@ -1,6 +1,6 @@
import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand } from '@aws-sdk/client-elastic-load-balancing-v2' import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand } from '@aws-sdk/client-elastic-load-balancing-v2'
import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2' import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2'
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer' import { Memorizer } from '../../Memorizer'
export class ALBWAFEnabled implements BPSet { export class ALBWAFEnabled implements BPSet {
@ -13,11 +13,73 @@ export class ALBWAFEnabled implements BPSet {
return response.LoadBalancers || [] return response.LoadBalancers || []
} }
public readonly check = async (): Promise<{ public readonly getMetadata = () => (
compliantResources: string[] {
nonCompliantResources: string[] name: 'ALBWAFEnabled',
requiredParametersForFix: { name: string }[] description: 'Ensures that WAF is associated with ALBs.',
}> => { priority: 1,
priorityReason: 'Associating WAF with ALBs protects against common web attacks.',
awsService: 'Elastic Load Balancing',
awsServiceCategory: 'Application Load Balancer',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'web-acl-arn',
description: 'The ARN of the WAF ACL to associate with the ALB.',
default: '',
example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example'
}
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetWebAclForResourceCommand',
reason: 'Check if a WAF is associated with the ALB.'
}
],
commandUsedInFixFunction: [
{
name: 'AssociateWebAclCommand',
reason: 'Associate a WAF ACL with the ALB.'
}
],
adviseBeforeFixFunction: 'Ensure the WAF ACL has the appropriate rules for the application\'s requirements.'
})
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: []
}
public readonly getStats = () =>
this.stats
public readonly clearStats = () => {
this.stats.compliantResources = []
this.stats.nonCompliantResources = []
this.stats.status = 'LOADED'
this.stats.errorMessage = []
}
public readonly check = async () => {
this.stats.status = 'CHECKING'
await this.checkImpl()
.then(
() => this.stats.status = 'FINISHED',
(err) => {
this.stats.status = 'ERROR'
this.stats.errorMessage.push({
date: new Date(),
message: err
}
)
})
}
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()
@ -33,17 +95,27 @@ export class ALBWAFEnabled implements BPSet {
} }
} }
return { this.stats.compliantResources = compliantResources
compliantResources, this.stats.nonCompliantResources = nonCompliantResources
nonCompliantResources, this.stats.status = 'FINISHED'
requiredParametersForFix: [{ name: 'web-acl-arn' }]
}
} }
public readonly fix = async (
nonCompliantResources: string[], public readonly fix: BPSetFixFn = async (...args) => {
requiredParametersForFix: { name: string; value: string }[] await this.fixImpl(...args)
): Promise<void> => { .then(
() => this.stats.status = 'FINISHED',
(err) => {
this.stats.status = 'ERROR'
this.stats.errorMessage.push({
date: new Date(),
message: err
}
)
})
}
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) {

View File

@ -2,63 +2,127 @@ 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 } 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 = () => ({
name: 'ELBCrossZoneLoadBalancingEnabled',
description: 'Ensures that cross-zone load balancing is enabled for Elastic Load Balancers.',
priority: 2,
priorityReason: 'Cross-zone load balancing helps evenly distribute traffic, improving resilience.',
awsService: 'Elastic Load Balancing',
awsServiceCategory: 'Classic Load Balancer',
bestPracticeCategory: 'Performance',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if cross-zone load balancing is enabled for ELBs.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable cross-zone load balancing for ELBs.',
},
],
adviseBeforeFixFunction: 'Ensure this setting aligns with your traffic distribution requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const loadBalancers = await this.getLoadBalancers() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 === 'load_balancing.cross_zone.enabled' && attr.Value === 'true' (attr) =>
) 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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,63 +2,127 @@ 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 } 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 = () => ({
name: 'ELBDeletionProtectionEnabled',
description: 'Ensures that deletion protection is enabled for Elastic Load Balancers.',
priority: 1,
priorityReason: 'Deletion protection prevents accidental deletion of critical resources.',
awsService: 'Elastic Load Balancing',
awsServiceCategory: 'Classic Load Balancer',
bestPracticeCategory: 'Resilience',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if deletion protection is enabled for ELBs.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable deletion protection for ELBs.',
},
],
adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with resource management policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const loadBalancers = await this.getLoadBalancers() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,61 +2,127 @@ 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 } 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 = () => ({
name: 'ELBLoggingEnabled',
description: 'Ensures that access logging is enabled for Elastic Load Balancers.',
priority: 1,
priorityReason: 'Access logging provides critical data for troubleshooting and compliance.',
awsService: 'Elastic Load Balancing',
awsServiceCategory: 'Classic Load Balancer',
bestPracticeCategory: 'Logging and Monitoring',
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/' },
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeLoadBalancerAttributesCommand',
reason: 'Verify if access logging is enabled for ELBs.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyLoadBalancerAttributesCommand',
reason: 'Enable access logging for ELBs and set S3 bucket and prefix.',
},
],
adviseBeforeFixFunction: 'Ensure the specified S3 bucket and prefix exist and are accessible.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const loadBalancers = await this.getLoadBalancers() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 's3-bucket-name' }, { name: 's3-prefix' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const bucketName = requiredParametersForFix.find(param => param.name === 's3-bucket-name')?.value this.stats.status = 'ERROR';
const bucketPrefix = requiredParametersForFix.find(param => param.name === 's3-prefix')?.value this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const bucketName = requiredParametersForFix.find((param) => param.name === 's3-bucket-name')?.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) {
@ -66,10 +132,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,70 +1,146 @@
import { import {
ApiGatewayV2Client, ApiGatewayV2Client,
GetApisCommand, GetApisCommand,
GetStagesCommand GetStagesCommand,
} from '@aws-sdk/client-apigatewayv2' } from '@aws-sdk/client-apigatewayv2';
import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2' import {
import { BPSet } from '../../types' WAFV2Client,
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'APIGatewayAssociatedWithWAF',
description: 'Ensures that API Gateway stages are associated with WAF.',
priority: 2,
priorityReason: 'Associating WAF with API Gateway stages enhances security by protecting against web attacks.',
awsService: 'API Gateway',
awsServiceCategory: 'API Management',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'web-acl-arn',
description: 'The ARN of the WAF ACL to associate with the API Gateway stage.',
default: '',
example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetWebACLForResourceCommand',
reason: 'Verify if a WAF is associated with the API Gateway stage.',
},
],
commandUsedInFixFunction: [
{
name: 'AssociateWebACLCommand',
reason: 'Associate a WAF ACL with the API Gateway stage.',
},
],
adviseBeforeFixFunction: 'Ensure the WAF ACL has the appropriate rules for the application\'s requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const apis = await this.getHttpApis() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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(new GetWebACLForResourceCommand({ ResourceArn: stageArn })) const response = await this.wafClient.send(
new GetWebACLForResourceCommand({ ResourceArn: stageArn })
);
if (response.WebACL) { if (response.WebACL) {
compliantResources.push(stageArn) compliantResources.push(stageArn);
} else { } else {
nonCompliantResources.push(stageArn) nonCompliantResources.push(stageArn);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'web-acl-arn' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
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

@ -2,63 +2,135 @@ import {
ApiGatewayV2Client, ApiGatewayV2Client,
GetApisCommand, GetApisCommand,
GetStagesCommand, GetStagesCommand,
UpdateStageCommand UpdateStageCommand,
} from '@aws-sdk/client-apigatewayv2' } from '@aws-sdk/client-apigatewayv2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'APIGatewayExecutionLoggingEnabled',
description: 'Ensures that execution logging is enabled for API Gateway stages.',
priority: 3,
priorityReason: 'Execution logging is critical for monitoring and troubleshooting API Gateway usage.',
awsService: 'API Gateway',
awsServiceCategory: 'API Management',
bestPracticeCategory: 'Logging and Monitoring',
requiredParametersForFix: [
{
name: 'log-destination-arn',
description: 'The ARN of the CloudWatch log group for storing API Gateway logs.',
default: '',
example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetStagesCommand',
reason: 'Verify if execution logging is enabled for API Gateway stages.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateStageCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const apis = await this.getHttpApis() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'log-destination-arn' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const logDestinationArn = requiredParametersForFix.find(
(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({
@ -66,10 +138,10 @@ export class APIGatewayExecutionLoggingEnabled implements BPSet {
StageName: stageName, StageName: stageName,
AccessLogSettings: { AccessLogSettings: {
DestinationArn: logDestinationArn, DestinationArn: logDestinationArn,
Format: '$context.requestId' Format: '$context.requestId',
} },
}) })
) );
}
} }
};
} }

View File

@ -2,64 +2,136 @@ import {
ApiGatewayV2Client, ApiGatewayV2Client,
GetApisCommand, GetApisCommand,
GetStagesCommand, GetStagesCommand,
UpdateStageCommand UpdateStageCommand,
} from '@aws-sdk/client-apigatewayv2' } from '@aws-sdk/client-apigatewayv2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'APIGatewayV2AccessLogsEnabled',
description: 'Ensures that access logging is enabled for API Gateway v2 stages.',
priority: 3,
priorityReason: 'Access logging provides critical data for monitoring and troubleshooting API Gateway usage.',
awsService: 'API Gateway',
awsServiceCategory: 'API Management',
bestPracticeCategory: 'Logging and Monitoring',
requiredParametersForFix: [
{
name: 'log-destination-arn',
description: 'The ARN of the CloudWatch log group for storing API Gateway logs.',
default: '',
example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetStagesCommand',
reason: 'Verify if access logging is enabled for API Gateway stages.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateStageCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const apis = await this.getHttpApis() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'log-destination-arn' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const logDestinationArn = requiredParametersForFix.find(
(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({
@ -67,10 +139,10 @@ export class APIGatewayV2AccessLogsEnabled implements BPSet {
StageName: stageName, StageName: stageName,
AccessLogSettings: { AccessLogSettings: {
DestinationArn: logDestinationArn, DestinationArn: logDestinationArn,
Format: '$context.requestId' Format: '$context.requestId',
} },
}) })
) );
}
} }
};
} }

View File

@ -2,77 +2,149 @@ import {
ApiGatewayV2Client, ApiGatewayV2Client,
GetApisCommand, GetApisCommand,
GetRoutesCommand, GetRoutesCommand,
UpdateRouteCommand UpdateRouteCommand,
} from '@aws-sdk/client-apigatewayv2' } from '@aws-sdk/client-apigatewayv2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'APIGatewayV2AuthorizationTypeConfigured',
description: 'Ensures that authorization type is configured for API Gateway v2 routes.',
priority: 2,
priorityReason: 'Configuring authorization ensures API security by restricting unauthorized access.',
awsService: 'API Gateway',
awsServiceCategory: 'API Management',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'authorization-type',
description: 'The authorization type to configure for the routes.',
default: '',
example: 'JWT',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetRoutesCommand',
reason: 'Verify if authorization type is configured for API Gateway routes.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateRouteCommand',
reason: 'Set the authorization type for API Gateway routes.',
},
],
adviseBeforeFixFunction: 'Ensure the chosen authorization type aligns with application requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const apis = await this.getHttpApis() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'authorization-type' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const authorizationType = requiredParametersForFix.find(param => param.name === 'authorization-type')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const authorizationType = requiredParametersForFix.find(
(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!, // Use RouteId instead of RouteKey RouteId: route.RouteId!,
AuthorizationType: authorizationType as any AuthorizationType: authorizationType as any,
}) })
) );
}
} }
};
} }

View File

@ -1,48 +1,118 @@
import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling' import {
import { BPSet } from '../../types' AutoScalingClient,
import { Memorizer } from '../../Memorizer' DescribeAutoScalingGroupsCommand,
UpdateAutoScalingGroupCommand,
} from '@aws-sdk/client-auto-scaling';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
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 = () => ({
name: 'AutoScalingGroupELBHealthCheckRequired',
description: 'Ensures that Auto Scaling groups with ELB or Target Groups use ELB health checks.',
priority: 2,
priorityReason: 'ELB health checks ensure accurate instance health monitoring in Auto Scaling groups.',
awsService: 'Auto Scaling',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Resilience',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeAutoScalingGroupsCommand',
reason: 'Retrieve Auto Scaling groups to check health check type.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateAutoScalingGroupCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const asgs = await this.getAutoScalingGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const asgs = await this.getAutoScalingGroups();
for (const asg of asgs) { for (const asg of asgs) {
if ( if (
(asg.LoadBalancerNames?.length || asg.TargetGroupARNs?.length) && (asg.LoadBalancerNames?.length || asg.TargetGroupARNs?.length) &&
asg.HealthCheckType !== 'ELB' asg.HealthCheckType !== 'ELB'
) { ) {
nonCompliantResources.push(asg.AutoScalingGroupARN!) nonCompliantResources.push(asg.AutoScalingGroupARN!);
} else { } else {
compliantResources.push(asg.AutoScalingGroupARN!) compliantResources.push(asg.AutoScalingGroupARN!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,58 +1,130 @@
import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling' import {
import { BPSet } from '../../types' AutoScalingClient,
import { Memorizer } from '../../Memorizer' DescribeAutoScalingGroupsCommand,
UpdateAutoScalingGroupCommand,
} from '@aws-sdk/client-auto-scaling';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
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 = () => ({
name: 'AutoScalingLaunchTemplate',
description: 'Ensures that Auto Scaling groups use a launch template instead of a launch configuration.',
priority: 3,
priorityReason: 'Launch templates provide enhanced capabilities and flexibility compared to launch configurations.',
awsService: 'Auto Scaling',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Management',
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' },
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeAutoScalingGroupsCommand',
reason: 'Retrieve Auto Scaling groups to check for launch template usage.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateAutoScalingGroupCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const asgs = await this.getAutoScalingGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'launch-template-id' }, { name: 'version' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const launchTemplateId = requiredParametersForFix.find(param => param.name === 'launch-template-id')?.value this.stats.status = 'ERROR';
const version = requiredParametersForFix.find(param => param.name === 'version')?.value this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const launchTemplateId = requiredParametersForFix.find(
(param) => param.name === 'launch-template-id'
)?.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,54 +1,130 @@
import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling' import {
import { BPSet } from '../../types' AutoScalingClient,
import { Memorizer } from '../../Memorizer' DescribeAutoScalingGroupsCommand,
UpdateAutoScalingGroupCommand,
} from '@aws-sdk/client-auto-scaling';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
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 = () => ({
name: 'AutoScalingMultipleAZ',
description: 'Ensures that Auto Scaling groups are configured to use multiple Availability Zones.',
priority: 2,
priorityReason: 'Using multiple AZs improves fault tolerance and availability for Auto Scaling groups.',
awsService: 'Auto Scaling',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Resilience',
requiredParametersForFix: [
{
name: 'availability-zones',
description: 'Comma-separated list of Availability Zones to assign to the Auto Scaling group.',
default: '',
example: 'us-east-1a,us-east-1b',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeAutoScalingGroupsCommand',
reason: 'Retrieve Auto Scaling groups to verify their Availability Zone configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateAutoScalingGroupCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const asgs = await this.getAutoScalingGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const asgs = await this.getAutoScalingGroups();
for (const asg of asgs) { for (const asg of asgs) {
if (asg.AvailabilityZones?.length! > 1) { if (asg.AvailabilityZones?.length! > 1) {
compliantResources.push(asg.AutoScalingGroupARN!) compliantResources.push(asg.AutoScalingGroupARN!);
} else { } else {
nonCompliantResources.push(asg.AutoScalingGroupARN!) nonCompliantResources.push(asg.AutoScalingGroupARN!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'availability-zones' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const availabilityZones = requiredParametersForFix.find(param => param.name === 'availability-zones')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const availabilityZones = requiredParametersForFix.find(
(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,66 +2,140 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront' } from '@aws-sdk/client-cloudfront';
import { BPSet } 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 = () => ({
name: 'CloudFrontAccessLogsEnabled',
description: 'Ensures that access logging is enabled for CloudFront distributions.',
priority: 1,
priorityReason: 'Access logs are critical for monitoring and troubleshooting CloudFront distributions.',
awsService: 'CloudFront',
awsServiceCategory: 'CDN',
bestPracticeCategory: 'Logging and Monitoring',
requiredParametersForFix: [
{
name: 'log-bucket-name',
description: 'The S3 bucket name for storing access logs.',
default: '',
example: 'my-cloudfront-logs-bucket.s3.amazonaws.com',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions.',
},
{
name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to check logging settings.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateDistributionCommand',
reason: 'Enable logging and update distribution settings.',
},
],
adviseBeforeFixFunction: 'Ensure the specified S3 bucket exists and has proper permissions for CloudFront logging.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const distributions = await this.getDistributions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 ( if (details.DistributionConfig?.Logging?.Enabled) {
details.DistributionConfig?.Logging?.Enabled compliantResources.push(details.ARN!);
) {
compliantResources.push(details.ARN!)
} else { } else {
nonCompliantResources.push(details.ARN!) nonCompliantResources.push(details.ARN!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'log-bucket-name' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const logBucketName = requiredParametersForFix.find(param => param.name === 'log-bucket-name')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const logBucketName = requiredParametersForFix.find(
(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,
@ -69,17 +143,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 // Include all required properties DistributionConfig: updatedConfig as any,
}) })
) );
}
} }
};
} }

View File

@ -2,76 +2,148 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront' } from '@aws-sdk/client-cloudfront';
import { BPSet } 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 = () => ({
name: 'CloudFrontAssociatedWithWAF',
description: 'Ensures that CloudFront distributions are associated with a WAF.',
priority: 1,
priorityReason: 'Associating WAF with CloudFront distributions enhances security against web attacks.',
awsService: 'CloudFront',
awsServiceCategory: 'CDN',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'web-acl-id',
description: 'The ID of the Web ACL to associate with the CloudFront distribution.',
default: '',
example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for WAF association.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateDistributionCommand',
reason: 'Associate the specified WAF with the CloudFront distribution.',
},
],
adviseBeforeFixFunction: 'Ensure the Web ACL is configured correctly for the applications security requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const distributions = await this.getDistributions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'web-acl-id' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const webAclId = requiredParametersForFix.find(param => param.name === 'web-acl-id')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const webAclId = requiredParametersForFix.find(
(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 // Include all required properties DistributionConfig: updatedConfig as any,
}) })
) );
}
} }
};
} }

View File

@ -2,77 +2,153 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront' } from '@aws-sdk/client-cloudfront';
import { BPSet } 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 = () => ({
name: 'CloudFrontDefaultRootObjectConfigured',
description: 'Ensures that CloudFront distributions have a default root object configured.',
priority: 3,
priorityReason: 'A default root object ensures users access the correct content when navigating to the distribution domain.',
awsService: 'CloudFront',
awsServiceCategory: 'CDN',
bestPracticeCategory: 'Configuration',
requiredParametersForFix: [
{
name: 'default-root-object',
description: 'The default root object for the CloudFront distribution.',
default: '',
example: 'index.html',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for a default root object.',
},
{
name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to verify the default root object setting.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateDistributionCommand',
reason: 'Set the default root object for the CloudFront distribution.',
},
],
adviseBeforeFixFunction: 'Ensure the default root object exists in the origin to avoid 404 errors.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const distributions = await this.getDistributions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'default-root-object' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const defaultRootObject = requiredParametersForFix.find(param => param.name === 'default-root-object')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const defaultRootObject = requiredParametersForFix.find(
(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 any,
}) })
) );
}
} }
};
} }

View File

@ -2,66 +2,136 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront' } from '@aws-sdk/client-cloudfront';
import { BPSet } 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 = () => ({
name: 'CloudFrontNoDeprecatedSSLProtocols',
description: 'Ensures that CloudFront distributions do not use deprecated SSL protocols like SSLv3.',
priority: 2,
priorityReason: 'Deprecated SSL protocols pose significant security risks and should be avoided.',
awsService: 'CloudFront',
awsServiceCategory: 'CDN',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for deprecated SSL protocols.',
},
{
name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to identify deprecated SSL protocols in origin configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateDistributionCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const distributions = await this.getDistributions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 &&
origin.CustomOriginConfig.OriginSslProtocols?.Items?.includes('SSLv3') 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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,
Origins: { Origins: {
Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => { Items: distribution.DistributionConfig?.Origins?.Items?.map((origin) => {
if (origin.CustomOriginConfig) { if (origin.CustomOriginConfig) {
return { return {
...origin, ...origin,
@ -70,24 +140,24 @@ export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
OriginSslProtocols: { OriginSslProtocols: {
...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 any,
}) })
) );
}
} }
};
} }

View File

@ -2,95 +2,169 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront' } from '@aws-sdk/client-cloudfront';
import { BPSet } 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 = () => ({
name: 'CloudFrontS3OriginAccessControlEnabled',
description: 'Ensures that CloudFront distributions with S3 origins have Origin Access Control (OAC) enabled.',
priority: 3,
priorityReason: 'Using Origin Access Control enhances security by ensuring only CloudFront can access the S3 origin.',
awsService: 'CloudFront',
awsServiceCategory: 'CDN',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'origin-access-control-id',
description: 'The ID of the Origin Access Control to associate with the S3 origin.',
default: '',
example: 'oac-0abcd1234efgh5678',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check for S3 origins.',
},
{
name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to verify Origin Access Control configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateDistributionCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const distributions = await this.getDistributions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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.S3OriginConfig &&
(!origin.OriginAccessControlId || origin.OriginAccessControlId === '') (!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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'origin-access-control-id' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,
Origins: { Origins: {
Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => { Items: distribution.DistributionConfig?.Origins?.Items?.map((origin) => {
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 any,
}) })
) );
}
} }
};
} }

View File

@ -2,83 +2,153 @@ import {
CloudFrontClient, CloudFrontClient,
ListDistributionsCommand, ListDistributionsCommand,
GetDistributionCommand, GetDistributionCommand,
UpdateDistributionCommand UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront' } from '@aws-sdk/client-cloudfront';
import { BPSet } 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 = () => ({
name: 'CloudFrontViewerPolicyHTTPS',
description: 'Ensures that CloudFront distributions enforce HTTPS for viewer requests.',
priority: 1,
priorityReason: 'Enforcing HTTPS improves security by ensuring secure communication between viewers and CloudFront.',
awsService: 'CloudFront',
awsServiceCategory: 'CDN',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListDistributionsCommand',
reason: 'List all CloudFront distributions to check viewer protocol policies.',
},
{
name: 'GetDistributionCommand',
reason: 'Retrieve distribution details to verify viewer protocol policy settings.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateDistributionCommand',
reason: 'Update the viewer protocol policy to enforce HTTPS.',
},
],
adviseBeforeFixFunction: 'Ensure all origins and endpoints support HTTPS to prevent connectivity issues.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const distributions = await this.getDistributions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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 any,
}) })
) );
}
} }
};
} }

View File

@ -1,60 +1,130 @@
import { import {
CloudWatchLogsClient, CloudWatchLogsClient,
DescribeLogGroupsCommand, DescribeLogGroupsCommand,
PutRetentionPolicyCommand PutRetentionPolicyCommand,
} from '@aws-sdk/client-cloudwatch-logs' } from '@aws-sdk/client-cloudwatch-logs';
import { BPSet } 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 = () => ({
name: 'CWLogGroupRetentionPeriodCheck',
description: 'Ensures all CloudWatch log groups have a retention period set.',
priority: 3,
priorityReason: 'Setting a retention period for log groups helps manage storage costs and compliance.',
awsService: 'CloudWatch Logs',
awsServiceCategory: 'Monitoring',
bestPracticeCategory: 'Configuration',
requiredParametersForFix: [
{
name: 'retention-period-days',
description: 'Retention period in days to apply to log groups.',
default: '',
example: '30',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeLogGroupsCommand',
reason: 'Retrieve all CloudWatch log groups to verify retention settings.',
},
],
commandUsedInFixFunction: [
{
name: 'PutRetentionPolicyCommand',
reason: 'Set the retention period for log groups without a defined retention policy.',
},
],
adviseBeforeFixFunction: 'Ensure the specified retention period meets your organizational compliance requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const logGroups = await this.getLogGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'retention-period-days' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find(
param => param.name === 'retention-period-days' (param) => param.name === 'retention-period-days'
)?.value )?.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,73 +1,137 @@
import { import {
CloudWatchClient, CloudWatchClient,
DescribeAlarmsCommand, DescribeAlarmsCommand,
PutMetricAlarmCommand PutMetricAlarmCommand,
} from '@aws-sdk/client-cloudwatch' } from '@aws-sdk/client-cloudwatch';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'CloudWatchAlarmSettingsCheck',
description: 'Ensures that CloudWatch alarms have the required settings configured.',
priority: 3,
priorityReason: 'Correct alarm settings are essential for effective monitoring and alerting.',
awsService: 'CloudWatch',
awsServiceCategory: 'Monitoring',
bestPracticeCategory: 'Configuration',
requiredParametersForFix: [
{ 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: '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: '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,
commandUsedInCheckFunction: [
{
name: 'DescribeAlarmsCommand',
reason: 'Retrieve all CloudWatch alarms to verify their settings.',
},
],
commandUsedInFixFunction: [
{
name: 'PutMetricAlarmCommand',
reason: 'Update or create alarms with the required settings.',
},
],
adviseBeforeFixFunction: 'Ensure the required settings are correctly configured for your monitoring needs.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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,
} };
const alarms = await this.getAlarms()
for (const alarm of alarms) { for (const alarm of alarms) {
for (const parameter of Object.keys(parameters).filter(key => (parameters as any)[key] !== null)) { let isCompliant = true;
if (alarm.MetricName !== parameters.MetricName) {
continue
}
if (alarm[parameter as keyof typeof alarm] !== parameters[parameter as keyof typeof parameters]) { for (const key of Object.keys(parameters).filter((k) => (parameters as any)[k] !== null)) {
nonCompliantResources.push(alarm.AlarmArn!) if (alarm[key as keyof typeof alarm] !== parameters[key as keyof typeof parameters]) {
break isCompliant = false;
break;
} }
} }
compliantResources.push(alarm.AlarmArn!) if (isCompliant) {
} compliantResources.push(alarm.AlarmArn!);
} else {
return { nonCompliantResources.push(alarm.AlarmArn!);
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'metric-name' },
{ name: 'threshold' },
{ name: 'evaluation-periods' },
{ name: 'period' },
{ name: 'comparison-operator' },
{ name: 'statistic' }
]
} }
} }
public readonly fix = async ( this.stats.compliantResources = compliantResources;
nonCompliantResources: string[], this.stats.nonCompliantResources = nonCompliantResources;
requiredParametersForFix: { name: string; value: string }[] };
) => {
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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({
@ -77,9 +141,9 @@ export class CloudWatchAlarmSettingsCheck implements BPSet {
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 any,
Statistic: requiredSettings['statistic'] as any Statistic: requiredSettings['statistic'] as any,
}) })
) );
}
} }
};
} }

View File

@ -2,65 +2,136 @@ import {
CodeBuildClient, CodeBuildClient,
ListProjectsCommand, ListProjectsCommand,
BatchGetProjectsCommand, BatchGetProjectsCommand,
UpdateProjectCommand UpdateProjectCommand,
} from '@aws-sdk/client-codebuild' } from '@aws-sdk/client-codebuild';
import { BPSet } 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 = () => ({
name: 'CodeBuildProjectEnvironmentPrivilegedCheck',
description: 'Ensures that AWS CodeBuild projects are not using privileged mode for their environment.',
priority: 3,
priorityReason: 'Privileged mode in CodeBuild environments increases the risk of unauthorized access and actions.',
awsService: 'CodeBuild',
awsServiceCategory: 'Build',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListProjectsCommand',
reason: 'Retrieve all CodeBuild projects to verify environment settings.',
},
{
name: 'BatchGetProjectsCommand',
reason: 'Fetch detailed configuration for each CodeBuild project.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateProjectCommand',
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.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const projects = await this.getProjects() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
} public readonly fix: BPSetFixFn = async (...args) => {
} await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const projects = await this.getProjects();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const projectName = arn.split(':').pop()! const projectName = arn.split(':').pop()!;
const projects = await this.getProjects() 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(
new UpdateProjectCommand({ new UpdateProjectCommand({
name: projectName, name: projectName,
environment: { environment: {
...projectToFix.environment as any, ...projectToFix.environment,
privilegedMode: false privilegedMode: false,
} } as any,
}) })
) );
}
} }
};
} }

View File

@ -2,58 +2,129 @@ import {
CodeBuildClient, CodeBuildClient,
ListProjectsCommand, ListProjectsCommand,
BatchGetProjectsCommand, BatchGetProjectsCommand,
UpdateProjectCommand UpdateProjectCommand,
} from '@aws-sdk/client-codebuild' } from '@aws-sdk/client-codebuild';
import { BPSet } 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 = () => ({
name: 'CodeBuildProjectLoggingEnabled',
description: 'Ensures that logging is enabled for AWS CodeBuild projects.',
priority: 3,
priorityReason: 'Enabling logging allows for monitoring and debugging build processes effectively.',
awsService: 'CodeBuild',
awsServiceCategory: 'Build',
bestPracticeCategory: 'Logging and Monitoring',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListProjectsCommand',
reason: 'Retrieve all CodeBuild projects to verify logging settings.',
},
{
name: 'BatchGetProjectsCommand',
reason: 'Fetch detailed configuration for each CodeBuild project.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateProjectCommand',
reason: 'Enable logging for projects that have it disabled.',
},
],
adviseBeforeFixFunction: 'Ensure the default log group and stream names are suitable for your organization.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const projects = await this.getProjects() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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?.cloudWatchLogs?.status === 'ENABLED' ||
logsConfig?.s3Logs?.status === 'ENABLED' logsConfig?.s3Logs?.status === 'ENABLED'
) { ) {
compliantResources.push(project.arn!) compliantResources.push(project.arn!);
} else { } else {
nonCompliantResources.push(project.arn!) nonCompliantResources.push(project.arn!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
} public readonly fix: BPSetFixFn = async (...args) => {
} await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const projects = await this.getProjects();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
const projectName = arn.split(':').pop()! const projectName = arn.split(':').pop()!;
const projects = await this.getProjects() 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(
@ -64,11 +135,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,70 +3,146 @@ import {
ListApplicationsCommand, ListApplicationsCommand,
ListDeploymentGroupsCommand, ListDeploymentGroupsCommand,
BatchGetDeploymentGroupsCommand, BatchGetDeploymentGroupsCommand,
UpdateDeploymentGroupCommand UpdateDeploymentGroupCommand,
} from '@aws-sdk/client-codedeploy' } from '@aws-sdk/client-codedeploy';
import { BPSet } 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 = () => ({
name: 'CodeDeployAutoRollbackMonitorEnabled',
description: 'Ensures that auto-rollback and alarm monitoring are enabled for CodeDeploy deployment groups.',
priority: 2,
priorityReason: 'Enabling auto-rollback and alarms helps prevent deployment issues and allows for automated recovery.',
awsService: 'CodeDeploy',
awsServiceCategory: 'Deployment',
bestPracticeCategory: 'Resilience',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListApplicationsCommand',
reason: 'Retrieve all CodeDeploy applications to analyze their deployment groups.',
},
{
name: 'ListDeploymentGroupsCommand',
reason: 'Fetch deployment groups for each application.',
},
{
name: 'BatchGetDeploymentGroupsCommand',
reason: 'Get detailed information about deployment groups to check configurations.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateDeploymentGroupCommand',
reason: 'Enable alarm monitoring and auto-rollback configurations for deployment groups.',
},
],
adviseBeforeFixFunction:
'Ensure your alarms and configurations align with organizational standards before enabling auto-rollback and monitoring.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const deploymentGroups = await this.getDeploymentGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const deploymentGroups = await this.getDeploymentGroups();
for (const deploymentGroup of deploymentGroups) { for (const deploymentGroup of deploymentGroups) {
if ( if (
deploymentGroup.alarmConfiguration?.enabled && deploymentGroup.alarmConfiguration?.enabled &&
deploymentGroup.autoRollbackConfiguration?.enabled deploymentGroup.autoRollbackConfiguration?.enabled
) { ) {
compliantResources.push(deploymentGroup.deploymentGroupId!) compliantResources.push(deploymentGroup.deploymentGroupId!);
} else { } else {
nonCompliantResources.push(deploymentGroup.deploymentGroupId!) nonCompliantResources.push(deploymentGroup.deploymentGroupId!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
} public readonly fix: BPSetFixFn = async (...args) => {
} await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const deploymentGroups = await this.getDeploymentGroups();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const groupId of nonCompliantResources) { for (const groupId of nonCompliantResources) {
const deploymentGroups = await this.getDeploymentGroups()
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(
@ -75,14 +151,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,75 +1,154 @@
import { import {
DynamoDBClient, DynamoDBClient,
ListTablesCommand, ListTablesCommand,
DescribeTableCommand DescribeTableCommand,
} from '@aws-sdk/client-dynamodb' } 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 } 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 = () => ({
name: 'DynamoDBAutoscalingEnabled',
description: 'Ensures DynamoDB tables have autoscaling enabled for both read and write capacity.',
priority: 2,
priorityReason: 'Autoscaling ensures DynamoDB tables dynamically adjust capacity to meet demand, reducing costs and preventing throttling.',
awsService: 'DynamoDB',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Scalability',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTablesCommand',
reason: 'List all DynamoDB tables to check their configurations.',
},
{
name: 'DescribeTableCommand',
reason: 'Retrieve details of DynamoDB tables to analyze billing mode and autoscaling.',
},
{
name: 'DescribeScalingPoliciesCommand',
reason: 'Fetch scaling policies for each table to check autoscaling settings.',
},
],
commandUsedInFixFunction: [
{
name: 'RegisterScalableTargetCommand',
reason: 'Register read and write capacity units for autoscaling.',
},
{
name: 'PutScalingPolicyCommand',
reason: 'Configure target tracking scaling policies for read and write capacity.',
},
],
adviseBeforeFixFunction:
'Ensure the tables read and write workloads are predictable to configure appropriate scaling limits.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const tables = await this.getTables() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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(
@ -78,9 +157,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({
@ -88,9 +167,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(
@ -105,11 +184,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({
@ -123,11 +202,11 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ScaleInCooldown: 60, ScaleInCooldown: 60,
ScaleOutCooldown: 60, ScaleOutCooldown: 60,
PredefinedMetricSpecification: { PredefinedMetricSpecification: {
PredefinedMetricType: 'DynamoDBWriteCapacityUtilization' PredefinedMetricType: 'DynamoDBWriteCapacityUtilization',
} },
} },
}) })
) );
}
} }
};
} }

View File

@ -1,78 +1,167 @@
import { import {
DynamoDBClient, DynamoDBClient,
ListTablesCommand, ListTablesCommand,
DescribeTableCommand DescribeTableCommand,
} from '@aws-sdk/client-dynamodb' } from '@aws-sdk/client-dynamodb';
import { import {
BackupClient, BackupClient,
ListRecoveryPointsByResourceCommand, ListRecoveryPointsByResourceCommand,
StartBackupJobCommand StartBackupJobCommand,
} from '@aws-sdk/client-backup' } from '@aws-sdk/client-backup';
import { BPSet } from '../../types' import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import { Memorizer } from '../../Memorizer' 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 memoClient = Memorizer.memo(this.client) private readonly stsClient = new STSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private accountId: string | undefined;
private readonly fetchAccountId = async () => {
if (!this.accountId) {
const identity = await this.stsClient.send(new GetCallerIdentityCommand({}));
this.accountId = identity.Account!;
}
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 = () => ({
name: 'DynamoDBLastBackupRecoveryPointCreated',
description: 'Ensures that DynamoDB tables have a recent recovery point within the last 24 hours.',
priority: 3,
priorityReason: 'Recent backups are critical for data recovery and minimizing data loss.',
awsService: 'DynamoDB',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Backup and Recovery',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to check for backups.',
},
{
name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table.',
},
{
name: 'ListRecoveryPointsByResourceCommand',
reason: 'Check recovery points for DynamoDB tables in AWS Backup.',
},
],
commandUsedInFixFunction: [
{
name: 'StartBackupJobCommand',
reason: 'Initiate a backup job for non-compliant DynamoDB tables.',
},
],
adviseBeforeFixFunction:
'Ensure the backup vault and IAM role are properly configured for your backup strategy.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const tables = await this.getTables() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
} public readonly fix: BPSetFixFn = async (...args) => {
} await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
const accountId = await this.fetchAccountId();
public readonly fix = async (nonCompliantResources: string[]) => {
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::account-id:role/service-role/BackupDefaultServiceRole', IamRoleArn: `arn:aws:iam::${accountId}:role/service-role/BackupDefaultServiceRole`,
}) })
) );
}
} }
};
} }

View File

@ -3,68 +3,142 @@ import {
ListTablesCommand, ListTablesCommand,
DescribeTableCommand, DescribeTableCommand,
DescribeContinuousBackupsCommand, DescribeContinuousBackupsCommand,
UpdateContinuousBackupsCommand UpdateContinuousBackupsCommand,
} from '@aws-sdk/client-dynamodb' } from '@aws-sdk/client-dynamodb';
import { BPSet } 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 = () => ({
name: 'DynamoDBPITREnabled',
description: 'Ensures that Point-In-Time Recovery (PITR) is enabled for DynamoDB tables.',
priority: 1,
priorityReason: 'PITR provides continuous backups of DynamoDB tables, enabling recovery to any second within the last 35 days.',
awsService: 'DynamoDB',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Backup and Recovery',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify PITR settings.',
},
{
name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table.',
},
{
name: 'DescribeContinuousBackupsCommand',
reason: 'Check if PITR is enabled for each table.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateContinuousBackupsCommand',
reason: 'Enable PITR for non-compliant DynamoDB tables.',
},
],
adviseBeforeFixFunction: 'Ensure enabling PITR aligns with organizational backup policies and compliance requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const tables = await this.getTables() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 === 'ENABLED' ?.PointInTimeRecoveryStatus === 'ENABLED'
) { ) {
compliantResources.push(table.TableArn!) compliantResources.push(table.TableArn!);
} else { } else {
nonCompliantResources.push(table.TableArn!) nonCompliantResources.push(table.TableArn!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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

@ -2,57 +2,127 @@ import {
DynamoDBClient, DynamoDBClient,
ListTablesCommand, ListTablesCommand,
DescribeTableCommand, DescribeTableCommand,
UpdateTableCommand UpdateTableCommand,
} from '@aws-sdk/client-dynamodb' } from '@aws-sdk/client-dynamodb';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'DynamoDBTableDeletionProtectionEnabled',
description: 'Ensures that deletion protection is enabled for DynamoDB tables.',
priority: 2,
priorityReason: 'Deletion protection prevents accidental table deletion, safeguarding critical data.',
awsService: 'DynamoDB',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify deletion protection.',
},
{
name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table, including deletion protection settings.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateTableCommand',
reason: 'Enable deletion protection for non-compliant DynamoDB tables.',
},
],
adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with operational and compliance requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const tables = await this.getTables() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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

@ -2,62 +2,136 @@ import {
DynamoDBClient, DynamoDBClient,
ListTablesCommand, ListTablesCommand,
DescribeTableCommand, DescribeTableCommand,
UpdateTableCommand UpdateTableCommand,
} from '@aws-sdk/client-dynamodb' } from '@aws-sdk/client-dynamodb';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'DynamoDBTableEncryptedKMS',
description: 'Ensures that DynamoDB tables are encrypted with AWS KMS.',
priority: 2,
priorityReason: 'Encrypting DynamoDB tables with KMS enhances data security and meets compliance requirements.',
awsService: 'DynamoDB',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'kms-key-id',
description: 'The ID of the KMS key used to encrypt the DynamoDB table.',
default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.',
},
{
name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table, including SSE configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateTableCommand',
reason: 'Enable KMS encryption for non-compliant DynamoDB tables.',
},
],
adviseBeforeFixFunction: 'Ensure the specified KMS key is accessible and meets your security policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const tables = await this.getTables() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const tables = await this.getTables();
for (const table of tables) { for (const table of tables) {
if ( if (
table.SSEDescription?.Status === 'ENABLED' && table.SSEDescription?.Status === 'ENABLED' &&
table.SSEDescription?.SSEType === 'KMS' table.SSEDescription?.SSEType === 'KMS'
) { ) {
compliantResources.push(table.TableArn!) compliantResources.push(table.TableArn!);
} else { } else {
nonCompliantResources.push(table.TableArn!) nonCompliantResources.push(table.TableArn!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
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({
@ -65,10 +139,10 @@ export class DynamoDBTableEncryptedKMS implements BPSet {
SSESpecification: { SSESpecification: {
Enabled: true, Enabled: true,
SSEType: 'KMS', SSEType: 'KMS',
KMSMasterKeyId: kmsKeyId KMSMasterKeyId: kmsKeyId,
} },
}) })
) );
}
} }
};
} }

View File

@ -2,59 +2,129 @@ import {
DynamoDBClient, DynamoDBClient,
ListTablesCommand, ListTablesCommand,
DescribeTableCommand, DescribeTableCommand,
UpdateTableCommand UpdateTableCommand,
} from '@aws-sdk/client-dynamodb' } from '@aws-sdk/client-dynamodb';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'DynamoDBTableEncryptionEnabled',
description: 'Ensures that DynamoDB tables have server-side encryption enabled.',
priority: 3,
priorityReason: 'Enabling server-side encryption ensures data security and compliance with organizational policies.',
awsService: 'DynamoDB',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTablesCommand',
reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.',
},
{
name: 'DescribeTableCommand',
reason: 'Fetch details of each DynamoDB table, including encryption settings.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateTableCommand',
reason: 'Enable server-side encryption for non-compliant DynamoDB tables.',
},
],
adviseBeforeFixFunction: 'Ensure enabling encryption aligns with organizational data security policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const tables = await this.getTables() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,36 +1,102 @@
import { import {
EC2Client, EC2Client,
DescribeVolumesCommand, DescribeVolumesCommand,
EnableEbsEncryptionByDefaultCommand GetEbsEncryptionByDefaultCommand,
} from '@aws-sdk/client-ec2' EnableEbsEncryptionByDefaultCommand,
import { BPSet } from '../../types' } from '@aws-sdk/client-ec2';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2EbsEncryptionByDefault',
description: 'Ensures that EBS encryption is enabled by default for all volumes in the AWS account.',
priority: 1,
priorityReason: 'Enabling EBS encryption by default ensures data at rest is encrypted, enhancing security and compliance.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetEbsEncryptionByDefaultCommand',
reason: 'Verify if EBS encryption by default is enabled in the AWS account.',
},
],
commandUsedInFixFunction: [
{
name: 'EnableEbsEncryptionByDefaultCommand',
reason: 'Enable EBS encryption by default for the account.',
},
],
adviseBeforeFixFunction:
'Ensure enabling EBS encryption by default aligns with your organizations security policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeVolumesCommand({}))
for (const volume of response.Volumes || []) { await this.checkImpl().then(
if (volume.Encrypted) { () => (this.stats.status = 'FINISHED'),
compliantResources.push(volume.VolumeId!) (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const response = await this.client.send(new GetEbsEncryptionByDefaultCommand({}));
if (response.EbsEncryptionByDefault) {
compliantResources.push('EBS Encryption By Default');
} else { } else {
nonCompliantResources.push(volume.VolumeId!) nonCompliantResources.push('EBS Encryption By Default');
}
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async () => { public readonly fix: BPSetFixFn = async (...args) => {
await this.client.send(new EnableEbsEncryptionByDefaultCommand({})) await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
} }
);
};
public readonly fixImpl: BPSetFixFn = async () => {
await this.client.send(new EnableEbsEncryptionByDefaultCommand({}));
};
} }

View File

@ -1,45 +1,111 @@
import { import {
DescribeInstancesCommand, DescribeInstancesCommand,
EC2Client, EC2Client,
ModifyInstanceMetadataOptionsCommand ModifyInstanceMetadataOptionsCommand,
} from '@aws-sdk/client-ec2' } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2Imdsv2Check',
description: 'Ensures that EC2 instances enforce the use of IMDSv2 for enhanced metadata security.',
priority: 1,
priorityReason: 'Requiring IMDSv2 improves the security of instance metadata by mitigating SSRF attacks.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeInstancesCommand',
reason: 'Retrieve all EC2 instances and check their metadata options.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyInstanceMetadataOptionsCommand',
reason: 'Update EC2 instance metadata options to enforce IMDSv2.',
},
],
adviseBeforeFixFunction: 'Ensure modifying metadata options aligns with operational policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})) await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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.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!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,42 +1,111 @@
import { import {
DescribeInstancesCommand, DescribeInstancesCommand,
EC2Client, EC2Client,
MonitorInstancesCommand MonitorInstancesCommand,
} from '@aws-sdk/client-ec2' } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2InstanceDetailedMonitoringEnabled',
description: 'Ensures that EC2 instances have detailed monitoring enabled.',
priority: 2,
priorityReason: 'Detailed monitoring provides enhanced visibility into instance performance metrics.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Monitoring and Logging',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeInstancesCommand',
reason: 'Retrieve the list of all EC2 instances and their monitoring state.',
},
],
commandUsedInFixFunction: [
{
name: 'MonitorInstancesCommand',
reason: 'Enable detailed monitoring for non-compliant EC2 instances.',
},
],
adviseBeforeFixFunction:
'Ensure enabling detailed monitoring aligns with organizational policies and cost considerations.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})) await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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.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!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => {
if (nonCompliantResources.length > 0) {
await this.client.send( await this.client.send(
new MonitorInstancesCommand({ new MonitorInstancesCommand({
InstanceIds: nonCompliantResources InstanceIds: nonCompliantResources,
}) })
) );
} }
};
} }

View File

@ -1,48 +1,104 @@
import { import {
EC2Client, EC2Client,
DescribeInstancesCommand DescribeInstancesCommand,
} from '@aws-sdk/client-ec2' } from '@aws-sdk/client-ec2';
import { SSMClient, DescribeInstanceInformationCommand } from '@aws-sdk/client-ssm' import {
import { BPSet } from '../../types' SSMClient,
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2InstanceManagedBySystemsManager',
description: 'Ensures that EC2 instances are managed by AWS Systems Manager.',
priority: 2,
priorityReason: 'Management through Systems Manager ensures efficient and secure configuration and operation of EC2 instances.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Management and Governance',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInFixFunction: [],
commandUsedInCheckFunction: [
{
name: 'DescribeInstancesCommand',
reason: 'Retrieve the list of all EC2 instances.',
},
{
name: 'DescribeInstanceInformationCommand',
reason: 'Retrieve information about instances managed by Systems Manager.',
},
],
adviseBeforeFixFunction:
'Ensure Systems Manager Agent (SSM Agent) is installed and configured properly on non-compliant instances.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})) await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 (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!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = 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,56 +1,127 @@
import { import {
EC2Client, EC2Client,
DescribeInstancesCommand, DescribeInstancesCommand,
AssociateIamInstanceProfileCommand AssociateIamInstanceProfileCommand,
} from '@aws-sdk/client-ec2' } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2InstanceProfileAttached',
description: 'Ensures that all EC2 instances have an IAM instance profile attached.',
priority: 2,
priorityReason: 'Attaching an IAM instance profile enables instances to securely interact with AWS services.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'iam-instance-profile',
description: 'The name of the IAM instance profile to attach.',
default: '',
example: 'EC2InstanceProfile',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeInstancesCommand',
reason: 'Retrieve all EC2 instances and their associated IAM instance profiles.',
},
],
commandUsedInFixFunction: [
{
name: 'AssociateIamInstanceProfileCommand',
reason: 'Attach an IAM instance profile to non-compliant EC2 instances.',
},
],
adviseBeforeFixFunction:
'Ensure the specified IAM instance profile exists and aligns with your access control policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources: string[] = [] this.stats.status = 'CHECKING';
const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})) await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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.IamInstanceProfile) { if (instance.IamInstanceProfile) {
compliantResources.push(instance.InstanceId!) compliantResources.push(instance.InstanceId!);
} else { } else {
nonCompliantResources.push(instance.InstanceId!) nonCompliantResources.push(instance.InstanceId!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'iam-instance-profile' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const iamInstanceProfile = requiredParametersForFix.find( const iamInstanceProfile = requiredParametersForFix.find(
param => param.name === 'iam-instance-profile' (param) => param.name === 'iam-instance-profile'
)?.value )?.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,39 +1,88 @@
import { import {
EC2Client, EC2Client,
DescribeInstancesCommand DescribeInstancesCommand,
} from '@aws-sdk/client-ec2' } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2NoAmazonKeyPair',
description: 'Ensures that EC2 instances are not using an Amazon Key Pair.',
priority: 3,
priorityReason: 'Amazon Key Pairs pose a potential security risk if not properly managed.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeInstancesCommand',
reason: 'Retrieve all EC2 instances and verify if an Amazon Key Pair is used.',
},
],
commandUsedInFixFunction: [],
adviseBeforeFixFunction:
'Ensure instances are launched without a Key Pair or configure SSH access using alternative mechanisms like Systems Manager Session Manager.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})) await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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.KeyName) { if (instance.KeyName) {
nonCompliantResources.push(instance.InstanceId!) nonCompliantResources.push(instance.InstanceId!);
} else { } else {
compliantResources.push(instance.InstanceId!) compliantResources.push(instance.InstanceId!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
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,46 +1,114 @@
import { import {
EC2Client, EC2Client,
DescribeInstancesCommand, DescribeInstancesCommand,
TerminateInstancesCommand TerminateInstancesCommand,
} from '@aws-sdk/client-ec2' } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2StoppedInstance',
description: 'Ensures that stopped EC2 instances are identified and terminated if necessary.',
priority: 3,
priorityReason:
'Stopped instances can incur costs for storage and IP addresses without being in use.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Cost Optimization',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeInstancesCommand',
reason: 'Retrieve EC2 instances to check their state.',
},
],
commandUsedInFixFunction: [
{
name: 'TerminateInstancesCommand',
reason: 'Terminate stopped EC2 instances to prevent unnecessary costs.',
},
],
adviseBeforeFixFunction:
'Ensure terminated instances do not contain any critical data or configurations before proceeding.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})) await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,19 +1,77 @@
import { import {
EC2Client, EC2Client,
DescribeInstancesCommand, DescribeInstancesCommand,
ModifyInstanceMetadataOptionsCommand ModifyInstanceMetadataOptionsCommand,
} from '@aws-sdk/client-ec2' } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EC2TokenHopLimitCheck',
description: 'Ensures that EC2 instances have a Metadata Options HttpPutResponseHopLimit of 1.',
priority: 3,
priorityReason:
'Setting the HttpPutResponseHopLimit to 1 ensures secure access to the instance metadata.',
awsService: 'EC2',
awsServiceCategory: 'Compute',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeInstancesCommand',
reason: 'Retrieve EC2 instances and check the metadata options for HttpPutResponseHopLimit.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyInstanceMetadataOptionsCommand',
reason: 'Update the HttpPutResponseHopLimit to enforce secure metadata access.',
},
],
adviseBeforeFixFunction:
'Ensure modifying instance metadata options aligns with your operational policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({})) await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 || []) {
@ -21,28 +79,38 @@ export class EC2TokenHopLimitCheck implements BPSet {
instance.MetadataOptions?.HttpPutResponseHopLimit && instance.MetadataOptions?.HttpPutResponseHopLimit &&
instance.MetadataOptions.HttpPutResponseHopLimit < 2 instance.MetadataOptions.HttpPutResponseHopLimit < 2
) { ) {
compliantResources.push(instance.InstanceId!) compliantResources.push(instance.InstanceId!);
} else { } else {
nonCompliantResources.push(instance.InstanceId!) nonCompliantResources.push(instance.InstanceId!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,75 +5,162 @@ import {
ListImagesCommand, ListImagesCommand,
BatchGetImageCommand, BatchGetImageCommand,
PutImageCommand, PutImageCommand,
DeleteRepositoryCommand DeleteRepositoryCommand,
} from '@aws-sdk/client-ecr' } from '@aws-sdk/client-ecr';
import { BPSet } 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 = () => ({
name: 'ECRKmsEncryption1',
description: 'Ensures ECR repositories are encrypted using AWS KMS.',
priority: 2,
priorityReason: 'Encrypting ECR repositories with KMS enhances data security and meets compliance requirements.',
awsService: 'ECR',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'kms-key-id',
description: 'The ID of the KMS key used to encrypt the ECR repository.',
default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv',
},
],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeRepositoriesCommand',
reason: 'Retrieve the list of ECR repositories to verify encryption settings.',
},
],
commandUsedInFixFunction: [
{
name: 'CreateRepositoryCommand',
reason: 'Create a new repository with KMS encryption.',
},
{
name: 'ListImagesCommand',
reason: 'List all images in the existing repository for migration.',
},
{
name: 'BatchGetImageCommand',
reason: 'Retrieve image manifests for migration to the new repository.',
},
{
name: 'PutImageCommand',
reason: 'Push images to the newly created repository.',
},
{
name: 'DeleteRepositoryCommand',
reason: 'Delete the old repository after migration.',
},
],
adviseBeforeFixFunction:
'Ensure the specified KMS key is accessible, and deleting the old repository aligns with operational policies.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const repositories = await this.getRepositories() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
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 || []) {
@ -81,9 +168,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,
}) })
) );
} }
} }
@ -91,9 +178,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,50 +1,118 @@
import { import {
ECRClient, ECRClient,
DescribeRepositoriesCommand, DescribeRepositoriesCommand,
PutImageScanningConfigurationCommand PutImageScanningConfigurationCommand,
} from '@aws-sdk/client-ecr' } from '@aws-sdk/client-ecr';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'ECRPrivateImageScanningEnabled',
description: 'Ensures that image scanning on push is enabled for private ECR repositories.',
priority: 2,
priorityReason:
'Enabling image scanning on push helps identify vulnerabilities in container images during upload.',
awsService: 'ECR',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeRepositoriesCommand',
reason: 'Retrieve all ECR repositories and check their image scanning configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'PutImageScanningConfigurationCommand',
reason: 'Enable image scanning on push for non-compliant repositories.',
},
],
adviseBeforeFixFunction:
'Ensure enabling image scanning aligns with organizational policies and does not affect performance.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const repositories = await this.getRepositories() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,71 +2,147 @@ import {
ECRClient, ECRClient,
DescribeRepositoriesCommand, DescribeRepositoriesCommand,
PutLifecyclePolicyCommand, PutLifecyclePolicyCommand,
GetLifecyclePolicyCommand GetLifecyclePolicyCommand,
} from '@aws-sdk/client-ecr' } from '@aws-sdk/client-ecr';
import { BPSet } 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 = () => ({
name: 'ECRPrivateLifecyclePolicyConfigured',
description: 'Ensures that private ECR repositories have lifecycle policies configured.',
priority: 3,
priorityReason:
'Lifecycle policies reduce unnecessary costs by managing image retention and cleanup in ECR repositories.',
awsService: 'ECR',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Cost Optimization',
requiredParametersForFix: [
{
name: 'lifecycle-policy',
description: 'The JSON-formatted lifecycle policy text to apply to the repositories.',
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"}}]}',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeRepositoriesCommand',
reason: 'Retrieve all ECR repositories to check their lifecycle policy status.',
},
{
name: 'GetLifecyclePolicyCommand',
reason: 'Verify if a lifecycle policy exists for each repository.',
},
],
commandUsedInFixFunction: [
{
name: 'PutLifecyclePolicyCommand',
reason: 'Apply a lifecycle policy to non-compliant ECR repositories.',
},
],
adviseBeforeFixFunction:
'Ensure the provided lifecycle policy is well-tested and does not inadvertently remove critical images.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const repositories = await this.getRepositories() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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: any) {
if (error.name === 'LifecyclePolicyNotFoundException') { if (error.name === 'LifecyclePolicyNotFoundException') {
nonCompliantResources.push(repository.repositoryArn!) nonCompliantResources.push(repository.repositoryArn!);
} else { } else {
throw error throw error;
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'lifecycle-policy' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (...args) => {
nonCompliantResources: string[], await this.fixImpl(...args).then(
requiredParametersForFix: { name: string; value: string }[] () => (this.stats.status = 'FINISHED'),
) => { (err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const lifecyclePolicy = requiredParametersForFix.find( const lifecyclePolicy = requiredParametersForFix.find(
param => param.name === 'lifecycle-policy' (param) => param.name === 'lifecycle-policy'
)?.value )?.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,50 +1,119 @@
import { import {
ECRClient, ECRClient,
DescribeRepositoriesCommand, DescribeRepositoriesCommand,
PutImageTagMutabilityCommand PutImageTagMutabilityCommand,
} from '@aws-sdk/client-ecr' } from '@aws-sdk/client-ecr';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'ECRPrivateTagImmutabilityEnabled',
description: 'Ensures that private ECR repositories have tag immutability enabled.',
priority: 3,
priorityReason:
'Enabling tag immutability prevents accidental overwrites of image tags, ensuring integrity and reproducibility.',
awsService: 'ECR',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeRepositoriesCommand',
reason: 'Retrieve all ECR repositories to check their tag mutability setting.',
},
],
commandUsedInFixFunction: [
{
name: 'PutImageTagMutabilityCommand',
reason: 'Set image tag immutability for non-compliant repositories.',
},
],
adviseBeforeFixFunction:
'Ensure that enabling tag immutability aligns with your development and deployment processes.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const repositories = await this.getRepositories() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,55 +2,127 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } 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 = () => ({
name: 'ECSAwsVpcNetworkingEnabled',
description: 'Ensures that ECS task definitions are configured to use the awsvpc network mode.',
priority: 3,
priorityReason:
'Using the awsvpc network mode provides enhanced security and networking capabilities.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.',
},
{
name: 'DescribeTaskDefinitionCommand',
reason: 'Describe details of each ECS task definition.',
},
],
commandUsedInFixFunction: [
{
name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with awsvpc network mode.',
},
],
adviseBeforeFixFunction:
'Ensure that the awsvpc network mode is supported by your ECS setup and compatible with your workloads.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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({
@ -59,9 +131,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,51 +1,121 @@
import { import {
ECSClient, ECSClient,
DescribeClustersCommand, DescribeClustersCommand,
UpdateClusterSettingsCommand UpdateClusterSettingsCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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(new DescribeClustersCommand({ include: ['SETTINGS'] })) const response = await this.memoClient.send(
return response.clusters || [] new DescribeClustersCommand({ include: ['SETTINGS'] })
} );
return response.clusters || [];
};
public readonly getMetadata = () => ({
name: 'ECSContainerInsightsEnabled',
description: 'Ensures that ECS clusters have Container Insights enabled.',
priority: 3,
priorityReason:
'Enabling Container Insights provides enhanced monitoring and diagnostics for ECS clusters.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Monitoring',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeClustersCommand',
reason: 'Retrieve ECS clusters and their settings to check if Container Insights is enabled.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateClusterSettingsCommand',
reason: 'Enable Container Insights for non-compliant ECS clusters.',
},
],
adviseBeforeFixFunction:
'Ensure enabling Container Insights aligns with your monitoring strategy and does not incur unexpected costs.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const clusters = await this.getClusters() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const clusters = await this.getClusters();
for (const cluster of clusters) { for (const cluster of clusters) {
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,65 +2,137 @@ import {
ECSClient, ECSClient,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand ListTaskDefinitionsCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } 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 = () => ({
name: 'ECSContainersNonPrivileged',
description: 'Ensures that containers in ECS task definitions are not running in privileged mode.',
priority: 1,
priorityReason:
'Running containers in privileged mode poses significant security risks.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.',
},
{
name: 'DescribeTaskDefinitionCommand',
reason: 'Check the container configurations in ECS task definitions.',
},
],
commandUsedInFixFunction: [
{
name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with privileged mode disabled.',
},
],
adviseBeforeFixFunction:
'Ensure that containers do not rely on privileged mode for their functionality.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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({
@ -69,9 +141,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,65 +2,137 @@ import {
ECSClient, ECSClient,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand, RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand ListTaskDefinitionsCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } 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 = () => ({
name: 'ECSContainersReadonlyAccess',
description: 'Ensures that containers in ECS task definitions have readonly root filesystems enabled.',
priority: 1,
priorityReason:
'Readonly root filesystems prevent unauthorized changes to the container filesystem, enhancing security.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.',
},
{
name: 'DescribeTaskDefinitionCommand',
reason: 'Check the container configurations in ECS task definitions.',
},
],
commandUsedInFixFunction: [
{
name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with readonly root filesystems enabled.',
},
],
adviseBeforeFixFunction:
'Ensure enabling readonly root filesystems does not interfere with the functionality of your containers.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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({
@ -69,9 +141,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,68 +3,144 @@ import {
ListClustersCommand, ListClustersCommand,
ListServicesCommand, ListServicesCommand,
DescribeServicesCommand, DescribeServicesCommand,
UpdateServiceCommand UpdateServiceCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } 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 = () => ({
name: 'ECSFargateLatestPlatformVersion',
description: 'Ensures ECS Fargate services are using the latest platform version.',
priority: 3,
priorityReason:
'Using the latest platform version ensures access to the latest features, updates, and security patches.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Performance and Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListClustersCommand',
reason: 'Retrieve ECS clusters to identify associated services.',
},
{
name: 'ListServicesCommand',
reason: 'Retrieve services associated with each ECS cluster.',
},
{
name: 'DescribeServicesCommand',
reason: 'Check the platform version of each ECS service.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateServiceCommand',
reason: 'Update ECS services to use the latest platform version.',
},
],
adviseBeforeFixFunction:
'Ensure that updating to the latest platform version aligns with your workload requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const services = await this.getServices() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!) nonCompliantResources.push(service?.serviceArn!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
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,76 +2,155 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } 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 = () => ({
name: 'ECSTaskDefinitionLogConfiguration',
description: 'Ensures that ECS task definitions have log configuration enabled.',
priority: 1,
priorityReason:
'Enabling log configuration is critical for monitoring and debugging container workloads.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Monitoring',
requiredParametersForFix: [
{
name: 'log-configuration',
description: 'The log configuration to apply to non-compliant task definitions.',
default: '{}',
example: '{"logDriver": "awslogs", "options": {"awslogs-group": "/ecs/logs"}}',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.',
},
{
name: 'DescribeTaskDefinitionCommand',
reason: 'Check the container configurations in ECS task definitions.',
},
],
commandUsedInFixFunction: [
{
name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with updated log configuration.',
},
],
adviseBeforeFixFunction:
'Ensure the provided log configuration aligns with your logging strategy and infrastructure.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (
nonCompliantResources, nonCompliantResources,
requiredParametersForFix: [{ name: 'log-configuration' }] requiredParametersForFix
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const logConfiguration = requiredParametersForFix.find( const logConfiguration = requiredParametersForFix.find(
param => param.name === 'log-configuration' (param) => param.name === 'log-configuration'
)?.value )?.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({
@ -80,9 +159,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,65 +2,152 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } 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 = () => ({
name: 'ECSTaskDefinitionMemoryHardLimit',
description: 'Ensures all containers in ECS task definitions have a memory hard limit set.',
priority: 1,
priorityReason:
'Setting memory hard limits is essential to prevent resource contention and ensure container isolation.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Resource Management',
requiredParametersForFix: [
{
name: 'default-memory-limit',
description: 'The default memory hard limit to set for containers without a memory limit.',
default: '512',
example: '1024',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.',
},
{
name: 'DescribeTaskDefinitionCommand',
reason: 'Check container configurations in ECS task definitions for memory limits.',
},
],
commandUsedInFixFunction: [
{
name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with updated memory limits.',
},
],
adviseBeforeFixFunction:
'Ensure the default memory limit aligns with your container resource requirements.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (
nonCompliantResources, nonCompliantResources,
requiredParametersForFix: [] requiredParametersForFix
} ) => {
} const defaultMemoryLimit = parseInt(
requiredParametersForFix.find((param) => param.name === 'default-memory-limit')?.value || '512',
10
);
public readonly fix = async (nonCompliantResources: string[]) => {
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 || 512 // Default hard limit memory value memory: container.memory || defaultMemoryLimit,
}) })
) );
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -69,9 +156,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,65 +2,151 @@ import {
ECSClient, ECSClient,
ListTaskDefinitionsCommand, ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand, DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs' } from '@aws-sdk/client-ecs';
import { BPSet } 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 = () => ({
name: 'ECSTaskDefinitionNonRootUser',
description: 'Ensures all ECS containers in task definitions run as non-root users.',
priority: 1,
priorityReason:
'Running containers as non-root users improves security by reducing the potential impact of compromised containers.',
awsService: 'ECS',
awsServiceCategory: 'Container',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'default-non-root-user',
description: 'The default non-root user to assign for containers without a specified user.',
default: 'ecs-user',
example: 'app-user',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListTaskDefinitionsCommand',
reason: 'Retrieve all active ECS task definitions.',
},
{
name: 'DescribeTaskDefinitionCommand',
reason: 'Check container configurations in ECS task definitions for user settings.',
},
],
commandUsedInFixFunction: [
{
name: 'RegisterTaskDefinitionCommand',
reason: 'Re-register ECS task definitions with non-root user configurations.',
},
],
adviseBeforeFixFunction:
'Ensure the default non-root user has sufficient permissions to execute the container workload.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (
nonCompliantResources, nonCompliantResources,
requiredParametersForFix: [] requiredParametersForFix
} ) => {
} const defaultNonRootUser = requiredParametersForFix.find(
(param) => param.name === 'default-non-root-user'
)?.value || 'ecs-user';
public readonly fix = async (nonCompliantResources: string[]) => {
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 || 'ecs-user' // Default non-root user user: container.user || defaultNonRootUser,
}) })
) );
await this.client.send( await this.client.send(
new RegisterTaskDefinitionCommand({ new RegisterTaskDefinitionCommand({
@ -69,9 +155,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,70 +2,149 @@ import {
EFSClient, EFSClient,
DescribeAccessPointsCommand, DescribeAccessPointsCommand,
DeleteAccessPointCommand, DeleteAccessPointCommand,
CreateAccessPointCommand CreateAccessPointCommand,
} from '@aws-sdk/client-efs' } from '@aws-sdk/client-efs';
import { BPSet } 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 = () => ({
name: 'EFSAccessPointEnforceRootDirectory',
description: 'Ensures that EFS Access Points enforce a specific root directory.',
priority: 1,
priorityReason:
'Enforcing a consistent root directory path for EFS Access Points ensures proper access control and organization.',
awsService: 'EFS',
awsServiceCategory: 'File System',
bestPracticeCategory: 'Resource Configuration',
requiredParametersForFix: [
{
name: 'root-directory-path',
description: 'The root directory path to enforce for EFS Access Points.',
default: '/',
example: '/data',
},
],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeAccessPointsCommand',
reason: 'Retrieve all existing EFS Access Points and their configurations.',
},
],
commandUsedInFixFunction: [
{
name: 'DeleteAccessPointCommand',
reason: 'Delete non-compliant EFS Access Points.',
},
{
name: 'CreateAccessPointCommand',
reason: 'Recreate EFS Access Points with the enforced root directory.',
},
],
adviseBeforeFixFunction:
'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const accessPoints = await this.getAccessPoints() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (
nonCompliantResources, nonCompliantResources,
requiredParametersForFix: [{ name: 'root-directory-path' }] requiredParametersForFix
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const rootDirectoryPath = requiredParametersForFix.find( const rootDirectoryPath = requiredParametersForFix.find(
param => param.name === 'root-directory-path' (param) => param.name === 'root-directory-path'
)?.value )?.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,68 +2,147 @@ import {
EFSClient, EFSClient,
DescribeAccessPointsCommand, DescribeAccessPointsCommand,
DeleteAccessPointCommand, DeleteAccessPointCommand,
CreateAccessPointCommand CreateAccessPointCommand,
} from '@aws-sdk/client-efs' } from '@aws-sdk/client-efs';
import { BPSet } 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 = () => ({
name: 'EFSAccessPointEnforceUserIdentity',
description: 'Ensures that EFS Access Points enforce a specific PosixUser identity.',
priority: 1,
priorityReason:
'Setting a specific PosixUser identity for EFS Access Points ensures controlled access and proper security.',
awsService: 'EFS',
awsServiceCategory: 'File System',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'posix-user',
description: 'The PosixUser configuration to enforce for EFS Access Points.',
default: '{"Uid": "1000", "Gid": "1000"}',
example: '{"Uid": "1234", "Gid": "1234"}',
},
],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeAccessPointsCommand',
reason: 'Retrieve all existing EFS Access Points and their configurations.',
},
],
commandUsedInFixFunction: [
{
name: 'DeleteAccessPointCommand',
reason: 'Delete non-compliant EFS Access Points.',
},
{
name: 'CreateAccessPointCommand',
reason: 'Recreate EFS Access Points with the enforced PosixUser.',
},
],
adviseBeforeFixFunction:
'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const accessPoints = await this.getAccessPoints() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (
nonCompliantResources, nonCompliantResources,
requiredParametersForFix: [{ name: 'posix-user' }] requiredParametersForFix
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const posixUser = requiredParametersForFix.find(param => param.name === 'posix-user')?.value 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,54 +2,129 @@ import {
EFSClient, EFSClient,
DescribeFileSystemsCommand, DescribeFileSystemsCommand,
PutBackupPolicyCommand, PutBackupPolicyCommand,
DescribeBackupPolicyCommand DescribeBackupPolicyCommand,
} from '@aws-sdk/client-efs' } from '@aws-sdk/client-efs';
import { BPSet } 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 = () => ({
name: 'EFSAutomaticBackupsEnabled',
description: 'Ensures that EFS file systems have automatic backups enabled.',
priority: 1,
priorityReason:
'Enabling automatic backups helps protect against data loss and supports recovery from unintended modifications or deletions.',
awsService: 'EFS',
awsServiceCategory: 'File System',
bestPracticeCategory: 'Backup and Recovery',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeFileSystemsCommand',
reason: 'Retrieve the list of EFS file systems.',
},
{
name: 'DescribeBackupPolicyCommand',
reason: 'Check if a backup policy is enabled for the file system.',
},
],
commandUsedInFixFunction: [
{
name: 'PutBackupPolicyCommand',
reason: 'Enable automatic backups for the file system.',
},
],
adviseBeforeFixFunction:
'Ensure that enabling backups aligns with the organizations cost and recovery objectives.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const fileSystems = await this.getFileSystems() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (
nonCompliantResources, nonCompliantResources,
requiredParametersForFix: [] requiredParametersForFix
} ) => {
}
public readonly fix = async (nonCompliantResources: string[]) => {
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,53 +2,128 @@ import {
EFSClient, EFSClient,
DescribeFileSystemsCommand, DescribeFileSystemsCommand,
CreateFileSystemCommand, CreateFileSystemCommand,
DeleteFileSystemCommand DeleteFileSystemCommand,
} from '@aws-sdk/client-efs' } from '@aws-sdk/client-efs';
import { BPSet } 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 = () => ({
name: 'EFSEncryptedCheck',
description: 'Ensures that all EFS file systems are encrypted.',
priority: 1,
priorityReason:
'Encrypting EFS file systems helps ensure data protection and compliance with security best practices.',
awsService: 'EFS',
awsServiceCategory: 'File System',
bestPracticeCategory: 'Encryption',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeFileSystemsCommand',
reason: 'Retrieve all existing EFS file systems and their encryption status.',
},
],
commandUsedInFixFunction: [
{
name: 'DeleteFileSystemCommand',
reason: 'Delete non-compliant EFS file systems.',
},
{
name: 'CreateFileSystemCommand',
reason: 'Recreate EFS file systems with encryption enabled.',
},
],
adviseBeforeFixFunction:
'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 = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const fileSystems = await this.getFileSystems() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix: BPSetFixFn = async (...args) => {
await this.fixImpl(...args).then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
public readonly fixImpl: BPSetFixFn = async (
nonCompliantResources, nonCompliantResources,
requiredParametersForFix: [] requiredParametersForFix
} ) => {
}
public readonly fix = async (nonCompliantResources: string[]) => {
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(
@ -56,9 +131,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,71 +1,132 @@
import { import {
EFSClient, EFSClient,
DescribeFileSystemsCommand, DescribeFileSystemsCommand,
DescribeMountTargetsCommand DescribeMountTargetsCommand,
} from '@aws-sdk/client-efs' } from '@aws-sdk/client-efs';
import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2' import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EFSMountTargetPublicAccessible',
description: 'Checks if EFS mount targets are publicly accessible.',
priority: 2,
priorityReason:
'Publicly accessible EFS mount targets may pose a security risk by exposing data to unintended access.',
awsService: 'EFS',
awsServiceCategory: 'File System',
bestPracticeCategory: 'Network Configuration',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeFileSystemsCommand',
reason: 'Retrieve the list of EFS file systems.',
},
{
name: 'DescribeMountTargetsCommand',
reason: 'Retrieve the list of mount targets for a file system.',
},
{
name: 'DescribeRouteTablesCommand',
reason: 'Check route tables associated with the mount target subnets.',
},
],
commandUsedInFixFunction: [],
adviseBeforeFixFunction:
'Ensure that the network configurations are reviewed carefully to avoid breaking application connectivity.',
});
private readonly stats: BPSetStats = {
nonCompliantResources: [],
compliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const fileSystems = await this.getFileSystems() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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;
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!);
for (const route of routes) {
if ( if (
route.DestinationCidrBlock === '0.0.0.0/0' && routes.some(
route.GatewayId?.startsWith('igw-') (route) =>
route.DestinationCidrBlock === '0.0.0.0/0' && route.GatewayId?.startsWith('igw-')
)
) { ) {
nonCompliantResources.push(fileSystem.FileSystemArn!) nonCompliantResources.push(fileSystem.FileSystemArn!);
break isNonCompliant = true;
} break;
}
}
if (!nonCompliantResources.includes(fileSystem.FileSystemArn!)) {
compliantResources.push(fileSystem.FileSystemArn!)
} }
} }
return { if (!isNonCompliant) {
compliantResources, compliantResources.push(fileSystem.FileSystemArn!);
nonCompliantResources,
requiredParametersForFix: []
} }
} }
public readonly fix = async (nonCompliantResources: string[]) => { this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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

@ -2,52 +2,111 @@ import {
EKSClient, EKSClient,
ListClustersCommand, ListClustersCommand,
DescribeClusterCommand, DescribeClusterCommand,
UpdateClusterConfigCommand UpdateClusterConfigCommand,
} from '@aws-sdk/client-eks' } from '@aws-sdk/client-eks';
import { BPSet } from '../../types' import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EKSClusterLoggingEnabled',
description: 'Ensures that all EKS clusters have full logging enabled.',
priority: 1,
priorityReason:
'Cluster logging is essential for monitoring, debugging, and auditing purposes.',
awsService: 'EKS',
awsServiceCategory: 'Kubernetes Service',
bestPracticeCategory: 'Observability',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListClustersCommand',
reason: 'Retrieve the list of EKS clusters.',
},
{
name: 'DescribeClusterCommand',
reason: 'Fetch details about the EKS cluster, including logging configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateClusterConfigCommand',
reason: 'Enable all logging types for the EKS cluster.',
},
],
adviseBeforeFixFunction:
'Ensure that enabling full logging does not generate excessive costs or logs.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const clusters = await this.getClusters() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { 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({
@ -56,12 +115,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,61 +2,124 @@ import {
EKSClient, EKSClient,
ListClustersCommand, ListClustersCommand,
DescribeClusterCommand, DescribeClusterCommand,
AssociateEncryptionConfigCommand AssociateEncryptionConfigCommand,
} from '@aws-sdk/client-eks' } from '@aws-sdk/client-eks';
import { BPSet } 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 = () => ({
name: 'EKSClusterSecretsEncrypted',
description: 'Ensures that all EKS clusters have secrets encrypted with a KMS key.',
priority: 1,
priorityReason:
'Encrypting secrets ensures the security and compliance of sensitive data in EKS clusters.',
awsService: 'EKS',
awsServiceCategory: 'Kubernetes Service',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'kms-key-id',
description: 'The KMS key ARN to enable encryption for EKS secrets.',
default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/example-key-id',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListClustersCommand',
reason: 'Retrieve the list of EKS clusters.',
},
{
name: 'DescribeClusterCommand',
reason: 'Fetch details about the EKS cluster, including encryption configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'AssociateEncryptionConfigCommand',
reason: 'Enable encryption for EKS secrets using the provided KMS key.',
},
],
adviseBeforeFixFunction:
'Ensure that the specified KMS key is accessible to the EKS service and cluster.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const clusters = await this.getClusters() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
nonCompliantResources: string[], const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value;
requiredParametersForFix: { name: string; value: string }[]
) => {
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({
@ -64,11 +127,11 @@ export class EKSClusterSecretsEncrypted implements BPSet {
encryptionConfig: [ encryptionConfig: [
{ {
resources: ['secrets'], resources: ['secrets'],
provider: { keyArn: kmsKeyId } provider: { keyArn: kmsKeyId },
} },
] ],
}) })
) );
}
} }
};
} }

View File

@ -2,61 +2,118 @@ import {
EKSClient, EKSClient,
ListClustersCommand, ListClustersCommand,
DescribeClusterCommand, DescribeClusterCommand,
UpdateClusterConfigCommand UpdateClusterConfigCommand,
} from '@aws-sdk/client-eks' } from '@aws-sdk/client-eks';
import { BPSet } from '../../types' import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'EKSEndpointNoPublicAccess',
description: 'Ensures EKS cluster endpoint does not have public access enabled.',
priority: 1,
priorityReason: 'Disabling public access to the cluster endpoint enhances security by limiting exposure to public networks.',
awsService: 'EKS',
awsServiceCategory: 'Kubernetes Service',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListClustersCommand',
reason: 'Retrieves the list of EKS clusters.',
},
{
name: 'DescribeClusterCommand',
reason: 'Fetches detailed configuration of each EKS cluster.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateClusterConfigCommand',
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.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const clusters = await this.getClusters() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { 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,49 +1,102 @@
import { import {
ElastiCacheClient, ElastiCacheClient,
DescribeCacheClustersCommand, DescribeCacheClustersCommand,
ModifyCacheClusterCommand ModifyCacheClusterCommand,
} from '@aws-sdk/client-elasticache' } from '@aws-sdk/client-elasticache';
import { BPSet } from '../../types' import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'ElastiCacheAutoMinorVersionUpgradeCheck',
description: 'Ensures that ElastiCache clusters have auto minor version upgrade enabled.',
priority: 2,
priorityReason: 'Auto minor version upgrades help ensure clusters stay up-to-date with the latest security and bug fixes.',
awsService: 'ElastiCache',
awsServiceCategory: 'Cache Service',
bestPracticeCategory: 'Reliability',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeCacheClustersCommand',
reason: 'Fetches the list and configurations of ElastiCache clusters.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyCacheClusterCommand',
reason: 'Enables auto minor version upgrade on ElastiCache clusters.',
},
],
adviseBeforeFixFunction: 'Ensure application compatibility with updated ElastiCache versions before enabling this setting.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const clusters = await this.getClusters() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { 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,60 +1,117 @@
import { import {
ElastiCacheClient, ElastiCacheClient,
DescribeReplicationGroupsCommand, DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand ModifyReplicationGroupCommand,
} from '@aws-sdk/client-elasticache' } from '@aws-sdk/client-elasticache';
import { BPSet } 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 = () => ({
name: 'ElastiCacheRedisClusterAutomaticBackupCheck',
description: 'Ensures that Redis clusters in ElastiCache have automatic backups enabled.',
priority: 2,
priorityReason: 'Automatic backups are crucial for disaster recovery and data safety.',
awsService: 'ElastiCache',
awsServiceCategory: 'Cache Service',
bestPracticeCategory: 'Reliability',
requiredParametersForFix: [
{
name: 'snapshot-retention-period',
description: 'Number of days to retain automatic snapshots.',
default: '7',
example: '7',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches details of replication groups to verify backup settings.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyReplicationGroupCommand',
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.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'snapshot-retention-period' }]
}
}
public readonly fix = async ( public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find(
param => param.name === 'snapshot-retention-period' (param) => param.name === 'snapshot-retention-period'
)?.value )?.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,49 +1,102 @@
import { import {
ElastiCacheClient, ElastiCacheClient,
DescribeReplicationGroupsCommand, DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand ModifyReplicationGroupCommand,
} from '@aws-sdk/client-elasticache' } from '@aws-sdk/client-elasticache';
import { BPSet } 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 = () => ({
name: 'ElastiCacheReplGrpAutoFailoverEnabled',
description: 'Ensures that automatic failover is enabled for ElastiCache replication groups.',
priority: 1,
priorityReason: 'Automatic failover is critical for high availability and reliability of ElastiCache clusters.',
awsService: 'ElastiCache',
awsServiceCategory: 'Cache Service',
bestPracticeCategory: 'Availability',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches replication group details to verify automatic failover settings.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyReplicationGroupCommand',
reason: 'Enables automatic failover for replication groups.',
},
],
adviseBeforeFixFunction: 'Ensure the environment supports multi-AZ configurations before enabling automatic failover.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { 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,43 +1,90 @@
import { import {
ElastiCacheClient, ElastiCacheClient,
DescribeReplicationGroupsCommand, DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand } from '@aws-sdk/client-elasticache';
} from '@aws-sdk/client-elasticache' import { BPSet, BPSetStats } from '../../types';
import { BPSet } from '../../types' import { Memorizer } from '../../Memorizer';
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 = () => ({
name: 'ElastiCacheReplGrpEncryptedAtRest',
description: 'Ensures that ElastiCache replication groups are encrypted at rest.',
priority: 1,
priorityReason: 'Encryption at rest is crucial for protecting data in compliance with security standards.',
awsService: 'ElastiCache',
awsServiceCategory: 'Cache Service',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches replication group details to verify encryption settings.',
},
],
commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Recreation of the replication group is required for encryption. Ensure data backups are available.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { 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,43 +1,90 @@
import { import {
ElastiCacheClient, ElastiCacheClient,
DescribeReplicationGroupsCommand, DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand } from '@aws-sdk/client-elasticache';
} from '@aws-sdk/client-elasticache' import { BPSet, BPSetStats } from '../../types';
import { BPSet } from '../../types' import { Memorizer } from '../../Memorizer';
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 = () => ({
name: 'ElastiCacheReplGrpEncryptedInTransit',
description: 'Ensures that ElastiCache replication groups have in-transit encryption enabled.',
priority: 1,
priorityReason: 'In-transit encryption is essential for securing data during transmission.',
awsService: 'ElastiCache',
awsServiceCategory: 'Cache Service',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeReplicationGroupsCommand',
reason: 'Fetches replication group details to verify in-transit encryption settings.',
},
],
commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Recreation of the replication group is required for enabling in-transit encryption. Ensure data backups are available.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { 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,68 +3,132 @@ import {
DescribeCacheClustersCommand, DescribeCacheClustersCommand,
DeleteCacheClusterCommand, DeleteCacheClusterCommand,
CreateCacheClusterCommand CreateCacheClusterCommand
} from '@aws-sdk/client-elasticache' } from '@aws-sdk/client-elasticache';
import { BPSet } 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 = () => ({
name: 'ElastiCacheSubnetGroupCheck',
description: 'Ensures ElastiCache clusters are not using the default subnet group.',
priority: 2,
priorityReason: 'Using the default subnet group is not recommended for production workloads.',
awsService: 'ElastiCache',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Networking',
requiredParametersForFix: [
{
name: 'subnet-group-name',
description: 'The name of the desired subnet group to associate with the cluster.',
default: '',
example: 'custom-subnet-group',
} }
],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeCacheClustersCommand',
reason: 'Fetches the details of all ElastiCache clusters to check their subnet group.',
}
],
commandUsedInFixFunction: [
{
name: 'DeleteCacheClusterCommand',
reason: 'Deletes non-compliant ElastiCache clusters.',
},
{
name: 'CreateCacheClusterCommand',
reason: 'Recreates ElastiCache clusters with the desired subnet group.',
}
],
adviseBeforeFixFunction: 'Ensure data backups are available before fixing as clusters will be deleted and recreated.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const clusters = await this.getClusters() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'subnet-group-name' }]
}
}
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' (param) => param.name === 'subnet-group-name'
)?.value )?.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(
@ -74,11 +138,13 @@ 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(group => group.SecurityGroupId) as string[], SecurityGroupIds: clusterDetails.SecurityGroups?.map(
(group) => group.SecurityGroupId
) as string[],
PreferredMaintenanceWindow: clusterDetails.PreferredMaintenanceWindow, PreferredMaintenanceWindow: clusterDetails.PreferredMaintenanceWindow,
EngineVersion: clusterDetails.EngineVersion EngineVersion: clusterDetails.EngineVersion,
}) })
) );
}
} }
};
} }

View File

@ -3,38 +3,98 @@ import {
ListPoliciesCommand, ListPoliciesCommand,
GetPolicyVersionCommand, GetPolicyVersionCommand,
DeletePolicyCommand DeletePolicyCommand
} from '@aws-sdk/client-iam' } from '@aws-sdk/client-iam';
import { BPSet } from '../../types' import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = () => ({
name: 'IAMPolicyNoStatementsWithAdminAccess',
description: 'Ensures IAM policies do not contain statements granting full administrative access.',
priority: 1,
priorityReason: 'Granting full administrative access can lead to security vulnerabilities.',
awsService: 'IAM',
awsServiceCategory: 'Security, Identity, & Compliance',
bestPracticeCategory: 'IAM',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'ListPoliciesCommand',
reason: 'Fetches all local IAM policies.',
},
{
name: 'GetPolicyVersionCommand',
reason: 'Retrieves the default version of each policy.',
},
],
commandUsedInFixFunction: [
{
name: 'DeletePolicyCommand',
reason: 'Deletes non-compliant IAM policies.',
},
],
adviseBeforeFixFunction: 'Deleting policies is irreversible. Verify policies before applying fixes.',
});
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const policies = await this.getPolicies() await this.checkImpl().then(
() => (this.stats.status = 'FINISHED'),
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 (
@ -42,26 +102,23 @@ export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
statement?.Resource === '*' && statement?.Resource === '*' &&
statement?.Effect === 'Allow' statement?.Effect === 'Allow'
) { ) {
nonCompliantResources.push(policy.Arn!) nonCompliantResources.push(policy.Arn!);
break break;
} }
} }
if (!nonCompliantResources.includes(policy.Arn!)) { if (!nonCompliantResources.includes(policy.Arn!)) {
compliantResources.push(policy.Arn!) compliantResources.push(policy.Arn!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
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

@ -1,23 +1,90 @@
import { IAMClient, ListPoliciesCommand, GetPolicyVersionCommand } from "@aws-sdk/client-iam"; import {
import { BPSet } from "../../types"; IAMClient,
ListPoliciesCommand,
GetPolicyVersionCommand,
CreatePolicyVersionCommand,
DeletePolicyVersionCommand,
} from "@aws-sdk/client-iam";
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 = {
compliantResources: [],
nonCompliantResources: [],
status: "LOADED",
errorMessage: [],
};
public readonly getMetadata = (): BPSetMetadata => ({
name: "IAMPolicyNoStatementsWithFullAccess",
description: "Ensures IAM policies do not have statements granting full access.",
priority: 2,
priorityReason: "Granting full access poses a significant security risk.",
awsService: "IAM",
awsServiceCategory: "Access Management",
bestPracticeCategory: "Security",
requiredParametersForFix: [
{
name: "policy-revision-strategy",
description: "Strategy to revise policies (e.g., remove, restrict actions)",
default: "remove",
example: "remove",
},
],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{ name: "ListPoliciesCommand", reason: "Fetch all customer-managed policies." },
{ name: "GetPolicyVersionCommand", reason: "Retrieve the default version of the policy." },
],
commandUsedInFixFunction: [
{ name: "CreatePolicyVersionCommand", reason: "Create a new policy version." },
{ name: "DeletePolicyVersionCommand", reason: "Delete outdated policy versions." },
],
adviseBeforeFixFunction:
"Ensure revised policies meet the organization's security and access requirements.",
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = "LOADED";
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
this.stats.status = "CHECKING";
await this.checkImpl()
.then(
() => {
this.stats.status = "FINISHED";
},
(err) => {
this.stats.status = "ERROR";
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = []; const compliantResources: string[] = [];
const nonCompliantResources: string[] = []; const nonCompliantResources: string[] = [];
// Fetch all customer-managed IAM policies
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) {
// Get the default version of the policy
const policyVersionResponse = await this.memoClient.send( const policyVersionResponse = await this.memoClient.send(
new GetPolicyVersionCommand({ new GetPolicyVersionCommand({
PolicyArn: policy.Arn!, PolicyArn: policy.Arn!,
@ -29,7 +96,6 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string) decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
); );
// Check statements for full access
const hasFullAccess = policyDocument.Statement.some((statement: any) => { const hasFullAccess = policyDocument.Statement.some((statement: any) => {
if (statement.Effect === "Deny") return false; if (statement.Effect === "Deny") return false;
const actions = Array.isArray(statement.Action) const actions = Array.isArray(statement.Action)
@ -45,23 +111,77 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources,
requiredParametersForFix: [],
};
}; };
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
for (const policyArn of nonCompliantResources) { await this.fixImpl(nonCompliantResources, requiredParametersForFix)
// Add logic to remove or modify the statements with full access .then(
// Note: Updating an IAM policy requires creating a new version and setting it as default () => {
console.error( this.stats.status = "FINISHED";
`Fix operation is not implemented for policy ${policyArn}. Manual intervention is required.` },
(err) => {
this.stats.status = "ERROR";
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
); );
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const strategy = requiredParametersForFix.find(
(param) => param.name === "policy-revision-strategy"
)?.value;
if (!strategy) {
throw new Error("Required parameter 'policy-revision-strategy' is missing.");
}
for (const policyArn of nonCompliantResources) {
const policyVersionResponse = await this.memoClient.send(
new GetPolicyVersionCommand({
PolicyArn: policyArn,
VersionId: "v1",
})
);
const policyDocument = JSON.parse(
decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
);
policyDocument.Statement = policyDocument.Statement.filter((statement: any) => {
if (statement.Effect === "Deny") return true;
const actions = Array.isArray(statement.Action)
? statement.Action
: [statement.Action];
return !actions.some((action: string) => action.endsWith(":*"));
});
const createVersionResponse = await this.client.send(
new CreatePolicyVersionCommand({
PolicyArn: policyArn,
PolicyDocument: JSON.stringify(policyDocument),
SetAsDefault: true,
})
);
if (createVersionResponse.PolicyVersion?.VersionId) {
await this.client.send(
new DeletePolicyVersionCommand({
PolicyArn: policyArn,
VersionId: policyVersionResponse.PolicyVersion!.VersionId,
})
);
}
} }
}; };
} }

View File

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

View File

@ -1,58 +1,136 @@
import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda' import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { BPSet } 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 getFunctions = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListFunctionsCommand({})) compliantResources: [],
return response.Functions || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaDLQCheck',
description: 'Ensures that Lambda functions have a configured Dead Letter Queue (DLQ).',
priority: 2,
priorityReason: 'A DLQ is critical for handling failed events in Lambda, enhancing reliability.',
awsService: 'Lambda',
awsServiceCategory: 'Serverless',
bestPracticeCategory: 'Reliability',
requiredParametersForFix: [
{
name: 'dlq-arn',
description: 'The ARN of the Dead Letter Queue to associate with the Lambda function.',
default: '',
example: 'arn:aws:sqs:us-east-1:123456789012:example-queue',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateFunctionConfigurationCommand',
reason: 'Update the DLQ configuration for Lambda functions.',
},
],
adviseBeforeFixFunction: 'Ensure that the specified DLQ exists and is correctly configured to handle failed events.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(
() => {
this.stats.status = 'FINISHED';
},
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
} }
);
};
public readonly check = async (): Promise<{ private readonly checkImpl = async () => {
compliantResources: string[] const compliantResources: string[] = [];
nonCompliantResources: string[] const nonCompliantResources: string[] = [];
requiredParametersForFix: { name: string }[] const functions = await this.getFunctions();
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'dlq-arn' }]
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
const dlqArn = requiredParametersForFix.find(param => param.name === 'dlq-arn')?.value await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(
() => {
this.stats.status = 'FINISHED';
},
(err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
}
);
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
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 () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}));
return response.Functions || [];
};
} }

View File

@ -2,85 +2,153 @@ import {
LambdaClient, LambdaClient,
ListFunctionsCommand, ListFunctionsCommand,
GetPolicyCommand, GetPolicyCommand,
RemovePermissionCommand RemovePermissionCommand,
} from '@aws-sdk/client-lambda' } from '@aws-sdk/client-lambda';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getFunctions = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListFunctionsCommand({})) compliantResources: [],
return response.Functions || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'LambdaFunctionPublicAccessProhibited',
nonCompliantResources: string[] description: 'Ensures that Lambda functions do not allow public access via their resource-based policies.',
requiredParametersForFix: { name: string }[] priority: 1,
}> => { priorityReason: 'Publicly accessible Lambda functions pose significant security risks.',
const compliantResources: string[] = [] awsService: 'Lambda',
const nonCompliantResources: string[] = [] awsServiceCategory: 'Serverless',
const functions = await this.getFunctions() bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.',
},
{
name: 'GetPolicyCommand',
reason: 'Fetch the resource-based policy of a Lambda function.',
},
],
commandUsedInFixFunction: [
{
name: 'RemovePermissionCommand',
reason: 'Remove public access permissions from a Lambda function.',
},
],
adviseBeforeFixFunction: 'Ensure that removing permissions does not disrupt legitimate use of the Lambda function.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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: any) => 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 any).name === 'ResourceNotFoundException') {
nonCompliantResources.push(func.FunctionArn!) compliantResources.push(func.FunctionArn!);
} else { } else {
throw error throw error;
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
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 any).name !== 'ResourceNotFoundException') {
throw error throw error;
}
} }
} }
} }
};
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}));
return response.Functions || [];
};
} }

View File

@ -1,65 +1,147 @@
import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda' import {
import { BPSet } from '../../types' LambdaClient,
import { Memorizer } from '../../Memorizer' ListFunctionsCommand,
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 getFunctions = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListFunctionsCommand({})) compliantResources: [],
return response.Functions || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'LambdaFunctionSettingsCheck',
nonCompliantResources: string[] description: 'Ensures Lambda functions have non-default timeout and memory size configurations.',
requiredParametersForFix: { name: string }[] priority: 2,
}> => { priorityReason: 'Default configurations may not be suitable for production workloads.',
const compliantResources: string[] = [] awsService: 'Lambda',
const nonCompliantResources: string[] = [] awsServiceCategory: 'Serverless',
const defaultTimeout = 3 bestPracticeCategory: 'Configuration',
const defaultMemorySize = 128 requiredParametersForFix: [
const functions = await this.getFunctions() {
name: 'timeout',
description: 'Timeout value in seconds for the Lambda function.',
default: '3',
example: '30',
},
{
name: 'memory-size',
description: 'Memory size in MB for the Lambda function.',
default: '128',
example: '256',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateFunctionConfigurationCommand',
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.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const defaultTimeout = 3;
const defaultMemorySize = 128;
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [
{ name: 'timeout' },
{ name: 'memory-size' }
]
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
const timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.value await this.fixImpl(nonCompliantResources, requiredParametersForFix)
const memorySize = requiredParametersForFix.find(param => param.name === 'memory-size')?.value .then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.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 () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}));
return response.Functions || [];
};
} }

View File

@ -2,59 +2,131 @@ import {
LambdaClient, LambdaClient,
ListFunctionsCommand, ListFunctionsCommand,
UpdateFunctionConfigurationCommand UpdateFunctionConfigurationCommand
} from '@aws-sdk/client-lambda' } from '@aws-sdk/client-lambda';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getFunctions = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListFunctionsCommand({})) compliantResources: [],
return response.Functions || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'LambdaInsideVPC',
nonCompliantResources: string[] description: 'Ensures Lambda functions are configured to run inside a VPC.',
requiredParametersForFix: { name: string }[] priority: 2,
}> => { priorityReason: 'Running Lambda inside a VPC enhances security by restricting access.',
const compliantResources: string[] = [] awsService: 'Lambda',
const nonCompliantResources: string[] = [] awsServiceCategory: 'Serverless',
const functions = await this.getFunctions() bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'subnet-ids',
description: 'Comma-separated list of VPC subnet IDs.',
default: '',
example: 'subnet-abc123,subnet-def456',
},
{
name: 'security-group-ids',
description: 'Comma-separated list of VPC security group IDs.',
default: '',
example: 'sg-abc123,sg-def456',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListFunctionsCommand',
reason: 'Retrieve all Lambda functions in the account.',
},
],
commandUsedInFixFunction: [
{
name: 'UpdateFunctionConfigurationCommand',
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.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [
{ name: 'subnet-ids' },
{ name: 'security-group-ids' }
]
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
const subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-ids')?.value await this.fixImpl(nonCompliantResources, requiredParametersForFix)
const securityGroupIds = requiredParametersForFix.find(param => param.name === 'security-group-ids')?.value .then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-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,
@ -63,7 +135,12 @@ export class LambdaInsideVPC implements BPSet {
SecurityGroupIds: securityGroupIds.split(',') SecurityGroupIds: securityGroupIds.split(',')
} }
}) })
) );
}
} }
};
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}));
return response.Functions || [];
};
} }

View File

@ -2,79 +2,161 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { import {
BackupClient, BackupClient,
ListRecoveryPointsByResourceCommand ListRecoveryPointsByResourceCommand
} from '@aws-sdk/client-backup' } from '@aws-sdk/client-backup';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
private readonly getRecoveryPoints = async (resourceArn: string) => { public readonly getMetadata = (): BPSetMetadata => ({
const response = await this.backupClient.send( name: 'AuroraLastBackupRecoveryPointCreated',
new ListRecoveryPointsByResourceCommand({ ResourceArn: resourceArn }) description: 'Ensures that Aurora DB clusters have a recovery point created within the last 24 hours.',
) priority: 1,
return response.RecoveryPoints || [] priorityReason: 'Ensuring regular backups protects against data loss.',
awsService: 'RDS',
awsServiceCategory: 'Aurora',
bestPracticeCategory: 'Backup',
requiredParametersForFix: [
{
name: 'backup-retention-period',
description: 'The number of days to retain backups for the DB cluster.',
default: '7',
example: '7',
} }
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'Retrieve information about Aurora DB clusters.',
},
{
name: 'ListRecoveryPointsByResourceCommand',
reason: 'Check the recovery points associated with the DB cluster.',
}
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
reason: 'Update the backup retention period for the DB cluster.',
}
],
adviseBeforeFixFunction: 'Ensure that extending the backup retention period aligns with your data protection policies.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 && recoveryDates.length > 0 &&
new Date().getTime() - recoveryDates[0].getTime() < 24 * 60 * 60 * 1000 new Date().getTime() - recoveryDates[0].getTime() < 24 * 60 * 60 * 1000
) { ) {
compliantResources.push(cluster.DBClusterArn!) compliantResources.push(cluster.DBClusterArn!);
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!) nonCompliantResources.push(cluster.DBClusterArn!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'backup-retention-period', value: '7' }]
}
}
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)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find(
param => param.name === 'backup-retention-period' param => param.name === 'backup-retention-period'
)?.value )?.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 () => {
const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
private readonly getRecoveryPoints = async (resourceArn: string) => {
const response = await this.backupClient.send(
new ListRecoveryPointsByResourceCommand({ ResourceArn: resourceArn })
);
return response.RecoveryPoints || [];
};
} }

View File

@ -2,51 +2,122 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'AuroraMySQLBacktrackingEnabled',
description: 'Ensures that backtracking is enabled for Aurora MySQL clusters.',
priority: 1,
priorityReason: 'Enabling backtracking provides point-in-time recovery for Aurora MySQL databases.',
awsService: 'RDS',
awsServiceCategory: 'Aurora',
bestPracticeCategory: 'Data Protection',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'Fetch Aurora MySQL DB clusters and check backtracking configuration.',
},
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
reason: 'Enable backtracking for non-compliant Aurora MySQL DB clusters.',
},
],
adviseBeforeFixFunction: 'Ensure that enabling backtracking aligns with your application requirements.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
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 () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -2,59 +2,137 @@ import {
RDSClient, RDSClient,
DescribeDBInstancesCommand, DescribeDBInstancesCommand,
ModifyDBInstanceCommand ModifyDBInstanceCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBInstances = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({})) compliantResources: [],
return response.DBInstances || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'DBInstanceBackupEnabled',
description: 'Ensures that backups are enabled for RDS instances.',
priority: 1,
priorityReason: 'Enabling backups is critical for data recovery and compliance.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Data Protection',
requiredParametersForFix: [
{
name: 'retention-period',
description: 'The number of days to retain backups.',
default: '7',
example: '7'
} }
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBInstancesCommand',
reason: 'Fetch information about RDS instances to check backup retention.'
}
],
commandUsedInFixFunction: [
{
name: 'ModifyDBInstanceCommand',
reason: 'Enable backup retention for non-compliant RDS instances.'
}
],
adviseBeforeFixFunction: 'Ensure the retention period aligns with your organizations backup policy.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbInstances = await this.getDBInstances() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'retention-period', value: '7' }]
}
}
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)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const retentionPeriod = requiredParametersForFix.find( const retentionPeriod = requiredParametersForFix.find(
param => param.name === 'retention-period' (param) => param.name === 'retention-period'
)?.value )?.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 () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({}));
return response.DBInstances || [];
};
} }

View File

@ -2,49 +2,120 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterAutoMinorVersionUpgradeEnabled',
description: 'Ensures Auto Minor Version Upgrade is enabled for RDS clusters.',
priority: 1,
priorityReason: 'Auto minor version upgrades help keep the database engine updated with minimal effort, ensuring security and performance improvements.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Configuration Management',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'Fetch information about RDS clusters to check auto minor version upgrade setting.'
} }
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
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.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
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 () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -2,66 +2,147 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterDefaultAdminCheck',
description: 'Ensures that RDS clusters do not use default administrative usernames (e.g., admin, postgres).',
priority: 2,
priorityReason: 'Using default administrative usernames increases the risk of brute force attacks.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'new-master-username',
description: 'The new master username for the RDS cluster.',
default: '',
example: 'secureAdminUser'
},
{
name: 'new-master-password',
description: 'The new master password for the RDS cluster.',
default: '',
example: 'SecureP@ssword123'
} }
],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'Fetches information about RDS clusters, including their master username.'
}
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
reason: 'Updates the master user password for non-compliant RDS clusters.'
}
],
adviseBeforeFixFunction: 'Ensure that the new master username and password comply with your security policies.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [
{ name: 'new-master-username', value: '<NEW_MASTER_USERNAME>' },
{ name: 'new-master-password', value: '<NEW_MASTER_PASSWORD>' }
]
}
}
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)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const newMasterUsername = requiredParametersForFix.find( const newMasterUsername = requiredParametersForFix.find(
param => param.name === 'new-master-username' (param) => param.name === 'new-master-username'
)?.value )?.value;
const newMasterPassword = requiredParametersForFix.find( const newMasterPassword = requiredParametersForFix.find(
param => param.name === 'new-master-password' (param) => param.name === 'new-master-password'
)?.value )?.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 () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -2,49 +2,120 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterDeletionProtectionEnabled',
description: 'Ensures that RDS clusters have deletion protection enabled.',
priority: 2,
priorityReason: 'Deletion protection helps to prevent accidental deletion of critical database clusters.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Resilience',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'To fetch details about RDS clusters, including their deletion protection status.'
} }
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
reason: 'To enable deletion protection on non-compliant RDS clusters.'
}
],
adviseBeforeFixFunction: 'Ensure that enabling deletion protection aligns with your operational policies.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
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 () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -1,43 +1,99 @@
import { import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand
ModifyDBClusterCommand } from '@aws-sdk/client-rds';
} from '@aws-sdk/client-rds' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { BPSet } from '../../types' import { Memorizer } from '../../Memorizer';
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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterEncryptedAtRest',
description: 'Ensures that RDS clusters have encryption at rest enabled.',
priority: 1,
priorityReason: 'Encryption at rest is critical for data security and compliance.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'To check the encryption status of RDS clusters.'
} }
],
commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Manually recreate the RDS cluster with encryption at rest enabled, as fixing this requires destructive operations.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
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 () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -2,52 +2,128 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterIAMAuthenticationEnabled',
description: 'Ensures that IAM Database Authentication is enabled for RDS clusters.',
priority: 1,
priorityReason: 'IAM Authentication enhances security by allowing fine-grained access control.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'To fetch the list of RDS clusters and their IAM authentication status.'
} }
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
reason: 'To enable IAM Database Authentication for non-compliant clusters.'
}
],
adviseBeforeFixFunction: 'Ensure that enabling IAM Database Authentication aligns with your security and application needs.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const dbClusters = await this.getDBClusters();
for (const cluster of dbClusters) { for (const cluster of dbClusters) {
if ( if (
cluster.Engine === 'docdb' || cluster.Engine === 'docdb' ||
cluster.IAMDatabaseAuthenticationEnabled cluster.IAMDatabaseAuthenticationEnabled
) { ) {
compliantResources.push(cluster.DBClusterArn!) compliantResources.push(cluster.DBClusterArn!);
} else { } else {
nonCompliantResources.push(cluster.DBClusterArn!) nonCompliantResources.push(cluster.DBClusterArn!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
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 () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -1,43 +1,121 @@
import { import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand
ModifyDBClusterCommand } from '@aws-sdk/client-rds';
} from '@aws-sdk/client-rds' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { BPSet } from '../../types' import { Memorizer } from '../../Memorizer';
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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSClusterMultiAZEnabled',
description: 'Ensures that RDS clusters are deployed across multiple availability zones.',
priority: 1,
priorityReason: 'Multi-AZ deployment improves availability and resilience of RDS clusters.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Availability',
requiredParametersForFix: [
{
name: 'additional-azs',
description: 'Number of additional availability zones to add for Multi-AZ configuration.',
default: '2',
example: '2'
} }
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'To fetch the list of RDS clusters and their availability zone configuration.'
}
],
commandUsedInFixFunction: [],
adviseBeforeFixFunction:
'Enabling Multi-AZ for an existing cluster may require significant reconfiguration and potential downtime. Proceed with caution.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbClusters = await this.getDBClusters() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'additional-azs', value: '2' }]
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
throw new Error( throw new Error(
'Enabling Multi-AZ requires cluster reconfiguration. This must be performed manually.' 'Enabling Multi-AZ requires cluster reconfiguration. This must be performed manually.'
) );
} };
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -2,63 +2,140 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2' import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSDBSecurityGroupNotAllowed',
description: 'Ensures RDS clusters are not associated with the default security group.',
priority: 2,
priorityReason: 'Default security groups may allow unrestricted access, posing a security risk.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'Fetch RDS cluster details including associated security groups.'
},
{
name: 'DescribeSecurityGroupsCommand',
reason: 'Fetch details of default security groups.'
}
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
reason: 'Remove default security groups from the RDS cluster configuration.'
}
],
adviseBeforeFixFunction:
'Ensure valid non-default security groups are associated with the clusters before applying the fix.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const dbClusters = await this.getDBClusters();
const defaultSecurityGroupIds = (await this.getDefaultSecurityGroups()).map(sg => sg.GroupId!);
for (const cluster of dbClusters) {
const activeSecurityGroups = cluster.VpcSecurityGroups?.filter(sg => sg.Status === 'active') || [];
if (activeSecurityGroups.some(sg => defaultSecurityGroupIds.includes(sg.VpcSecurityGroupId!))) {
nonCompliantResources.push(cluster.DBClusterArn!);
} else {
compliantResources.push(cluster.DBClusterArn!);
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1];
await this.rdsClient.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
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 || [];
} };
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
const defaultSecurityGroupIds = (await this.getDefaultSecurityGroups()).map(sg => sg.GroupId!)
for (const cluster of dbClusters) {
const activeSecurityGroups = cluster.VpcSecurityGroups?.filter(sg => sg.Status === 'active') || []
if (activeSecurityGroups.some(sg => defaultSecurityGroupIds.includes(sg.VpcSecurityGroupId!))) {
nonCompliantResources.push(cluster.DBClusterArn!)
} else {
compliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]
// Remove default security groups by modifying the cluster's security group configuration
await this.rdsClient.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
VpcSecurityGroupIds: [] // Update to valid non-default security groups
})
)
}
}
} }

View File

@ -2,60 +2,140 @@ import {
RDSClient, RDSClient,
DescribeDBInstancesCommand, DescribeDBInstancesCommand,
ModifyDBInstanceCommand ModifyDBInstanceCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBInstances = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({})) compliantResources: [],
return response.DBInstances || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSEnhancedMonitoringEnabled',
description: 'Ensures that Enhanced Monitoring is enabled for RDS instances.',
priority: 2,
priorityReason: 'Enhanced Monitoring provides valuable metrics for better monitoring and troubleshooting.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Monitoring',
requiredParametersForFix: [
{
name: 'monitoring-interval',
description: 'The interval in seconds for Enhanced Monitoring.',
default: '60',
example: '60'
} }
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBInstancesCommand',
reason: 'Fetch RDS instance details including monitoring configuration.'
}
],
commandUsedInFixFunction: [
{
name: 'ModifyDBInstanceCommand',
reason: 'Enable Enhanced Monitoring for non-compliant RDS instances.'
}
],
adviseBeforeFixFunction: 'Ensure that enabling Enhanced Monitoring does not conflict with existing configurations.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbInstances = await this.getDBInstances() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'monitoring-interval', value: '60' }]
}
}
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';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => { ) => {
const monitoringInterval = requiredParametersForFix.find( const monitoringInterval = requiredParametersForFix.find(
param => param.name === 'monitoring-interval' (param) => param.name === 'monitoring-interval'
)?.value )?.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 () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({}));
return response.DBInstances || [];
};
} }

View File

@ -2,49 +2,125 @@ import {
RDSClient, RDSClient,
DescribeDBInstancesCommand, DescribeDBInstancesCommand,
ModifyDBInstanceCommand ModifyDBInstanceCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBInstances = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({})) compliantResources: [],
return response.DBInstances || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSInstancePublicAccessCheck',
description: 'Ensures RDS instances are not publicly accessible.',
priority: 1,
priorityReason: 'Publicly accessible RDS instances expose databases to potential security risks.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBInstancesCommand',
reason: 'Fetches the list of RDS instances and their public access settings.'
} }
],
commandUsedInFixFunction: [
{
name: 'ModifyDBInstanceCommand',
reason: 'Disables public access for non-compliant RDS instances.'
}
],
adviseBeforeFixFunction: 'Ensure there are valid private network configurations in place before disabling public access.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const dbInstances = await this.getDBInstances() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
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 () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({}));
return response.DBInstances || [];
};
} }

View File

@ -2,69 +2,147 @@ import {
RDSClient, RDSClient,
DescribeDBClustersCommand, DescribeDBClustersCommand,
ModifyDBClusterCommand ModifyDBClusterCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusters = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClustersCommand({})) compliantResources: [],
return response.DBClusters || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSLoggingEnabled',
description: 'Ensures that logging is enabled for RDS clusters.',
priority: 1,
priorityReason: 'Enabling logs ensures visibility into database activities for security and troubleshooting.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Monitoring',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClustersCommand',
reason: 'Fetches the list of RDS clusters and their logging configurations.'
} }
],
commandUsedInFixFunction: [
{
name: 'ModifyDBClusterCommand',
reason: 'Enables CloudWatch logging for non-compliant RDS clusters.'
}
],
adviseBeforeFixFunction: 'Ensure that enabling logs does not negatively impact application performance.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const logsForEngine = { await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: 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 cluster of dbClusters) { for (const cluster of dbClusters) {
if ( const requiredLogs = logsForEngine[cluster.Engine!] || [];
JSON.stringify(cluster.EnabledCloudwatchLogsExports || []) === const enabledLogs = cluster.EnabledCloudwatchLogsExports || [];
JSON.stringify((logsForEngine as any)[cluster.Engine!] || [])
) { 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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
for (const arn of nonCompliantResources) { nonCompliantResources: string[],
const clusterId = arn.split(':cluster/')[1] requiredParametersForFix: { name: string; value: string }[]
const logsForEngine = { ) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: 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();
const cluster = dbClusters.find(c => c.DBClusterArn === arn)
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1];
const cluster = dbClusters.find((c) => c.DBClusterArn === arn);
if (cluster) { if (cluster) {
const logsToEnable = (logsForEngine as any)[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 () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
} }

View File

@ -2,53 +2,128 @@ import {
RDSClient, RDSClient,
DescribeDBClusterSnapshotsCommand, DescribeDBClusterSnapshotsCommand,
CopyDBClusterSnapshotCommand CopyDBClusterSnapshotCommand
} from '@aws-sdk/client-rds' } from '@aws-sdk/client-rds';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getDBClusterSnapshots = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new DescribeDBClusterSnapshotsCommand({})) compliantResources: [],
return response.DBClusterSnapshots || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'RDSSnapshotEncrypted',
description: 'Ensures RDS cluster snapshots are encrypted.',
priority: 1,
priorityReason: 'Encryption ensures data security and compliance with regulations.',
awsService: 'RDS',
awsServiceCategory: 'Database',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'kms-key-id',
description: 'KMS key ID to encrypt the snapshot.',
default: '',
example: 'arn:aws:kms:region:account-id:key/key-id'
} }
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'DescribeDBClusterSnapshotsCommand',
reason: 'Fetches RDS cluster snapshots and their encryption status.'
}
],
commandUsedInFixFunction: [
{
name: 'CopyDBClusterSnapshotCommand',
reason: 'Copies the snapshot with encryption enabled using the provided KMS key.'
}
],
adviseBeforeFixFunction: 'Ensure that the KMS key is properly configured and accessible.'
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources = [] this.stats.status = 'CHECKING';
const nonCompliantResources = []
const snapshots = await this.getDBClusterSnapshots() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [
{ name: 'kms-key-id', value: '<KMS_KEY_ID>' } // Replace with your KMS key ID
]
}
}
public readonly fix = async ( public readonly fix = 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 this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
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 snapshotId = arn.split(':snapshot:')[1] const snapshotId = arn.split(':snapshot:')[1];
await this.client.send( await this.client.send(
new CopyDBClusterSnapshotCommand({ new CopyDBClusterSnapshotCommand({
@ -56,7 +131,12 @@ export class RDSSnapshotEncrypted implements BPSet {
TargetDBClusterSnapshotIdentifier: `${snapshotId}-encrypted`, TargetDBClusterSnapshotIdentifier: `${snapshotId}-encrypted`,
KmsKeyId: kmsKeyId KmsKeyId: kmsKeyId
}) })
) );
}
} }
};
private readonly getDBClusterSnapshots = async () => {
const response = await this.memoClient.send(new DescribeDBClusterSnapshotsCommand({}));
return response.DBClusterSnapshots || [];
};
} }

View File

@ -3,68 +3,145 @@ 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 } 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 getAccountId = async (): Promise<string> => { private readonly stats: BPSetStats = {
const response = await this.stsClient.send(new GetCallerIdentityCommand({})) compliantResources: [],
return response.Account! nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3AccessPointInVpcOnly',
description: 'Ensures that all S3 access points are restricted to a VPC.',
priority: 1,
priorityReason: 'Restricting access points to a VPC ensures enhanced security and control.',
awsService: 'S3',
awsServiceCategory: 'Access Points',
bestPracticeCategory: 'Security',
requiredParametersForFix: [
{
name: 'your-vpc-id',
description: 'The VPC ID to associate with the access points.',
default: '',
example: 'vpc-1234567890abcdef'
} }
],
isFixFunctionUsesDestructiveCommand: true,
commandUsedInCheckFunction: [
{
name: 'ListAccessPointsCommand',
reason: 'Lists all S3 access points in the account.'
}
],
commandUsedInFixFunction: [
{
name: 'DeleteAccessPointCommand',
reason: 'Deletes access points that are not restricted to a VPC.'
},
{
name: 'CreateAccessPointCommand',
reason: 'Recreates access points with a VPC configuration.'
}
],
adviseBeforeFixFunction: 'Ensure that the specified VPC ID is correct and accessible.'
});
public readonly check = async (): Promise<{ public readonly getStats = () => this.stats;
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const requiredParametersForFix = [{ name: 'your-vpc-id' }]
const accountId = await this.getAccountId() public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!);
} }
} }
return { compliantResources, nonCompliantResources, requiredParametersForFix } this.stats.compliantResources = compliantResources;
} 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 }[]
): Promise<void> => { ) => {
const accountId = await this.getAccountId() this.stats.status = 'CHECKING';
const vpcId = requiredParametersForFix.find(param => param.name === 'your-vpc-id')?.value
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const accountId = await this.getAccountId();
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({
@ -75,7 +152,12 @@ export class S3AccessPointInVpcOnly implements BPSet {
VpcId: vpcId VpcId: vpcId
} }
}) })
) );
}
} }
};
private readonly getAccountId = async (): Promise<string> => {
const response = await this.stsClient.send(new GetCallerIdentityCommand({}));
return response.Account!;
};
} }

View File

@ -3,56 +3,120 @@ import {
ListBucketsCommand, ListBucketsCommand,
GetObjectLockConfigurationCommand, GetObjectLockConfigurationCommand,
PutObjectLockConfigurationCommand PutObjectLockConfigurationCommand
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BPSet } 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketDefaultLockEnabled',
description: 'Ensures that all S3 buckets have default object lock configuration enabled.',
priority: 2,
priorityReason: 'Object lock configuration ensures immutability of bucket objects, protecting them from unintended deletions or modifications.',
awsService: 'S3',
awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Data Protection',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetObjectLockConfigurationCommand',
reason: 'Checks if the object lock configuration is enabled for the bucket.'
} }
],
commandUsedInFixFunction: [
{
name: 'PutObjectLockConfigurationCommand',
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.'
});
public readonly check = async (): Promise<{ public readonly getStats = () => this.stats;
compliantResources: string[]
nonCompliantResources: string[] public readonly clearStats = () => {
requiredParametersForFix: { name: string }[] this.stats.compliantResources = [];
}> => { this.stats.nonCompliantResources = [];
const compliantResources: string[] = [] this.stats.status = 'LOADED';
const nonCompliantResources: string[] = [] this.stats.errorMessage = [];
const buckets = await this.getBuckets() };
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 any).name === 'ObjectLockConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else { } else {
throw error throw error;
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
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,
@ -66,7 +130,12 @@ export class S3BucketDefaultLockEnabled implements BPSet {
} }
} }
}) })
) );
}
} }
};
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
} }

View File

@ -3,62 +3,126 @@ import {
ListBucketsCommand, ListBucketsCommand,
GetPublicAccessBlockCommand, GetPublicAccessBlockCommand,
PutPublicAccessBlockCommand PutPublicAccessBlockCommand
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BPSet } 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketLevelPublicAccessProhibited',
description: 'Ensures that S3 buckets have public access blocked at the bucket level.',
priority: 1,
priorityReason: 'Blocking public access at the bucket level ensures security and prevents unauthorized access.',
awsService: 'S3',
awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetPublicAccessBlockCommand',
reason: 'Retrieves the public access block configuration for the bucket.'
} }
],
commandUsedInFixFunction: [
{
name: 'PutPublicAccessBlockCommand',
reason: 'Enforces public access block configuration at the bucket level.'
}
],
adviseBeforeFixFunction: 'Ensure no legitimate use cases require public access before applying this fix.'
});
public readonly check = async (): Promise<{ public readonly getStats = () => this.stats;
compliantResources: string[]
nonCompliantResources: string[] public readonly clearStats = () => {
requiredParametersForFix: { name: string }[] this.stats.compliantResources = [];
}> => { this.stats.nonCompliantResources = [];
const compliantResources: string[] = [] this.stats.status = 'LOADED';
const nonCompliantResources: string[] = [] this.stats.errorMessage = [];
const buckets = await this.getBuckets() };
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 (error) { } catch {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message
});
});
};
private readonly fixImpl = async (
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,
@ -69,7 +133,12 @@ export class S3BucketLevelPublicAccessProhibited implements BPSet {
RestrictPublicBuckets: true RestrictPublicBuckets: true
} }
}) })
) );
}
} }
};
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
} }

View File

@ -2,72 +2,154 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketLoggingCommand, GetBucketLoggingCommand,
PutBucketLoggingCommand PutBucketLoggingCommand,
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'S3BucketLoggingEnabled',
nonCompliantResources: string[] description: 'Ensures that S3 buckets have logging enabled.',
requiredParametersForFix: { name: string }[] priority: 2,
}> => { priorityReason:
const compliantResources: string[] = [] 'Enabling logging on S3 buckets provides audit and security capabilities.',
const nonCompliantResources: string[] = [] awsService: 'S3',
const buckets = await this.getBuckets() awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Logging',
requiredParametersForFix: [
{
name: 'log-destination-bucket',
description: 'The bucket where access logs should be stored.',
default: '',
example: 'my-log-bucket',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetBucketLoggingCommand',
reason: 'Retrieves the logging configuration for the bucket.',
},
],
commandUsedInFixFunction: [
{
name: 'PutBucketLoggingCommand',
reason: 'Enables logging on the bucket.',
},
],
adviseBeforeFixFunction:
'Ensure the destination bucket for logs exists and has proper permissions for logging.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const buckets = await this.getBuckets();
for (const bucket of buckets) { for (const bucket of buckets) {
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) {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'log-destination-bucket' }]
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
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 () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
} }

View File

@ -2,19 +2,176 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketPolicyCommand, GetBucketPolicyCommand,
PutBucketPolicyCommand PutBucketPolicyCommand,
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketSSLRequestsOnly',
description: 'Ensures that all S3 bucket requests are made using SSL.',
priority: 2,
priorityReason: 'SSL ensures secure data transmission to and from S3 buckets.',
awsService: 'S3',
awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetBucketPolicyCommand',
reason: 'Retrieves the bucket policy to check for SSL conditions.',
},
],
commandUsedInFixFunction: [
{
name: 'PutBucketPolicyCommand',
reason: 'Updates the bucket policy to enforce SSL requests.',
},
],
adviseBeforeFixFunction:
'Ensure existing bucket policies will not conflict with the SSL-only policy.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const buckets = await this.getBuckets();
for (const bucket of buckets) {
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucket.Name! })
);
const policy = JSON.parse(response.Policy!);
const hasSSLCondition = policy.Statement.some(
(stmt: any) =>
stmt.Condition &&
stmt.Condition.Bool &&
stmt.Condition.Bool['aws:SecureTransport'] === 'false'
);
if (hasSSLCondition) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} }
} catch (error) {
if ((error as any).name === 'NoSuchBucketPolicy') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
throw error;
}
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!;
let existingPolicy: any;
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucketName })
);
existingPolicy = JSON.parse(response.Policy!);
} catch (error) {
if ((error as any).name !== 'NoSuchBucketPolicy') {
throw error;
}
}
const sslPolicyStatement = {
Sid: 'DenyNonSSLRequests',
Effect: 'Deny',
Principal: '*',
Action: 's3:*',
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
};
let updatedPolicy;
if (existingPolicy) {
existingPolicy.Statement.push(sslPolicyStatement);
updatedPolicy = JSON.stringify(existingPolicy);
} else {
updatedPolicy = this.createSSLOnlyPolicy(bucketName);
}
await this.client.send(
new PutBucketPolicyCommand({
Bucket: bucketName,
Policy: updatedPolicy,
})
);
}
};
private readonly createSSLOnlyPolicy = (bucketName: string): string => { private readonly createSSLOnlyPolicy = (bucketName: string): string => {
return JSON.stringify({ return JSON.stringify({
@ -28,103 +185,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',
} },
} },
} },
] ],
}) });
} };
public readonly check = async (): Promise<{ private readonly getBuckets = async () => {
compliantResources: string[] const response = await this.memoClient.send(new ListBucketsCommand({}));
nonCompliantResources: string[] return response.Buckets || [];
requiredParametersForFix: { name: string }[] };
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucket.Name! })
)
const policy = JSON.parse(response.Policy!)
const hasSSLCondition = policy.Statement.some(
(stmt: any) =>
stmt.Condition &&
stmt.Condition.Bool &&
stmt.Condition.Bool['aws:SecureTransport'] === 'false'
)
if (hasSSLCondition) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
} catch (error) {
if ((error as any).name === 'NoSuchBucketPolicy') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
let existingPolicy: any
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucketName })
)
existingPolicy = JSON.parse(response.Policy!)
} catch (error) {
if ((error as any).name !== 'NoSuchBucketPolicy') {
throw error
}
}
const sslPolicyStatement = {
Sid: 'DenyNonSSLRequests',
Effect: 'Deny',
Principal: '*',
Action: 's3:*',
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: {
Bool: {
'aws:SecureTransport': 'false'
}
}
}
let updatedPolicy
if (existingPolicy) {
existingPolicy.Statement.push(sslPolicyStatement)
updatedPolicy = JSON.stringify(existingPolicy)
} else {
updatedPolicy = this.createSSLOnlyPolicy(bucketName)
}
await this.client.send(
new PutBucketPolicyCommand({
Bucket: bucketName,
Policy: updatedPolicy
})
)
}
}
} }

View File

@ -2,61 +2,130 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketVersioningCommand, GetBucketVersioningCommand,
PutBucketVersioningCommand PutBucketVersioningCommand,
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BPSet } 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'S3BucketVersioningEnabled',
nonCompliantResources: string[] description: 'Ensures that versioning is enabled on all S3 buckets.',
requiredParametersForFix: { name: string }[] priority: 1,
}> => { priorityReason: 'Enabling versioning protects against accidental data loss and allows recovery of previous versions.',
const compliantResources: string[] = [] awsService: 'S3',
const nonCompliantResources: string[] = [] awsServiceCategory: 'Buckets',
const buckets = await this.getBuckets() bestPracticeCategory: 'Data Protection',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetBucketVersioningCommand',
reason: 'Retrieve the current versioning status of the bucket.',
},
],
commandUsedInFixFunction: [
{
name: 'PutBucketVersioningCommand',
reason: 'Enable versioning on the bucket.',
},
],
adviseBeforeFixFunction: 'Ensure that enabling versioning aligns with your data lifecycle and cost considerations.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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!}`);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
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 () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
} }

View File

@ -2,74 +2,146 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketEncryptionCommand, GetBucketEncryptionCommand,
PutBucketEncryptionCommand PutBucketEncryptionCommand,
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BPSet } 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'S3DefaultEncryptionKMS',
nonCompliantResources: string[] description: 'Ensures that all S3 buckets have default encryption enabled using AWS KMS.',
requiredParametersForFix: { name: string }[] priority: 1,
}> => { priorityReason: 'Default encryption protects sensitive data stored in S3 buckets.',
const compliantResources: string[] = [] awsService: 'S3',
const nonCompliantResources: string[] = [] awsServiceCategory: 'Buckets',
const buckets = await this.getBuckets() bestPracticeCategory: 'Data Protection',
requiredParametersForFix: [
{
name: 'kms-key-id',
description: 'The KMS Key ID used for bucket encryption.',
default: '',
example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ab-cdef-EXAMPLE12345',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetBucketEncryptionCommand',
reason: 'Retrieve the encryption configuration for a bucket.',
},
],
commandUsedInFixFunction: [
{
name: 'PutBucketEncryptionCommand',
reason: 'Enable KMS encryption for the bucket.',
},
],
adviseBeforeFixFunction:
'Ensure the KMS key is properly configured with necessary permissions for S3 operations.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 any).name === 'ServerSideEncryptionConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else { } else {
throw error throw error;
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
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,
@ -78,13 +150,18 @@ export class S3DefaultEncryptionKMS implements BPSet {
{ {
ApplyServerSideEncryptionByDefault: { ApplyServerSideEncryptionByDefault: {
SSEAlgorithm: 'aws:kms', SSEAlgorithm: 'aws:kms',
KMSMasterKeyID: kmsKeyId KMSMasterKeyID: kmsKeyId,
} },
} },
] ],
} },
}) })
) );
}
} }
};
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
} }

View File

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

View File

@ -1,52 +1,107 @@
import { import {
S3Client, S3Client,
ListBucketsCommand ListBucketsCommand,
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup' import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'S3LastBackupRecoveryPointCreated',
nonCompliantResources: string[] description: 'Ensures that S3 buckets have recent backup recovery points.',
requiredParametersForFix: { name: string }[] priority: 2,
}> => { priorityReason: 'Backup recovery points are critical for disaster recovery and data resilience.',
const compliantResources: string[] = [] awsService: 'S3',
const nonCompliantResources: string[] = [] awsServiceCategory: 'Buckets',
const buckets = await this.getBuckets() bestPracticeCategory: 'Backup & Recovery',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListRecoveryPointsByResourceCommand',
reason: 'Checks for recent recovery points for the S3 bucket.',
},
],
commandUsedInFixFunction: [],
adviseBeforeFixFunction: 'Ensure the backup plan for S3 buckets is appropriately configured before proceeding.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 (recoveryPoints.RecoveryPoints && recoveryPoints.RecoveryPoints.length > 0) { if (
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) recoveryPoints.RecoveryPoints &&
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!}`);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (): Promise<void> => { public readonly fix = async () => {
throw new Error('Fixing recovery points requires custom implementation for backup setup.') this.stats.status = 'ERROR';
} this.stats.errorMessage.push({
date: new Date(),
message: 'Fixing recovery points requires custom implementation for backup setup.',
});
throw new Error(
'Fixing recovery points requires custom implementation for backup setup.'
);
};
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
} }

View File

@ -2,73 +2,149 @@ import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
GetBucketLifecycleConfigurationCommand, GetBucketLifecycleConfigurationCommand,
PutBucketLifecycleConfigurationCommand PutBucketLifecycleConfigurationCommand,
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' import { Memorizer } from '../../Memorizer';
export class S3LifecyclePolicyCheck implements BPSet { export class S3LifecyclePolicyCheck 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 getBuckets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListBucketsCommand({})) compliantResources: [],
return response.Buckets || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{ public readonly getMetadata = (): BPSetMetadata => ({
compliantResources: string[] name: 'S3LifecyclePolicyCheck',
nonCompliantResources: string[] description: 'Ensures that all S3 buckets have lifecycle policies configured.',
requiredParametersForFix: { name: string }[] priority: 2,
}> => { priorityReason:
const compliantResources: string[] = [] 'Lifecycle policies help manage storage costs by automatically transitioning or expiring objects.',
const nonCompliantResources: string[] = [] awsService: 'S3',
const buckets = await this.getBuckets() awsServiceCategory: 'Buckets',
bestPracticeCategory: 'Cost Management',
requiredParametersForFix: [
{
name: 'lifecycle-policy-rule-id',
description: 'The ID of the lifecycle policy rule.',
default: '',
example: 'expire-old-objects',
},
{
name: 'expiration-days',
description: 'Number of days after which objects should expire.',
default: '30',
example: '30',
},
],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'GetBucketLifecycleConfigurationCommand',
reason: 'To determine if the bucket has a lifecycle policy configured.',
},
],
commandUsedInFixFunction: [
{
name: 'PutBucketLifecycleConfigurationCommand',
reason: 'To configure a lifecycle policy for the bucket.',
},
],
adviseBeforeFixFunction:
'Ensure that the lifecycle policy settings align with organizational storage management policies.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => {
this.stats.status = 'CHECKING';
await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
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 GetBucketLifecycleConfigurationCommand({ Bucket: bucket.Name! }) new GetBucketLifecycleConfigurationCommand({ 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 === 'NoSuchLifecycleConfiguration') { if ((error as any).name === 'NoSuchLifecycleConfiguration') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else { } else {
throw error throw error;
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: [
{ name: 'lifecycle-policy-rule-id' },
{ name: 'expiration-days' }
]
}
}
public readonly fix = async ( public readonly fix = async (
nonCompliantResources: string[], nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[] requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => { ) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const ruleId = requiredParametersForFix.find( const ruleId = requiredParametersForFix.find(
param => param.name === 'lifecycle-policy-rule-id' (param) => param.name === 'lifecycle-policy-rule-id'
)?.value )?.value;
const expirationDays = requiredParametersForFix.find( const expirationDays = requiredParametersForFix.find(
param => param.name === 'expiration-days' (param) => param.name === 'expiration-days'
)?.value )?.value;
if (!ruleId || !expirationDays) { if (!ruleId || !expirationDays) {
throw new Error( throw new Error(
"Required parameters 'lifecycle-policy-rule-id' and/or 'expiration-days' are missing." "Required parameters 'lifecycle-policy-rule-id' and/or 'expiration-days' 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 PutBucketLifecycleConfigurationCommand({ new PutBucketLifecycleConfigurationCommand({
Bucket: bucketName, Bucket: bucketName,
@ -78,13 +154,18 @@ export class S3LifecyclePolicyCheck implements BPSet {
ID: ruleId, ID: ruleId,
Status: 'Enabled', Status: 'Enabled',
Expiration: { Expiration: {
Days: parseInt(expirationDays, 10) Days: parseInt(expirationDays, 10),
} },
} },
] ],
} },
}) })
) );
}
} }
};
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
} }

View File

@ -2,47 +2,124 @@ import {
SecretsManagerClient, SecretsManagerClient,
ListSecretsCommand, ListSecretsCommand,
RotateSecretCommand, RotateSecretCommand,
UpdateSecretCommand UpdateSecretCommand,
} from '@aws-sdk/client-secrets-manager' } from '@aws-sdk/client-secrets-manager';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' import { Memorizer } from '../../Memorizer';
export class SecretsManagerRotationEnabledCheck implements BPSet { export class SecretsManagerRotationEnabledCheck implements BPSet {
private readonly client = new SecretsManagerClient({}) private readonly client = new SecretsManagerClient({});
private readonly memoClient = Memorizer.memo(this.client) private readonly memoClient = Memorizer.memo(this.client);
private readonly getSecrets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListSecretsCommand({})) compliantResources: [],
return response.SecretList || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'SecretsManagerRotationEnabledCheck',
description: 'Ensures all Secrets Manager secrets have rotation enabled.',
priority: 2,
priorityReason: 'Enabling rotation helps keep secrets secure by periodically rotating them.',
awsService: 'Secrets Manager',
awsServiceCategory: 'Secrets',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListSecretsCommand',
reason: 'To list all secrets managed by Secrets Manager.',
},
],
commandUsedInFixFunction: [
{
name: 'RotateSecretCommand',
reason: 'To enable rotation for secrets without it enabled.',
},
],
adviseBeforeFixFunction:
'Ensure that the secrets have a rotation lambda or custom rotation strategy configured before enabling rotation.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources: string[] = [] this.stats.status = 'CHECKING';
const nonCompliantResources: string[] = []
const secrets = await this.getSecrets() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const secrets = await this.getSecrets();
for (const secret of secrets) { for (const secret of secrets) {
if (secret.RotationEnabled) { if (secret.RotationEnabled) {
compliantResources.push(secret.ARN!) compliantResources.push(secret.ARN!);
} else { } else {
nonCompliantResources.push(secret.ARN!) nonCompliantResources.push(secret.ARN!);
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
await this.client.send( await this.client.send(
new RotateSecretCommand({ new RotateSecretCommand({
SecretId: arn SecretId: arn,
}) })
) );
}
} }
};
private readonly getSecrets = async () => {
const response = await this.memoClient.send(new ListSecretsCommand({}));
return response.SecretList || [];
};
} }

View File

@ -1,55 +1,140 @@
import { import {
SecretsManagerClient, SecretsManagerClient,
ListSecretsCommand, ListSecretsCommand,
RotateSecretCommand RotateSecretCommand,
} from '@aws-sdk/client-secrets-manager' } from '@aws-sdk/client-secrets-manager';
import { BPSet } from '../../types' import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer' import { Memorizer } from '../../Memorizer';
export class SecretsManagerScheduledRotationSuccessCheck implements BPSet { export class SecretsManagerScheduledRotationSuccessCheck implements BPSet {
private readonly client = new SecretsManagerClient({}) private readonly client = new SecretsManagerClient({});
private readonly memoClient = Memorizer.memo(this.client) private readonly memoClient = Memorizer.memo(this.client);
private readonly getSecrets = async () => { private readonly stats: BPSetStats = {
const response = await this.memoClient.send(new ListSecretsCommand({})) compliantResources: [],
return response.SecretList || [] nonCompliantResources: [],
} status: 'LOADED',
errorMessage: [],
};
public readonly getMetadata = (): BPSetMetadata => ({
name: 'SecretsManagerScheduledRotationSuccessCheck',
description: 'Checks if Secrets Manager secrets have successfully rotated within their scheduled period.',
priority: 2,
priorityReason:
'Ensuring secrets are rotated as per schedule is critical to maintaining security.',
awsService: 'Secrets Manager',
awsServiceCategory: 'Secrets',
bestPracticeCategory: 'Security',
requiredParametersForFix: [],
isFixFunctionUsesDestructiveCommand: false,
commandUsedInCheckFunction: [
{
name: 'ListSecretsCommand',
reason: 'Lists all secrets managed by Secrets Manager to determine rotation status.',
},
],
commandUsedInFixFunction: [
{
name: 'RotateSecretCommand',
reason: 'Manually rotates secrets that have not been rotated successfully within their scheduled period.',
},
],
adviseBeforeFixFunction:
'Ensure the secrets have a valid rotation lambda or custom rotation strategy configured before triggering a manual rotation.',
});
public readonly getStats = () => this.stats;
public readonly clearStats = () => {
this.stats.compliantResources = [];
this.stats.nonCompliantResources = [];
this.stats.status = 'LOADED';
this.stats.errorMessage = [];
};
public readonly check = async () => { public readonly check = async () => {
const compliantResources: string[] = [] this.stats.status = 'CHECKING';
const nonCompliantResources: string[] = []
const secrets = await this.getSecrets() await this.checkImpl()
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const secrets = await this.getSecrets();
for (const secret of secrets) { for (const secret of secrets) {
if (secret.RotationEnabled) { if (secret.RotationEnabled) {
const now = new Date() const now = new Date();
const lastRotated = secret.LastRotatedDate ? new Date(secret.LastRotatedDate) : undefined const lastRotated = secret.LastRotatedDate
? new Date(secret.LastRotatedDate)
: undefined;
const rotationPeriod = secret.RotationRules?.AutomaticallyAfterDays const rotationPeriod = secret.RotationRules?.AutomaticallyAfterDays
? secret.RotationRules.AutomaticallyAfterDays + 2 ? secret.RotationRules.AutomaticallyAfterDays + 2
: undefined : undefined;
if (!lastRotated || !rotationPeriod || now.getTime() - lastRotated.getTime() > rotationPeriod * 24 * 60 * 60 * 1000) { if (
nonCompliantResources.push(secret.ARN!) !lastRotated ||
!rotationPeriod ||
now.getTime() - lastRotated.getTime() >
rotationPeriod * 24 * 60 * 60 * 1000
) {
nonCompliantResources.push(secret.ARN!);
} else { } else {
compliantResources.push(secret.ARN!) compliantResources.push(secret.ARN!);
} }
} }
} }
return { this.stats.compliantResources = compliantResources;
compliantResources, this.stats.nonCompliantResources = nonCompliantResources;
nonCompliantResources, };
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => { public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
this.stats.status = 'CHECKING';
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
.then(() => {
this.stats.status = 'FINISHED';
})
.catch((err) => {
this.stats.status = 'ERROR';
this.stats.errorMessage.push({
date: new Date(),
message: err.message,
});
});
};
private readonly fixImpl = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const arn of nonCompliantResources) { for (const arn of nonCompliantResources) {
await this.client.send( await this.client.send(
new RotateSecretCommand({ new RotateSecretCommand({
SecretId: arn SecretId: arn,
}) })
) );
}
} }
};
private readonly getSecrets = async () => {
const response = await this.memoClient.send(new ListSecretsCommand({}));
return response.SecretList || [];
};
} }

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