feat: add more bps

This commit is contained in:
2024-12-24 09:19:06 +09:00
parent a450604c13
commit e845d4a9c3
64 changed files with 4897 additions and 0 deletions

View File

@ -10,13 +10,28 @@
"license": "MIT",
"dependencies": {
"@aws-sdk/client-apigatewayv2": "^3.716.0",
"@aws-sdk/client-application-auto-scaling": "^3.716.0",
"@aws-sdk/client-auto-scaling": "^3.716.0",
"@aws-sdk/client-backup": "^3.716.0",
"@aws-sdk/client-cloudfront": "^3.716.0",
"@aws-sdk/client-cloudwatch": "^3.716.0",
"@aws-sdk/client-cloudwatch-logs": "^3.716.0",
"@aws-sdk/client-codebuild": "^3.716.0",
"@aws-sdk/client-codedeploy": "^3.716.0",
"@aws-sdk/client-dynamodb": "^3.716.0",
"@aws-sdk/client-ec2": "^3.716.0",
"@aws-sdk/client-ecr": "^3.718.0",
"@aws-sdk/client-ecs": "^3.716.0",
"@aws-sdk/client-efs": "^3.716.0",
"@aws-sdk/client-eks": "^3.718.0",
"@aws-sdk/client-elastic-load-balancing-v2": "^3.716.0",
"@aws-sdk/client-elasticache": "^3.716.0",
"@aws-sdk/client-iam": "^3.716.0",
"@aws-sdk/client-lambda": "^3.716.0",
"@aws-sdk/client-rds": "^3.716.0",
"@aws-sdk/client-s3": "^3.717.0",
"@aws-sdk/client-s3-control": "^3.716.0",
"@aws-sdk/client-ssm": "^3.716.0",
"@aws-sdk/client-sts": "^3.716.0",
"@aws-sdk/client-wafv2": "^3.716.0",
"@smithy/smithy-client": "^3.5.1",

965
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,85 @@
import {
CloudWatchClient,
DescribeAlarmsCommand,
PutMetricAlarmCommand
} from '@aws-sdk/client-cloudwatch'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CloudWatchAlarmSettingsCheck implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const parameters = {
MetricName: '', // Required
Threshold: null,
EvaluationPeriods: null,
Period: null,
ComparisonOperator: null,
Statistic: null
}
const alarms = await this.getAlarms()
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
}
if (alarm[parameter as keyof typeof alarm] !== parameters[parameter as keyof typeof parameters]) {
nonCompliantResources.push(alarm.AlarmArn!)
break
}
}
compliantResources.push(alarm.AlarmArn!)
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'metric-name' },
{ name: 'threshold' },
{ name: 'evaluation-periods' },
{ name: 'period' },
{ name: 'comparison-operator' },
{ name: 'statistic' }
]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const requiredSettings = Object.fromEntries(
requiredParametersForFix.map(param => [param.name, param.value])
)
for (const alarmArn of nonCompliantResources) {
const alarmName = alarmArn.split(':').pop()!
await this.client.send(
new PutMetricAlarmCommand({
AlarmName: alarmName,
MetricName: requiredSettings['metric-name'],
Threshold: parseFloat(requiredSettings['threshold']),
EvaluationPeriods: parseInt(requiredSettings['evaluation-periods'], 10),
Period: parseInt(requiredSettings['period'], 10),
ComparisonOperator: requiredSettings['comparison-operator'] as any,
Statistic: requiredSettings['statistic'] as any
})
)
}
}
}

View File

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

View File

@ -0,0 +1,74 @@
import {
CodeBuildClient,
ListProjectsCommand,
BatchGetProjectsCommand,
UpdateProjectCommand
} from '@aws-sdk/client-codebuild'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class CodeBuildProjectLoggingEnabled implements BPSet {
private readonly client = new CodeBuildClient({})
private readonly memoClient = Memorizer.memo(this.client)
private readonly getProjects = async () => {
const projectNames = await this.memoClient.send(new ListProjectsCommand({}))
if (!projectNames.projects?.length) {
return []
}
const response = await this.memoClient.send(
new BatchGetProjectsCommand({ names: projectNames.projects })
)
return response.projects || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const projects = await this.getProjects()
for (const project of projects) {
const logsConfig = project.logsConfig
if (
logsConfig?.cloudWatchLogs?.status === 'ENABLED' ||
logsConfig?.s3Logs?.status === 'ENABLED'
) {
compliantResources.push(project.arn!)
} else {
nonCompliantResources.push(project.arn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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)
if (!projectToFix) {
continue
}
await this.client.send(
new UpdateProjectCommand({
name: projectName,
logsConfig: {
...projectToFix.logsConfig,
cloudWatchLogs: {
status: 'ENABLED',
groupName: 'default-cloudwatch-group',
streamName: 'default-stream'
}
}
})
)
}
}
}

View File

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

View File

@ -0,0 +1,133 @@
import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand
} from '@aws-sdk/client-dynamodb'
import {
ApplicationAutoScalingClient,
RegisterScalableTargetCommand,
PutScalingPolicyCommand,
DescribeScalingPoliciesCommand
} from '@aws-sdk/client-application-auto-scaling'
import { BPSet } from '../BPSet'
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 getTables = async () => {
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!)
}
return tables
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
for (const table of tables) {
if (table.BillingModeSummary?.BillingMode === 'PAY_PER_REQUEST') {
compliantResources.push(table.TableArn!)
continue
}
const scalingPolicies = await this.autoScalingClient.send(
new DescribeScalingPoliciesCommand({
ServiceNamespace: 'dynamodb',
ResourceId: `table/${table.TableName}`
})
)
const scalingPolicyDimensions = scalingPolicies.ScalingPolicies?.map(
policy => policy.ScalableDimension
)
if (
scalingPolicyDimensions?.includes('dynamodb:table:ReadCapacityUnits') &&
scalingPolicyDimensions?.includes('dynamodb:table:WriteCapacityUnits')
) {
compliantResources.push(table.TableArn!)
} else {
nonCompliantResources.push(table.TableArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!
// Register scalable targets for read and write capacity
await this.autoScalingClient.send(
new RegisterScalableTargetCommand({
ServiceNamespace: 'dynamodb',
ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:ReadCapacityUnits',
MinCapacity: 1,
MaxCapacity: 100
})
)
await this.autoScalingClient.send(
new RegisterScalableTargetCommand({
ServiceNamespace: 'dynamodb',
ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:WriteCapacityUnits',
MinCapacity: 1,
MaxCapacity: 100
})
)
// Put scaling policies for read and write capacity
await this.autoScalingClient.send(
new PutScalingPolicyCommand({
ServiceNamespace: 'dynamodb',
ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:ReadCapacityUnits',
PolicyName: `${tableName}-ReadPolicy`,
PolicyType: 'TargetTrackingScaling',
TargetTrackingScalingPolicyConfiguration: {
TargetValue: 70.0,
ScaleInCooldown: 60,
ScaleOutCooldown: 60,
PredefinedMetricSpecification: {
PredefinedMetricType: 'DynamoDBReadCapacityUtilization'
}
}
})
)
await this.autoScalingClient.send(
new PutScalingPolicyCommand({
ServiceNamespace: 'dynamodb',
ResourceId: `table/${tableName}`,
ScalableDimension: 'dynamodb:table:WriteCapacityUnits',
PolicyName: `${tableName}-WritePolicy`,
PolicyType: 'TargetTrackingScaling',
TargetTrackingScalingPolicyConfiguration: {
TargetValue: 70.0,
ScaleInCooldown: 60,
ScaleOutCooldown: 60,
PredefinedMetricSpecification: {
PredefinedMetricType: 'DynamoDBWriteCapacityUtilization'
}
}
})
)
}
}
}

View File

@ -0,0 +1,78 @@
import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand
} from '@aws-sdk/client-dynamodb'
import {
BackupClient,
ListRecoveryPointsByResourceCommand,
StartBackupJobCommand
} from '@aws-sdk/client-backup'
import { BPSet } from '../BPSet'
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 getTables = async () => {
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!)
}
return tables
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
for (const table of tables) {
const recoveryPointsResponse = await this.backupClient.send(
new ListRecoveryPointsByResourceCommand({
ResourceArn: table.TableArn
})
)
const recoveryPoints = recoveryPointsResponse.RecoveryPoints || []
if (recoveryPoints.length === 0) {
nonCompliantResources.push(table.TableArn!)
continue
}
const latestRecoveryPoint = recoveryPoints
.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!)
} else {
compliantResources.push(table.TableArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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',
})
)
}
}
}

View File

@ -0,0 +1,70 @@
import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand,
DescribeContinuousBackupsCommand,
UpdateContinuousBackupsCommand
} from '@aws-sdk/client-dynamodb'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class DynamoDBPITREnabled implements BPSet {
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 = []
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
new DescribeTableCommand({ TableName: tableName })
)
tables.push(tableDetails.Table!)
}
return tables
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
for (const table of tables) {
const backupStatus = await this.memoClient.send(
new DescribeContinuousBackupsCommand({
TableName: table.TableName!
})
)
if (
backupStatus.ContinuousBackupsDescription?.PointInTimeRecoveryDescription
?.PointInTimeRecoveryStatus === 'ENABLED'
) {
compliantResources.push(table.TableArn!)
} else {
nonCompliantResources.push(table.TableArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!
await this.client.send(
new UpdateContinuousBackupsCommand({
TableName: tableName,
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true
}
})
)
}
}
}

View File

@ -0,0 +1,58 @@
import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand,
UpdateTableCommand
} from '@aws-sdk/client-dynamodb'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class DynamoDBTableDeletionProtectionEnabled implements BPSet {
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 = []
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
new DescribeTableCommand({ TableName: tableName })
)
tables.push(tableDetails.Table!)
}
return tables
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
for (const table of tables) {
if (table.DeletionProtectionEnabled) {
compliantResources.push(table.TableArn!)
} else {
nonCompliantResources.push(table.TableArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!
await this.client.send(
new UpdateTableCommand({
TableName: tableName,
DeletionProtectionEnabled: true
})
)
}
}
}

View File

@ -0,0 +1,74 @@
import {
DynamoDBClient,
ListTablesCommand,
DescribeTableCommand,
UpdateTableCommand
} from '@aws-sdk/client-dynamodb'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class DynamoDBTableEncryptedKMS implements BPSet {
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 = []
for (const tableName of tableNames.TableNames || []) {
const tableDetails = await this.memoClient.send(
new DescribeTableCommand({ TableName: tableName })
)
tables.push(tableDetails.Table!)
}
return tables
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const tables = await this.getTables()
for (const table of tables) {
if (
table.SSEDescription?.Status === 'ENABLED' &&
table.SSEDescription?.SSEType === 'KMS'
) {
compliantResources.push(table.TableArn!)
} else {
nonCompliantResources.push(table.TableArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = 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.")
}
for (const arn of nonCompliantResources) {
const tableName = arn.split('/').pop()!
await this.client.send(
new UpdateTableCommand({
TableName: tableName,
SSESpecification: {
Enabled: true,
SSEType: 'KMS',
KMSMasterKeyId: kmsKeyId
}
})
)
}
}
}

View File

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

View File

@ -0,0 +1,36 @@
import {
EC2Client,
DescribeVolumesCommand,
EnableEbsEncryptionByDefaultCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EC2EbsEncryptionByDefault implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeVolumesCommand({}))
for (const volume of response.Volumes || []) {
if (volume.Encrypted) {
compliantResources.push(volume.VolumeId!)
} else {
nonCompliantResources.push(volume.VolumeId!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async () => {
await this.client.send(new EnableEbsEncryptionByDefaultCommand({}))
}
}

View File

@ -0,0 +1,45 @@
import {
DescribeInstancesCommand,
EC2Client,
ModifyInstanceMetadataOptionsCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EC2Imdsv2Check implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
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!)
} else {
nonCompliantResources.push(instance.InstanceId!)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const instanceId of nonCompliantResources) {
await this.client.send(
new ModifyInstanceMetadataOptionsCommand({
InstanceId: instanceId,
HttpTokens: 'required'
})
)
}
}
}

View File

@ -0,0 +1,42 @@
import {
DescribeInstancesCommand,
EC2Client,
MonitorInstancesCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EC2InstanceDetailedMonitoringEnabled implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
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!)
} else {
nonCompliantResources.push(instance.InstanceId!)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
await this.client.send(
new MonitorInstancesCommand({
InstanceIds: nonCompliantResources
})
)
}
}

View File

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

View File

@ -0,0 +1,56 @@
import {
EC2Client,
DescribeInstancesCommand,
AssociateIamInstanceProfileCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EC2InstanceProfileAttached implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
public readonly check = 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!)
} else {
nonCompliantResources.push(instance.InstanceId!)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'iam-instance-profile' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const iamInstanceProfile = requiredParametersForFix.find(
param => param.name === 'iam-instance-profile'
)?.value
if (!iamInstanceProfile) {
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 }
})
)
}
}
}

View File

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

View File

@ -0,0 +1,46 @@
import {
EC2Client,
DescribeInstancesCommand,
TerminateInstancesCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EC2StoppedInstance implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
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!)
} else {
compliantResources.push(instance.InstanceId!)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
if (nonCompliantResources.length === 0) {
return // No stopped instances to terminate
}
await this.client.send(
new TerminateInstancesCommand({
InstanceIds: nonCompliantResources
})
)
}
}

View File

@ -0,0 +1,48 @@
import {
EC2Client,
DescribeInstancesCommand,
ModifyInstanceMetadataOptionsCommand
} from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EC2TokenHopLimitCheck implements BPSet {
private readonly client = new EC2Client({})
private readonly memoClient = Memorizer.memo(this.client)
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const response = await this.memoClient.send(new DescribeInstancesCommand({}))
for (const reservation of response.Reservations || []) {
for (const instance of reservation.Instances || []) {
if (
instance.MetadataOptions?.HttpPutResponseHopLimit &&
instance.MetadataOptions.HttpPutResponseHopLimit < 2
) {
compliantResources.push(instance.InstanceId!)
} else {
nonCompliantResources.push(instance.InstanceId!)
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const instanceId of nonCompliantResources) {
await this.client.send(
new ModifyInstanceMetadataOptionsCommand({
InstanceId: instanceId,
HttpPutResponseHopLimit: 1
})
)
}
}
}

View File

@ -0,0 +1,99 @@
import {
ECRClient,
DescribeRepositoriesCommand,
CreateRepositoryCommand,
ListImagesCommand,
BatchGetImageCommand,
PutImageCommand,
DeleteRepositoryCommand
} from '@aws-sdk/client-ecr'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECRKmsEncryption1 implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const repositories = await this.getRepositories()
for (const repository of repositories) {
if (repository.encryptionConfiguration?.encryptionType === 'KMS') {
compliantResources.push(repository.repositoryArn!)
} else {
nonCompliantResources.push(repository.repositoryArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = 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.")
}
for (const arn of nonCompliantResources) {
const repositoryName = arn.split('/').pop()!
// Create a new repository with KMS encryption
const newRepositoryName = `${repositoryName}-kms`
await this.client.send(
new CreateRepositoryCommand({
repositoryName: newRepositoryName,
encryptionConfiguration: {
encryptionType: 'KMS',
kmsKey: kmsKeyId
}
})
)
// Get all images in the existing repository
const listImagesResponse = await this.client.send(
new ListImagesCommand({ repositoryName })
)
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 || []) {
await this.client.send(
new PutImageCommand({
repositoryName: newRepositoryName,
imageManifest: image.imageManifest,
imageTag: image.imageId?.imageTag
})
)
}
}
// Delete the old repository
await this.client.send(
new DeleteRepositoryCommand({
repositoryName,
force: true
})
)
}
}
}

View File

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

View File

@ -0,0 +1,72 @@
import {
ECRClient,
DescribeRepositoriesCommand,
PutLifecyclePolicyCommand,
GetLifecyclePolicyCommand
} from '@aws-sdk/client-ecr'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECRPrivateLifecyclePolicyConfigured implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const repositories = await this.getRepositories()
for (const repository of repositories) {
try {
await this.client.send(
new GetLifecyclePolicyCommand({
registryId: repository.registryId,
repositoryName: repository.repositoryName
})
)
compliantResources.push(repository.repositoryArn!)
} catch (error: any) {
if (error.name === 'LifecyclePolicyNotFoundException') {
nonCompliantResources.push(repository.repositoryArn!)
} else {
throw error
}
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'lifecycle-policy' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const lifecyclePolicy = requiredParametersForFix.find(
param => param.name === 'lifecycle-policy'
)?.value
if (!lifecyclePolicy) {
throw new Error("Required parameter 'lifecycle-policy' is missing.")
}
for (const arn of nonCompliantResources) {
const repositoryName = arn.split('/').pop()!
await this.client.send(
new PutLifecyclePolicyCommand({
repositoryName,
lifecyclePolicyText: lifecyclePolicy
})
)
}
}
}

View File

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

View File

@ -0,0 +1,67 @@
import {
ECSClient,
ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECSAwsVpcNetworkingEnabled implements BPSet {
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 = []
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
taskDefinitions.push(taskDefinition.taskDefinition!)
}
return taskDefinitions
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) {
if (taskDefinition.networkMode === 'awsvpc') {
compliantResources.push(taskDefinition.taskDefinitionArn!)
} else {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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
await this.client.send(
new RegisterTaskDefinitionCommand({
family,
containerDefinitions: taskDefinition.taskDefinition?.containerDefinitions,
networkMode: 'awsvpc',
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
})
)
}
}
}

View File

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

View File

@ -0,0 +1,77 @@
import {
ECSClient,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECSContainersNonPrivileged implements BPSet {
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 = []
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
taskDefinitions.push(taskDefinition.taskDefinition!)
}
return taskDefinitions
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) {
const privilegedContainers = taskDefinition.containerDefinitions?.filter(
container => container.privileged
)
if (privilegedContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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 updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
...container,
privileged: false
})
)
await this.client.send(
new RegisterTaskDefinitionCommand({
family,
containerDefinitions: updatedContainers,
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
})
)
}
}
}

View File

@ -0,0 +1,77 @@
import {
ECSClient,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand,
ListTaskDefinitionsCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECSContainersReadonlyAccess implements BPSet {
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 = []
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
taskDefinitions.push(taskDefinition.taskDefinition!)
}
return taskDefinitions
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) {
const notReadonlyContainers = taskDefinition.containerDefinitions?.filter(
container => !container.readonlyRootFilesystem
)
if (notReadonlyContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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 updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
...container,
readonlyRootFilesystem: true
})
)
await this.client.send(
new RegisterTaskDefinitionCommand({
family,
containerDefinitions: updatedContainers,
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
})
)
}
}
}

View File

@ -0,0 +1,70 @@
import {
ECSClient,
ListClustersCommand,
ListServicesCommand,
DescribeServicesCommand,
UpdateServiceCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECSFargateLatestPlatformVersion implements BPSet {
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 }[] = []
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 })
}
}
return services
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
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]
if (service?.platformVersion === 'LATEST') {
compliantResources.push(service.serviceArn!)
} else {
nonCompliantResources.push(service?.serviceArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const serviceArn of nonCompliantResources) {
const clusterArn = serviceArn.split(':cluster/')[1].split(':service/')[0]
await this.client.send(
new UpdateServiceCommand({
cluster: clusterArn,
service: serviceArn,
platformVersion: 'LATEST'
})
)
}
}
}

View File

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

View File

@ -0,0 +1,77 @@
import {
ECSClient,
ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECSTaskDefinitionMemoryHardLimit implements BPSet {
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 = []
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
taskDefinitions.push(taskDefinition.taskDefinition!)
}
return taskDefinitions
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) {
const containersWithoutMemoryLimit = taskDefinition.containerDefinitions?.filter(
container => !container.memory
)
if (containersWithoutMemoryLimit?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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 updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
...container,
memory: container.memory || 512 // Default hard limit memory value
})
)
await this.client.send(
new RegisterTaskDefinitionCommand({
family,
containerDefinitions: updatedContainers,
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
})
)
}
}
}

View File

@ -0,0 +1,77 @@
import {
ECSClient,
ListTaskDefinitionsCommand,
DescribeTaskDefinitionCommand,
RegisterTaskDefinitionCommand
} from '@aws-sdk/client-ecs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ECSTaskDefinitionNonRootUser implements BPSet {
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 = []
for (const arn of taskDefinitionArns.taskDefinitionArns || []) {
const taskDefinition = await this.memoClient.send(
new DescribeTaskDefinitionCommand({ taskDefinition: arn })
)
taskDefinitions.push(taskDefinition.taskDefinition!)
}
return taskDefinitions
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const taskDefinitions = await this.getTaskDefinitions()
for (const taskDefinition of taskDefinitions) {
const privilegedContainers = taskDefinition.containerDefinitions?.filter(
container => !container.user || container.user === 'root'
)
if (privilegedContainers?.length) {
nonCompliantResources.push(taskDefinition.taskDefinitionArn!)
} else {
compliantResources.push(taskDefinition.taskDefinitionArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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 updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map(
container => ({
...container,
user: container.user || 'ecs-user' // Default non-root user
})
)
await this.client.send(
new RegisterTaskDefinitionCommand({
family,
containerDefinitions: updatedContainers,
networkMode: taskDefinition.taskDefinition?.networkMode,
requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities,
cpu: taskDefinition.taskDefinition?.cpu,
memory: taskDefinition.taskDefinition?.memory
})
)
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,55 @@
import {
EFSClient,
DescribeFileSystemsCommand,
PutBackupPolicyCommand,
DescribeBackupPolicyCommand
} from '@aws-sdk/client-efs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EFSAutomaticBackupsEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
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!)
} else {
nonCompliantResources.push(fileSystem.FileSystemArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const fileSystemId = arn.split('/').pop()!
await this.client.send(
new PutBackupPolicyCommand({
FileSystemId: fileSystemId,
BackupPolicy: { Status: 'ENABLED' }
})
)
}
}
}

View File

@ -0,0 +1,64 @@
import {
EFSClient,
DescribeFileSystemsCommand,
CreateFileSystemCommand,
DeleteFileSystemCommand
} from '@aws-sdk/client-efs'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EFSEncryptedCheck implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const fileSystems = await this.getFileSystems()
for (const fileSystem of fileSystems) {
if (fileSystem.Encrypted) {
compliantResources.push(fileSystem.FileSystemArn!)
} else {
nonCompliantResources.push(fileSystem.FileSystemArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
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
})
)
// Recreate the file system with encryption enabled
await this.client.send(
new CreateFileSystemCommand({
Encrypted: true,
PerformanceMode: fileSystem.FileSystems?.[0]?.PerformanceMode,
ThroughputMode: fileSystem.FileSystems?.[0]?.ThroughputMode,
ProvisionedThroughputInMibps: fileSystem.FileSystems?.[0]?.ProvisionedThroughputInMibps
})
)
}
}
}

View File

@ -0,0 +1,71 @@
import {
EFSClient,
DescribeFileSystemsCommand,
DescribeMountTargetsCommand
} from '@aws-sdk/client-efs'
import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
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 getFileSystems = async () => {
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] }]
})
)
return response.RouteTables?.[0]?.Routes || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const fileSystems = await this.getFileSystems()
for (const fileSystem of fileSystems) {
const mountTargets = await this.memoEFSClient.send(
new DescribeMountTargetsCommand({ FileSystemId: fileSystem.FileSystemId! })
)
for (const mountTarget of mountTargets.MountTargets || []) {
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 (!nonCompliantResources.includes(fileSystem.FileSystemArn!)) {
compliantResources.push(fileSystem.FileSystemArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
throw new Error(
'Fixing public accessibility for mount targets requires manual network reconfiguration.'
)
}
}

View File

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

View File

@ -0,0 +1,74 @@
import {
EKSClient,
ListClustersCommand,
DescribeClusterCommand,
AssociateEncryptionConfigCommand
} from '@aws-sdk/client-eks'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class EKSClusterSecretsEncrypted implements BPSet {
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 = []
for (const clusterName of clusterNames) {
const cluster = await this.memoClient.send(
new DescribeClusterCommand({ name: clusterName })
)
clusters.push(cluster.cluster!)
}
return clusters
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
for (const cluster of clusters) {
const encryptionConfig = cluster.encryptionConfig?.[0]
if (encryptionConfig?.resources?.includes('secrets')) {
compliantResources.push(cluster.arn!)
} else {
nonCompliantResources.push(cluster.arn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'kms-key-id' }]
}
}
public readonly fix = 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.")
}
for (const arn of nonCompliantResources) {
const clusterName = arn.split(':cluster/')[1]
await this.client.send(
new AssociateEncryptionConfigCommand({
clusterName,
encryptionConfig: [
{
resources: ['secrets'],
provider: { keyArn: kmsKeyId }
}
]
})
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
import {
ElastiCacheClient,
DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ElastiCacheReplGrpEncryptedAtRest implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups()
for (const group of replicationGroups) {
if (group.AtRestEncryptionEnabled) {
compliantResources.push(group.ARN!)
} else {
nonCompliantResources.push(group.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
throw new Error(
'Fixing encryption at rest for replication groups requires recreation. Please create a new replication group with AtRestEncryptionEnabled set to true.'
)
}
}

View File

@ -0,0 +1,43 @@
import {
ElastiCacheClient,
DescribeReplicationGroupsCommand,
ModifyReplicationGroupCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ElastiCacheReplGrpEncryptedInTransit implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const replicationGroups = await this.getReplicationGroups()
for (const group of replicationGroups) {
if (group.TransitEncryptionEnabled) {
compliantResources.push(group.ARN!)
} else {
nonCompliantResources.push(group.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
throw new Error(
'Fixing in-transit encryption for replication groups requires recreation. Please create a new replication group with TransitEncryptionEnabled set to true.'
)
}
}

View File

@ -0,0 +1,84 @@
import {
ElastiCacheClient,
DescribeCacheClustersCommand,
DeleteCacheClusterCommand,
CreateCacheClusterCommand
} from '@aws-sdk/client-elasticache'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class ElastiCacheSubnetGroupCheck implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const clusters = await this.getClusters()
for (const cluster of clusters) {
if (cluster.CacheSubnetGroupName !== 'default') {
compliantResources.push(cluster.ARN!)
} else {
nonCompliantResources.push(cluster.ARN!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'subnet-group-name' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const subnetGroupName = requiredParametersForFix.find(
param => param.name === 'subnet-group-name'
)?.value
if (!subnetGroupName) {
throw new Error("Required parameter 'subnet-group-name' is missing.")
}
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster:')[1]
const cluster = await this.memoClient.send(
new DescribeCacheClustersCommand({ CacheClusterId: clusterId })
)
const clusterDetails = cluster.CacheClusters?.[0]
if (!clusterDetails) {
continue
}
// Delete the non-compliant cluster
await this.client.send(
new DeleteCacheClusterCommand({
CacheClusterId: clusterId
})
)
// Recreate the cluster with the desired subnet group
await this.client.send(
new CreateCacheClusterCommand({
CacheClusterId: clusterDetails.CacheClusterId!,
Engine: clusterDetails.Engine!,
CacheNodeType: clusterDetails.CacheNodeType!,
NumCacheNodes: clusterDetails.NumCacheNodes!,
CacheSubnetGroupName: subnetGroupName,
SecurityGroupIds: clusterDetails.SecurityGroups?.map(group => group.SecurityGroupId) as string[],
PreferredMaintenanceWindow: clusterDetails.PreferredMaintenanceWindow,
EngineVersion: clusterDetails.EngineVersion
})
)
}
}
}

View File

@ -0,0 +1,67 @@
import {
IAMClient,
ListPoliciesCommand,
GetPolicyVersionCommand,
DeletePolicyCommand
} from '@aws-sdk/client-iam'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
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 || []
}
private readonly getPolicyDefaultVersions = async (policyArn: string, versionId: string) => {
const response = await this.memoClient.send(
new GetPolicyVersionCommand({ PolicyArn: policyArn, VersionId: versionId })
)
return response.PolicyVersion!
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const policies = await this.getPolicies()
for (const policy of policies) {
const policyVersion = await this.getPolicyDefaultVersions(policy.Arn!, policy.DefaultVersionId!)
const policyDocument = JSON.parse(JSON.stringify(policyVersion.Document)) // Parse Document JSON string
const statements = Array.isArray(policyDocument.Statement)
? policyDocument.Statement
: [policyDocument.Statement]
for (const statement of statements) {
if (
statement.Action === '*' &&
statement.Resource === '*' &&
statement.Effect === 'Allow'
) {
nonCompliantResources.push(policy.Arn!)
break
}
}
if (!nonCompliantResources.includes(policy.Arn!)) {
compliantResources.push(policy.Arn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
await this.client.send(new DeletePolicyCommand({ PolicyArn: arn }))
}
}
}

View File

@ -0,0 +1,67 @@
import {
IAMClient,
ListPoliciesCommand,
GetPolicyVersionCommand,
DeletePolicyCommand
} from '@aws-sdk/client-iam'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
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 || []
}
private readonly getPolicyDefaultVersions = async (policyArn: string, versionId: string) => {
const response = await this.memoClient.send(
new GetPolicyVersionCommand({ PolicyArn: policyArn, VersionId: versionId })
)
return response.PolicyVersion!
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const policies = await this.getPolicies()
for (const policy of policies) {
const policyVersion = await this.getPolicyDefaultVersions(policy.Arn!, policy.DefaultVersionId!)
const policyDocument = JSON.parse(JSON.stringify(policyVersion.Document)) // Parse Document JSON string
const statements = Array.isArray(policyDocument.Statement)
? policyDocument.Statement
: [policyDocument.Statement]
for (const statement of statements) {
if (
statement.Action === '*' &&
statement.Resource === '*' &&
statement.Effect === 'Allow'
) {
nonCompliantResources.push(policy.Arn!)
break
}
}
if (!nonCompliantResources.includes(policy.Arn!)) {
compliantResources.push(policy.Arn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
await this.client.send(new DeletePolicyCommand({ PolicyArn: arn }))
}
}
}

View File

@ -0,0 +1,56 @@
import {
IAMClient,
ListPoliciesCommand,
ListEntitiesForPolicyCommand
} from '@aws-sdk/client-iam'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class IAMRoleManagedPolicyCheck implements BPSet {
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 || []
}
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.'
)
}
}

View File

@ -0,0 +1,80 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import {
BackupClient,
ListRecoveryPointsByResourceCommand
} from '@aws-sdk/client-backup'
import { BPSet } from '../BPSet'
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 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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
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())
if (
recoveryDates.length > 0 &&
new Date().getTime() - recoveryDates[0].getTime() < 24 * 60 * 60 * 1000
) {
compliantResources.push(cluster.DBClusterArn!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'backup-retention-period', value: '7' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const retentionPeriod = requiredParametersForFix.find(
param => param.name === 'backup-retention-period'
)?.value
if (!retentionPeriod) {
throw new Error("Required parameter 'backup-retention-period' is missing.")
}
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]
await this.rdsClient.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
BackupRetentionPeriod: parseInt(retentionPeriod, 10)
})
)
}
}
}

View File

@ -0,0 +1,52 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class AuroraMySQLBacktrackingEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) {
if (
cluster.Engine === 'aurora-mysql' &&
(!cluster.EarliestBacktrackTime || cluster.EarliestBacktrackTime === null)
) {
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]
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
BacktrackWindow: 3600 // Set backtracking window to 1 hour
})
)
}
}
}

View File

@ -0,0 +1,60 @@
import {
RDSClient,
DescribeDBInstancesCommand,
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class DBInstanceBackupEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbInstances = await this.getDBInstances()
for (const instance of dbInstances) {
if (instance.BackupRetentionPeriod && instance.BackupRetentionPeriod > 0) {
compliantResources.push(instance.DBInstanceArn!)
} else {
nonCompliantResources.push(instance.DBInstanceArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'retention-period', value: '7' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const retentionPeriod = requiredParametersForFix.find(
param => param.name === 'retention-period'
)?.value
if (!retentionPeriod) {
throw new Error("Required parameter 'retention-period' is missing.")
}
for (const arn of nonCompliantResources) {
const instanceId = arn.split(':instance/')[1]
await this.client.send(
new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId,
BackupRetentionPeriod: parseInt(retentionPeriod, 10)
})
)
}
}
}

View File

@ -0,0 +1,50 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSClusterAutoMinorVersionUpgradeEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) {
if (cluster.Engine === 'docdb' || cluster.AutoMinorVersionUpgrade) {
compliantResources.push(cluster.DBClusterArn!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
AutoMinorVersionUpgrade: true
})
)
}
}
}

View File

@ -0,0 +1,67 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSClusterDefaultAdminCheck implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) {
if (!['admin', 'postgres'].includes(cluster.MasterUsername!)) {
compliantResources.push(cluster.DBClusterArn!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'new-master-username', value: '<NEW_MASTER_USERNAME>' },
{ name: 'new-master-password', value: '<NEW_MASTER_PASSWORD>' }
]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const newMasterUsername = requiredParametersForFix.find(
param => param.name === 'new-master-username'
)?.value
const newMasterPassword = requiredParametersForFix.find(
param => param.name === 'new-master-password'
)?.value
if (!newMasterUsername || !newMasterPassword) {
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]
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
MasterUserPassword: newMasterPassword
})
)
}
}
}

View File

@ -0,0 +1,50 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSClusterDeletionProtectionEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) {
if (cluster.DeletionProtection) {
compliantResources.push(cluster.DBClusterArn!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
DeletionProtection: true
})
)
}
}
}

View File

@ -0,0 +1,43 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSClusterEncryptedAtRest implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) {
if (cluster.StorageEncrypted) {
compliantResources.push(cluster.DBClusterArn!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
throw new Error(
'Fixing encryption at rest requires recreating the cluster. Please manually recreate the cluster with encryption enabled.'
)
}
}

View File

@ -0,0 +1,53 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSClusterIAMAuthenticationEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) {
if (
cluster.Engine === 'docdb' ||
cluster.IAMDatabaseAuthenticationEnabled
) {
compliantResources.push(cluster.DBClusterArn!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const clusterId = arn.split(':cluster/')[1]
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
EnableIAMDatabaseAuthentication: true
})
)
}
}
}

View File

@ -0,0 +1,43 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSClusterMultiAZEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbClusters = await this.getDBClusters()
for (const cluster of dbClusters) {
if ((cluster.AvailabilityZones || []).length > 1) {
compliantResources.push(cluster.DBClusterArn!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'additional-azs', value: '2' }]
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
throw new Error(
'Enabling Multi-AZ requires cluster reconfiguration. This must be performed manually.'
)
}
}

View File

@ -0,0 +1,64 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2'
import { BPSet } from '../BPSet'
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 getDBClusters = async () => {
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
})
)
}
}
}

View File

@ -0,0 +1,61 @@
import {
RDSClient,
DescribeDBInstancesCommand,
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSEnhancedMonitoringEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbInstances = await this.getDBInstances()
for (const instance of dbInstances) {
if (instance.MonitoringInterval && instance.MonitoringInterval > 0) {
compliantResources.push(instance.DBInstanceArn!)
} else {
nonCompliantResources.push(instance.DBInstanceArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [{ name: 'monitoring-interval', value: '60' }]
}
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
const monitoringInterval = requiredParametersForFix.find(
param => param.name === 'monitoring-interval'
)?.value
if (!monitoringInterval) {
throw new Error("Required parameter 'monitoring-interval' is missing.")
}
for (const arn of nonCompliantResources) {
const instanceId = arn.split(':instance/')[1]
await this.client.send(
new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId,
MonitoringInterval: parseInt(monitoringInterval, 10)
})
)
}
}
}

View File

@ -0,0 +1,50 @@
import {
RDSClient,
DescribeDBInstancesCommand,
ModifyDBInstanceCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSInstancePublicAccessCheck implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const dbInstances = await this.getDBInstances()
for (const instance of dbInstances) {
if (instance.PubliclyAccessible) {
nonCompliantResources.push(instance.DBInstanceArn!)
} else {
compliantResources.push(instance.DBInstanceArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
public readonly fix = async (nonCompliantResources: string[]) => {
for (const arn of nonCompliantResources) {
const instanceId = arn.split(':instance/')[1]
await this.client.send(
new ModifyDBInstanceCommand({
DBInstanceIdentifier: instanceId,
PubliclyAccessible: false
})
)
}
}
}

View File

@ -0,0 +1,70 @@
import {
RDSClient,
DescribeDBClustersCommand,
ModifyDBClusterCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSLoggingEnabled implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const logsForEngine = {
'aurora-mysql': ['audit', 'error', 'general', 'slowquery'],
'aurora-postgresql': ['postgresql'],
'docdb': ['audit', 'profiler']
}
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!)
} else {
nonCompliantResources.push(cluster.DBClusterArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: []
}
}
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)
if (cluster) {
const logsToEnable = (logsForEngine as any)[cluster.Engine!]
await this.client.send(
new ModifyDBClusterCommand({
DBClusterIdentifier: clusterId,
CloudwatchLogsExportConfiguration: { EnableLogTypes: logsToEnable }
})
)
}
}
}
}

View File

@ -0,0 +1,62 @@
import {
RDSClient,
DescribeDBClusterSnapshotsCommand,
CopyDBClusterSnapshotCommand
} from '@aws-sdk/client-rds'
import { BPSet } from '../BPSet'
import { Memorizer } from '../../Memorizer'
export class RDSSnapshotEncrypted implements BPSet {
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 || []
}
public readonly check = async () => {
const compliantResources = []
const nonCompliantResources = []
const snapshots = await this.getDBClusterSnapshots()
for (const snapshot of snapshots) {
if (snapshot.StorageEncrypted) {
compliantResources.push(snapshot.DBClusterSnapshotArn!)
} else {
nonCompliantResources.push(snapshot.DBClusterSnapshotArn!)
}
}
return {
compliantResources,
nonCompliantResources,
requiredParametersForFix: [
{ name: 'kms-key-id', value: '<KMS_KEY_ID>' } // Replace with your KMS key ID
]
}
}
public readonly fix = 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.")
}
for (const arn of nonCompliantResources) {
const snapshotId = arn.split(':snapshot:')[1]
await this.client.send(
new CopyDBClusterSnapshotCommand({
SourceDBClusterSnapshotIdentifier: arn,
TargetDBClusterSnapshotIdentifier: `${snapshotId}-encrypted`,
KmsKeyId: kmsKeyId
})
)
}
}
}