feat: move external metadata to embeded
All checks were successful
/ deploy_site (push) Successful in 2m24s
All checks were successful
/ deploy_site (push) Successful in 2m24s
This commit is contained in:
3149
bpset_metadata.json
3149
bpset_metadata.json
File diff suppressed because it is too large
Load Diff
@ -56,10 +56,7 @@
|
||||
},
|
||||
"pkg": {
|
||||
"scripts": "build/**/*",
|
||||
"assets": [
|
||||
"views/**/*",
|
||||
"bpset_metadata.json"
|
||||
],
|
||||
"assets": "views/**/*",
|
||||
"targets": [
|
||||
"node22-linuxstatic-x86_64",
|
||||
"node22-linuxstatic-arm64"
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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', {
|
||||
|
@ -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' },
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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' }],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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' }],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 },
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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(','),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 application’s 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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 table’s 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',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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`,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 organization’s 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({}));
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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.'
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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.'
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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' }],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 organization’s 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' },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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.'
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 },
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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.'
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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.'
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 organization’s 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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 organization’s 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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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!;
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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 || [];
|
||||
};
|
||||
}
|
||||
|
@ -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
Reference in New Issue
Block a user