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": {
"scripts": "build/**/*",
"assets": [
"views/**/*",
"bpset_metadata.json"
],
"assets": "views/**/*",
"targets": [
"node22-linuxstatic-x86_64",
"node22-linuxstatic-arm64"

View File

@ -1,5 +1,5 @@
import { BPSet, BPSetMetadata } from "./types";
import { readdir, readFile } from 'node:fs/promises'
import { BPSet } from "./types";
import { readdir } from 'node:fs/promises'
import path from 'node:path'
export class BPManager {
@ -13,12 +13,8 @@ export class BPManager {
private readonly bpSets:
Record<string, BPSet> = {}
private readonly bpSetMetadatas:
Record<string, BPSetMetadata> = {}
private constructor() {
this.loadBPSets()
this.loadBPSetMetadatas()
}
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) {
return this
.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'
})
return this.bpSets[name].check()
}
public runCheckAll(finished = (name: string) => {}) {
const checkJobs =
Object
.values(this.bpSetMetadatas)
.map(({ name }) => {
this.bpSetMetadatas[name].status = 'CHECKING'
const checkJobs: Promise<void>[] = []
return this
.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'
finished(name)
})
})
for (const bpset of Object.values(this.bpSets))
checkJobs.push(
bpset
.check()
.then(() =>
finished(bpset.getMetadata().name))
)
return Promise.all(checkJobs)
}
@ -113,20 +57,11 @@ export class BPManager {
return this
.bpSets[name]
.fix(
this.bpSetMetadatas[name].nonCompliantResources,
this.bpSets[name].getStats().nonCompliantResources,
requiredParametersForFix
)
}
public readonly getBPSet = (name: string) =>
this.bpSets[name]
public readonly getBPSetMetadata = (name: string) =>
this.bpSetMetadatas[name]
public readonly getBPSets = () =>
Object.values(this.bpSets)
public readonly getBPSetMetadatas = () =>
Object.values(this.bpSetMetadatas)
}

View File

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

View File

@ -2,71 +2,129 @@ import {
ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ModifyLoadBalancerAttributesCommand,
} from '@aws-sdk/client-elastic-load-balancing-v2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ALBHttpDropInvalidHeaderEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElasticLoadBalancingV2Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []
}
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}));
return response.LoadBalancers || [];
};
private readonly getLoadBalancerAttributes = async (
loadBalancerArn: string
) => {
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
)
return response.Attributes || []
}
);
return response.Attributes || [];
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers()
public readonly getMetadata = () => ({
name: 'ALBHttpDropInvalidHeaderEnabled',
description: 'Ensures that ALBs have invalid HTTP headers dropped.',
priority: 1,
priorityReason: 'Dropping invalid headers enhances security and avoids unexpected behavior.',
awsService: 'Elastic Load Balancing',
awsServiceCategory: 'Application Load Balancer',
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) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!);
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) {
compliantResources.push(lb.LoadBalancerArn!)
compliantResources.push(lb.LoadBalancerArn!);
} else {
nonCompliantResources.push(lb.LoadBalancerArn!)
nonCompliantResources.push(lb.LoadBalancerArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
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 lbArn of nonCompliantResources) {
await this.client.send(
new ModifyLoadBalancerAttributesCommand({
LoadBalancerArn: lbArn,
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 { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2'
import { BPSet } from '../../types'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
export class ALBWAFEnabled implements BPSet {
@ -13,11 +13,73 @@ export class ALBWAFEnabled implements BPSet {
return response.LoadBalancers || []
}
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
public readonly getMetadata = () => (
{
name: 'ALBWAFEnabled',
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 nonCompliantResources: string[] = []
const loadBalancers = await this.getLoadBalancers()
@ -33,17 +95,27 @@ export class ALBWAFEnabled implements BPSet {
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'web-acl-arn' }]
}
this.stats.compliantResources = compliantResources
this.stats.nonCompliantResources = nonCompliantResources
this.stats.status = 'FINISHED'
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
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
}
)
})
}
public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value
if (!webAclArn) {

View File

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

View File

@ -2,61 +2,127 @@ import {
ElasticLoadBalancingV2Client,
DescribeLoadBalancersCommand,
DescribeLoadBalancerAttributesCommand,
ModifyLoadBalancerAttributesCommand
} from '@aws-sdk/client-elastic-load-balancing-v2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ModifyLoadBalancerAttributesCommand,
} from '@aws-sdk/client-elastic-load-balancing-v2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ELBLoggingEnabled implements BPSet {
private readonly client = new ElasticLoadBalancingV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElasticLoadBalancingV2Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getLoadBalancers = async () => {
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}))
return response.LoadBalancers || []
}
const response = await this.memoClient.send(new DescribeLoadBalancersCommand({}));
return response.LoadBalancers || [];
};
private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => {
const response = await this.memoClient.send(
new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn })
)
return response.Attributes || []
}
);
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 () => {
const compliantResources = []
const nonCompliantResources = []
const loadBalancers = await this.getLoadBalancers()
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) {
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!)
const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!);
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) {
compliantResources.push(lb.LoadBalancerArn!)
compliantResources.push(lb.LoadBalancerArn!);
} else {
nonCompliantResources.push(lb.LoadBalancerArn!)
nonCompliantResources.push(lb.LoadBalancerArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 's3-bucket-name' }, { name: 's3-prefix' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const bucketName = requiredParametersForFix.find(param => param.name === 's3-bucket-name')?.value
const bucketPrefix = requiredParametersForFix.find(param => param.name === 's3-prefix')?.value
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 bucketName = requiredParametersForFix.find((param) => param.name === 's3-bucket-name')?.value;
const bucketPrefix = requiredParametersForFix.find((param) => param.name === 's3-prefix')?.value;
if (!bucketName || !bucketPrefix) {
throw new Error("Required parameters 's3-bucket-name' and/or 's3-prefix' are missing.")
throw new Error("Required parameters 's3-bucket-name' and/or 's3-prefix' are missing.");
}
for (const lbArn of nonCompliantResources) {
@ -66,10 +132,10 @@ export class ELBLoggingEnabled implements BPSet {
Attributes: [
{ Key: 'access_logs.s3.enabled', Value: 'true' },
{ 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 {
ApiGatewayV2Client,
GetApisCommand,
GetStagesCommand
} from '@aws-sdk/client-apigatewayv2'
import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
GetStagesCommand,
} from '@aws-sdk/client-apigatewayv2';
import {
WAFV2Client,
GetWebACLForResourceCommand,
AssociateWebACLCommand,
} from '@aws-sdk/client-wafv2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class APIGatewayAssociatedWithWAF implements BPSet {
private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly wafClient = Memorizer.memo(new WAFV2Client({}))
private readonly client = new ApiGatewayV2Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly wafClient = Memorizer.memo(new WAFV2Client({}));
private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []
}
const response = await this.memoClient.send(new GetApisCommand({}));
return response.Items || [];
};
private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []
}
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const apis = await this.getHttpApis()
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 apis = await this.getHttpApis();
for (const api of apis) {
const stages = await this.getStages(api.ApiId!)
const stages = await this.getStages(api.ApiId!);
for (const stage of stages) {
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`
const response = await this.wafClient.send(new GetWebACLForResourceCommand({ ResourceArn: stageArn }))
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`;
const response = await this.wafClient.send(
new GetWebACLForResourceCommand({ ResourceArn: stageArn })
);
if (response.WebACL) {
compliantResources.push(stageArn)
compliantResources.push(stageArn);
} else {
nonCompliantResources.push(stageArn)
nonCompliantResources.push(stageArn);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'web-acl-arn' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value
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 webAclArn = requiredParametersForFix.find((param) => param.name === 'web-acl-arn')?.value;
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) {
await this.wafClient.send(
new AssociateWebACLCommand({
ResourceArn: stageArn,
WebACLArn: webAclArn
WebACLArn: webAclArn,
})
)
);
}
}
};
}

View File

@ -2,63 +2,135 @@ import {
ApiGatewayV2Client,
GetApisCommand,
GetStagesCommand,
UpdateStageCommand
} from '@aws-sdk/client-apigatewayv2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateStageCommand,
} from '@aws-sdk/client-apigatewayv2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class APIGatewayExecutionLoggingEnabled implements BPSet {
private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ApiGatewayV2Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []
}
const response = await this.memoClient.send(new GetApisCommand({}));
return response.Items || [];
};
private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []
}
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const apis = await this.getHttpApis()
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 apis = await this.getHttpApis();
for (const api of apis) {
const stages = await this.getStages(api.ApiId!)
const stages = await this.getStages(api.ApiId!);
for (const stage of stages) {
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`
const loggingLevel = stage.AccessLogSettings?.Format
const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`;
const loggingLevel = stage.AccessLogSettings?.Format;
if (loggingLevel && loggingLevel !== 'OFF') {
compliantResources.push(stageArn)
compliantResources.push(stageArn);
} else {
nonCompliantResources.push(stageArn)
nonCompliantResources.push(stageArn);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-destination-arn' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value
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 logDestinationArn = requiredParametersForFix.find(
(param) => param.name === 'log-destination-arn'
)?.value;
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) {
const [apiId, stageName] = stageArn.split('/').slice(-2)
const [apiId, stageName] = stageArn.split('/').slice(-2);
await this.client.send(
new UpdateStageCommand({
@ -66,10 +138,10 @@ export class APIGatewayExecutionLoggingEnabled implements BPSet {
StageName: stageName,
AccessLogSettings: {
DestinationArn: logDestinationArn,
Format: '$context.requestId'
}
Format: '$context.requestId',
},
})
)
);
}
}
};
}

View File

@ -2,64 +2,136 @@ import {
ApiGatewayV2Client,
GetApisCommand,
GetStagesCommand,
UpdateStageCommand
} from '@aws-sdk/client-apigatewayv2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateStageCommand,
} from '@aws-sdk/client-apigatewayv2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class APIGatewayV2AccessLogsEnabled implements BPSet {
private readonly client = new ApiGatewayV2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ApiGatewayV2Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getHttpApis = async () => {
const response = await this.memoClient.send(new GetApisCommand({}))
return response.Items || []
}
const response = await this.memoClient.send(new GetApisCommand({}));
return response.Items || [];
};
private readonly getStages = async (apiId: string) => {
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }))
return response.Items || []
}
const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId }));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const apis = await this.getHttpApis()
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 apis = await this.getHttpApis();
for (const api of apis) {
const stages = await this.getStages(api.ApiId!)
const stages = await this.getStages(api.ApiId!);
for (const stage of stages) {
const stageIdentifier = `${api.Name!} / ${stage.StageName!}`
const stageIdentifier = `${api.Name!} / ${stage.StageName!}`;
if (!stage.AccessLogSettings) {
nonCompliantResources.push(stageIdentifier)
nonCompliantResources.push(stageIdentifier);
} else {
compliantResources.push(stageIdentifier)
compliantResources.push(stageIdentifier);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-destination-arn' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value
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 logDestinationArn = requiredParametersForFix.find(
(param) => param.name === 'log-destination-arn'
)?.value;
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) {
const [apiName, stageName] = resource.split(' / ')
const api = (await this.getHttpApis()).find(a => a.Name === apiName)
const [apiName, stageName] = resource.split(' / ');
const api = (await this.getHttpApis()).find((a) => a.Name === apiName);
if (!api) continue
if (!api) continue;
await this.client.send(
new UpdateStageCommand({
@ -67,10 +139,10 @@ export class APIGatewayV2AccessLogsEnabled implements BPSet {
StageName: stageName,
AccessLogSettings: {
DestinationArn: logDestinationArn,
Format: '$context.requestId'
}
Format: '$context.requestId',
},
})
)
);
}
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,66 +2,140 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CloudFrontAccessLogsEnabled implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CloudFrontClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
const response = await this.memoClient.send(new ListDistributionsCommand({}));
return response.DistributionList?.Items || [];
};
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
);
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
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 () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
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 distributions = await this.getDistributions();
for (const distribution of distributions) {
const { distribution: details } = await this.getDistributionDetails(distribution.Id!)
if (
details.DistributionConfig?.Logging?.Enabled
) {
compliantResources.push(details.ARN!)
const { distribution: details } = await this.getDistributionDetails(distribution.Id!);
if (details.DistributionConfig?.Logging?.Enabled) {
compliantResources.push(details.ARN!);
} else {
nonCompliantResources.push(details.ARN!)
nonCompliantResources.push(details.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-bucket-name' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const logBucketName = requiredParametersForFix.find(param => param.name === 'log-bucket-name')?.value
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 logBucketName = requiredParametersForFix.find(
(param) => param.name === 'log-bucket-name'
)?.value;
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) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const distributionId = arn.split('/').pop()!;
const { distribution, etag } = await this.getDistributionDetails(distributionId);
const updatedConfig = {
...distribution.DistributionConfig,
@ -69,17 +143,17 @@ export class CloudFrontAccessLogsEnabled implements BPSet {
Enabled: true,
Bucket: logBucketName,
IncludeCookies: false,
Prefix: ''
}
}
Prefix: '',
},
};
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any // Include all required properties
DistributionConfig: updatedConfig as any,
})
)
);
}
}
};
}

View File

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

View File

@ -2,77 +2,153 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CloudFrontDefaultRootObjectConfigured implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CloudFrontClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
const response = await this.memoClient.send(new ListDistributionsCommand({}));
return response.DistributionList?.Items || [];
};
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
);
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
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 () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
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 distributions = await this.getDistributions();
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 !== '') {
compliantResources.push(details.ARN!)
compliantResources.push(details.ARN!);
} else {
nonCompliantResources.push(details.ARN!)
nonCompliantResources.push(details.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'default-root-object' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const defaultRootObject = requiredParametersForFix.find(param => param.name === 'default-root-object')?.value
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 defaultRootObject = requiredParametersForFix.find(
(param) => param.name === 'default-root-object'
)?.value;
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) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const distributionId = arn.split('/').pop()!;
const { distribution, etag } = await this.getDistributionDetails(distributionId);
const updatedConfig = {
...distribution.DistributionConfig,
DefaultRootObject: defaultRootObject
}
DefaultRootObject: defaultRootObject,
};
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any
DistributionConfig: updatedConfig as any,
})
)
);
}
}
};
}

View File

@ -2,66 +2,136 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CloudFrontClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
const response = await this.memoClient.send(new ListDistributionsCommand({}));
return response.DistributionList?.Items || [];
};
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
);
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
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 () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
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 distributions = await this.getDistributions();
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(
origin =>
(origin) =>
origin.CustomOriginConfig &&
origin.CustomOriginConfig.OriginSslProtocols?.Items?.includes('SSLv3')
)
);
if (hasDeprecatedSSL) {
nonCompliantResources.push(details.ARN!)
nonCompliantResources.push(details.ARN!);
} else {
compliantResources.push(details.ARN!)
compliantResources.push(details.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const distributionId = arn.split('/').pop()!;
const { distribution, etag } = await this.getDistributionDetails(distributionId);
const updatedConfig = {
...distribution.DistributionConfig,
Origins: {
Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => {
Items: distribution.DistributionConfig?.Origins?.Items?.map((origin) => {
if (origin.CustomOriginConfig) {
return {
...origin,
@ -70,24 +140,24 @@ export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
OriginSslProtocols: {
...origin.CustomOriginConfig.OriginSslProtocols,
Items: origin.CustomOriginConfig.OriginSslProtocols?.Items?.filter(
protocol => protocol !== 'SSLv3'
)
}
}
}
(protocol) => protocol !== 'SSLv3'
),
},
},
};
}
return origin
})
}
}
return origin;
}),
},
};
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any
DistributionConfig: updatedConfig as any,
})
)
);
}
}
};
}

View File

@ -2,95 +2,169 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
} from '@aws-sdk/client-cloudfront'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateDistributionCommand,
} from '@aws-sdk/client-cloudfront';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CloudFrontS3OriginAccessControlEnabled implements BPSet {
private readonly client = new CloudFrontClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CloudFrontClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDistributions = async () => {
const response = await this.memoClient.send(new ListDistributionsCommand({}))
return response.DistributionList?.Items || []
}
const response = await this.memoClient.send(new ListDistributionsCommand({}));
return response.DistributionList?.Items || [];
};
private readonly getDistributionDetails = async (distributionId: string) => {
const response = await this.memoClient.send(
new GetDistributionCommand({ Id: distributionId })
)
);
return {
distribution: response.Distribution!,
etag: response.ETag!
}
}
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 () => {
const compliantResources = []
const nonCompliantResources = []
const distributions = await this.getDistributions()
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 distributions = await this.getDistributions();
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(
origin =>
(origin) =>
origin.S3OriginConfig &&
(!origin.OriginAccessControlId || origin.OriginAccessControlId === '')
)
);
if (hasNonCompliantOrigin) {
nonCompliantResources.push(details.ARN!)
nonCompliantResources.push(details.ARN!);
} else {
compliantResources.push(details.ARN!)
compliantResources.push(details.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'origin-access-control-id' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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 originAccessControlId = requiredParametersForFix.find(
param => param.name === 'origin-access-control-id'
)?.value
(param) => param.name === 'origin-access-control-id'
)?.value;
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) {
const distributionId = arn.split('/').pop()!
const { distribution, etag } = await this.getDistributionDetails(distributionId)
const distributionId = arn.split('/').pop()!;
const { distribution, etag } = await this.getDistributionDetails(distributionId);
const updatedConfig = {
...distribution.DistributionConfig,
Origins: {
Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => {
Items: distribution.DistributionConfig?.Origins?.Items?.map((origin) => {
if (origin.S3OriginConfig) {
return {
...origin,
OriginAccessControlId: originAccessControlId
}
OriginAccessControlId: originAccessControlId,
};
}
return origin
})
}
}
return origin;
}),
},
};
await this.client.send(
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as any
DistributionConfig: updatedConfig as any,
})
)
);
}
}
};
}

View File

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

View File

@ -1,60 +1,130 @@
import {
CloudWatchLogsClient,
DescribeLogGroupsCommand,
PutRetentionPolicyCommand
} from '@aws-sdk/client-cloudwatch-logs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutRetentionPolicyCommand,
} from '@aws-sdk/client-cloudwatch-logs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CWLogGroupRetentionPeriodCheck implements BPSet {
private readonly client = new CloudWatchLogsClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CloudWatchLogsClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getLogGroups = async () => {
const response = await this.memoClient.send(new DescribeLogGroupsCommand({}))
return response.logGroups || []
}
const response = await this.memoClient.send(new DescribeLogGroupsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const logGroups = await this.getLogGroups()
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 logGroups = await this.getLogGroups();
for (const logGroup of logGroups) {
if (logGroup.retentionInDays) {
compliantResources.push(logGroup.logGroupArn!)
compliantResources.push(logGroup.logGroupArn!);
} else {
nonCompliantResources.push(logGroup.logGroupArn!)
nonCompliantResources.push(logGroup.logGroupArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'retention-period-days' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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 retentionPeriod = requiredParametersForFix.find(
param => param.name === 'retention-period-days'
)?.value
(param) => param.name === 'retention-period-days'
)?.value;
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) {
const logGroupName = logGroupArn.split(':').pop()!
const logGroupName = logGroupArn.split(':').pop()!;
await this.client.send(
new PutRetentionPolicyCommand({
logGroupName,
retentionInDays: parseInt(retentionPeriod, 10)
retentionInDays: parseInt(retentionPeriod, 10),
})
)
);
}
}
};
}

View File

@ -1,73 +1,137 @@
import {
CloudWatchClient,
DescribeAlarmsCommand,
PutMetricAlarmCommand
} from '@aws-sdk/client-cloudwatch'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutMetricAlarmCommand,
} from '@aws-sdk/client-cloudwatch';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CloudWatchAlarmSettingsCheck implements BPSet {
private readonly client = new CloudWatchClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CloudWatchClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getAlarms = async () => {
const response = await this.memoClient.send(new DescribeAlarmsCommand({}))
return response.MetricAlarms || []
}
const response = await this.memoClient.send(new DescribeAlarmsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
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 alarms = await this.getAlarms();
const parameters = {
MetricName: '', // Required
Threshold: null,
EvaluationPeriods: null,
Period: null,
ComparisonOperator: null,
Statistic: null
}
const alarms = await this.getAlarms()
Statistic: null,
};
for (const alarm of alarms) {
for (const parameter of Object.keys(parameters).filter(key => (parameters as any)[key] !== null)) {
if (alarm.MetricName !== parameters.MetricName) {
continue
}
let isCompliant = true;
if (alarm[parameter as keyof typeof alarm] !== parameters[parameter as keyof typeof parameters]) {
nonCompliantResources.push(alarm.AlarmArn!)
break
for (const key of Object.keys(parameters).filter((k) => (parameters as any)[k] !== null)) {
if (alarm[key as keyof typeof alarm] !== parameters[key as keyof typeof parameters]) {
isCompliant = false;
break;
}
}
compliantResources.push(alarm.AlarmArn!)
if (isCompliant) {
compliantResources.push(alarm.AlarmArn!);
} else {
nonCompliantResources.push(alarm.AlarmArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'metric-name' },
{ name: 'threshold' },
{ name: 'evaluation-periods' },
{ name: 'period' },
{ name: 'comparison-operator' },
{ name: 'statistic' }
]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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(
requiredParametersForFix.map(param => [param.name, param.value])
)
requiredParametersForFix.map((param) => [param.name, param.value])
);
for (const alarmArn of nonCompliantResources) {
const alarmName = alarmArn.split(':').pop()!
const alarmName = alarmArn.split(':').pop()!;
await this.client.send(
new PutMetricAlarmCommand({
@ -77,9 +141,9 @@ export class CloudWatchAlarmSettingsCheck implements BPSet {
EvaluationPeriods: parseInt(requiredSettings['evaluation-periods'], 10),
Period: parseInt(requiredSettings['period'], 10),
ComparisonOperator: requiredSettings['comparison-operator'] as any,
Statistic: requiredSettings['statistic'] as any
Statistic: requiredSettings['statistic'] as any,
})
)
);
}
}
};
}

View File

@ -2,65 +2,136 @@ import {
CodeBuildClient,
ListProjectsCommand,
BatchGetProjectsCommand,
UpdateProjectCommand
} from '@aws-sdk/client-codebuild'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateProjectCommand,
} from '@aws-sdk/client-codebuild';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CodeBuildProjectEnvironmentPrivilegedCheck implements BPSet {
private readonly client = new CodeBuildClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CodeBuildClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getProjects = async () => {
const projectNames = await this.memoClient.send(new ListProjectsCommand({}))
const projectNames = await this.memoClient.send(new ListProjectsCommand({}));
if (!projectNames.projects?.length) {
return []
return [];
}
const response = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const projects = await this.getProjects()
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 projects = await this.getProjects();
for (const project of projects) {
if (!project.environment?.privilegedMode) {
compliantResources.push(project.arn!)
compliantResources.push(project.arn!);
} else {
nonCompliantResources.push(project.arn!)
nonCompliantResources.push(project.arn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.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) => {
const projects = await this.getProjects();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const projectName = arn.split(':').pop()!
const projects = await this.getProjects()
const projectToFix = projects.find(project => project.arn === arn)
const projectName = arn.split(':').pop()!;
const projectToFix = projects.find((project) => project.arn === arn);
if (!projectToFix) {
continue
continue;
}
await this.client.send(
new UpdateProjectCommand({
name: projectName,
environment: {
...projectToFix.environment as any,
privilegedMode: false
}
...projectToFix.environment,
privilegedMode: false,
} as any,
})
)
);
}
}
};
}

View File

@ -2,58 +2,129 @@ import {
CodeBuildClient,
ListProjectsCommand,
BatchGetProjectsCommand,
UpdateProjectCommand
} from '@aws-sdk/client-codebuild'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateProjectCommand,
} from '@aws-sdk/client-codebuild';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CodeBuildProjectLoggingEnabled implements BPSet {
private readonly client = new CodeBuildClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CodeBuildClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getProjects = async () => {
const projectNames = await this.memoClient.send(new ListProjectsCommand({}))
const projectNames = await this.memoClient.send(new ListProjectsCommand({}));
if (!projectNames.projects?.length) {
return []
return [];
}
const response = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const projects = await this.getProjects()
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 projects = await this.getProjects();
for (const project of projects) {
const logsConfig = project.logsConfig
const logsConfig = project.logsConfig;
if (
logsConfig?.cloudWatchLogs?.status === 'ENABLED' ||
logsConfig?.s3Logs?.status === 'ENABLED'
) {
compliantResources.push(project.arn!)
compliantResources.push(project.arn!);
} else {
nonCompliantResources.push(project.arn!)
nonCompliantResources.push(project.arn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.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) => {
const projects = await this.getProjects();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const projectName = arn.split(':').pop()!
const projects = await this.getProjects()
const projectToFix = projects.find(project => project.arn === arn)
const projectName = arn.split(':').pop()!;
const projectToFix = projects.find((project) => project.arn === arn);
if (!projectToFix) {
continue
continue;
}
await this.client.send(
@ -64,11 +135,11 @@ export class CodeBuildProjectLoggingEnabled implements BPSet {
cloudWatchLogs: {
status: 'ENABLED',
groupName: 'default-cloudwatch-group',
streamName: 'default-stream'
}
}
streamName: 'default-stream',
},
},
})
)
);
}
}
};
}

View File

@ -3,70 +3,146 @@ import {
ListApplicationsCommand,
ListDeploymentGroupsCommand,
BatchGetDeploymentGroupsCommand,
UpdateDeploymentGroupCommand
} from '@aws-sdk/client-codedeploy'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateDeploymentGroupCommand,
} from '@aws-sdk/client-codedeploy';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class CodeDeployAutoRollbackMonitorEnabled implements BPSet {
private readonly client = new CodeDeployClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new CodeDeployClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDeploymentGroups = async () => {
const applications = await this.memoClient.send(new ListApplicationsCommand({}))
const deploymentGroupsInfo = []
const applications = await this.memoClient.send(new ListApplicationsCommand({}));
const deploymentGroupsInfo = [];
for (const application of applications.applications || []) {
const deploymentGroups = await this.memoClient.send(
new ListDeploymentGroupsCommand({ applicationName: application })
)
);
if (!deploymentGroups.deploymentGroups?.length) {
continue
continue;
}
const batchResponse = await this.memoClient.send(
new BatchGetDeploymentGroupsCommand({
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 () => {
const compliantResources = []
const nonCompliantResources = []
const deploymentGroups = await this.getDeploymentGroups()
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 deploymentGroups = await this.getDeploymentGroups();
for (const deploymentGroup of deploymentGroups) {
if (
deploymentGroup.alarmConfiguration?.enabled &&
deploymentGroup.autoRollbackConfiguration?.enabled
) {
compliantResources.push(deploymentGroup.deploymentGroupId!)
compliantResources.push(deploymentGroup.deploymentGroupId!);
} else {
nonCompliantResources.push(deploymentGroup.deploymentGroupId!)
nonCompliantResources.push(deploymentGroup.deploymentGroupId!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.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) => {
const deploymentGroups = await this.getDeploymentGroups();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const groupId of nonCompliantResources) {
const deploymentGroups = await this.getDeploymentGroups()
const deploymentGroupToFix = deploymentGroups.find(
group => group.deploymentGroupId === groupId
)
(group) => group.deploymentGroupId === groupId
);
if (!deploymentGroupToFix) {
continue
continue;
}
await this.client.send(
@ -75,14 +151,14 @@ export class CodeDeployAutoRollbackMonitorEnabled implements BPSet {
currentDeploymentGroupName: deploymentGroupToFix.deploymentGroupName!,
alarmConfiguration: {
...deploymentGroupToFix.alarmConfiguration,
enabled: true
enabled: true,
},
autoRollbackConfiguration: {
...deploymentGroupToFix.autoRollbackConfiguration,
enabled: true
}
enabled: true,
},
})
)
);
}
}
};
}

View File

@ -1,75 +1,154 @@
import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand
} from '@aws-sdk/client-dynamodb'
DescribeTableCommand,
} from '@aws-sdk/client-dynamodb';
import {
ApplicationAutoScalingClient,
RegisterScalableTargetCommand,
PutScalingPolicyCommand,
DescribeScalingPoliciesCommand
} from '@aws-sdk/client-application-auto-scaling'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DescribeScalingPoliciesCommand,
} from '@aws-sdk/client-application-auto-scaling';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBAutoscalingEnabled implements BPSet {
private readonly client = new DynamoDBClient({})
private readonly autoScalingClient = new ApplicationAutoScalingClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new DynamoDBClient({});
private readonly autoScalingClient = new ApplicationAutoScalingClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []
const tableNames = await this.memoClient.send(new ListTablesCommand({}));
const tables = [];
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
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 tables = await this.getTables();
for (const table of tables) {
if (table.BillingModeSummary?.BillingMode === 'PAY_PER_REQUEST') {
compliantResources.push(table.TableArn!)
continue
compliantResources.push(table.TableArn!);
continue;
}
const scalingPolicies = await this.autoScalingClient.send(
new DescribeScalingPoliciesCommand({
ServiceNamespace: 'dynamodb',
ResourceId: `table/${table.TableName}`
ResourceId: `table/${table.TableName}`,
})
)
);
const scalingPolicyDimensions = scalingPolicies.ScalingPolicies?.map(
policy => policy.ScalableDimension
)
(policy) => policy.ScalableDimension
);
if (
scalingPolicyDimensions?.includes('dynamodb:table:ReadCapacityUnits') &&
scalingPolicyDimensions?.includes('dynamodb:table:WriteCapacityUnits')
) {
compliantResources.push(table.TableArn!)
compliantResources.push(table.TableArn!);
} else {
nonCompliantResources.push(table.TableArn!)
nonCompliantResources.push(table.TableArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const tableName = arn.split('/').pop()!
const tableName = arn.split('/').pop()!;
// Register scalable targets for read and write capacity
await this.autoScalingClient.send(
@ -78,9 +157,9 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:ReadCapacityUnits',
MinCapacity: 1,
MaxCapacity: 100
MaxCapacity: 100,
})
)
);
await this.autoScalingClient.send(
new RegisterScalableTargetCommand({
@ -88,9 +167,9 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:WriteCapacityUnits',
MinCapacity: 1,
MaxCapacity: 100
MaxCapacity: 100,
})
)
);
// Put scaling policies for read and write capacity
await this.autoScalingClient.send(
@ -105,11 +184,11 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ScaleInCooldown: 60,
ScaleOutCooldown: 60,
PredefinedMetricSpecification: {
PredefinedMetricType: 'DynamoDBReadCapacityUtilization'
}
}
PredefinedMetricType: 'DynamoDBReadCapacityUtilization',
},
},
})
)
);
await this.autoScalingClient.send(
new PutScalingPolicyCommand({
@ -123,11 +202,11 @@ export class DynamoDBAutoscalingEnabled implements BPSet {
ScaleInCooldown: 60,
ScaleOutCooldown: 60,
PredefinedMetricSpecification: {
PredefinedMetricType: 'DynamoDBWriteCapacityUtilization'
}
}
PredefinedMetricType: 'DynamoDBWriteCapacityUtilization',
},
},
})
)
);
}
}
};
}

View File

@ -1,78 +1,167 @@
import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand
} from '@aws-sdk/client-dynamodb'
DescribeTableCommand,
} from '@aws-sdk/client-dynamodb';
import {
BackupClient,
ListRecoveryPointsByResourceCommand,
StartBackupJobCommand
} from '@aws-sdk/client-backup'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
StartBackupJobCommand,
} from '@aws-sdk/client-backup';
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBLastBackupRecoveryPointCreated implements BPSet {
private readonly client = new DynamoDBClient({})
private readonly backupClient = new BackupClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new DynamoDBClient({});
private readonly backupClient = new BackupClient({});
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 () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []
const tableNames = await this.memoClient.send(new ListTablesCommand({}));
const tables = [];
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
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 tables = await this.getTables();
for (const table of tables) {
const recoveryPointsResponse = await this.backupClient.send(
new ListRecoveryPointsByResourceCommand({
ResourceArn: table.TableArn
ResourceArn: table.TableArn,
})
)
const recoveryPoints = recoveryPointsResponse.RecoveryPoints || []
);
const recoveryPoints = recoveryPointsResponse.RecoveryPoints || [];
if (recoveryPoints.length === 0) {
nonCompliantResources.push(table.TableArn!)
continue
nonCompliantResources.push(table.TableArn!);
continue;
}
const latestRecoveryPoint = recoveryPoints
.map(point => new Date(point.CreationDate!))
.sort((a, b) => b.getTime() - a.getTime())[0]
.map((point) => new Date(point.CreationDate!))
.sort((a, b) => b.getTime() - a.getTime())[0];
if (new Date().getTime() - latestRecoveryPoint.getTime() > 86400000) {
nonCompliantResources.push(table.TableArn!)
nonCompliantResources.push(table.TableArn!);
} else {
compliantResources.push(table.TableArn!)
compliantResources.push(table.TableArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.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) => {
const accountId = await this.fetchAccountId();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
await this.backupClient.send(
new StartBackupJobCommand({
ResourceArn: arn,
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,
DescribeTableCommand,
DescribeContinuousBackupsCommand,
UpdateContinuousBackupsCommand
} from '@aws-sdk/client-dynamodb'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateContinuousBackupsCommand,
} from '@aws-sdk/client-dynamodb';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBPITREnabled implements BPSet {
private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new DynamoDBClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []
const tableNames = await this.memoClient.send(new ListTablesCommand({}));
const tables = [];
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
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 tables = await this.getTables();
for (const table of tables) {
const backupStatus = await this.memoClient.send(
new DescribeContinuousBackupsCommand({
TableName: table.TableName!
TableName: table.TableName!,
})
)
);
if (
backupStatus.ContinuousBackupsDescription?.PointInTimeRecoveryDescription
?.PointInTimeRecoveryStatus === 'ENABLED'
) {
compliantResources.push(table.TableArn!)
compliantResources.push(table.TableArn!);
} else {
nonCompliantResources.push(table.TableArn!)
nonCompliantResources.push(table.TableArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const tableName = arn.split('/').pop()!
const tableName = arn.split('/').pop()!;
await this.client.send(
new UpdateContinuousBackupsCommand({
TableName: tableName,
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true
}
PointInTimeRecoveryEnabled: true,
},
})
)
);
}
}
};
}

View File

@ -2,57 +2,127 @@ import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand,
UpdateTableCommand
} from '@aws-sdk/client-dynamodb'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateTableCommand,
} from '@aws-sdk/client-dynamodb';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBTableDeletionProtectionEnabled implements BPSet {
private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new DynamoDBClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []
const tableNames = await this.memoClient.send(new ListTablesCommand({}));
const tables = [];
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
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 tables = await this.getTables();
for (const table of tables) {
if (table.DeletionProtectionEnabled) {
compliantResources.push(table.TableArn!)
compliantResources.push(table.TableArn!);
} else {
nonCompliantResources.push(table.TableArn!)
nonCompliantResources.push(table.TableArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const tableName = arn.split('/').pop()!
const tableName = arn.split('/').pop()!;
await this.client.send(
new UpdateTableCommand({
TableName: tableName,
DeletionProtectionEnabled: true
DeletionProtectionEnabled: true,
})
)
);
}
}
};
}

View File

@ -2,62 +2,136 @@ import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand,
UpdateTableCommand
} from '@aws-sdk/client-dynamodb'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateTableCommand,
} from '@aws-sdk/client-dynamodb';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBTableEncryptedKMS implements BPSet {
private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new DynamoDBClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []
const tableNames = await this.memoClient.send(new ListTablesCommand({}));
const tables = [];
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
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 tables = await this.getTables();
for (const table of tables) {
if (
table.SSEDescription?.Status === 'ENABLED' &&
table.SSEDescription?.SSEType === 'KMS'
) {
compliantResources.push(table.TableArn!)
compliantResources.push(table.TableArn!);
} else {
nonCompliantResources.push(table.TableArn!)
nonCompliantResources.push(table.TableArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value
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 kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value;
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) {
const tableName = arn.split('/').pop()!
const tableName = arn.split('/').pop()!;
await this.client.send(
new UpdateTableCommand({
@ -65,10 +139,10 @@ export class DynamoDBTableEncryptedKMS implements BPSet {
SSESpecification: {
Enabled: true,
SSEType: 'KMS',
KMSMasterKeyId: kmsKeyId
}
KMSMasterKeyId: kmsKeyId,
},
})
)
);
}
}
};
}

View File

@ -2,59 +2,129 @@ import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand,
UpdateTableCommand
} from '@aws-sdk/client-dynamodb'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateTableCommand,
} from '@aws-sdk/client-dynamodb';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DynamoDBTableEncryptionEnabled implements BPSet {
private readonly client = new DynamoDBClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new DynamoDBClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTables = async () => {
const tableNames = await this.memoClient.send(new ListTablesCommand({}))
const tables = []
const tableNames = await this.memoClient.send(new ListTablesCommand({}));
const tables = [];
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
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 tables = await this.getTables();
for (const table of tables) {
if (table.SSEDescription?.Status === 'ENABLED') {
compliantResources.push(table.TableArn!)
compliantResources.push(table.TableArn!);
} else {
nonCompliantResources.push(table.TableArn!)
nonCompliantResources.push(table.TableArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const tableName = arn.split('/').pop()!
const tableName = arn.split('/').pop()!;
await this.client.send(
new UpdateTableCommand({
TableName: tableName,
SSESpecification: {
Enabled: true
}
Enabled: true,
},
})
)
);
}
}
};
}

View File

@ -1,36 +1,102 @@
import {
EC2Client,
DescribeVolumesCommand,
EnableEbsEncryptionByDefaultCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
GetEbsEncryptionByDefaultCommand,
EnableEbsEncryptionByDefaultCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2EbsEncryptionByDefault implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
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 () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeVolumesCommand({}))
this.stats.status = 'CHECKING';
for (const volume of response.Volumes || []) {
if (volume.Encrypted) {
compliantResources.push(volume.VolumeId!)
} else {
nonCompliantResources.push(volume.VolumeId!)
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.client.send(new GetEbsEncryptionByDefaultCommand({}));
if (response.EbsEncryptionByDefault) {
compliantResources.push('EBS Encryption By Default');
} else {
nonCompliantResources.push('EBS Encryption By Default');
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async () => {
await this.client.send(new EnableEbsEncryptionByDefaultCommand({}))
}
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 () => {
await this.client.send(new EnableEbsEncryptionByDefaultCommand({}));
};
}

View File

@ -1,45 +1,111 @@
import {
DescribeInstancesCommand,
EC2Client,
ModifyInstanceMetadataOptionsCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ModifyInstanceMetadataOptionsCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2Imdsv2Check implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
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 () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
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 response = await this.memoClient.send(new DescribeInstancesCommand({}));
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
if (instance.MetadataOptions?.HttpTokens === 'required') {
compliantResources.push(instance.InstanceId!)
compliantResources.push(instance.InstanceId!);
} else {
nonCompliantResources.push(instance.InstanceId!)
nonCompliantResources.push(instance.InstanceId!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
await this.client.send(
new ModifyInstanceMetadataOptionsCommand({
InstanceId: instanceId,
HttpTokens: 'required'
HttpTokens: 'required',
})
)
);
}
}
};
}

View File

@ -1,42 +1,111 @@
import {
DescribeInstancesCommand,
EC2Client,
MonitorInstancesCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
MonitorInstancesCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2InstanceDetailedMonitoringEnabled implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
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 () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
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 response = await this.memoClient.send(new DescribeInstancesCommand({}));
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
if (instance.Monitoring?.State === 'enabled') {
compliantResources.push(instance.InstanceId!)
compliantResources.push(instance.InstanceId!);
} else {
nonCompliantResources.push(instance.InstanceId!)
nonCompliantResources.push(instance.InstanceId!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
await this.client.send(
new MonitorInstancesCommand({
InstanceIds: 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) => {
if (nonCompliantResources.length > 0) {
await this.client.send(
new MonitorInstancesCommand({
InstanceIds: nonCompliantResources,
})
);
}
};
}

View File

@ -1,48 +1,104 @@
import {
EC2Client,
DescribeInstancesCommand
} from '@aws-sdk/client-ec2'
import { SSMClient, DescribeInstanceInformationCommand } from '@aws-sdk/client-ssm'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DescribeInstancesCommand,
} from '@aws-sdk/client-ec2';
import {
SSMClient,
DescribeInstanceInformationCommand,
} from '@aws-sdk/client-ssm';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2InstanceManagedBySystemsManager implements BPSet {
private readonly client = new EC2Client({})
private readonly ssmClient = new SSMClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
private readonly ssmClient = new SSMClient({});
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 () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
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 response = await this.memoClient.send(new DescribeInstancesCommand({}));
const ssmResponse = await this.ssmClient.send(
new DescribeInstanceInformationCommand({})
)
);
const managedInstanceIds = ssmResponse.InstanceInformationList?.map(
info => info.InstanceId
)
(info) => info.InstanceId
);
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
if (managedInstanceIds?.includes(instance.InstanceId!)) {
compliantResources.push(instance.InstanceId!)
compliantResources.push(instance.InstanceId!);
} else {
nonCompliantResources.push(instance.InstanceId!)
nonCompliantResources.push(instance.InstanceId!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async () => {
public readonly fix: BPSetFixFn = async () => {
throw new Error(
'Fix logic for EC2InstanceManagedBySystemsManager is not directly applicable. Systems Manager Agent setup requires manual intervention.'
)
}
);
};
}

View File

@ -1,56 +1,127 @@
import {
EC2Client,
DescribeInstancesCommand,
AssociateIamInstanceProfileCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
AssociateIamInstanceProfileCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2InstanceProfileAttached implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
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 () => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
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 response = await this.memoClient.send(new DescribeInstancesCommand({}));
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
if (instance.IamInstanceProfile) {
compliantResources.push(instance.InstanceId!)
compliantResources.push(instance.InstanceId!);
} else {
nonCompliantResources.push(instance.InstanceId!)
nonCompliantResources.push(instance.InstanceId!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'iam-instance-profile' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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 iamInstanceProfile = requiredParametersForFix.find(
param => param.name === 'iam-instance-profile'
)?.value
(param) => param.name === 'iam-instance-profile'
)?.value;
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) {
await this.client.send(
new AssociateIamInstanceProfileCommand({
InstanceId: instanceId,
IamInstanceProfile: { Name: iamInstanceProfile }
IamInstanceProfile: { Name: iamInstanceProfile },
})
)
);
}
}
};
}

View File

@ -1,39 +1,88 @@
import {
EC2Client,
DescribeInstancesCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DescribeInstancesCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2NoAmazonKeyPair implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
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 () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
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 response = await this.memoClient.send(new DescribeInstancesCommand({}));
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
if (instance.KeyName) {
nonCompliantResources.push(instance.InstanceId!)
nonCompliantResources.push(instance.InstanceId!);
} else {
compliantResources.push(instance.InstanceId!)
compliantResources.push(instance.InstanceId!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async () => {
throw new Error(
'Fix logic for EC2NoAmazonKeyPair is not applicable. Key pairs must be removed manually or during instance creation.'
)
}
);
};
}

View File

@ -1,46 +1,114 @@
import {
EC2Client,
DescribeInstancesCommand,
TerminateInstancesCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
TerminateInstancesCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2StoppedInstance implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
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 () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
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 response = await this.memoClient.send(new DescribeInstancesCommand({}));
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
if (instance.State?.Name === 'stopped') {
nonCompliantResources.push(instance.InstanceId!)
nonCompliantResources.push(instance.InstanceId!);
} else {
compliantResources.push(instance.InstanceId!)
compliantResources.push(instance.InstanceId!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
return // No stopped instances to terminate
return; // No stopped instances to terminate
}
await this.client.send(
new TerminateInstancesCommand({
InstanceIds: nonCompliantResources
InstanceIds: nonCompliantResources,
})
)
}
);
};
}

View File

@ -1,19 +1,77 @@
import {
EC2Client,
DescribeInstancesCommand,
ModifyInstanceMetadataOptionsCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ModifyInstanceMetadataOptionsCommand,
} from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EC2TokenHopLimitCheck implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EC2Client({});
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 () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
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 response = await this.memoClient.send(new DescribeInstancesCommand({}));
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
@ -21,28 +79,38 @@ export class EC2TokenHopLimitCheck implements BPSet {
instance.MetadataOptions?.HttpPutResponseHopLimit &&
instance.MetadataOptions.HttpPutResponseHopLimit < 2
) {
compliantResources.push(instance.InstanceId!)
compliantResources.push(instance.InstanceId!);
} else {
nonCompliantResources.push(instance.InstanceId!)
nonCompliantResources.push(instance.InstanceId!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
await this.client.send(
new ModifyInstanceMetadataOptionsCommand({
InstanceId: instanceId,
HttpPutResponseHopLimit: 1
HttpPutResponseHopLimit: 1,
})
)
);
}
}
};
}

View File

@ -5,75 +5,162 @@ import {
ListImagesCommand,
BatchGetImageCommand,
PutImageCommand,
DeleteRepositoryCommand
} from '@aws-sdk/client-ecr'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DeleteRepositoryCommand,
} from '@aws-sdk/client-ecr';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECRKmsEncryption1 implements BPSet {
private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECRClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []
}
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const repositories = await this.getRepositories()
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 repositories = await this.getRepositories();
for (const repository of repositories) {
if (repository.encryptionConfiguration?.encryptionType === 'KMS') {
compliantResources.push(repository.repositoryArn!)
compliantResources.push(repository.repositoryArn!);
} else {
nonCompliantResources.push(repository.repositoryArn!)
nonCompliantResources.push(repository.repositoryArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value
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 kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value;
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) {
const repositoryName = arn.split('/').pop()!
const repositoryName = arn.split('/').pop()!;
// Create a new repository with KMS encryption
const newRepositoryName = `${repositoryName}-kms`
const newRepositoryName = `${repositoryName}-kms`;
await this.client.send(
new CreateRepositoryCommand({
repositoryName: newRepositoryName,
encryptionConfiguration: {
encryptionType: 'KMS',
kmsKey: kmsKeyId
}
kmsKey: kmsKeyId,
},
})
)
);
// Get all images in the existing repository
const listImagesResponse = await this.client.send(
new ListImagesCommand({ repositoryName })
)
const imageIds = listImagesResponse.imageIds || []
);
const imageIds = listImagesResponse.imageIds || [];
if (imageIds.length > 0) {
const batchGetImageResponse = await this.client.send(
new BatchGetImageCommand({ repositoryName, imageIds })
)
);
// Push images to the new repository
for (const image of batchGetImageResponse.images || []) {
@ -81,9 +168,9 @@ export class ECRKmsEncryption1 implements BPSet {
new PutImageCommand({
repositoryName: newRepositoryName,
imageManifest: image.imageManifest,
imageTag: image.imageId?.imageTag
imageTag: image.imageId?.imageTag,
})
)
);
}
}
@ -91,9 +178,9 @@ export class ECRKmsEncryption1 implements BPSet {
await this.client.send(
new DeleteRepositoryCommand({
repositoryName,
force: true
force: true,
})
)
);
}
}
};
}

View File

@ -1,50 +1,118 @@
import {
ECRClient,
DescribeRepositoriesCommand,
PutImageScanningConfigurationCommand
} from '@aws-sdk/client-ecr'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutImageScanningConfigurationCommand,
} from '@aws-sdk/client-ecr';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECRPrivateImageScanningEnabled implements BPSet {
private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECRClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []
}
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const repositories = await this.getRepositories()
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 repositories = await this.getRepositories();
for (const repository of repositories) {
if (repository.imageScanningConfiguration?.scanOnPush) {
compliantResources.push(repository.repositoryArn!)
compliantResources.push(repository.repositoryArn!);
} else {
nonCompliantResources.push(repository.repositoryArn!)
nonCompliantResources.push(repository.repositoryArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const repositoryName = arn.split('/').pop()!
const repositoryName = arn.split('/').pop()!;
await this.client.send(
new PutImageScanningConfigurationCommand({
repositoryName,
imageScanningConfiguration: { scanOnPush: true }
imageScanningConfiguration: { scanOnPush: true },
})
)
);
}
}
};
}

View File

@ -2,71 +2,147 @@ import {
ECRClient,
DescribeRepositoriesCommand,
PutLifecyclePolicyCommand,
GetLifecyclePolicyCommand
} from '@aws-sdk/client-ecr'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
GetLifecyclePolicyCommand,
} from '@aws-sdk/client-ecr';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECRPrivateLifecyclePolicyConfigured implements BPSet {
private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECRClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []
}
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const repositories = await this.getRepositories()
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 repositories = await this.getRepositories();
for (const repository of repositories) {
try {
await this.client.send(
new GetLifecyclePolicyCommand({
registryId: repository.registryId,
repositoryName: repository.repositoryName
repositoryName: repository.repositoryName,
})
)
compliantResources.push(repository.repositoryArn!)
);
compliantResources.push(repository.repositoryArn!);
} catch (error: any) {
if (error.name === 'LifecyclePolicyNotFoundException') {
nonCompliantResources.push(repository.repositoryArn!)
nonCompliantResources.push(repository.repositoryArn!);
} else {
throw error
throw error;
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'lifecycle-policy' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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 lifecyclePolicy = requiredParametersForFix.find(
param => param.name === 'lifecycle-policy'
)?.value
(param) => param.name === 'lifecycle-policy'
)?.value;
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) {
const repositoryName = arn.split('/').pop()!
const repositoryName = arn.split('/').pop()!;
await this.client.send(
new PutLifecyclePolicyCommand({
repositoryName,
lifecyclePolicyText: lifecyclePolicy
lifecyclePolicyText: lifecyclePolicy,
})
)
);
}
}
};
}

View File

@ -1,50 +1,119 @@
import {
ECRClient,
DescribeRepositoriesCommand,
PutImageTagMutabilityCommand
} from '@aws-sdk/client-ecr'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutImageTagMutabilityCommand,
} from '@aws-sdk/client-ecr';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECRPrivateTagImmutabilityEnabled implements BPSet {
private readonly client = new ECRClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECRClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getRepositories = async () => {
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}))
return response.repositories || []
}
const response = await this.memoClient.send(new DescribeRepositoriesCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const repositories = await this.getRepositories()
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 repositories = await this.getRepositories();
for (const repository of repositories) {
if (repository.imageTagMutability === 'IMMUTABLE') {
compliantResources.push(repository.repositoryArn!)
compliantResources.push(repository.repositoryArn!);
} else {
nonCompliantResources.push(repository.repositoryArn!)
nonCompliantResources.push(repository.repositoryArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const repositoryName = arn.split('/').pop()!
const repositoryName = arn.split('/').pop()!;
await this.client.send(
new PutImageTagMutabilityCommand({
repositoryName,
imageTagMutability: 'IMMUTABLE'
imageTagMutability: 'IMMUTABLE',
})
)
);
}
}
};
}

View File

@ -2,55 +2,127 @@ import {
ECSClient,
ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSAwsVpcNetworkingEnabled implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send(
new ListTaskDefinitionsCommand({ status: 'ACTIVE' })
)
const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
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 taskDefinitions = await this.getTaskDefinitions();
for (const taskDefinition of taskDefinitions) {
if (taskDefinition.networkMode === 'awsvpc') {
compliantResources.push(taskDefinition.taskDefinitionArn!)
compliantResources.push(taskDefinition.taskDefinitionArn!);
} else {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
nonCompliantResources.push(taskDefinition.taskDefinitionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
await this.client.send(
new RegisterTaskDefinitionCommand({
@ -59,9 +131,9 @@ export class ECSAwsVpcNetworkingEnabled implements BPSet {
networkMode: 'awsvpc',
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
memory: taskDefinition.taskDefinition?.memory,
})
)
);
}
}
};
}

View File

@ -1,51 +1,121 @@
import {
ECSClient,
DescribeClustersCommand,
UpdateClusterSettingsCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateClusterSettingsCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSContainerInsightsEnabled implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getClusters = async () => {
const response = await this.memoClient.send(new DescribeClustersCommand({ include: ['SETTINGS'] }))
return response.clusters || []
}
const response = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
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 clusters = await this.getClusters();
for (const cluster of clusters) {
const containerInsightsSetting = cluster.settings?.find(
setting => setting.name === 'containerInsights'
)
(setting) => setting.name === 'containerInsights'
);
if (containerInsightsSetting?.value === 'enabled') {
compliantResources.push(cluster.clusterArn!)
compliantResources.push(cluster.clusterArn!);
} else {
nonCompliantResources.push(cluster.clusterArn!)
nonCompliantResources.push(cluster.clusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
await this.client.send(
new UpdateClusterSettingsCommand({
cluster: arn,
settings: [{ name: 'containerInsights', value: 'enabled' }]
settings: [{ name: 'containerInsights', value: 'enabled' }],
})
)
);
}
}
};
}

View File

@ -2,65 +2,137 @@ import {
ECSClient,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ListTaskDefinitionsCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSContainersNonPrivileged implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send(
new ListTaskDefinitionsCommand({ status: 'ACTIVE' })
)
const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
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 taskDefinitions = await this.getTaskDefinitions();
for (const taskDefinition of taskDefinitions) {
const privilegedContainers = taskDefinition.containerDefinitions?.filter(
container => container.privileged
)
(container) => container.privileged
);
if (privilegedContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
nonCompliantResources.push(taskDefinition.taskDefinitionArn!);
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
compliantResources.push(taskDefinition.taskDefinitionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
(container) => ({
...container,
privileged: false
privileged: false,
})
)
);
await this.client.send(
new RegisterTaskDefinitionCommand({
@ -69,9 +141,9 @@ export class ECSContainersNonPrivileged implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
memory: taskDefinition.taskDefinition?.memory,
})
)
);
}
}
};
}

View File

@ -2,65 +2,137 @@ import {
ECSClient,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ListTaskDefinitionsCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSContainersReadonlyAccess implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send(
new ListTaskDefinitionsCommand({ status: 'ACTIVE' })
)
const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
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 taskDefinitions = await this.getTaskDefinitions();
for (const taskDefinition of taskDefinitions) {
const notReadonlyContainers = taskDefinition.containerDefinitions?.filter(
container => !container.readonlyRootFilesystem
)
(container) => !container.readonlyRootFilesystem
);
if (notReadonlyContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
nonCompliantResources.push(taskDefinition.taskDefinitionArn!);
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
compliantResources.push(taskDefinition.taskDefinitionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
(container) => ({
...container,
readonlyRootFilesystem: true
readonlyRootFilesystem: true,
})
)
);
await this.client.send(
new RegisterTaskDefinitionCommand({
@ -69,9 +141,9 @@ export class ECSContainersReadonlyAccess implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
memory: taskDefinition.taskDefinition?.memory,
})
)
);
}
}
};
}

View File

@ -3,68 +3,144 @@ import {
ListClustersCommand,
ListServicesCommand,
DescribeServicesCommand,
UpdateServiceCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateServiceCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSFargateLatestPlatformVersion implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getServices = async () => {
const clustersResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterArns = clustersResponse.clusterArns || []
const services: { clusterArn: string; serviceArn: string }[] = []
const clustersResponse = await this.memoClient.send(new ListClustersCommand({}));
const clusterArns = clustersResponse.clusterArns || [];
const services: { clusterArn: string; serviceArn: string }[] = [];
for (const clusterArn of clusterArns) {
const servicesResponse = await this.memoClient.send(
new ListServicesCommand({ cluster: clusterArn })
)
);
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 () => {
const compliantResources = []
const nonCompliantResources = []
const services = await this.getServices()
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 services = await this.getServices();
for (const { clusterArn, serviceArn } of services) {
const serviceResponse = await this.memoClient.send(
new DescribeServicesCommand({ cluster: clusterArn, services: [serviceArn] })
)
);
const service = serviceResponse.services?.[0]
const service = serviceResponse.services?.[0];
if (service?.platformVersion === 'LATEST') {
compliantResources.push(service.serviceArn!)
compliantResources.push(service.serviceArn!);
} else {
nonCompliantResources.push(service?.serviceArn!)
nonCompliantResources.push(service?.serviceArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const clusterArn = serviceArn.split(':cluster/')[1].split(':service/')[0]
const clusterArn = serviceArn.split(':cluster/')[1].split(':service/')[0];
await this.client.send(
new UpdateServiceCommand({
cluster: clusterArn,
service: serviceArn,
platformVersion: 'LATEST'
platformVersion: 'LATEST',
})
)
);
}
}
};
}

View File

@ -2,76 +2,155 @@ import {
ECSClient,
ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSTaskDefinitionLogConfiguration implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send(
new ListTaskDefinitionsCommand({ status: 'ACTIVE' })
)
const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
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 taskDefinitions = await this.getTaskDefinitions();
for (const taskDefinition of taskDefinitions) {
const logDisabledContainers = taskDefinition.containerDefinitions?.filter(
container => !container.logConfiguration
)
(container) => !container.logConfiguration
);
if (logDisabledContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
nonCompliantResources.push(taskDefinition.taskDefinitionArn!);
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
compliantResources.push(taskDefinition.taskDefinitionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-configuration' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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 logConfiguration = requiredParametersForFix.find(
param => param.name === 'log-configuration'
)?.value
(param) => param.name === 'log-configuration'
)?.value;
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) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
(container) => ({
...container,
logConfiguration: JSON.parse(logConfiguration)
logConfiguration: JSON.parse(logConfiguration),
})
)
);
await this.client.send(
new RegisterTaskDefinitionCommand({
@ -80,9 +159,9 @@ export class ECSTaskDefinitionLogConfiguration implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
memory: taskDefinition.taskDefinition?.memory,
})
)
);
}
}
};
}

View File

@ -2,65 +2,152 @@ import {
ECSClient,
ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSTaskDefinitionMemoryHardLimit implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send(
new ListTaskDefinitionsCommand({ status: 'ACTIVE' })
)
const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
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 taskDefinitions = await this.getTaskDefinitions();
for (const taskDefinition of taskDefinitions) {
const containersWithoutMemoryLimit = taskDefinition.containerDefinitions?.filter(
container => !container.memory
)
(container) => !container.memory
);
if (containersWithoutMemoryLimit?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
nonCompliantResources.push(taskDefinition.taskDefinitionArn!);
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
compliantResources.push(taskDefinition.taskDefinitionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.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,
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) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
(container) => ({
...container,
memory: container.memory || 512 // Default hard limit memory value
memory: container.memory || defaultMemoryLimit,
})
)
);
await this.client.send(
new RegisterTaskDefinitionCommand({
@ -69,9 +156,9 @@ export class ECSTaskDefinitionMemoryHardLimit implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
memory: taskDefinition.taskDefinition?.memory,
})
)
);
}
}
};
}

View File

@ -2,65 +2,151 @@ import {
ECSClient,
ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
RegisterTaskDefinitionCommand,
} from '@aws-sdk/client-ecs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ECSTaskDefinitionNonRootUser implements BPSet {
private readonly client = new ECSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ECSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getTaskDefinitions = async () => {
const taskDefinitionArns = await this.memoClient.send(
new ListTaskDefinitionsCommand({ status: 'ACTIVE' })
)
const taskDefinitions = []
);
const taskDefinitions = [];
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
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 taskDefinitions = await this.getTaskDefinitions();
for (const taskDefinition of taskDefinitions) {
const privilegedContainers = taskDefinition.containerDefinitions?.filter(
container => !container.user || container.user === 'root'
)
(container) => !container.user || container.user === 'root'
);
if (privilegedContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
nonCompliantResources.push(taskDefinition.taskDefinitionArn!);
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
compliantResources.push(taskDefinition.taskDefinitionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.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,
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) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
const family = taskDefinition.taskDefinition?.family
);
const family = taskDefinition.taskDefinition?.family;
const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
(container) => ({
...container,
user: container.user || 'ecs-user' // Default non-root user
user: container.user || defaultNonRootUser,
})
)
);
await this.client.send(
new RegisterTaskDefinitionCommand({
@ -69,9 +155,9 @@ export class ECSTaskDefinitionNonRootUser implements BPSet {
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
memory: taskDefinition.taskDefinition?.memory,
})
)
);
}
}
};
}

View File

@ -2,70 +2,149 @@ import {
EFSClient,
DescribeAccessPointsCommand,
DeleteAccessPointCommand,
CreateAccessPointCommand
} from '@aws-sdk/client-efs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
CreateAccessPointCommand,
} from '@aws-sdk/client-efs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EFSAccessPointEnforceRootDirectory implements BPSet {
private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EFSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getAccessPoints = async () => {
const response = await this.memoClient.send(new DescribeAccessPointsCommand({}))
return response.AccessPoints || []
}
const response = await this.memoClient.send(new DescribeAccessPointsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const accessPoints = await this.getAccessPoints()
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 accessPoints = await this.getAccessPoints();
for (const accessPoint of accessPoints) {
if (accessPoint.RootDirectory?.Path !== '/') {
compliantResources.push(accessPoint.AccessPointArn!)
if (accessPoint.RootDirectory?.Path === '/') {
compliantResources.push(accessPoint.AccessPointArn!);
} else {
nonCompliantResources.push(accessPoint.AccessPointArn!)
nonCompliantResources.push(accessPoint.AccessPointArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'root-directory-path' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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 rootDirectoryPath = requiredParametersForFix.find(
param => param.name === 'root-directory-path'
)?.value
(param) => param.name === 'root-directory-path'
)?.value;
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) {
const accessPointId = arn.split('/').pop()!
const fileSystemId = arn.split(':file-system/')[1].split('/')[0]
const accessPointId = arn.split('/').pop()!;
const fileSystemId = arn.split(':file-system/')[1].split('/')[0];
// Delete the existing access point
await this.client.send(
new DeleteAccessPointCommand({
AccessPointId: accessPointId
AccessPointId: accessPointId,
})
)
);
// Recreate the access point with the desired root directory
await this.client.send(
new CreateAccessPointCommand({
FileSystemId: fileSystemId,
RootDirectory: { Path: rootDirectoryPath }
RootDirectory: { Path: rootDirectoryPath },
})
)
);
}
}
};
}

View File

@ -2,68 +2,147 @@ import {
EFSClient,
DescribeAccessPointsCommand,
DeleteAccessPointCommand,
CreateAccessPointCommand
} from '@aws-sdk/client-efs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
CreateAccessPointCommand,
} from '@aws-sdk/client-efs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EFSAccessPointEnforceUserIdentity implements BPSet {
private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EFSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getAccessPoints = async () => {
const response = await this.memoClient.send(new DescribeAccessPointsCommand({}))
return response.AccessPoints || []
}
const response = await this.memoClient.send(new DescribeAccessPointsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const accessPoints = await this.getAccessPoints()
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 accessPoints = await this.getAccessPoints();
for (const accessPoint of accessPoints) {
if (accessPoint.PosixUser) {
compliantResources.push(accessPoint.AccessPointArn!)
compliantResources.push(accessPoint.AccessPointArn!);
} else {
nonCompliantResources.push(accessPoint.AccessPointArn!)
nonCompliantResources.push(accessPoint.AccessPointArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'posix-user' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
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 posixUser = requiredParametersForFix.find(param => param.name === 'posix-user')?.value
const posixUser = requiredParametersForFix.find((param) => param.name === 'posix-user')?.value;
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) {
const accessPointId = arn.split('/').pop()!
const fileSystemId = arn.split(':file-system/')[1].split('/')[0]
const accessPointId = arn.split('/').pop()!;
const fileSystemId = arn.split(':file-system/')[1].split('/')[0];
// Delete the existing access point
await this.client.send(
new DeleteAccessPointCommand({
AccessPointId: accessPointId
AccessPointId: accessPointId,
})
)
);
// Recreate the access point with the desired PosixUser
await this.client.send(
new CreateAccessPointCommand({
FileSystemId: fileSystemId,
PosixUser: JSON.parse(posixUser)
PosixUser: JSON.parse(posixUser),
})
)
);
}
}
};
}

View File

@ -2,54 +2,129 @@ import {
EFSClient,
DescribeFileSystemsCommand,
PutBackupPolicyCommand,
DescribeBackupPolicyCommand
} from '@aws-sdk/client-efs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DescribeBackupPolicyCommand,
} from '@aws-sdk/client-efs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EFSAutomaticBackupsEnabled implements BPSet {
private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EFSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getFileSystems = async () => {
const response = await this.memoClient.send(new DescribeFileSystemsCommand({}))
return response.FileSystems || []
}
const response = await this.memoClient.send(new DescribeFileSystemsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const fileSystems = await this.getFileSystems()
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 fileSystems = await this.getFileSystems();
for (const fileSystem of fileSystems) {
const response = await this.client.send(
new DescribeBackupPolicyCommand({ FileSystemId: fileSystem.FileSystemId! })
)
);
if (response.BackupPolicy?.Status === 'ENABLED') {
compliantResources.push(fileSystem.FileSystemArn!)
compliantResources.push(fileSystem.FileSystemArn!);
} else {
nonCompliantResources.push(fileSystem.FileSystemArn!)
nonCompliantResources.push(fileSystem.FileSystemArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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,
requiredParametersForFix
) => {
for (const arn of nonCompliantResources) {
const fileSystemId = arn.split('/').pop()!
const fileSystemId = arn.split('/').pop()!;
await this.client.send(
new PutBackupPolicyCommand({
FileSystemId: fileSystemId,
BackupPolicy: { Status: 'ENABLED' }
BackupPolicy: { Status: 'ENABLED' },
})
)
);
}
}
};
}

View File

@ -2,53 +2,128 @@ import {
EFSClient,
DescribeFileSystemsCommand,
CreateFileSystemCommand,
DeleteFileSystemCommand
} from '@aws-sdk/client-efs'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DeleteFileSystemCommand,
} from '@aws-sdk/client-efs';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EFSEncryptedCheck implements BPSet {
private readonly client = new EFSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EFSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getFileSystems = async () => {
const response = await this.memoClient.send(new DescribeFileSystemsCommand({}))
return response.FileSystems || []
}
const response = await this.memoClient.send(new DescribeFileSystemsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const fileSystems = await this.getFileSystems()
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 fileSystems = await this.getFileSystems();
for (const fileSystem of fileSystems) {
if (fileSystem.Encrypted) {
compliantResources.push(fileSystem.FileSystemArn!)
compliantResources.push(fileSystem.FileSystemArn!);
} else {
nonCompliantResources.push(fileSystem.FileSystemArn!)
nonCompliantResources.push(fileSystem.FileSystemArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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,
requiredParametersForFix
) => {
for (const arn of nonCompliantResources) {
const fileSystemId = arn.split('/').pop()!
const fileSystemId = arn.split('/').pop()!;
const fileSystem = await this.memoClient.send(
new DescribeFileSystemsCommand({ FileSystemId: fileSystemId })
)
);
// Delete the non-compliant file system
await this.client.send(
new DeleteFileSystemCommand({
FileSystemId: fileSystemId
FileSystemId: fileSystemId,
})
)
);
// Recreate the file system with encryption enabled
await this.client.send(
@ -56,9 +131,9 @@ export class EFSEncryptedCheck implements BPSet {
Encrypted: true,
PerformanceMode: fileSystem.FileSystems?.[0]?.PerformanceMode,
ThroughputMode: fileSystem.FileSystems?.[0]?.ThroughputMode,
ProvisionedThroughputInMibps: fileSystem.FileSystems?.[0]?.ProvisionedThroughputInMibps
ProvisionedThroughputInMibps: fileSystem.FileSystems?.[0]?.ProvisionedThroughputInMibps,
})
)
);
}
}
};
}

View File

@ -1,71 +1,132 @@
import {
EFSClient,
DescribeFileSystemsCommand,
DescribeMountTargetsCommand
} from '@aws-sdk/client-efs'
import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DescribeMountTargetsCommand,
} from '@aws-sdk/client-efs';
import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2';
import { BPSet, BPSetFixFn, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EFSMountTargetPublicAccessible implements BPSet {
private readonly efsClient = new EFSClient({})
private readonly ec2Client = new EC2Client({})
private readonly memoEFSClient = Memorizer.memo(this.efsClient)
private readonly memoEC2Client = Memorizer.memo(this.ec2Client)
private readonly efsClient = new EFSClient({});
private readonly ec2Client = new EC2Client({});
private readonly memoEFSClient = Memorizer.memo(this.efsClient);
private readonly memoEC2Client = Memorizer.memo(this.ec2Client);
private readonly getFileSystems = async () => {
const response = await this.memoEFSClient.send(new DescribeFileSystemsCommand({}))
return response.FileSystems || []
}
const response = await this.memoEFSClient.send(new DescribeFileSystemsCommand({}));
return response.FileSystems || [];
};
private readonly getRoutesForSubnet = async (subnetId: string) => {
const response = await this.memoEC2Client.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const fileSystems = await this.getFileSystems()
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 fileSystems = await this.getFileSystems();
for (const fileSystem of fileSystems) {
const mountTargets = await this.memoEFSClient.send(
new DescribeMountTargetsCommand({ FileSystemId: fileSystem.FileSystemId! })
)
);
let isNonCompliant = false;
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 (
route.DestinationCidrBlock === '0.0.0.0/0' &&
route.GatewayId?.startsWith('igw-')
) {
nonCompliantResources.push(fileSystem.FileSystemArn!)
break
}
if (
routes.some(
(route) =>
route.DestinationCidrBlock === '0.0.0.0/0' && route.GatewayId?.startsWith('igw-')
)
) {
nonCompliantResources.push(fileSystem.FileSystemArn!);
isNonCompliant = true;
break;
}
}
if (!nonCompliantResources.includes(fileSystem.FileSystemArn!)) {
compliantResources.push(fileSystem.FileSystemArn!)
if (!isNonCompliant) {
compliantResources.push(fileSystem.FileSystemArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
public readonly fix: BPSetFixFn = async () => {
throw new Error(
'Fixing public accessibility for mount targets requires manual network reconfiguration.'
)
}
);
};
}

View File

@ -2,52 +2,111 @@ import {
EKSClient,
ListClustersCommand,
DescribeClusterCommand,
UpdateClusterConfigCommand
} from '@aws-sdk/client-eks'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateClusterConfigCommand,
} from '@aws-sdk/client-eks';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EKSClusterLoggingEnabled implements BPSet {
private readonly client = new EKSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EKSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getClusters = async () => {
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterNames = clusterNamesResponse.clusters || []
const clusters = []
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}));
const clusterNames = clusterNamesResponse.clusters || [];
const clusters = [];
for (const clusterName of clusterNames) {
const cluster = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
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 clusters = await this.getClusters();
for (const cluster of clusters) {
const clusterLogging = cluster.logging?.clusterLogging?.[0]
const clusterLogging = cluster.logging?.clusterLogging?.[0];
if (clusterLogging?.enabled && clusterLogging.types?.length === 5) {
compliantResources.push(cluster.arn!)
compliantResources.push(cluster.arn!);
} else {
nonCompliantResources.push(cluster.arn!)
nonCompliantResources.push(cluster.arn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) {
const clusterName = arn.split(':cluster/')[1]
const clusterName = arn.split(':cluster/')[1];
await this.client.send(
new UpdateClusterConfigCommand({
@ -56,12 +115,12 @@ export class EKSClusterLoggingEnabled implements BPSet {
clusterLogging: [
{
enabled: true,
types: ['api', 'audit', 'authenticator', 'controllerManager', 'scheduler']
}
]
}
types: ['api', 'audit', 'authenticator', 'controllerManager', 'scheduler'],
},
],
},
})
)
);
}
}
};
}

View File

@ -2,61 +2,124 @@ import {
EKSClient,
ListClustersCommand,
DescribeClusterCommand,
AssociateEncryptionConfigCommand
} from '@aws-sdk/client-eks'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
AssociateEncryptionConfigCommand,
} from '@aws-sdk/client-eks';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EKSClusterSecretsEncrypted implements BPSet {
private readonly client = new EKSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EKSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getClusters = async () => {
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterNames = clusterNamesResponse.clusters || []
const clusters = []
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}));
const clusterNames = clusterNamesResponse.clusters || [];
const clusters = [];
for (const clusterName of clusterNames) {
const cluster = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
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 clusters = await this.getClusters();
for (const cluster of clusters) {
const encryptionConfig = cluster.encryptionConfig?.[0]
const encryptionConfig = cluster.encryptionConfig?.[0];
if (encryptionConfig?.resources?.includes('secrets')) {
compliantResources.push(cluster.arn!)
compliantResources.push(cluster.arn!);
} else {
nonCompliantResources.push(cluster.arn!)
nonCompliantResources.push(cluster.arn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value
public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value;
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) {
const clusterName = arn.split(':cluster/')[1]
const clusterName = arn.split(':cluster/')[1];
await this.client.send(
new AssociateEncryptionConfigCommand({
@ -64,11 +127,11 @@ export class EKSClusterSecretsEncrypted implements BPSet {
encryptionConfig: [
{
resources: ['secrets'],
provider: { keyArn: kmsKeyId }
}
]
provider: { keyArn: kmsKeyId },
},
],
})
)
);
}
}
};
}

View File

@ -2,61 +2,118 @@ import {
EKSClient,
ListClustersCommand,
DescribeClusterCommand,
UpdateClusterConfigCommand
} from '@aws-sdk/client-eks'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateClusterConfigCommand,
} from '@aws-sdk/client-eks';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class EKSEndpointNoPublicAccess implements BPSet {
private readonly client = new EKSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new EKSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getClusters = async () => {
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}))
const clusterNames = clusterNamesResponse.clusters || []
const clusters = []
const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({}));
const clusterNames = clusterNamesResponse.clusters || [];
const clusters = [];
for (const clusterName of clusterNames) {
const cluster = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
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 clusters = await this.getClusters();
for (const cluster of clusters) {
const endpointPublicAccess = cluster.resourcesVpcConfig?.endpointPublicAccess
const endpointPublicAccess = cluster.resourcesVpcConfig?.endpointPublicAccess;
if (endpointPublicAccess) {
nonCompliantResources.push(cluster.arn!)
nonCompliantResources.push(cluster.arn!);
} else {
compliantResources.push(cluster.arn!)
compliantResources.push(cluster.arn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) {
const clusterName = arn.split(':cluster/')[1]
const clusterName = arn.split(':cluster/')[1];
await this.client.send(
new UpdateClusterConfigCommand({
name: clusterName,
resourcesVpcConfig: {
endpointPublicAccess: false
}
endpointPublicAccess: false,
},
})
)
);
}
}
};
}

View File

@ -1,49 +1,102 @@
import {
ElastiCacheClient,
DescribeCacheClustersCommand,
ModifyCacheClusterCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ModifyCacheClusterCommand,
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheAutoMinorVersionUpgradeCheck implements BPSet {
private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElastiCacheClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getClusters = async () => {
const response = await this.memoClient.send(new DescribeCacheClustersCommand({}))
return response.CacheClusters || []
}
const response = await this.memoClient.send(new DescribeCacheClustersCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
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 clusters = await this.getClusters();
for (const cluster of clusters) {
if (cluster.AutoMinorVersionUpgrade) {
compliantResources.push(cluster.ARN!)
compliantResources.push(cluster.ARN!);
} else {
nonCompliantResources.push(cluster.ARN!)
nonCompliantResources.push(cluster.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster:')[1]
const clusterId = arn.split(':cluster:')[1];
await this.client.send(
new ModifyCacheClusterCommand({
CacheClusterId: clusterId,
AutoMinorVersionUpgrade: true
AutoMinorVersionUpgrade: true,
})
)
);
}
}
};
}

View File

@ -1,60 +1,117 @@
import {
ElastiCacheClient,
DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ModifyReplicationGroupCommand,
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheRedisClusterAutomaticBackupCheck implements BPSet {
private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElastiCacheClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []
}
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups()
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 replicationGroups = await this.getReplicationGroups();
for (const group of replicationGroups) {
if (group.SnapshottingClusterId) {
compliantResources.push(group.ARN!)
compliantResources.push(group.ARN!);
} else {
nonCompliantResources.push(group.ARN!)
nonCompliantResources.push(group.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'snapshot-retention-period' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => {
const retentionPeriod = requiredParametersForFix.find(
param => param.name === 'snapshot-retention-period'
)?.value
(param) => param.name === 'snapshot-retention-period'
)?.value;
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) {
const groupId = arn.split(':replication-group:')[1]
const groupId = arn.split(':replication-group:')[1];
await this.client.send(
new ModifyReplicationGroupCommand({
ReplicationGroupId: groupId,
SnapshotRetentionLimit: parseInt(retentionPeriod, 10)
SnapshotRetentionLimit: parseInt(retentionPeriod, 10),
})
)
);
}
}
};
}

View File

@ -1,49 +1,102 @@
import {
ElastiCacheClient,
DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ModifyReplicationGroupCommand,
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats, BPSetFixFn } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheReplGrpAutoFailoverEnabled implements BPSet {
private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElastiCacheClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []
}
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups()
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 replicationGroups = await this.getReplicationGroups();
for (const group of replicationGroups) {
if (group.AutomaticFailover === 'enabled') {
compliantResources.push(group.ARN!)
compliantResources.push(group.ARN!);
} else {
nonCompliantResources.push(group.ARN!)
nonCompliantResources.push(group.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
public readonly fix: BPSetFixFn = async (nonCompliantResources) => {
for (const arn of nonCompliantResources) {
const groupId = arn.split(':replication-group:')[1]
const groupId = arn.split(':replication-group:')[1];
await this.client.send(
new ModifyReplicationGroupCommand({
ReplicationGroupId: groupId,
AutomaticFailoverEnabled: true
AutomaticFailoverEnabled: true,
})
)
);
}
}
};
}

View File

@ -1,43 +1,90 @@
import {
ElastiCacheClient,
DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheReplGrpEncryptedAtRest implements BPSet {
private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElastiCacheClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []
}
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups()
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 replicationGroups = await this.getReplicationGroups();
for (const group of replicationGroups) {
if (group.AtRestEncryptionEnabled) {
compliantResources.push(group.ARN!)
compliantResources.push(group.ARN!);
} else {
nonCompliantResources.push(group.ARN!)
nonCompliantResources.push(group.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
public readonly fix = async () => {
throw new Error(
'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 {
ElastiCacheClient,
DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheReplGrpEncryptedInTransit implements BPSet {
private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElastiCacheClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getReplicationGroups = async () => {
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}))
return response.ReplicationGroups || []
}
const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups()
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 replicationGroups = await this.getReplicationGroups();
for (const group of replicationGroups) {
if (group.TransitEncryptionEnabled) {
compliantResources.push(group.ARN!)
compliantResources.push(group.ARN!);
} else {
nonCompliantResources.push(group.ARN!)
nonCompliantResources.push(group.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
public readonly fix = async () => {
throw new Error(
'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,
DeleteCacheClusterCommand,
CreateCacheClusterCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-elasticache';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class ElastiCacheSubnetGroupCheck implements BPSet {
private readonly client = new ElastiCacheClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new ElastiCacheClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getClusters = async () => {
const response = await this.memoClient.send(new DescribeCacheClustersCommand({}))
return response.CacheClusters || []
}
const response = await this.memoClient.send(new DescribeCacheClustersCommand({}));
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 () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
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 clusters = await this.getClusters();
for (const cluster of clusters) {
if (cluster.CacheSubnetGroupName !== 'default') {
compliantResources.push(cluster.ARN!)
compliantResources.push(cluster.ARN!);
} else {
nonCompliantResources.push(cluster.ARN!)
nonCompliantResources.push(cluster.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'subnet-group-name' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const subnetGroupName = requiredParametersForFix.find(
param => param.name === 'subnet-group-name'
)?.value
(param) => param.name === 'subnet-group-name'
)?.value;
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) {
const clusterId = arn.split(':cluster:')[1]
const clusterId = arn.split(':cluster:')[1];
const cluster = await this.memoClient.send(
new DescribeCacheClustersCommand({ CacheClusterId: clusterId })
)
const clusterDetails = cluster.CacheClusters?.[0]
);
const clusterDetails = cluster.CacheClusters?.[0];
if (!clusterDetails) {
continue
continue;
}
// Delete the non-compliant cluster
await this.client.send(
new DeleteCacheClusterCommand({
CacheClusterId: clusterId
CacheClusterId: clusterId,
})
)
);
// Recreate the cluster with the desired subnet group
await this.client.send(
@ -74,11 +138,13 @@ export class ElastiCacheSubnetGroupCheck implements BPSet {
CacheNodeType: clusterDetails.CacheNodeType!,
NumCacheNodes: clusterDetails.NumCacheNodes!,
CacheSubnetGroupName: subnetGroupName,
SecurityGroupIds: clusterDetails.SecurityGroups?.map(group => group.SecurityGroupId) as string[],
SecurityGroupIds: clusterDetails.SecurityGroups?.map(
(group) => group.SecurityGroupId
) as string[],
PreferredMaintenanceWindow: clusterDetails.PreferredMaintenanceWindow,
EngineVersion: clusterDetails.EngineVersion
EngineVersion: clusterDetails.EngineVersion,
})
)
);
}
}
};
}

View File

@ -3,38 +3,98 @@ import {
ListPoliciesCommand,
GetPolicyVersionCommand,
DeletePolicyCommand
} from '@aws-sdk/client-iam'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-iam';
import { BPSet, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
private readonly client = new IAMClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new IAMClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getPolicies = async () => {
const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' }))
return response.Policies || []
}
const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' }));
return response.Policies || [];
};
private readonly getPolicyDefaultVersions = async (policyArn: string, versionId: string) => {
const response = await this.memoClient.send(
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 () => {
const compliantResources = []
const nonCompliantResources = []
const policies = await this.getPolicies()
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 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)
? policyDocument.Statement
: [policyDocument.Statement]
: [policyDocument.Statement];
for (const statement of statements) {
if (
@ -42,26 +102,23 @@ export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
statement?.Resource === '*' &&
statement?.Effect === 'Allow'
) {
nonCompliantResources.push(policy.Arn!)
break
nonCompliantResources.push(policy.Arn!);
break;
}
}
if (!nonCompliantResources.includes(policy.Arn!)) {
compliantResources.push(policy.Arn!)
compliantResources.push(policy.Arn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (nonCompliantResources: string[]) => {
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 { BPSet } from "../../types";
import {
IAMClient,
ListPoliciesCommand,
GetPolicyVersionCommand,
CreatePolicyVersionCommand,
DeletePolicyVersionCommand,
} from "@aws-sdk/client-iam";
import { BPSet, BPSetMetadata, BPSetStats } from "../../types";
import { Memorizer } from "../../Memorizer";
export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
private readonly client = new IAMClient({});
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 () => {
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[] = [];
// Fetch all customer-managed IAM policies
const policiesResponse = await this.memoClient.send(
new ListPoliciesCommand({ Scope: "Local" })
);
const policies = policiesResponse.Policies || [];
for (const policy of policies) {
// Get the default version of the policy
const policyVersionResponse = await this.memoClient.send(
new GetPolicyVersionCommand({
PolicyArn: policy.Arn!,
@ -29,7 +96,6 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
);
// Check statements for full access
const hasFullAccess = policyDocument.Statement.some((statement: any) => {
if (statement.Effect === "Deny") return false;
const actions = Array.isArray(statement.Action)
@ -45,23 +111,77 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [],
};
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
for (const policyArn of nonCompliantResources) {
// Add logic to remove or modify the statements with full access
// Note: Updating an IAM policy requires creating a new version and setting it as default
console.error(
`Fix operation is not implemented for policy ${policyArn}. Manual intervention is required.`
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 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 {
IAMClient,
ListPoliciesCommand,
ListEntitiesForPolicyCommand
} from '@aws-sdk/client-iam'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ListEntitiesForPolicyCommand,
} from '@aws-sdk/client-iam';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class IAMRoleManagedPolicyCheck implements BPSet {
private readonly client = new IAMClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new IAMClient({});
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 () => {
const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' }))
return response.Policies || []
}
const response = await this.memoClient.send(
new ListPoliciesCommand({ Scope: 'Local' })
);
return response.Policies || [];
};
private readonly checkEntitiesForPolicy = async (policyArn: string) => {
const response = await this.memoClient.send(
new ListEntitiesForPolicyCommand({ PolicyArn: policyArn })
)
);
return {
attached: Boolean(
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.'
)
}
response.PolicyGroups?.length ||
response.PolicyUsers?.length ||
response.PolicyRoles?.length
),
};
};
}

View File

@ -1,58 +1,136 @@
import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class LambdaDLQCheck implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new LambdaClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const functions = await this.getFunctions()
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,
});
}
);
};
private readonly checkImpl = async () => {
const compliantResources: string[] = [];
const nonCompliantResources: string[] = [];
const functions = await this.getFunctions();
for (const func of functions) {
if (func.DeadLetterConfig) {
compliantResources.push(func.FunctionArn!)
compliantResources.push(func.FunctionArn!);
} else {
nonCompliantResources.push(func.FunctionArn!)
nonCompliantResources.push(func.FunctionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'dlq-arn' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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) {
throw new Error("Required parameter 'dlq-arn' is missing.")
throw new Error("Required parameter 'dlq-arn' is missing.");
}
for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!
const functionName = functionArn.split(':').pop()!;
await this.client.send(
new UpdateFunctionConfigurationCommand({
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,
ListFunctionsCommand,
GetPolicyCommand,
RemovePermissionCommand
} from '@aws-sdk/client-lambda'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
RemovePermissionCommand,
} from '@aws-sdk/client-lambda';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class LambdaFunctionPublicAccessProhibited implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new LambdaClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const functions = await this.getFunctions()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaFunctionPublicAccessProhibited',
description: 'Ensures that Lambda functions do not allow public access via their resource-based policies.',
priority: 1,
priorityReason: 'Publicly accessible Lambda functions pose significant security risks.',
awsService: 'Lambda',
awsServiceCategory: 'Serverless',
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) {
try {
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! }))
const policy = JSON.parse(response.Policy!)
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! }));
const policy = JSON.parse(response.Policy!);
const hasPublicAccess = policy.Statement.some(
(statement: any) => statement.Principal === '*' || statement.Principal?.AWS === '*'
)
);
if (hasPublicAccess) {
nonCompliantResources.push(func.FunctionArn!)
nonCompliantResources.push(func.FunctionArn!);
} else {
compliantResources.push(func.FunctionArn!)
compliantResources.push(func.FunctionArn!);
}
} catch (error) {
if ((error as any).name === 'ResourceNotFoundException') {
nonCompliantResources.push(func.FunctionArn!)
compliantResources.push(func.FunctionArn!);
} else {
throw error
throw error;
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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) {
const functionName = functionArn.split(':').pop()!
const functionName = functionArn.split(':').pop()!;
try {
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: functionName }))
const policy = JSON.parse(response.Policy!)
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: functionName }));
const policy = JSON.parse(response.Policy!);
for (const statement of policy.Statement) {
if (statement.Principal === '*' || statement.Principal?.AWS === '*') {
await this.client.send(
new RemovePermissionCommand({
FunctionName: functionName,
StatementId: statement.Sid // Use the actual StatementId from the policy
StatementId: statement.Sid, // Use the actual StatementId from the policy
})
)
);
}
}
} catch (error) {
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 { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
import {
LambdaClient,
ListFunctionsCommand,
UpdateFunctionConfigurationCommand
} from '@aws-sdk/client-lambda';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class LambdaFunctionSettingsCheck implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new LambdaClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const defaultTimeout = 3
const defaultMemorySize = 128
const functions = await this.getFunctions()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaFunctionSettingsCheck',
description: 'Ensures Lambda functions have non-default timeout and memory size configurations.',
priority: 2,
priorityReason: 'Default configurations may not be suitable for production workloads.',
awsService: 'Lambda',
awsServiceCategory: 'Serverless',
bestPracticeCategory: 'Configuration',
requiredParametersForFix: [
{
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) {
if (func.Timeout === defaultTimeout || func.MemorySize === defaultMemorySize) {
nonCompliantResources.push(func.FunctionArn!)
nonCompliantResources.push(func.FunctionArn!);
} else {
compliantResources.push(func.FunctionArn!)
compliantResources.push(func.FunctionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'timeout' },
{ name: 'memory-size' }
]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.value
const memorySize = requiredParametersForFix.find(param => param.name === 'memory-size')?.value
) => {
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 timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.value;
const memorySize = requiredParametersForFix.find(param => param.name === 'memory-size')?.value;
if (!timeout || !memorySize) {
throw new Error("Required parameters 'timeout' and/or 'memory-size' are missing.")
throw new Error("Required parameters 'timeout' and/or 'memory-size' are missing.");
}
for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!
const functionName = functionArn.split(':').pop()!;
await this.client.send(
new UpdateFunctionConfigurationCommand({
FunctionName: functionName,
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,
ListFunctionsCommand,
UpdateFunctionConfigurationCommand
} from '@aws-sdk/client-lambda'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-lambda';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class LambdaInsideVPC implements BPSet {
private readonly client = new LambdaClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new LambdaClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getFunctions = async () => {
const response = await this.memoClient.send(new ListFunctionsCommand({}))
return response.Functions || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const functions = await this.getFunctions()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'LambdaInsideVPC',
description: 'Ensures Lambda functions are configured to run inside a VPC.',
priority: 2,
priorityReason: 'Running Lambda inside a VPC enhances security by restricting access.',
awsService: 'Lambda',
awsServiceCategory: 'Serverless',
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) {
if (func.VpcConfig && Object.keys(func.VpcConfig).length > 0) {
compliantResources.push(func.FunctionArn!)
compliantResources.push(func.FunctionArn!);
} else {
nonCompliantResources.push(func.FunctionArn!)
nonCompliantResources.push(func.FunctionArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'subnet-ids' },
{ name: 'security-group-ids' }
]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-ids')?.value
const securityGroupIds = requiredParametersForFix.find(param => param.name === 'security-group-ids')?.value
) => {
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 subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-ids')?.value;
const securityGroupIds = requiredParametersForFix.find(param => param.name === 'security-group-ids')?.value;
if (!subnetIds || !securityGroupIds) {
throw new Error("Required parameters 'subnet-ids' and/or 'security-group-ids' are missing.")
throw new Error("Required parameters 'subnet-ids' and/or 'security-group-ids' are missing.");
}
for (const functionArn of nonCompliantResources) {
const functionName = functionArn.split(':').pop()!
const functionName = functionArn.split(':').pop()!;
await this.client.send(
new UpdateFunctionConfigurationCommand({
FunctionName: functionName,
@ -63,7 +135,12 @@ export class LambdaInsideVPC implements BPSet {
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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
} from '@aws-sdk/client-rds';
import {
BackupClient,
ListRecoveryPointsByResourceCommand
} from '@aws-sdk/client-backup'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-backup';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class AuroraLastBackupRecoveryPointCreated implements BPSet {
private readonly rdsClient = new RDSClient({})
private readonly backupClient = new BackupClient({})
private readonly memoRdsClient = Memorizer.memo(this.rdsClient)
private readonly rdsClient = new RDSClient({});
private readonly backupClient = new BackupClient({});
private readonly memoRdsClient = Memorizer.memo(this.rdsClient);
private readonly getDBClusters = async () => {
const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
private readonly getRecoveryPoints = async (resourceArn: string) => {
const response = await this.backupClient.send(
new ListRecoveryPointsByResourceCommand({ ResourceArn: resourceArn })
)
return response.RecoveryPoints || []
}
public readonly getMetadata = (): BPSetMetadata => ({
name: 'AuroraLastBackupRecoveryPointCreated',
description: 'Ensures that Aurora DB clusters have a recovery point created within the last 24 hours.',
priority: 1,
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
const recoveryPoints = await this.getRecoveryPoints(cluster.DBClusterArn!)
const recoveryDates = recoveryPoints.map(rp => new Date(rp.CreationDate!))
recoveryDates.sort((a, b) => b.getTime() - a.getTime())
const recoveryPoints = await this.getRecoveryPoints(cluster.DBClusterArn!);
const recoveryDates = recoveryPoints.map(rp => new Date(rp.CreationDate!));
recoveryDates.sort((a, b) => b.getTime() - a.getTime());
if (
recoveryDates.length > 0 &&
new Date().getTime() - recoveryDates[0].getTime() < 24 * 60 * 60 * 1000
) {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'backup-retention-period', value: '7' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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(
param => param.name === 'backup-retention-period'
)?.value
)?.value;
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) {
const clusterId = arn.split(':cluster/')[1]
const clusterId = arn.split(':cluster/')[1];
await this.rdsClient.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class AuroraMySQLBacktrackingEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
if (
cluster.Engine === 'aurora-mysql' &&
(!cluster.EarliestBacktrackTime || cluster.EarliestBacktrackTime === null)
) {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
} else {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const clusterId = arn.split(':cluster/')[1]
const clusterId = arn.split(':cluster/')[1];
await this.client.send(
new ModifyDBClusterCommand({
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,
DescribeDBInstancesCommand,
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class DBInstanceBackupEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBInstances = async () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({}))
return response.DBInstances || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbInstances = await this.getDBInstances()
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 dbInstances = await this.getDBInstances();
for (const instance of dbInstances) {
if (instance.BackupRetentionPeriod && instance.BackupRetentionPeriod > 0) {
compliantResources.push(instance.DBInstanceArn!)
compliantResources.push(instance.DBInstanceArn!);
} else {
nonCompliantResources.push(instance.DBInstanceArn!)
nonCompliantResources.push(instance.DBInstanceArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'retention-period', value: '7' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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(
param => param.name === 'retention-period'
)?.value
(param) => param.name === 'retention-period'
)?.value;
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) {
const instanceId = arn.split(':instance/')[1]
const instanceId = arn.split(':instance/')[1];
await this.client.send(
new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId,
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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterAutoMinorVersionUpgradeEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
if (cluster.Engine === 'docdb' || cluster.AutoMinorVersionUpgrade) {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const clusterId = arn.split(':cluster/')[1]
const clusterId = arn.split(':cluster/')[1];
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterDefaultAdminCheck implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
if (!['admin', 'postgres'].includes(cluster.MasterUsername!)) {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'new-master-username', value: '<NEW_MASTER_USERNAME>' },
{ name: 'new-master-password', value: '<NEW_MASTER_PASSWORD>' }
]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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(
param => param.name === 'new-master-username'
)?.value
(param) => param.name === 'new-master-username'
)?.value;
const newMasterPassword = requiredParametersForFix.find(
param => param.name === 'new-master-password'
)?.value
(param) => param.name === 'new-master-password'
)?.value;
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) {
const clusterId = arn.split(':cluster/')[1]
const clusterId = arn.split(':cluster/')[1];
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterDeletionProtectionEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
if (cluster.DeletionProtection) {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const clusterId = arn.split(':cluster/')[1]
const clusterId = arn.split(':cluster/')[1];
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
DeletionProtection: true
})
)
);
}
}
};
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
}

View File

@ -1,43 +1,99 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DescribeDBClustersCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterEncryptedAtRest implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
if (cluster.StorageEncrypted) {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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(
'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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterIAMAuthenticationEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
if (
cluster.Engine === 'docdb' ||
cluster.IAMDatabaseAuthenticationEnabled
) {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const clusterId = arn.split(':cluster/')[1]
const clusterId = arn.split(':cluster/')[1];
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
EnableIAMDatabaseAuthentication: true
})
)
);
}
}
};
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
}

View File

@ -1,43 +1,121 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
DescribeDBClustersCommand
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSClusterMultiAZEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
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();
for (const cluster of dbClusters) {
if ((cluster.AvailabilityZones || []).length > 1) {
compliantResources.push(cluster.DBClusterArn!)
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'additional-azs', value: '2' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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(
'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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSDBSecurityGroupNotAllowed implements BPSet {
private readonly rdsClient = new RDSClient({})
private readonly ec2Client = new EC2Client({})
private readonly memoRdsClient = Memorizer.memo(this.rdsClient)
private readonly memoEc2Client = Memorizer.memo(this.ec2Client)
private readonly rdsClient = new RDSClient({});
private readonly ec2Client = new EC2Client({});
private readonly memoRdsClient = Memorizer.memo(this.rdsClient);
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 () => {
const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({}));
return response.DBClusters || [];
};
private readonly getDefaultSecurityGroups = async () => {
const response = await this.memoEc2Client.send(
new DescribeSecurityGroupsCommand({ Filters: [{ Name: 'group-name', Values: ['default'] }] })
)
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
})
)
}
}
);
return response.SecurityGroups || [];
};
}

View File

@ -2,60 +2,140 @@ import {
RDSClient,
DescribeDBInstancesCommand,
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSEnhancedMonitoringEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBInstances = async () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({}))
return response.DBInstances || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbInstances = await this.getDBInstances()
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 dbInstances = await this.getDBInstances();
for (const instance of dbInstances) {
if (instance.MonitoringInterval && instance.MonitoringInterval > 0) {
compliantResources.push(instance.DBInstanceArn!)
compliantResources.push(instance.DBInstanceArn!);
} else {
nonCompliantResources.push(instance.DBInstanceArn!)
nonCompliantResources.push(instance.DBInstanceArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'monitoring-interval', value: '60' }]
}
}
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 }[]
) => {
const monitoringInterval = requiredParametersForFix.find(
param => param.name === 'monitoring-interval'
)?.value
(param) => param.name === 'monitoring-interval'
)?.value;
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) {
const instanceId = arn.split(':instance/')[1]
const instanceId = arn.split(':instance/')[1];
await this.client.send(
new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId,
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,
DescribeDBInstancesCommand,
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSInstancePublicAccessCheck implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBInstances = async () => {
const response = await this.memoClient.send(new DescribeDBInstancesCommand({}))
return response.DBInstances || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const dbInstances = await this.getDBInstances()
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 dbInstances = await this.getDBInstances();
for (const instance of dbInstances) {
if (instance.PubliclyAccessible) {
nonCompliantResources.push(instance.DBInstanceArn!)
nonCompliantResources.push(instance.DBInstanceArn!);
} else {
compliantResources.push(instance.DBInstanceArn!)
compliantResources.push(instance.DBInstanceArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
const instanceId = arn.split(':instance/')[1]
const instanceId = arn.split(':instance/')[1];
await this.client.send(
new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId,
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,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSLoggingEnabled implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusters = async () => {
const response = await this.memoClient.send(new DescribeDBClustersCommand({}))
return response.DBClusters || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const logsForEngine = {
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 logsForEngine: Record<string, string[]> = {
'aurora-mysql': ['audit', 'error', 'general', 'slowquery'],
'aurora-postgresql': ['postgresql'],
'docdb': ['audit', 'profiler']
}
const dbClusters = await this.getDBClusters()
};
const dbClusters = await this.getDBClusters();
for (const cluster of dbClusters) {
if (
JSON.stringify(cluster.EnabledCloudwatchLogsExports || []) ===
JSON.stringify((logsForEngine as any)[cluster.Engine!] || [])
) {
compliantResources.push(cluster.DBClusterArn!)
const requiredLogs = logsForEngine[cluster.Engine!] || [];
const enabledLogs = cluster.EnabledCloudwatchLogsExports || [];
if (JSON.stringify(enabledLogs) === JSON.stringify(requiredLogs)) {
compliantResources.push(cluster.DBClusterArn!);
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
nonCompliantResources.push(cluster.DBClusterArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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 }[]
) => {
const logsForEngine: Record<string, string[]> = {
'aurora-mysql': ['audit', 'error', 'general', 'slowquery'],
'aurora-postgresql': ['postgresql'],
'docdb': ['audit', 'profiler']
};
const dbClusters = await this.getDBClusters();
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]
const logsForEngine = {
'aurora-mysql': ['audit', 'error', 'general', 'slowquery'],
'aurora-postgresql': ['postgresql'],
'docdb': ['audit', 'profiler']
}
const dbClusters = await this.getDBClusters()
const cluster = dbClusters.find(c => c.DBClusterArn === arn)
const clusterId = arn.split(':cluster/')[1];
const cluster = dbClusters.find((c) => c.DBClusterArn === arn);
if (cluster) {
const logsToEnable = (logsForEngine as any)[cluster.Engine!]
const logsToEnable = logsForEngine[cluster.Engine!] || [];
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
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,
DescribeDBClusterSnapshotsCommand,
CopyDBClusterSnapshotCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-rds';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class RDSSnapshotEncrypted implements BPSet {
private readonly client = new RDSClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new RDSClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getDBClusterSnapshots = async () => {
const response = await this.memoClient.send(new DescribeDBClusterSnapshotsCommand({}))
return response.DBClusterSnapshots || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources = []
const nonCompliantResources = []
const snapshots = await this.getDBClusterSnapshots()
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 snapshots = await this.getDBClusterSnapshots();
for (const snapshot of snapshots) {
if (snapshot.StorageEncrypted) {
compliantResources.push(snapshot.DBClusterSnapshotArn!)
compliantResources.push(snapshot.DBClusterSnapshotArn!);
} else {
nonCompliantResources.push(snapshot.DBClusterSnapshotArn!)
nonCompliantResources.push(snapshot.DBClusterSnapshotArn!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'kms-key-id', value: '<KMS_KEY_ID>' } // Replace with your KMS key ID
]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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) {
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) {
const snapshotId = arn.split(':snapshot:')[1]
const snapshotId = arn.split(':snapshot:')[1];
await this.client.send(
new CopyDBClusterSnapshotCommand({
@ -56,7 +131,12 @@ export class RDSSnapshotEncrypted implements BPSet {
TargetDBClusterSnapshotIdentifier: `${snapshotId}-encrypted`,
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,
DeleteAccessPointCommand,
CreateAccessPointCommand
} from '@aws-sdk/client-s3-control'
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-s3-control';
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3AccessPointInVpcOnly implements BPSet {
private readonly client = new S3ControlClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly stsClient = Memorizer.memo(new STSClient({}))
private readonly client = new S3ControlClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly stsClient = Memorizer.memo(new STSClient({}));
private readonly getAccountId = async (): Promise<string> => {
const response = await this.stsClient.send(new GetCallerIdentityCommand({}))
return response.Account!
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const requiredParametersForFix = [{ name: 'your-vpc-id' }]
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.'
});
const accountId = await this.getAccountId()
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 accountId = await this.getAccountId();
const response = await this.memoClient.send(
new ListAccessPointsCommand({ AccountId: accountId })
)
);
for (const accessPoint of response.AccessPointList || []) {
if (accessPoint.NetworkOrigin === 'VPC') {
compliantResources.push(accessPoint.AccessPointArn!)
compliantResources.push(accessPoint.AccessPointArn!);
} 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 (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
const accountId = await this.getAccountId()
const vpcId = requiredParametersForFix.find(param => param.name === 'your-vpc-id')?.value
) => {
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 accountId = await this.getAccountId();
const vpcId = requiredParametersForFix.find(param => param.name === 'your-vpc-id')?.value;
if (!vpcId) {
throw new Error("Required parameter 'your-vpc-id' is missing.")
throw new Error("Required parameter 'your-vpc-id' is missing.");
}
for (const accessPointArn of nonCompliantResources) {
const accessPointName = accessPointArn.split(':').pop()!
const bucketName = accessPointArn.split('/')[1]!
const accessPointName = accessPointArn.split(':').pop()!;
const bucketName = accessPointArn.split('/')[1]!;
await this.client.send(
new DeleteAccessPointCommand({
AccountId: accountId,
Name: accessPointName
})
)
);
await this.client.send(
new CreateAccessPointCommand({
@ -75,7 +152,12 @@ export class S3AccessPointInVpcOnly implements BPSet {
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,
GetObjectLockConfigurationCommand,
PutObjectLockConfigurationCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3BucketDefaultLockEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
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 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 {
await this.memoClient.send(
new GetObjectLockConfigurationCommand({ Bucket: bucket.Name! })
)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
);
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} catch (error) {
if ((error as any).name === 'ObjectLockConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
throw error
throw error;
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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) {
const bucketName = bucketArn.split(':::')[1]!
const bucketName = bucketArn.split(':::')[1]!;
await this.client.send(
new PutObjectLockConfigurationCommand({
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,
GetPublicAccessBlockCommand,
PutPublicAccessBlockCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3BucketLevelPublicAccessProhibited implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: []
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
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 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 GetPublicAccessBlockCommand({ Bucket: bucket.Name! })
)
const config = response.PublicAccessBlockConfiguration
);
const config = response.PublicAccessBlockConfiguration;
if (
config?.BlockPublicAcls &&
config?.IgnorePublicAcls &&
config?.BlockPublicPolicy &&
config?.RestrictPublicBuckets
) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
} catch (error) {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} catch {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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) {
const bucketName = bucketArn.split(':::')[1]!
const bucketName = bucketArn.split(':::')[1]!;
await this.client.send(
new PutPublicAccessBlockCommand({
Bucket: bucketName,
@ -69,7 +133,12 @@ export class S3BucketLevelPublicAccessProhibited implements BPSet {
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,
ListBucketsCommand,
GetBucketLoggingCommand,
PutBucketLoggingCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutBucketLoggingCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3BucketLoggingEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketLoggingEnabled',
description: 'Ensures that S3 buckets have logging enabled.',
priority: 2,
priorityReason:
'Enabling logging on S3 buckets provides audit and security capabilities.',
awsService: 'S3',
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) {
const response = await this.memoClient.send(
new GetBucketLoggingCommand({ Bucket: bucket.Name! })
)
if (response.LoggingEnabled) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
try {
const response = await this.memoClient.send(
new GetBucketLoggingCommand({ Bucket: bucket.Name! })
);
if (response.LoggingEnabled) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
} catch (error) {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'log-destination-bucket' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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(
param => param.name === 'log-destination-bucket'
)?.value
(param) => param.name === 'log-destination-bucket'
)?.value;
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) {
const bucketName = bucketArn.split(':::')[1]!
const bucketName = bucketArn.split(':::')[1]!;
await this.client.send(
new PutBucketLoggingCommand({
Bucket: bucketName,
BucketLoggingStatus: {
LoggingEnabled: {
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,
ListBucketsCommand,
GetBucketPolicyCommand,
PutBucketPolicyCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutBucketPolicyCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3BucketSSLRequestsOnly implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 => {
return JSON.stringify({
@ -28,103 +185,16 @@ export class S3BucketSSLRequestsOnly implements BPSet {
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: {
Bool: {
'aws:SecureTransport': 'false'
}
}
}
]
})
}
'aws:SecureTransport': 'false',
},
},
},
],
});
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
for (const bucket of buckets) {
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucket.Name! })
)
const policy = JSON.parse(response.Policy!)
const hasSSLCondition = policy.Statement.some(
(stmt: any) =>
stmt.Condition &&
stmt.Condition.Bool &&
stmt.Condition.Bool['aws:SecureTransport'] === 'false'
)
if (hasSSLCondition) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
} catch (error) {
if ((error as any).name === 'NoSuchBucketPolicy') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
): Promise<void> => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
let existingPolicy: any
try {
const response = await this.memoClient.send(
new GetBucketPolicyCommand({ Bucket: bucketName })
)
existingPolicy = JSON.parse(response.Policy!)
} catch (error) {
if ((error as any).name !== 'NoSuchBucketPolicy') {
throw error
}
}
const sslPolicyStatement = {
Sid: 'DenyNonSSLRequests',
Effect: 'Deny',
Principal: '*',
Action: 's3:*',
Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`],
Condition: {
Bool: {
'aws:SecureTransport': 'false'
}
}
}
let updatedPolicy
if (existingPolicy) {
existingPolicy.Statement.push(sslPolicyStatement)
updatedPolicy = JSON.stringify(existingPolicy)
} else {
updatedPolicy = this.createSSLOnlyPolicy(bucketName)
}
await this.client.send(
new PutBucketPolicyCommand({
Bucket: bucketName,
Policy: updatedPolicy
})
)
}
}
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}));
return response.Buckets || [];
};
}

View File

@ -2,61 +2,130 @@ import {
S3Client,
ListBucketsCommand,
GetBucketVersioningCommand,
PutBucketVersioningCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutBucketVersioningCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3BucketVersioningEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3BucketVersioningEnabled',
description: 'Ensures that versioning is enabled on all S3 buckets.',
priority: 1,
priorityReason: 'Enabling versioning protects against accidental data loss and allows recovery of previous versions.',
awsService: 'S3',
awsServiceCategory: 'Buckets',
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) {
const response = await this.memoClient.send(
new GetBucketVersioningCommand({ Bucket: bucket.Name! })
)
);
if (response.Status === 'Enabled') {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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) {
const bucketName = bucketArn.split(':::')[1]!
const bucketName = bucketArn.split(':::')[1]!;
await this.client.send(
new PutBucketVersioningCommand({
Bucket: bucketName,
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,
ListBucketsCommand,
GetBucketEncryptionCommand,
PutBucketEncryptionCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutBucketEncryptionCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3DefaultEncryptionKMS implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3DefaultEncryptionKMS',
description: 'Ensures that all S3 buckets have default encryption enabled using AWS KMS.',
priority: 1,
priorityReason: 'Default encryption protects sensitive data stored in S3 buckets.',
awsService: 'S3',
awsServiceCategory: 'Buckets',
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) {
try {
const response = await this.memoClient.send(
new GetBucketEncryptionCommand({ Bucket: bucket.Name! })
)
const encryption = response.ServerSideEncryptionConfiguration!
);
const encryption = response.ServerSideEncryptionConfiguration!;
const isKmsEnabled = encryption.Rules?.some(
rule =>
(rule) =>
rule.ApplyServerSideEncryptionByDefault &&
rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm === 'aws:kms'
)
);
if (isKmsEnabled) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
} catch (error) {
if ((error as any).name === 'ServerSideEncryptionConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
throw error
throw error;
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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) {
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) {
const bucketName = bucketArn.split(':::')[1]!
const bucketName = bucketArn.split(':::')[1]!;
await this.client.send(
new PutBucketEncryptionCommand({
Bucket: bucketName,
@ -78,13 +150,18 @@ export class S3DefaultEncryptionKMS implements BPSet {
{
ApplyServerSideEncryptionByDefault: {
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,
ListBucketsCommand,
GetBucketNotificationConfigurationCommand,
PutBucketNotificationConfigurationCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutBucketNotificationConfigurationCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3EventNotificationsEnabled implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3EventNotificationsEnabled',
description: 'Ensures that S3 buckets have event notifications configured.',
priority: 2,
priorityReason: 'Event notifications facilitate automated responses to S3 events, enhancing automation and security.',
awsService: 'S3',
awsServiceCategory: 'Buckets',
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) {
const response = await this.memoClient.send(
new GetBucketNotificationConfigurationCommand({ Bucket: bucket.Name! })
)
);
if (
response.LambdaFunctionConfigurations ||
response.QueueConfigurations ||
response.TopicConfigurations
) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'lambda-function-arn' },
{ name: 'event-type' }
]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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(
param => param.name === 'lambda-function-arn'
)?.value
(param) => param.name === 'lambda-function-arn'
)?.value;
const eventType = requiredParametersForFix.find(
param => param.name === 'event-type'
)?.value
(param) => param.name === 'event-type'
)?.value;
if (!lambdaArn || !eventType) {
throw new Error(
"Required parameters 'lambda-function-arn' and/or 'event-type' are missing."
)
);
}
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
const bucketName = bucketArn.split(':::')[1]!;
await this.client.send(
new PutBucketNotificationConfigurationCommand({
Bucket: bucketName,
@ -76,12 +150,17 @@ export class S3EventNotificationsEnabled implements BPSet {
LambdaFunctionConfigurations: [
{
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 {
S3Client,
ListBucketsCommand
} from '@aws-sdk/client-s3'
import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
ListBucketsCommand,
} from '@aws-sdk/client-s3';
import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3LastBackupRecoveryPointCreated implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly backupClient = Memorizer.memo(new BackupClient({}))
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly backupClient = Memorizer.memo(new BackupClient({}));
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3LastBackupRecoveryPointCreated',
description: 'Ensures that S3 buckets have recent backup recovery points.',
priority: 2,
priorityReason: 'Backup recovery points are critical for disaster recovery and data resilience.',
awsService: 'S3',
awsServiceCategory: 'Buckets',
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) {
const recoveryPoints = await this.backupClient.send(
new ListRecoveryPointsByResourceCommand({
ResourceArn: `arn:aws:s3:::${bucket.Name!}`
ResourceArn: `arn:aws:s3:::${bucket.Name!}`,
})
)
);
if (recoveryPoints.RecoveryPoints && recoveryPoints.RecoveryPoints.length > 0) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
if (
recoveryPoints.RecoveryPoints &&
recoveryPoints.RecoveryPoints.length > 0
) {
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (): Promise<void> => {
throw new Error('Fixing recovery points requires custom implementation for backup setup.')
}
public readonly fix = async () => {
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,
ListBucketsCommand,
GetBucketLifecycleConfigurationCommand,
PutBucketLifecycleConfigurationCommand
} from '@aws-sdk/client-s3'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
PutBucketLifecycleConfigurationCommand,
} from '@aws-sdk/client-s3';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class S3LifecyclePolicyCheck implements BPSet {
private readonly client = new S3Client({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new S3Client({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getBuckets = async () => {
const response = await this.memoClient.send(new ListBucketsCommand({}))
return response.Buckets || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
nonCompliantResources: [],
status: 'LOADED',
errorMessage: [],
};
public readonly check = async (): Promise<{
compliantResources: string[]
nonCompliantResources: string[]
requiredParametersForFix: { name: string }[]
}> => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const buckets = await this.getBuckets()
public readonly getMetadata = (): BPSetMetadata => ({
name: 'S3LifecyclePolicyCheck',
description: 'Ensures that all S3 buckets have lifecycle policies configured.',
priority: 2,
priorityReason:
'Lifecycle policies help manage storage costs by automatically transitioning or expiring objects.',
awsService: 'S3',
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) {
try {
await this.memoClient.send(
new GetBucketLifecycleConfigurationCommand({ Bucket: bucket.Name! })
)
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
);
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} catch (error) {
if ((error as any).name === 'NoSuchLifecycleConfiguration') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`);
} else {
throw error
throw error;
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'lifecycle-policy-rule-id' },
{ name: 'expiration-days' }
]
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
public readonly fix = async (
nonCompliantResources: 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(
param => param.name === 'lifecycle-policy-rule-id'
)?.value
(param) => param.name === 'lifecycle-policy-rule-id'
)?.value;
const expirationDays = requiredParametersForFix.find(
param => param.name === 'expiration-days'
)?.value
(param) => param.name === 'expiration-days'
)?.value;
if (!ruleId || !expirationDays) {
throw new Error(
"Required parameters 'lifecycle-policy-rule-id' and/or 'expiration-days' are missing."
)
);
}
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
const bucketName = bucketArn.split(':::')[1]!;
await this.client.send(
new PutBucketLifecycleConfigurationCommand({
Bucket: bucketName,
@ -78,13 +154,18 @@ export class S3LifecyclePolicyCheck implements BPSet {
ID: ruleId,
Status: 'Enabled',
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,
ListSecretsCommand,
RotateSecretCommand,
UpdateSecretCommand
} from '@aws-sdk/client-secrets-manager'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
UpdateSecretCommand,
} from '@aws-sdk/client-secrets-manager';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class SecretsManagerRotationEnabledCheck implements BPSet {
private readonly client = new SecretsManagerClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new SecretsManagerClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getSecrets = async () => {
const response = await this.memoClient.send(new ListSecretsCommand({}))
return response.SecretList || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const secrets = await this.getSecrets()
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 secrets = await this.getSecrets();
for (const secret of secrets) {
if (secret.RotationEnabled) {
compliantResources.push(secret.ARN!)
compliantResources.push(secret.ARN!);
} else {
nonCompliantResources.push(secret.ARN!)
nonCompliantResources.push(secret.ARN!);
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
await this.client.send(
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 {
SecretsManagerClient,
ListSecretsCommand,
RotateSecretCommand
} from '@aws-sdk/client-secrets-manager'
import { BPSet } from '../../types'
import { Memorizer } from '../../Memorizer'
RotateSecretCommand,
} from '@aws-sdk/client-secrets-manager';
import { BPSet, BPSetMetadata, BPSetStats } from '../../types';
import { Memorizer } from '../../Memorizer';
export class SecretsManagerScheduledRotationSuccessCheck implements BPSet {
private readonly client = new SecretsManagerClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly client = new SecretsManagerClient({});
private readonly memoClient = Memorizer.memo(this.client);
private readonly getSecrets = async () => {
const response = await this.memoClient.send(new ListSecretsCommand({}))
return response.SecretList || []
}
private readonly stats: BPSetStats = {
compliantResources: [],
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 () => {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const secrets = await this.getSecrets()
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 secrets = await this.getSecrets();
for (const secret of secrets) {
if (secret.RotationEnabled) {
const now = new Date()
const lastRotated = secret.LastRotatedDate ? new Date(secret.LastRotatedDate) : undefined
const now = new Date();
const lastRotated = secret.LastRotatedDate
? new Date(secret.LastRotatedDate)
: undefined;
const rotationPeriod = secret.RotationRules?.AutomaticallyAfterDays
? secret.RotationRules.AutomaticallyAfterDays + 2
: undefined
: undefined;
if (!lastRotated || !rotationPeriod || now.getTime() - lastRotated.getTime() > rotationPeriod * 24 * 60 * 60 * 1000) {
nonCompliantResources.push(secret.ARN!)
if (
!lastRotated ||
!rotationPeriod ||
now.getTime() - lastRotated.getTime() >
rotationPeriod * 24 * 60 * 60 * 1000
) {
nonCompliantResources.push(secret.ARN!);
} else {
compliantResources.push(secret.ARN!)
compliantResources.push(secret.ARN!);
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
this.stats.compliantResources = compliantResources;
this.stats.nonCompliantResources = nonCompliantResources;
};
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) {
await this.client.send(
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