diff --git a/deployment/build_packages.sh b/deployment/build_packages.sh index 2e00c69c..f1e4eaa2 100755 --- a/deployment/build_packages.sh +++ b/deployment/build_packages.sh @@ -23,7 +23,7 @@ SCRIPT_PATH="$( cd "$(dirname "$0")" ; pwd -P )" PACKAGES_DIR="${SCRIPT_PATH}/packages/" LIBRARY="${SCRIPT_PATH}/../hammer/library" -LAMBDAS="ami-info logs-forwarder ddb-tables-backup sg-issues-identification s3-acl-issues-identification s3-policy-issues-identification iam-keyrotation-issues-identification iam-user-inactive-keys-identification cloudtrails-issues-identification ebs-unencrypted-volume-identification ebs-public-snapshots-identification rds-public-snapshots-identification sqs-public-policy-identification s3-unencrypted-bucket-issues-identification rds-unencrypted-instance-identification ami-public-access-issues-identification api" +LAMBDAS="ami-info logs-forwarder ddb-tables-backup sg-issues-identification s3-acl-issues-identification s3-policy-issues-identification iam-keyrotation-issues-identification iam-user-inactive-keys-identification cloudtrails-issues-identification ebs-unencrypted-volume-identification ebs-public-snapshots-identification rds-public-snapshots-identification sqs-public-policy-identification s3-unencrypted-bucket-issues-identification rds-unencrypted-instance-identification kms-keyrotation-issues-identification ami-public-access-issues-identification api" pushd "${SCRIPT_PATH}" > /dev/null pushd ../hammer/identification/lambdas > /dev/null diff --git a/deployment/cf-templates/ddb.json b/deployment/cf-templates/ddb.json index 4ec7b653..647b594f 100755 --- a/deployment/cf-templates/ddb.json +++ b/deployment/cf-templates/ddb.json @@ -428,6 +428,38 @@ "TableName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, "rds-unencrypted" ] ]} } }, + "DynamoDBKMSKeyRotation": { + "Type": "AWS::DynamoDB::Table", + "DeletionPolicy": "Retain", + "DependsOn": ["DynamoDBCredentials"], + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "account_id", + "AttributeType": "S" + }, + { + "AttributeName": "issue_id", + "AttributeType": "S" + } + ], + "KeySchema": [ + { + "AttributeName": "account_id", + "KeyType": "HASH" + }, + { + "AttributeName": "issue_id", + "KeyType": "RANGE" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": "10", + "WriteCapacityUnits": "2" + }, + "TableName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, "kms-key-rotation" ] ]} + } + }, "DynamoDBAMIPublicAccess": { "Type": "AWS::DynamoDB::Table", "DeletionPolicy": "Retain", diff --git a/deployment/cf-templates/identification-crossaccount-role.json b/deployment/cf-templates/identification-crossaccount-role.json index e5c16c32..d668b7e7 100755 --- a/deployment/cf-templates/identification-crossaccount-role.json +++ b/deployment/cf-templates/identification-crossaccount-role.json @@ -73,6 +73,17 @@ ], "Resource": "*" }, + { + "Action": [ + "kms:ListKeys", + "kms:GetKeyRotationStatus", + "kms:DescribeKey", + "kms:ListResourceTags" + ], + "Resource": "*", + "Effect": "Allow", + "Sid": "KmsIssues" + }, { "Sid": "CloudTrailIssues", "Effect": "Allow", diff --git a/deployment/cf-templates/identification-role.json b/deployment/cf-templates/identification-role.json index 288897f2..bc72c869 100755 --- a/deployment/cf-templates/identification-role.json +++ b/deployment/cf-templates/identification-role.json @@ -132,6 +132,17 @@ ], "Resource": "*" }, + { + "Action": [ + "kms:ListKeys", + "kms:GetKeyRotationStatus", + "kms:DescribeKey", + "kms:ListResourceTags" + ], + "Resource": "*", + "Effect": "Allow", + "Sid": "KmsIssues" + }, { "Sid": "CloudTrailIssues", "Effect": "Allow", diff --git a/deployment/cf-templates/identification.json b/deployment/cf-templates/identification.json index 3618c393..0b427081 100755 --- a/deployment/cf-templates/identification.json +++ b/deployment/cf-templates/identification.json @@ -27,6 +27,7 @@ "SourceIdentificationEBSVolumes", "SourceIdentificationEBSSnapshots", "SourceIdentificationRDSSnapshots", + "SourceIdentificationKMSKeyRotation", "SourceIdentificationAMIPublicAccess" ] }, @@ -90,6 +91,9 @@ "SourceIdentificationRDSSnapshots": { "default": "Relative path to public RDS snapshots lambda sources" }, + "SourceIdentificationKMSKeyRotation": { + "default": "Relative path to KMS key rotation sources" + }, "SourceIdentificationAMIPublicAccess":{ "default": "Relative path to Public AMI sources" } @@ -169,6 +173,10 @@ "Type": "String", "Default": "rds-public-snapshots-identification.zip" }, + "SourceIdentificationKMSKeyRotation": { + "Type": "String", + "Default": "kms-keyrotation-issues-identification.zip" + }, "SourceIdentificationAMIPublicAccess": { "Type": "String", "Default": "ami-public-access-issues-identification.zip" @@ -229,6 +237,9 @@ "IdentificationMetricRDSSnapshotsError": { "value": "RDSSnapshotsError" }, + "IdentificationMetricKMSKeyRotationError": { + "value": "KMSKeyRotationError" + }, "IdentificationMetricAMIPublicAccessError": { "value": "AMIPublicAccessError" }, @@ -295,6 +306,12 @@ "SNSTopicNameRDSSnapshots": { "value": "describe-rds-public-snapshots-lambda" }, + "SNSDisplayNameKMSKeyRotation": { + "value": "describe-kms-key-rotation-sns" + }, + "SNSTopicNameKMSKeyRotation": { + "value": "describe-kms-key-rotation-lambda" + }, "SNSDisplayNameAMIPublicAccess": { "value": "describe-ami-public-access-sns" }, @@ -379,6 +396,12 @@ "IdentifyRDSSnapshotsLambdaFunctionName": { "value": "describe-rds-public-snapshots" }, + "InitiateKMSKeyRotationLambdaFunctionName": { + "value": "initiate-kms-key-rotation" + }, + "IdentifyKMSKeyRotationLambdaFunctionName": { + "value": "describe-kms-key-rotation" + }, "InitiateAMIPublicAccessLambdaFunctionName": { "value": "initiate-ami-public-access" }, @@ -1860,6 +1883,105 @@ "LogGroupName" : { "Ref": "LogGroupLambdaEvaluateRDSEncryption" } } }, + "LambdaInitiateKMSKeyRotationEvaluation": { + "Type": "AWS::Lambda::Function", + "DependsOn": ["SNSNotifyLambdaEvaluateKMSKeyRotation", "LogGroupLambdaInitiateKMSKeyRotationEvaluation"], + "Properties": { + "Code": { + "S3Bucket": { "Ref": "SourceS3Bucket" }, + "S3Key": { "Ref": "SourceIdentificationKMSKeyRotation" } + }, + "Environment": { + "Variables": { + "SNS_KMS_KEY_ROTATION_ARN": { "Ref": "SNSNotifyLambdaEvaluateKMSKeyRotation" } + } + }, + "Description": "Lambda function for initiate to identify disabled KMS key rotation issues.", + "FunctionName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "InitiateKMSKeyRotationLambdaFunctionName", "value"] } ] + ]}, + "Handler": "initiate_to_desc_kms_key_rotation.lambda_handler", + "MemorySize": 128, + "Timeout": "300", + "Role": {"Fn::Join" : ["", [ "arn:aws:iam::", + { "Ref": "AWS::AccountId" }, + ":role/", + { "Ref": "ResourcesPrefix" }, + { "Ref": "IdentificationIAMRole" } + ] ]}, + "Runtime": "python3.6" + } + }, + "LogGroupLambdaInitiateKMSKeyRotationEvaluation": { + "Type" : "AWS::Logs::LogGroup", + "Properties" : { + "LogGroupName": {"Fn::Join": ["", [ "/aws/lambda/", + { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", + "InitiateKMSKeyRotationLambdaFunctionName", + "value"] + } ] ] }, + "RetentionInDays": "7" + } + }, + "SubscriptionFilterLambdaInitiateKMSKeyRotationEvaluation": { + "Type" : "AWS::Logs::SubscriptionFilter", + "DependsOn": ["LambdaLogsForwarder", + "PermissionToInvokeLambdaLogsForwarderCloudWatchLogs", + "LogGroupLambdaInitiateKMSKeyRotationEvaluation"], + "Properties" : { + "DestinationArn" : { "Fn::GetAtt" : [ "LambdaLogsForwarder", "Arn" ] }, + "FilterPattern" : "[level != START && level != END && level != DEBUG, ...]", + "LogGroupName" : { "Ref": "LogGroupLambdaInitiateKMSKeyRotationEvaluation" } + } + }, + "LambdaEvaluateKMSKeyRotation": { + "Type": "AWS::Lambda::Function", + "DependsOn": ["LogGroupLambdaEvaluateKMSKeyRotation"], + "Properties": { + "Code": { + "S3Bucket": { "Ref": "SourceS3Bucket" }, + "S3Key": { "Ref": "SourceIdentificationKMSKeyRotation" } + }, + "Description": "Lambda function to describe disabled KMS key rotation issues.", + "FunctionName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "IdentifyKMSKeyRotationLambdaFunctionName", "value"] } ] + ]}, + "Handler": "describe_kms_key_rotation.lambda_handler", + "MemorySize": 256, + "Timeout": "300", + "Role": {"Fn::Join" : ["", [ "arn:aws:iam::", + { "Ref": "AWS::AccountId" }, + ":role/", + { "Ref": "ResourcesPrefix" }, + { "Ref": "IdentificationIAMRole" } + ] ]}, + "Runtime": "python3.6" + } + }, + "LogGroupLambdaEvaluateKMSKeyRotation": { + "Type" : "AWS::Logs::LogGroup", + "Properties" : { + "LogGroupName": {"Fn::Join": ["", [ "/aws/lambda/", + { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", + "IdentifyKMSKeyRotationLambdaFunctionName", + "value"] + } ] ] }, + "RetentionInDays": "7" + } + }, + "SubscriptionFilterLambdaEvaluateKMSKeyRotation": { + "Type" : "AWS::Logs::SubscriptionFilter", + "DependsOn": ["LambdaLogsForwarder", + "PermissionToInvokeLambdaLogsForwarderCloudWatchLogs", + "LogGroupLambdaEvaluateKMSKeyRotation"], + "Properties" : { + "DestinationArn" : { "Fn::GetAtt" : [ "LambdaLogsForwarder", "Arn" ] }, + "FilterPattern" : "[level != START && level != END && level != DEBUG, ...]", + "LogGroupName" : { "Ref": "LogGroupLambdaEvaluateKMSKeyRotation" } + } + }, "LambdaInitiateAMIPublicAccessEvaluation": { "Type": "AWS::Lambda::Function", "DependsOn": ["SNSNotifyLambdaEvaluateAMIPublicAccess", "LogGroupLambdaInitiateAMIPublicAccessEvaluation"], @@ -1912,7 +2034,6 @@ "LogGroupName" : { "Ref": "LogGroupLambdaInitiateAMIPublicAccessEvaluation" } } }, - "LambdaEvaluateAMIPublicAccess": { "Type": "AWS::Lambda::Function", "DependsOn": ["LogGroupLambdaEvaluateAMIPublicAccess"], @@ -2124,6 +2245,22 @@ ] } }, + "EventInitiateEvaluationKMSKeyRotation": { + "Type": "AWS::Events::Rule", + "DependsOn": ["LambdaInitiateKMSKeyRotationEvaluation"], + "Properties": { + "Description": "Hammer ScheduledRule to initiate KMS key rotation evaluations", + "Name": {"Fn::Join" : ["", [{ "Ref": "ResourcesPrefix" }, "InitiateEvaluationKMSKeyRotation"] ] }, + "ScheduleExpression": {"Fn::Join": ["", [ "cron(", "35 ", { "Ref": "IdentificationCheckRateExpression" }, ")" ] ]}, + "State": "ENABLED", + "Targets": [ + { + "Arn": { "Fn::GetAtt": ["LambdaInitiateKMSKeyRotationEvaluation", "Arn"] }, + "Id": "LambdaInitiateKMSKeyRotationEvaluation" + } + ] + } + }, "EventInitiateEvaluationAMIPublicAccess": { "Type": "AWS::Events::Rule", "DependsOn": ["LambdaInitiateAMIPublicAccessEvaluation"], @@ -2282,6 +2419,16 @@ "SourceArn": { "Fn::GetAtt": ["EventInitiateEvaluationRDSEncryption", "Arn"] } } }, + "PermissionToInvokeLambdaInitiateKMSKeyRotationEvaluationCloudWatchEvents": { + "Type": "AWS::Lambda::Permission", + "DependsOn": ["LambdaInitiateKMSKeyRotationEvaluation", "EventInitiateEvaluationKMSKeyRotation"], + "Properties": { + "FunctionName": { "Ref": "LambdaInitiateKMSKeyRotationEvaluation" }, + "Action": "lambda:InvokeFunction", + "Principal": "events.amazonaws.com", + "SourceArn": { "Fn::GetAtt": ["EventInitiateEvaluationKMSKeyRotation", "Arn"] } + } + }, "PermissionToInvokeLambdaInitiateAMIPublicAccessEvaluationCloudWatchEvents": { "Type": "AWS::Lambda::Permission", "DependsOn": ["LambdaInitiateAMIPublicAccessEvaluation", "EventInitiateEvaluationAMIPublicAccess"], @@ -2508,6 +2655,24 @@ }] } }, + "SNSNotifyLambdaEvaluateKMSKeyRotation": { + "Type": "AWS::SNS::Topic", + "DependsOn": "LambdaEvaluateKMSKeyRotation", + "Properties": { + "DisplayName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "SNSDisplayNameKMSKeyRotation", "value"] } ] + ]}, + "TopicName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "SNSTopicNameKMSKeyRotation", "value"] } ] + ]}, + "Subscription": [{ + "Endpoint": { + "Fn::GetAtt": ["LambdaEvaluateKMSKeyRotation", "Arn"] + }, + "Protocol": "lambda" + }] + } + }, "SNSNotifyLambdaEvaluateAMIPublicAccess": { "Type": "AWS::SNS::Topic", "DependsOn": "LambdaEvaluateAMIPublicAccess", @@ -2646,6 +2811,16 @@ "FunctionName": { "Fn::GetAtt": ["LambdaEvaluateRDSEncryption", "Arn"] } } }, + "PermissionToInvokeLambdaEvaluateKMSKeyRotationSNS": { + "Type": "AWS::Lambda::Permission", + "DependsOn": ["SNSNotifyLambdaEvaluateKMSKeyRotation", "LambdaEvaluateKMSKeyRotation"], + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "sns.amazonaws.com", + "SourceArn": { "Ref": "SNSNotifyLambdaEvaluateKMSKeyRotation" }, + "FunctionName": { "Fn::GetAtt": ["LambdaEvaluateKMSKeyRotation", "Arn"] } + } + }, "PermissionToInvokeLambdaEvaluateAMIPublicAccessSNS": { "Type": "AWS::Lambda::Permission", "DependsOn": ["SNSNotifyLambdaEvaluateAMIPublicAccess", "LambdaEvaluateAMIPublicAccess"], @@ -3258,6 +3433,52 @@ "TreatMissingData": "notBreaching" } }, + "AlarmErrorsLambdaInitiateKMSKeyRotationEvaluation": { + "Type": "AWS::CloudWatch::Alarm", + "DependsOn": ["SNSIdentificationErrors", "LambdaInitiateKMSKeyRotationEvaluation"], + "Properties": { + "AlarmActions": [ { "Ref": "SNSIdentificationErrors" } ], + "OKActions": [ { "Ref": "SNSIdentificationErrors" } ], + "AlarmName": {"Fn::Join": ["/", [ { "Ref": "LambdaInitiateKMSKeyRotationEvaluation" }, "LambdaError" ] ]}, + "EvaluationPeriods": 1, + "Namespace": "AWS/Lambda", + "MetricName": "Errors", + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { "Ref": "LambdaInitiateKMSKeyRotationEvaluation" } + } + ], + "Period": 3600, + "Statistic": "Maximum", + "ComparisonOperator" : "GreaterThanThreshold", + "Threshold": 0, + "TreatMissingData": "notBreaching" + } + }, + "AlarmErrorsLambdaKMSKeyRotationEvaluation": { + "Type": "AWS::CloudWatch::Alarm", + "DependsOn": ["SNSIdentificationErrors", "LambdaEvaluateKMSKeyRotation"], + "Properties": { + "AlarmActions": [ { "Ref": "SNSIdentificationErrors" } ], + "OKActions": [ { "Ref": "SNSIdentificationErrors" } ], + "AlarmName": {"Fn::Join": ["/", [ { "Ref": "LambdaEvaluateKMSKeyRotation" }, "LambdaError" ] ]}, + "EvaluationPeriods": 1, + "Namespace": "AWS/Lambda", + "MetricName": "Errors", + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { "Ref": "LambdaEvaluateKMSKeyRotation" } + } + ], + "Period": 3600, + "Statistic": "Maximum", + "ComparisonOperator" : "GreaterThanThreshold", + "Threshold": 0, + "TreatMissingData": "notBreaching" + } + }, "AlarmErrorsLambdaInitiateAMIPublicAccessEvaluation": { "Type": "AWS::CloudWatch::Alarm", "DependsOn": ["SNSIdentificationErrors", "LambdaInitiateAMIPublicAccessEvaluation"], diff --git a/deployment/cf-templates/reporting-remediation-crossaccount-role.json b/deployment/cf-templates/reporting-remediation-crossaccount-role.json index 4a1bda43..9c24c13e 100755 --- a/deployment/cf-templates/reporting-remediation-crossaccount-role.json +++ b/deployment/cf-templates/reporting-remediation-crossaccount-role.json @@ -90,6 +90,18 @@ ], "Resource": "*" }, + { + "Action": [ + "kms:ListKeys", + "kms:GetKeyRotationStatus", + "kms:DescribeKey", + "kms:ListResourceTags", + "kms:EnableKeyRotation" + ], + "Resource": "*", + "Effect": "Allow", + "Sid": "KmsIssues" + }, { "Sid": "CloudTrailIssues", "Effect": "Allow", diff --git a/deployment/cf-templates/reporting-remediation-role.json b/deployment/cf-templates/reporting-remediation-role.json index e4840a55..6320adbf 100755 --- a/deployment/cf-templates/reporting-remediation-role.json +++ b/deployment/cf-templates/reporting-remediation-role.json @@ -157,6 +157,18 @@ ], "Resource": "*" }, + { + "Action": [ + "kms:ListKeys", + "kms:GetKeyRotationStatus", + "kms:DescribeKey", + "kms:ListResourceTags", + "kms:EnableKeyRotation" + ], + "Resource": "*", + "Effect": "Allow", + "Sid": "KmsIssues" + }, { "Sid": "CloudTrailIssues", "Effect": "Allow", diff --git a/deployment/configs/config.json b/deployment/configs/config.json index 68bb3bef..03e1dd0e 100755 --- a/deployment/configs/config.json +++ b/deployment/configs/config.json @@ -133,6 +133,13 @@ "remediation": false, "remediation_retention_period": 0 }, + "kms_keys_rotation": { + "enabled": true, + "ddb.table_name": "hammer-kms-key-rotation", + "reporting": false, + "remediation": false, + "remediation_retention_period": 21 + }, "ec2_public_ami": { "enabled": true, "ddb.table_name": "hammer-ec2-public-ami", diff --git a/deployment/configs/whitelist.json b/deployment/configs/whitelist.json index 3cd1ac81..821fe8b2 100755 --- a/deployment/configs/whitelist.json +++ b/deployment/configs/whitelist.json @@ -36,9 +36,11 @@ "__comment__": "Detects public RDS snapshots (with 'all' in 'restore' attribute). Key - account id, values - snapshot ARNs.", "123456789012": ["arn:aws:rds:eu-central-1:123456789012:snapshot:public", "arn:aws:rds:eu-west-1:123456789012:snapshot:rds:snapshot1"] }, + "kms_keys_rotation":{ + }, "public_ami_issues": { - }, - "sqs_public_access":{ + }, + "sqs_public_access":{ "__comment__": "Detects public SQS polices (with 'all' in 'restore' attribute). Key - account id, values - SQS ARNs.", "123456789012": [""] }, diff --git a/deployment/terraform/modules/identification/identification.tf b/deployment/terraform/modules/identification/identification.tf index 38c7f93e..a8e9818f 100755 --- a/deployment/terraform/modules/identification/identification.tf +++ b/deployment/terraform/modules/identification/identification.tf @@ -12,6 +12,7 @@ resource "aws_cloudformation_stack" "identification" { "aws_s3_bucket_object.cloudtrails-issues-identification", "aws_s3_bucket_object.ebs-unencrypted-volume-identification", "aws_s3_bucket_object.ebs-public-snapshots-identification", + "aws_s3_bucket_object.kms-keyrotation-issues-identification", "aws_s3_bucket_object.ami-public-access-issues-identification", "aws_s3_bucket_object.sqs-public-policy-identification", "aws_s3_bucket_object.s3-unencrypted-bucket-issues-identification", @@ -38,6 +39,7 @@ resource "aws_cloudformation_stack" "identification" { SourceIdentificationEBSVolumes = "${aws_s3_bucket_object.ebs-unencrypted-volume-identification.id}" SourceIdentificationEBSSnapshots = "${aws_s3_bucket_object.ebs-public-snapshots-identification.id}" SourceIdentificationRDSSnapshots = "${aws_s3_bucket_object.rds-public-snapshots-identification.id}" + SourceIdentificationKMSKeyRotation = "${aws_s3_bucket_object.kms-keyrotation-issues-identification.id}" SourceIdentificationAMIPublicAccess = "${aws_s3_bucket_object.ami-public-access-issues-identification.id}" SourceIdentificationSQSPublicPolicy = "${aws_s3_bucket_object.sqs-public-policy-identification.id}" SourceIdentificationS3Encryption = "${aws_s3_bucket_object.s3-unencrypted-bucket-issues-identification.id}" diff --git a/deployment/terraform/modules/identification/sources.tf b/deployment/terraform/modules/identification/sources.tf index c839c312..7960b3d5 100755 --- a/deployment/terraform/modules/identification/sources.tf +++ b/deployment/terraform/modules/identification/sources.tf @@ -69,11 +69,19 @@ resource "aws_s3_bucket_object" "rds-public-snapshots-identification" { key = "lambda/${format("rds-public-snapshots-identification-%s.zip", "${md5(file("${path.module}/../../../packages/rds-public-snapshots-identification.zip"))}")}" source = "${path.module}/../../../packages/rds-public-snapshots-identification.zip" } + +resource "aws_s3_bucket_object" "kms-keyrotation-issues-identification" { + bucket = "${var.s3bucket}" + key = "lambda/${format("kms-keyrotation-issues-identification-%s.zip", "${md5(file("${path.module}/../../../packages/kms-keyrotation-issues-identification.zip"))}")}" + source = "${path.module}/../../../packages/kms-keyrotation-issues-identification.zip" +} + resource "aws_s3_bucket_object" "ami-public-access-issues-identification" { bucket = "${var.s3bucket}" key = "lambda/${format("ami-public-access-issues-identification-%s.zip", "${md5(file("${path.module}/../../../packages/ami-public-access-issues-identification.zip"))}")}" source = "${path.module}/../../../packages/ami-public-access-issues-identification.zip" } + resource "aws_s3_bucket_object" "sqs-public-policy-identification" { bucket = "${var.s3bucket}" key = "lambda/${format("sqs-public-policy-identification-%s.zip", "${md5(file("${path.module}/../../../packages/sqs-public-policy-identification.zip"))}")}" @@ -90,4 +98,3 @@ resource "aws_s3_bucket_object" "rds-unencrypted-instance-identification" { key = "lambda/${format("rds-unencrypted-instance-identification-%s.zip", "${md5(file("${path.module}/../../../packages/rds-unencrypted-instance-identification.zip"))}")}" source = "${path.module}/../../../packages/rds-unencrypted-instance-identification.zip" } - diff --git a/docs/_data/sidebars/mydoc_sidebar.yml b/docs/_data/sidebars/mydoc_sidebar.yml index c9c4bf6c..fa3e950d 100644 --- a/docs/_data/sidebars/mydoc_sidebar.yml +++ b/docs/_data/sidebars/mydoc_sidebar.yml @@ -119,3 +119,7 @@ entries: - title: RDS Unencrypted instances url: /playbook12_rds_unencryption.html output: web, pdf + + - title: KMS key rotation + url: /playbook14_kms_key_rotation.html + output: web, pdf \ No newline at end of file diff --git a/docs/pages/deployment_cloudformation.md b/docs/pages/deployment_cloudformation.md index c7331eb7..b94cd347 100644 --- a/docs/pages/deployment_cloudformation.md +++ b/docs/pages/deployment_cloudformation.md @@ -98,6 +98,7 @@ You will need to set the following parameters: * **SourceIdentificationSQSPublicPolicy**: the relative path to the Lambda package that identifies SQS public queue issues. The default value is **sqs-public-policy-identification.zip**. * **SourceIdentificationS3Encryption**: the relative path to the Lambda package that identifies S3 un-encrypted bucket issues. The default value is **s3-unencrypted-bucket-issues-identification.zip**. * **SourceIdentificationRDSEncryption**: the relative path to the Lambda package that identifies RDS unencrypted instances. The default value is **rds-unencrypted-instance-identification.zip**. +* **SourceIdentificationKMSKeyRotation**: the relative path to the Lambda package that identifies KMS key rotation issues. The default value is **kms-keyrotation-issues-identification.zip**. **VPC config (optional)**: * **LambdaSubnets**: comma-separated list, without spaces, of subnet IDs in your VPC to run identification lambdas in. diff --git a/docs/pages/editconfig.md b/docs/pages/editconfig.md index 23ff0938..e59ecacc 100644 --- a/docs/pages/editconfig.md +++ b/docs/pages/editconfig.md @@ -386,4 +386,16 @@ Parameters: * **ddb.table_name**: the name of the DynamoDB table where Dow Jones Hammer will put detection results. The default value is `hammer-rds-unencrypted`. * **accounts**: *optional* comma-separated list of accounts to check and report for issue in square brackets. Use this key to override accounts from **aws.accounts** in [config.json](#11-master-aws-account-settings); * **ignore_accounts**: *optional* comma-separated list of accounts to ignore during check. Use this key to exclude accounts from **aws.accounts** in [config.json](#11-master-aws-account-settings); -* **reporting**: defines whether Dow Jones Hammer will report detected issues to JIRA/Slack. The default value is `false`; \ No newline at end of file +* **reporting**: defines whether Dow Jones Hammer will report detected issues to JIRA/Slack. The default value is `false`; +### 2.14. KMS key rotation issues + +This section describes how to detect whether you have KMS key rotation issues or not. Refer to [issue-specific playbook](playbook14_kms_key_rotation.html) for further details. + +Edit the **kms_keys_rotation** section of the `config.json` file to configure the handling of this issue. + +Parameters: +* **enabled**: enables/disables issue identification. The default value is `true`; +* **ddb.table_name**: the name of the DynamoDB table where Dow Jones Hammer will put detection results. The default value is `hammer-kms-key-rotation`. +* **reporting**: defines whether Dow Jones Hammer will report detected issues to JIRA/Slack. The default value is `false`; +* **remediation**: defines whether Dow Jones Hammer will automatically remediate the detected issue. The default value is `false`; +* **remediation_retention_period**: the amount of days that should pass between the detection of an issue and its automatic remediation by Dow Jones Hammer. The default value is `0`. diff --git a/docs/pages/features.md b/docs/pages/features.md index 3b830f91..d3f20a4c 100644 --- a/docs/pages/features.md +++ b/docs/pages/features.md @@ -21,5 +21,6 @@ Dow Jones Hammer can identify and report the following issues: |[SQS Policy Public Access](playbook10_sqs_public_policy.html) |Detects publicly accessible SQS policy |Any of SQS queues is worldwide accessible by policy | |[S3 Unencrypted Buckets](playbook11_s3_unencryption.html) |Detects not encrypted at reset S3 buckets |Any of S3 bucket is not encrypted at rest | |[RDS Unencrypted instances](playbook12_rds_unencryption.html) |Detects not encrypted at rest RDS instances |Any one of RDS instances is not encrypted at reset | +|[AMIs public access](playbook14_kms_key_rotation.html) |Detects KMS key rotation issues |Any one of KMS key rotation required | Dow Jones Hammer can perform remediation for all issues [except](remediation_backup_rollback.html#1-overview) **EBS Unencrypted volumes**, **CloudTrail Logging Issues** and **RDS Unencrypted instances**. \ No newline at end of file diff --git a/docs/pages/playbook14_kms_key_rotation.md b/docs/pages/playbook14_kms_key_rotation.md new file mode 100644 index 00000000..a86b7192 --- /dev/null +++ b/docs/pages/playbook14_kms_key_rotation.md @@ -0,0 +1,178 @@ +--- +title: KMS key rotation issues +keywords: playbook14 +sidebar: mydoc_sidebar +permalink: playbook14_kms_key_rotation.html +--- + +# Playbook 14: KMS key rotation issues + +## Introduction + +This playbook describes how to configure Dow Jones Hammer to detect kms key rotation issues. + +## 1. Issue Identification + +Dow Jones Hammer identifies those KMS keys' ```CreateDate``` parameters. In case the difference between this parameter's value for a key and the current date exceeds the threshold set in the Dow Jones Hammer configuration. + +When Dow Jones Hammer detects an issue, it writes the issue to the designated DynamoDB table. + +According to the [Dow Jones Hammer architecture](/index.html), the issue identification functionality uses two Lambda functions. +The table lists the Python modules that implement this functionality: + +|Designation |Path | +|--------------|:--------------------:| +|Initialization|`hammer/identification/lambdas/kms-keyrotation-issues-identification/initiate_to_desc_kms_key_rotation.py`| +|Identification|`hammer/identification/lambdas/kms-keyrotation-issues-identification/describe_kms_key_rotation.py` | + +## 2. Issue Reporting + +You can configure automatic reporting of cases when Dow Jones Hammer identifies an issue of this type. Dow Jones Hammer supports integration with [JIRA](https://www.atlassian.com/software/jira) and [Slack](https://slack.com/). +These types of reporting are independent from one another and you can turn them on/off in the Dow Jones Hammer configuration. + +Thus, in case you have turned on the reporting functionality for this issue and configured corresponding integrations, Dow Jones Hammer, as [defined in the configuration](#43-the-ticket_ownersjson-file), can: +* raise a JIRA ticket and assign it to a specific person in your organization; +* send the issue notification to the Slack channel or directly to a Slack user. + +Additionally Dow Jones Hammer tries to detect person to report issue to by examining KMS keys created time and present time. In case the difference this parameter's value for a key and current date exceeds threshold **valid JIRA/Slack user**: +* for JIRA: `jira_owner` parameter from [ticket_owners.json](#43-the-ticket_ownersjson-file) **is ignored** and discovered `owner` **is used instead** as a JIRA assignee; +* for Slack: discovered `owner` **is used in addition to** `slack_owner` value from [ticket_owners.json](#43-the-ticket_ownersjson-file). + +This Python module implements the issue reporting functionality: +``` +hammer/reporting-remediation/reporting/create_kms_key_rotation_issue_tickets.py +``` + + +## 3. Setup Instructions For This Issue + +To configure the detection, reporting, you should edit the following sections of the Dow Jones Hammer configuration files: + +### 3.1. The config.json File + +The **config.json** file is the main configuration file for Dow Jones Hammer that is available at `deployment/terraform/accounts/sample/config/config.json`. +To identify and report issues of this type, you should add the following parameters in the **kms_keys_rotation** section of the **config.json** file: + +|Parameter Name |Description | Default Value| +|------------------------------|---------------------------------------|:------------:| +|`enabled` |Toggles issue detection for this issue |`true`| +|`ddb.table_name` |Name of the DynamoDB table where Dow Jones Hammer will store the identified issues of this type| `hammer-kms-key-rotation` | +|`reporting` |Toggle Dow Jones Hammer reporting functionality for this issue type |`false`| + +Sample **config.json** section: +``` +"kms_keys_rotation": { + "enabled": true, + "ddb.table_name": "hammer-kms-key-rotation", + "reporting": true, + "remediation": false, + "remediation_retention_period": 21 + } +``` + +### 3.2. The whitelist.json File + +You can define exceptions to the general automatic remediation settings for specific KMS keys. To configure such exceptions, you should edit the **kms-keys-rotation** section of the **whitelist.json** configuration file as follows: + +|Parameter Key | Parameter Value(s)| +|:------------:|:-----------------:| +|AWS Account ID|KMS key ids(s)| + +Sample **whitelist.json** section: +``` +"kms_keys_rotation": { + "123456789012": ["kms_key1", "kms_key2"] +} +``` + +### 3.3. The ticket_owners.json File + +You should use the **ticket_owners.json** file to configure the integration of Dow Jones Hammer with JIRA and/or Slack for the issue reporting purposes. + +You can configure these parameters for specific AWS accounts and globally. Account-specific settings precede the global settings in the **ticket_owners.json** configuration file. + +Check the following table for parameters: + +|Parameter Name |Description |Sample Value | +|---------------------|--------------------------------------------------------------------|:---------------:| +|`jira_project` |The name of the JIRA project where Dow Jones Hammer will create the issue | `AWSSEC` | +|`jira_owner` |The name of the JIRA user to whom Dow Jones Hammer will assign the issue | `Support-Cloud` | +|`jira_parent_ticket` |The JIRA ticket to which Dow Jones Hammer will link the new ticket it creates | `AWSSEC-1234` | +|`slack_owner` |Name(s) of the Slack channels (prefixed by `#`) and/or Slack users that will receive issue reports from Dow Jones Hammer | `["#devops-channel", "bob"]` | + +Sample **ticket_owners.json** section: + +Account-specific settings: +``` +{ + "account": { + "123456789012": { + "jira_project": "", + "jira_owner": "Support-Cloud", + "jira_parent_ticket": "", + "slack_owner": "" + } + }, + "jira_project": "AWSSEC", + "jira_owner": "Support-General", + "jira_parent_ticket": "AWSSEC-1234", + "slack_owner": ["#devops-channel", "bob"] +} +``` + +## 4. Logging + +Dow Jones Hammer uses **CloudWatch Logs** for logging purposes. + +Dow Jones Hammer automatically sets up CloudWatch Log Groups and Log Streams for this issue when you deploy Dow Jones Hammer. + +### 4.1. Issue Identification Logging + +Dow Jones Hammer issue identification functionality uses two Lambda functions: + +* Initialization: this Lambda function selects slave accounts to check for this issue as designated in the Dow Jones Hammer configuration files and triggers the check. +* Identification: this Lambda function identifies this issue for each account/region selected at the previous step. + +You can see the logs for each of these Lambda functions in the following Log Groups: + +|Lambda Function|CloudWatch Log Group Name | +|---------------|--------------------------------------------| +|Initialization |`/aws/lambda/initiate-kms-key-rotation`| +|Identification |`/aws/lambda/describe-kms-key-rotation`| + +### 4.2. Issue Reporting Logging + +Dow Jones Hammer issue reporting functionality uses ```/aws/ec2/hammer-reporting-remediation``` CloudWatch Log Group for logging. The Log Group contains issue-specific Log Streams named as follows: + +|Designation|CloudWatch Log Stream Name | +|-----------|---------------------------------------------------------| +|Reporting |`reporting.create_kms_keyrotation_issue_tickets`| + + +### 4.3. Slack Reports + +In case you have enabled Dow Jones Hammer and Slack integration, Dow Jones Hammer sends notifications about issue identification and reporting to the designated Slack channel and/or recipient(s). + +Check [ticket_owners.json](#43-the-ticket_ownersjson-file) configuration for further guidance. + +### 4.4. Using CloudWatch Logs for Dow Jones Hammer + +To access Dow Jones Hammer logs, proceed as follows: + +1. Open **AWS Management Console**. +2. Select **CloudWatch** service. +3. Select **Logs** from the CloudWatch sidebar. +4. Select the log group you want to explore. The log group will open. +5. Select the log stream you want to explore. + +Check [CloudWatch Logs documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html) for further guidance. + +## 5. Issue specific details in DynamoDB + +Dow Jones Hammer stores various issue specific details in DynamoDB as a map under `issue_details` key. You can use it to create your own reporting modules. + +|Key |Type |Description |Example | +|-------------|:----:|----------------------------------|------------------------------------------------| +|`id` |string|kms_key id |`test-key-id` | +|`name` |string|KMS key arn name |`key_arn` | +|`tags` |map |Tags associated with KMS key |`{"Name": "TestKey", "service": "archive"}`| \ No newline at end of file diff --git a/docs/pages/remediation_backup_rollback.md b/docs/pages/remediation_backup_rollback.md index d05fe010..75d9f07d 100644 --- a/docs/pages/remediation_backup_rollback.md +++ b/docs/pages/remediation_backup_rollback.md @@ -27,6 +27,7 @@ The following table gives an overview of Dow Jones Hammer remediation functional |[SQS Queue Public Access](playbook10_sqs_public_policy.html#3-issue-remediation) | Yes | Yes | |[S3 Unencrypted Buckets](playbook11_s3_unencryption.html#3-issue-remediation) | Yes | Yes | |[RDS Unencrypted instances](playbook12_rds_unencryption.html#3-issue-remediation) | `No` | `No` | +|[KMS key rotation issues](playbook14_kms_key_rotation.html#3-issue-remediation) | `Yes` | `No` | ## 2. How Remediation Backup Works diff --git a/hammer/identification/lambdas/kms-keyrotation-issues-identification/describe_kms_key_rotation.py b/hammer/identification/lambdas/kms-keyrotation-issues-identification/describe_kms_key_rotation.py new file mode 100644 index 00000000..f8559a82 --- /dev/null +++ b/hammer/identification/lambdas/kms-keyrotation-issues-identification/describe_kms_key_rotation.py @@ -0,0 +1,83 @@ +import json +import logging + + +from library.logger import set_logging +from library.config import Config +from library.aws.kms import KMSKeyChecker +from library.aws.utility import Account +from library.ddb_issues import IssueStatus, KMSKeyRotationIssue +from library.ddb_issues import Operations as IssueOperations +from library.aws.utility import Sns + + +def lambda_handler(event, context): + """ Lambda handler to evaluate kms keys rotation """ + set_logging(level=logging.INFO) + + try: + payload = json.loads(event["Records"][0]["Sns"]["Message"]) + account_id = payload['account_id'] + account_name = payload['account_name'] + # get the last region from the list to process + region = payload['regions'].pop() + except Exception: + logging.exception(f"Failed to parse event\n{event}") + return + + try: + config = Config() + + main_account = Account(region=config.aws.region) + ddb_table = main_account.resource("dynamodb").Table(config.kmsKeysRotation.ddb_table_name) + + account = Account(id=account_id, + name=account_name, + region=region, + role_name=config.aws.role_name_identification) + if account.session is None: + return + + logging.debug(f"Checking for KMS keys rotation for {account}") + + # existing open issues for account to check if resolved + open_issues = IssueOperations.get_account_open_issues(ddb_table, account_id, KMSKeyRotationIssue) + # make dictionary for fast search by id + # and filter by current region + open_issues = {issue.issue_id: issue for issue in open_issues if issue.issue_details.region == region} + logging.debug(f"KMS keys to rotate in DDB:\n{open_issues.keys()}") + + checker = KMSKeyChecker(account=account) + if checker.check(): + for key in checker.keys: + logging.debug(f"Checking {key.id}") + if not key.rotation_enabled: + issue = KMSKeyRotationIssue(account_id, key.id) + issue.issue_details.tags = key.tags + issue.issue_details.region = region + if config.kmsKeysRotation.in_whitelist(account_id, key.id): + issue.status = IssueStatus.Whitelisted + else: + issue.status = IssueStatus.Open + logging.debug(f"Setting {key.id}/{key.id} status {issue.status}") + IssueOperations.update(ddb_table, issue) + # remove issue id from issues_list_from_db (if exists) + # as we already checked it + open_issues.pop(key.id, None) + + logging.debug(f"Keys to rotate in DDB:\n{open_issues.keys()}") + # all other unresolved issues in DDB are for removed/remediated keys + for issue in open_issues.values(): + IssueOperations.set_status_resolved(ddb_table, issue) + except Exception: + logging.exception(f"Failed to check KMS keys rotation for '{account_id} ({account_name})'") + return + + # push SNS messages until the list with regions to check is empty + if len(payload['regions']) > 0: + try: + Sns.publish(payload["sns_arn"], payload) + except Exception: + logging.exception("Failed to chain KMS keys rotation checking") + + logging.debug(f"Checked KMS keys rotation for '{account_id} ({account_name})'") diff --git a/hammer/identification/lambdas/kms-keyrotation-issues-identification/initiate_to_desc_kms_key_rotation.py b/hammer/identification/lambdas/kms-keyrotation-issues-identification/initiate_to_desc_kms_key_rotation.py new file mode 100644 index 00000000..e103b1a8 --- /dev/null +++ b/hammer/identification/lambdas/kms-keyrotation-issues-identification/initiate_to_desc_kms_key_rotation.py @@ -0,0 +1,35 @@ +import os +import logging + +from library.logger import set_logging +from library.config import Config +from library.aws.utility import Sns + + +def lambda_handler(event, context): + """ Lambda handler to initiate to find stale KMS keys""" + set_logging(level=logging.INFO) + logging.debug("Initiating KMS keys rotation checking") + + try: + sns_arn = os.environ["SNS_KMS_KEY_ROTATION_ARN"] + config = Config() + + if not config.kmsKeysRotation.enabled: + logging.debug("KMS keys rotation checking disabled") + return + + logging.debug("Iterating over each account to initiate KMS keys rotation check") + for account_id, account_name in config.kmsKeysRotation.accounts.items(): + payload = {"account_id": account_id, + "account_name": account_name, + "regions": config.aws.regions, + "sns_arn": sns_arn + } + logging.debug(f"Initiating KMS keys rotation checking for '{account_name}'") + Sns.publish(sns_arn, payload) + except Exception: + logging.exception("Error occurred while initiation of KMS keys rotation check") + return + + logging.debug("KMS keys rotation checking initiation done") diff --git a/hammer/library/aws/kms.py b/hammer/library/aws/kms.py new file mode 100644 index 00000000..88d1d6c1 --- /dev/null +++ b/hammer/library/aws/kms.py @@ -0,0 +1,145 @@ +import logging + +from botocore.exceptions import ClientError + +from library.aws import utility +from library.utility import jsonDumps + + +class KMSOperations: + @classmethod + def enable_key_rotation(cls, kms_client, key_id): + """ + Enable rotation of given KMS key. + + :param kms_client: KMS boto3 client + :param key_id: key Id to enable key rotation + + :return: nothing + """ + kms_client.enable_key_rotation(KeyId=key_id) + + +class KMSKey(object): + """ + Basic class for KMS key. + Encapsulates access key Id, arn and rotation status. + """ + def __init__(self, account, key_id, key_arn, tags, key_rotation_enabled): + """ + :param account: Account + :param key_id: KMS key id + :param key_arn: KMS key arn + :param key_rotation_enabled: KMS key rotation status is enabled or not + """ + self.account = account + self.id = key_id + self.arn = key_arn + self.tags = utility.convert_tags( + tags, + tagKey="TagKey", tagValue="TagValue" + ) + self.rotation_enabled = key_rotation_enabled + + def __str__(self): + return (f"{self.__class__.__name__}(" + f"Id={self.id}, " + f"Status={self.rotation_enabled}, " + f")") + + def enable(self): + """ Enable rotation of current KMS key """ + KMSOperations.enable_key_rotation(self.account.client("kms"), self.id) + + +class KMSKeyChecker(object): + """ + Basic class for checking KMS key rotation details in account. + Encapsulates check settings and discovered kms keys. + """ + def __init__(self, account): + """ + :param account: `Account` instance with KMS keys to check + """ + self.account = account + self.keys = [] + + def get_key(self, key_id): + """ + :return: `Key` by key id (name) + """ + for key in self.keys: + if key.id == key_id: + return key + return None + + def check(self, keys_to_check=None): + """ + Walk through KMS keys in the account region and check them (rotation enabled/disabled). + Put all gathered Keys to `self.keys`. + + :param keys_to_check: list with KMS kesy to check, if it is not supplied - all KMS keys must be checked + + :return: boolean. True - if check was successful, + False - otherwise + """ + try: + # get all kms keys in account + keys = self.account.client("kms").list_keys()['Keys'] + except ClientError as err: + if err.response['Error']['Code'] in ["AccessDenied", "UnauthorizedOperation"]: + logging.error(f"Access denied in {self.account} " + f"(kms:{err.operation_name})") + else: + logging.exception(f"Failed to list kms keys in {self.account}") + return False + + logging.debug(f"Evaluating kms keys \n{jsonDumps(keys)}") + for key_response in keys: + key_id = key_response["KeyId"] + key_arn = key_response["KeyArn"] + + if keys_to_check is not None and key_id not in keys_to_check: + continue + + try: + key_metadata = self.account.client("kms").describe_key(KeyId=key_id)["KeyMetadata"] + except ClientError as err: + if err.response['Error']['Code'] in ["AccessDenied", "UnauthorizedOperation"]: + logging.error(f"Access denied in {self.account} " + f"(kms:{err.operation_name})") + else: + logging.exception(f"Failed to get kms key metadata in {self.account} for key id: {key_id}") + return False + + key_state = key_metadata["Enabled"] + origin = key_metadata["Origin"] + key_manager = key_metadata["KeyManager"] + if origin == "AWS_KMS" and key_manager == "CUSTOMER" and key_state: + try: + key_rotation_enabled = self.account.client("kms").get_key_rotation_status( + KeyId=key_id + )["KeyRotationEnabled"] + except ClientError as err: + if err.response['Error']['Code'] in ["AccessDenied", "UnauthorizedOperation"]: + logging.error(f"Access denied in {self.account} " + f"(kms:{err.operation_name})") + else: + logging.exception(f"Failed to get kms key rotation status in {self.account} for key id: {key_id}") + return False + + try: + tags = self.account.client("kms").list_resource_tags(KeyId=key_id).get("Tags", []) + except ClientError as err: + if err.response['Error']['Code'] in ["AccessDenied", "UnauthorizedOperation"]: + logging.error(f"Access denied in {self.account} " + f"(kms:{err.operation_name}, " + f"resource='{key_id}')") + else: + logging.exception(f"Failed to get '{key_id}' tags in {self.account}") + + key = KMSKey(account=self.account, key_id=key_id, key_arn=key_arn, tags=tags, + key_rotation_enabled=key_rotation_enabled) + self.keys.append(key) + + return True diff --git a/hammer/library/aws/utility.py b/hammer/library/aws/utility.py index 7a32d883..6b11b3e1 100755 --- a/hammer/library/aws/utility.py +++ b/hammer/library/aws/utility.py @@ -304,7 +304,7 @@ def put_lambda_metrics(cls, func_name, metrics): ) -def convert_tags(tags): +def convert_tags(tags, tagKey="Key", tagValue="Value"): """ Convert tags from AWS format [{'Key': '...', 'Value': '...'}, ...] to {'Key': 'Value', ...} format @@ -315,4 +315,4 @@ def convert_tags(tags): # dynamodb does not like empty strings # but Value can be empty, so convert it to None empty_converter = lambda x: x if x != "" else None - return {tag['Key']: empty_converter(tag['Value']) for tag in tags} if tags else {} + return {tag[tagKey]: empty_converter(tag[tagValue]) for tag in tags} if tags else {} diff --git a/hammer/library/config.py b/hammer/library/config.py index 504f1a1d..b851b6e6 100755 --- a/hammer/library/config.py +++ b/hammer/library/config.py @@ -56,6 +56,8 @@ def __init__(self, self.ebsSnapshot = ModuleConfig(self._config, "ebs_public_snapshot") # RDS public snapshot issue config self.rdsSnapshot = ModuleConfig(self._config, "rds_public_snapshot") + # KMS key rotation issue config + self.kmsKeysRotation = ModuleConfig(self._config, "kms_keys_rotation") # SQS public access issue config self.sqspolicy = ModuleConfig(self._config, "sqs_public_access") # S3 encryption issue config diff --git a/hammer/library/ddb_issues.py b/hammer/library/ddb_issues.py index d9ae7de2..993908a4 100755 --- a/hammer/library/ddb_issues.py +++ b/hammer/library/ddb_issues.py @@ -233,11 +233,16 @@ def __init__(self, *args): super().__init__(*args) +class KMSKeyRotationIssue(Issue): + def __init__(self, *args): + super().__init__(*args) + + class PublicAMIIssue(Issue): def __init__(self, *args): super().__init__(*args) - + class Operations(object): @staticmethod def find(ddb_table, issue): diff --git a/hammer/reporting-remediation/cronjobs/automation_scheduler.py b/hammer/reporting-remediation/cronjobs/automation_scheduler.py index 8afd30f8..c057971c 100755 --- a/hammer/reporting-remediation/cronjobs/automation_scheduler.py +++ b/hammer/reporting-remediation/cronjobs/automation_scheduler.py @@ -57,6 +57,7 @@ def automation_cronjob(config): ("SQS Public Access", config.sqspolicy, "create_sqs_policy_issue_tickets", "clean_sqs_policy_permissions"), ("S3 Unencrypted Buckets", config.s3Encrypt, "create_s3_unencrypted_bucket_issue_tickets", "clean_s3bucket_unencrypted"), ("RDS Unencrypted Instances", config.rdsEncrypt, "create_rds_unencrypted_instance_issue_tickets", None), + ("KMS Key Rotation", config.kmsKeysRotation, "create_kms_key_rotation_issue_tickets", "clean_kms_key_rotation") ] for title, module_config, reporting_script, remediation_script in modules: diff --git a/hammer/reporting-remediation/remediation/clean_kms_key_rotation.py b/hammer/reporting-remediation/remediation/clean_kms_key_rotation.py new file mode 100644 index 00000000..cb437ec9 --- /dev/null +++ b/hammer/reporting-remediation/remediation/clean_kms_key_rotation.py @@ -0,0 +1,129 @@ +""" +Class for KMS key rotation remediation. +""" +import sys +import logging +import argparse + + +from library.logger import set_logging, add_cw_logging +from library.config import Config +from library.jiraoperations import JiraReporting +from library.slack_utility import SlackNotification +from library.aws.kms import KMSKeyChecker +from library.ddb_issues import Operations as IssueOperations +from library.ddb_issues import KMSKeyRotationIssue +from library.aws.utility import Account +from library.utility import confirm +from library.utility import SingletonInstance, SingletonInstanceException + + +class CleanKMSKeyRotation(object): + """ Class for KMS key rotation remediation """ + def __init__(self, config): + self.config = config + + def clean_kms_key_rotation(self, batch=False): + """ Class method to remediate KMS key rotation """ + main_account = Account(region=config.aws.region) + ddb_table = main_account.resource("dynamodb").Table(self.config.kmsKeysRotation.ddb_table_name) + + retention_period = self.config.kmsKeysRotation.remediation_retention_period + + jira = JiraReporting(self.config) + slack = SlackNotification(self.config) + + for account_id, account_name in self.config.kmsKeysRotation.remediation_accounts.items(): + logging.debug(f"Checking '{account_name} / {account_id}'") + issues = IssueOperations.get_account_open_issues(ddb_table, account_id, KMSKeyRotationIssue) + for issue in issues: + if issue.timestamps.remediated is not None: + logging.debug(f"Skipping '{issue.issue_id}' (has been already remediated)") + continue + + in_whitelist = self.config.kmsKeysRotation.in_whitelist(account_id, issue.issue_id) + if in_whitelist: + logging.debug(f"Skipping '{issue.issue_id}' (in whitelist)") + continue + + if issue.timestamps.reported is None: + logging.debug(f"Skipping '{issue.issue_id}' (was not reported)") + continue + + updated_date = issue.timestamp_as_datetime + no_of_days_issue_created = (self.config.now - updated_date).days + + if no_of_days_issue_created >= retention_period: + owner = issue.jira_details.owner + bu = issue.jira_details.business_unit + product = issue.jira_details.product + + try: + if not batch and \ + not confirm(f"Do you want to remediate kms key rotation enabled '{issue.issue_id}'", False): + continue + + account = Account(id=account_id, + name=account_name, + region=issue.issue_details.region, + role_name=self.config.aws.role_name_reporting) + if account.session is None: + continue + + checker = KMSKeyChecker(account=account) + checker.check(keys_to_check=[issue.issue_id]) + kms_key = checker.get_key(issue.issue_id) + remediation_succeed = True + try: + kms_key.enable() + comment = (f"KMS '{issue.issue_id}' issue " + f"in '{account_name} / {account_id}' account, '{issue.issue_details.region}' region " + f"was remediated by hammer") + except Exception: + remediation_succeed = False + logging.exception(f"Failed to enable '{issue.issue_id}' kms key rotation") + comment = (f"Failed to remediate KMS key rotation '{issue.issue_id}' issue " + f"in '{account_name} / {account_id}' account, '{issue.issue_details.region}' region " + f"due to some limitations. Please, check manually") + + jira.remediate_issue( + ticket_id=issue.jira_details.ticket, + comment=comment, + reassign=remediation_succeed, + ) + slack.report_issue( + msg=f"{comment}" + f"{' (' + jira.ticket_url(issue.jira_details.ticket) + ')' if issue.jira_details.ticket else ''}", + owner=owner, + account_id=account_id, + bu=bu, product=product, + ) + IssueOperations.set_status_remediated(ddb_table, issue) + except Exception: + logging.exception(f"Error occurred while updating KMS keys {issue.issue_id} " + f"in {account_id}/{issue.issue_details.region}") + + +if __name__ == "__main__": + module_name = sys.modules[__name__].__loader__.name + set_logging(level=logging.DEBUG, logfile=f"/var/log/hammer/{module_name}.log") + config = Config() + add_cw_logging(config.local.log_group, + log_stream=module_name, + level=logging.DEBUG, + region=config.aws.region) + try: + si = SingletonInstance(module_name) + except SingletonInstanceException: + logging.error(f"Another instance of '{module_name}' is already running, quitting") + sys.exit(1) + + parser = argparse.ArgumentParser() + parser.add_argument('--batch', action='store_true', help='Do not ask confirmation for remediation') + args = parser.parse_args() + + try: + obj = CleanKMSKeyRotation(config) + obj.clean_kms_key_rotation(batch=args.batch) + except Exception: + logging.exception("Failed to enable kms key rotation") diff --git a/hammer/reporting-remediation/reporting/create_kms_key_rotation_issue_tickets.py b/hammer/reporting-remediation/reporting/create_kms_key_rotation_issue_tickets.py new file mode 100644 index 00000000..d5c5db12 --- /dev/null +++ b/hammer/reporting-remediation/reporting/create_kms_key_rotation_issue_tickets.py @@ -0,0 +1,155 @@ +""" +Class to create kms key rotation issue tickets. +""" +import sys +import logging + + +from library.logger import set_logging, add_cw_logging +from library.aws.utility import Account +from library.config import Config +from library.jiraoperations import JiraReporting, JiraOperations +from library.slack_utility import SlackNotification +from library.ddb_issues import IssueStatus, KMSKeyRotationIssue +from library.ddb_issues import Operations as IssueOperations +from library.utility import SingletonInstance, SingletonInstanceException + + +class CreateKMSKeyRotationIssueTickets(object): + """ Class to create kms key rotation issue tickets """ + def __init__(self, config): + self.config = config + + def create_tickets_kms_key_rotation(self): + """ Class method to create jira tickets """ + table_name = self.config.kmsKeysRotation.ddb_table_name + + main_account = Account(region=self.config.aws.region) + ddb_table = main_account.resource("dynamodb").Table(table_name) + jira = JiraReporting(self.config) + slack = SlackNotification(self.config) + + for account_id, account_name in self.config.aws.accounts.items(): + logging.debug(f"Checking '{account_name} / {account_id}'") + issues = IssueOperations.get_account_not_closed_issues(ddb_table, account_id, KMSKeyRotationIssue) + for issue in issues: + kms_key_id = issue.issue_id + region = issue.issue_details.region + tags = issue.issue_details.tags + # issue has been already reported + if issue.timestamps.reported is not None: + owner = issue.jira_details.owner + bu = issue.jira_details.business_unit + product = issue.jira_details.product + + if issue.status in [IssueStatus.Resolved, IssueStatus.Whitelisted]: + logging.debug(f"Closing {issue.status.value} KMS key rotation '{kms_key_id}' issue") + + comment = (f"Closing {issue.status.value} KMS key rotation '{kms_key_id}' issue " + f"in '{account_name} / {account_id}' account, '{region}' region") + jira.close_issue( + ticket_id=issue.jira_details.ticket, + comment=comment + ) + slack.report_issue( + msg=f"{comment}" + f"{' (' + jira.ticket_url(issue.jira_details.ticket) + ')' if issue.jira_details.ticket else ''}", + owner=owner, + account_id=account_id, + bu=bu, product=product, + ) + IssueOperations.set_status_closed(ddb_table, issue) + # issue.status != IssueStatus.Closed (should be IssueStatus.Open) + elif issue.timestamps.updated > issue.timestamps.reported: + logging.error(f"TODO: update jira ticket with new data: {table_name}, {account_id}, {kms_key_id}") + slack.report_issue( + msg=f"KMS key rotation '{kms_key_id}' issue is changed " + f"in '{account_name} / {account_id}' account, '{region}' region" + f"{' (' + jira.ticket_url(issue.jira_details.ticket) + ')' if issue.jira_details.ticket else ''}", + owner=owner, + account_id=account_id, + bu=bu, product=product, + ) + IssueOperations.set_status_updated(ddb_table, issue) + else: + logging.debug(f"No changes for '{kms_key_id}'") + # issue has not been reported yet + else: + logging.debug(f"Reporting KMS key rotation '{kms_key_id}' issue") + + owner = tags.get("owner", None) + bu = tags.get("bu", None) + product = tags.get("product", None) + + issue_summary = (f"KMS key rotation issue key '{kms_key_id}'" + f"in '{account_name} / {account_id}' account{' [' + bu + ']' if bu else ''}") + + issue_description = ( + f"The KMS key rotation is not enabled.\n\n" + f"*Risk*: High\n\n" + f"*Account Name*: {account_name}\n" + f"*Account ID*: {account_id}\n" + f"*Region*: {region}\n" + f"*KMS key ID*: {kms_key_id}\n") + + auto_remediation_date = (self.config.now + self.config.kmsKeysRotation.issue_retention_date).date() + issue_description += f"\n{{color:red}}*Auto-Remediation Date*: {auto_remediation_date}{{color}}\n\n" + + issue_description += JiraOperations.build_tags_table(tags) + + issue_description += "\n" + issue_description += ( + f"*Recommendation*: " + f"Enable key rotation status for KMS key." + ) + + try: + response = jira.add_issue( + issue_summary=issue_summary, issue_description=issue_description, + priority="Major", labels=["kms-key-rotation"], + owner=owner, + account_id=account_id, + bu=bu, product=product, + ) + except Exception: + logging.exception("Failed to create jira ticket") + continue + + if response is not None: + issue.jira_details.ticket = response.ticket_id + issue.jira_details.ticket_assignee_id = response.ticket_assignee_id + + issue.jira_details.owner = owner + issue.jira_details.business_unit = bu + issue.jira_details.product = product + + slack.report_issue( + msg=f"Discovered {issue_summary}" + f"{' (' + jira.ticket_url(issue.jira_details.ticket) + ')' if issue.jira_details.ticket else ''}", + owner=owner, + account_id=account_id, + bu=bu, product=product, + ) + + IssueOperations.set_status_reported(ddb_table, issue) + + +if __name__ == '__main__': + module_name = sys.modules[__name__].__loader__.name + set_logging(level=logging.DEBUG, logfile=f"/var/log/hammer/{module_name}.log") + config = Config() + add_cw_logging(config.local.log_group, + log_stream=module_name, + level=logging.DEBUG, + region=config.aws.region) + try: + si = SingletonInstance(module_name) + except SingletonInstanceException: + logging.error(f"Another instance of '{module_name}' is already running, quitting") + sys.exit(1) + + try: + obj = CreateKMSKeyRotationIssueTickets(config) + obj.create_tickets_kms_key_rotation() + except Exception: + logging.exception("Failed to create kms key rotation issue tickets")