mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-07 14:58:55 +00:00
Audit L-14, plus a latent Day-2 bug: seahaven-cis-alarms was encrypted with the AWS-managed alias/aws/sns key, whose policy cannot grant cloudwatch.amazonaws.com - CloudWatch alarms silently fail to publish to topics it encrypts. All 15 CIS alarms would have fired into the void. New customer-managed key (rotation on) grants CloudWatch GenerateDataKey*/Decrypt/DescribeKey scoped by SourceAccount. The unmanaged site-alerts topic now uses the same key (set via CLI). Cross-reviewed: no BLOCKs. Verified: forced ALARM on the payroll DLQ alarm published successfully through the encrypted site-alerts.
220 lines
10 KiB
TypeScript
220 lines
10 KiB
TypeScript
import * as cdk from "aws-cdk-lib";
|
||
import * as logs from "aws-cdk-lib/aws-logs";
|
||
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
|
||
import * as cwactions from "aws-cdk-lib/aws-cloudwatch-actions";
|
||
import * as sns from "aws-cdk-lib/aws-sns";
|
||
import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
|
||
import * as kms from "aws-cdk-lib/aws-kms";
|
||
import { Construct } from "constructs";
|
||
|
||
/**
|
||
* CIS AWS Foundations Benchmark v3.0 Section 4 — Monitoring (audit H-1).
|
||
*
|
||
* 15 metric filters on the account CloudTrail log group, each backed by a
|
||
* CloudWatch alarm that notifies a dedicated SNS topic. Closes CIS 4.1–4.15.
|
||
* (4.16 "Security Hub enabled" is not a metric filter — done Day 1, H-4.)
|
||
*
|
||
* Alarms fire on ALARM only (no OK/recovery actions) per Sea Haven preference.
|
||
*/
|
||
|
||
// The account CloudTrail (C-1) delivers to this CloudWatch Logs group. It is
|
||
// created by the L2 cloudtrail.Trail in account-baseline-stack.ts; we import it
|
||
// by name rather than replace it, so the live audit trail is never disrupted.
|
||
// Stable as long as the Trail is not recreated.
|
||
const TRAIL_LOG_GROUP_NAME =
|
||
"seahaven-account-baseline-TrailLogGroup4CBE3AF5-e7hMDCzj8e4d";
|
||
|
||
interface CisControl {
|
||
readonly id: string;
|
||
readonly metricName: string;
|
||
readonly pattern: string;
|
||
readonly description: string;
|
||
}
|
||
|
||
const CIS_CONTROLS: CisControl[] = [
|
||
{
|
||
id: "UnauthorizedApiCalls",
|
||
metricName: "UnauthorizedAPICalls",
|
||
pattern:
|
||
'{ ($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied*") && ($.sourceIPAddress != "delivery.logs.amazonaws.com") && ($.eventName != "HeadBucket") }',
|
||
description: "CIS 4.1 — unauthorized API calls",
|
||
},
|
||
{
|
||
id: "ConsoleSigninNoMfa",
|
||
metricName: "ConsoleSigninWithoutMFA",
|
||
pattern:
|
||
'{ ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") && ($.userIdentity.type = "IAMUser") && ($.responseElements.ConsoleLogin = "Success") }',
|
||
description: "CIS 4.2 — console sign-in without MFA",
|
||
},
|
||
{
|
||
id: "RootAccountUsage",
|
||
metricName: "RootAccountUsage",
|
||
pattern:
|
||
'{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }',
|
||
description: "CIS 4.3 — root account usage",
|
||
},
|
||
{
|
||
id: "IamPolicyChanges",
|
||
metricName: "IAMPolicyChanges",
|
||
pattern:
|
||
"{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}",
|
||
description: "CIS 4.4 — IAM policy changes",
|
||
},
|
||
{
|
||
id: "CloudTrailConfigChanges",
|
||
metricName: "CloudTrailConfigChanges",
|
||
pattern:
|
||
"{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }",
|
||
description: "CIS 4.5 — CloudTrail configuration changes",
|
||
},
|
||
{
|
||
id: "ConsoleAuthFailures",
|
||
metricName: "ConsoleAuthenticationFailures",
|
||
pattern:
|
||
'{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }',
|
||
description: "CIS 4.6 — console authentication failures",
|
||
},
|
||
{
|
||
id: "CmkDisableOrDelete",
|
||
metricName: "CMKDisableOrScheduledDelete",
|
||
pattern:
|
||
"{ ($.eventSource = kms.amazonaws.com) && (($.eventName = DisableKey) || ($.eventName = ScheduleKeyDeletion)) }",
|
||
description: "CIS 4.7 — disabling or scheduled deletion of CMKs",
|
||
},
|
||
{
|
||
id: "S3BucketPolicyChanges",
|
||
metricName: "S3BucketPolicyChanges",
|
||
pattern:
|
||
"{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }",
|
||
description: "CIS 4.8 — S3 bucket policy changes",
|
||
},
|
||
{
|
||
id: "ConfigChanges",
|
||
metricName: "AWSConfigChanges",
|
||
pattern:
|
||
"{ ($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder)) }",
|
||
description: "CIS 4.9 — AWS Config configuration changes",
|
||
},
|
||
{
|
||
id: "SecurityGroupChanges",
|
||
metricName: "SecurityGroupChanges",
|
||
pattern:
|
||
"{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }",
|
||
description: "CIS 4.10 — security group changes",
|
||
},
|
||
{
|
||
id: "NaclChanges",
|
||
metricName: "NetworkACLChanges",
|
||
pattern:
|
||
"{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }",
|
||
description: "CIS 4.11 — network ACL changes",
|
||
},
|
||
{
|
||
id: "NetworkGatewayChanges",
|
||
metricName: "NetworkGatewayChanges",
|
||
pattern:
|
||
"{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }",
|
||
description: "CIS 4.12 — network gateway changes",
|
||
},
|
||
{
|
||
id: "RouteTableChanges",
|
||
metricName: "RouteTableChanges",
|
||
pattern:
|
||
"{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }",
|
||
description: "CIS 4.13 — route table changes",
|
||
},
|
||
{
|
||
id: "VpcChanges",
|
||
metricName: "VPCChanges",
|
||
pattern:
|
||
"{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }",
|
||
description: "CIS 4.14 — VPC changes",
|
||
},
|
||
{
|
||
id: "OrganizationsChanges",
|
||
metricName: "OrganizationsChanges",
|
||
pattern:
|
||
'{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = "AcceptHandshake") || ($.eventName = "AttachPolicy") || ($.eventName = "CreateAccount") || ($.eventName = "CreateOrganizationalUnit") || ($.eventName = "CreatePolicy") || ($.eventName = "DeclineHandshake") || ($.eventName = "DeleteOrganization") || ($.eventName = "DeleteOrganizationalUnit") || ($.eventName = "DeletePolicy") || ($.eventName = "DetachPolicy") || ($.eventName = "DisablePolicyType") || ($.eventName = "EnablePolicyType") || ($.eventName = "InviteAccountToOrganization") || ($.eventName = "LeaveOrganization") || ($.eventName = "MoveAccount") || ($.eventName = "RemoveAccountFromOrganization") || ($.eventName = "UpdatePolicy") || ($.eventName = "UpdateOrganizationalUnit")) }',
|
||
description: "CIS 4.15 — AWS Organizations changes",
|
||
},
|
||
];
|
||
|
||
export interface CisMonitoringProps {
|
||
/** Email subscribed to the CIS alarm topic. */
|
||
readonly alarmEmail: string;
|
||
}
|
||
|
||
export class CisMonitoring extends Construct {
|
||
constructor(scope: Construct, id: string, props: CisMonitoringProps) {
|
||
super(scope, id);
|
||
|
||
// Customer-managed key for alarm topics (audit L-14). The AWS-managed
|
||
// alias/aws/sns key CANNOT be used here: its key policy can't grant
|
||
// cloudwatch.amazonaws.com, so CloudWatch alarms silently fail to publish
|
||
// to topics it encrypts — which is exactly what these topics receive.
|
||
// Also used by the unmanaged site-alerts topic (set via CLI; ARN output below).
|
||
const alarmTopicKey = new kms.Key(this, "AlarmTopicKey", {
|
||
alias: "seahaven-alarm-topics",
|
||
description:
|
||
"SSE for SNS alarm topics; grants CloudWatch alarms publish-side usage",
|
||
enableKeyRotation: true,
|
||
});
|
||
alarmTopicKey.addToResourcePolicy(
|
||
new cdk.aws_iam.PolicyStatement({
|
||
sid: "AllowCloudWatchAlarmsUse",
|
||
principals: [new cdk.aws_iam.ServicePrincipal("cloudwatch.amazonaws.com")],
|
||
actions: ["kms:GenerateDataKey*", "kms:Decrypt", "kms:DescribeKey"],
|
||
resources: ["*"],
|
||
conditions: {
|
||
StringEquals: { "aws:SourceAccount": cdk.Stack.of(this).account },
|
||
},
|
||
})
|
||
);
|
||
new cdk.CfnOutput(this, "AlarmTopicKeyArn", { value: alarmTopicKey.keyArn });
|
||
|
||
// Dedicated topic for security/CIS alarms (audit H-1, L-14).
|
||
const topic = new sns.Topic(this, "CisAlarmTopic", {
|
||
topicName: "seahaven-cis-alarms",
|
||
displayName: "Sea Haven CIS / security alarms",
|
||
masterKey: alarmTopicKey,
|
||
});
|
||
topic.addSubscription(new subscriptions.EmailSubscription(props.alarmEmail));
|
||
|
||
const logGroup = logs.LogGroup.fromLogGroupName(
|
||
this,
|
||
"TrailLogGroup",
|
||
TRAIL_LOG_GROUP_NAME
|
||
);
|
||
|
||
for (const c of CIS_CONTROLS) {
|
||
const mf = new logs.MetricFilter(this, `${c.id}Filter`, {
|
||
logGroup,
|
||
filterPattern: logs.FilterPattern.literal(c.pattern),
|
||
metricNamespace: "CISBenchmark",
|
||
metricName: c.metricName,
|
||
metricValue: "1",
|
||
defaultValue: 0,
|
||
});
|
||
|
||
const alarm = mf
|
||
.metric({
|
||
statistic: "Sum",
|
||
period: cdk.Duration.minutes(5),
|
||
})
|
||
.createAlarm(this, `${c.id}Alarm`, {
|
||
alarmName: `cis-${c.metricName}`,
|
||
alarmDescription: c.description,
|
||
threshold: 1,
|
||
comparisonOperator:
|
||
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
||
evaluationPeriods: 1,
|
||
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
||
});
|
||
|
||
// ALARM-only notification (no OK/recovery action) per Sea Haven preference.
|
||
alarm.addAlarmAction(new cwactions.SnsAction(topic));
|
||
}
|
||
|
||
new cdk.CfnOutput(this, "CisAlarmTopicArn", { value: topic.topicArn });
|
||
}
|
||
}
|