diff --git a/deployment/build_packages.sh b/deployment/build_packages.sh index f4219872..d1b59686 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" +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 sqs-unencrypted-queues-identification" 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 2ab4843c..e35f2fdf 100755 --- a/deployment/cf-templates/ddb.json +++ b/deployment/cf-templates/ddb.json @@ -428,6 +428,39 @@ }, "TableName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, "rds-unencrypted" ] ]} } + }, + + "DynamoDBSQSQueueUnencrypted": { + "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" }, "sqs-unencrypted" ] ]} + } } } } diff --git a/deployment/cf-templates/identification.json b/deployment/cf-templates/identification.json index 431bf3f4..a9037e4d 100755 --- a/deployment/cf-templates/identification.json +++ b/deployment/cf-templates/identification.json @@ -26,7 +26,8 @@ "SourceIdentificationIAMUserInactiveKeys", "SourceIdentificationEBSVolumes", "SourceIdentificationEBSSnapshots", - "SourceIdentificationRDSSnapshots" + "SourceIdentificationRDSSnapshots", + "SourceIdentificationSQSEncryption" ] }, { @@ -88,6 +89,9 @@ }, "SourceIdentificationRDSSnapshots": { "default": "Relative path to public RDS snapshots lambda sources" + }, + "SourceIdentificationSQSEncryption":{ + "default": "Relative path to Unencrypted SQS queues lambda sources" } } } @@ -176,6 +180,10 @@ "SourceIdentificationRDSEncryption": { "Type": "String", "Default": "rds-unencrypted-instance-identification.zip" + }, + "SourceIdentificationSQSEncryption": { + "Type": "String", + "Default": "sqs-unencrypted-queues-identification.zip" } }, "Conditions": { @@ -230,6 +238,9 @@ "IdentificationMetricRDSEncryptionError": { "value": "RDSEncryptionError" }, + "IdentificationMetricSQSEncryptionError": { + "value": "SQSEncryptionError" + }, "SNSDisplayNameSecurityGroups": { "value": "describe-security-groups-sns" }, @@ -302,6 +313,12 @@ "SNSTopicNameRDSEncryption": { "value": "describe-rds-encryption-lambda" }, + "SNSDisplayNameSQSEncryption": { + "value": "describe-sqs-encryption-sns" + }, + "SNSTopicNameSQSEncryption": { + "value": "describe-sqs-encryption-lambda" + }, "LogsForwarderLambdaFunctionName": { "value": "logs-forwarder" }, @@ -379,6 +396,12 @@ }, "IdentifyRDSEncryptionLambdaFunctionName": { "value": "describe-rds-encryption" + }, + "InitiateSQSEncryptionLambdaFunctionName": { + "value": "initiate-sqs-encryption" + }, + "IdentifySQSEncryptionLambdaFunctionName": { + "value": "describe-sqs-encryption" } } }, @@ -1840,6 +1863,107 @@ } }, + "LambdaInitiateSQSEncryptionEvaluation": { + "Type": "AWS::Lambda::Function", + "DependsOn": ["SNSNotifyLambdaEvaluateSQSEncryption", "LogGroupLambdaInitiateSQSEncryptionEvaluation"], + "Properties": { + "Code": { + "S3Bucket": { "Ref": "SourceS3Bucket" }, + "S3Key": { "Ref": "SourceIdentificationSQSEncryption" } + }, + "Environment": { + "Variables": { + "SNS_SQS_ENCRYPT_ARN": { "Ref": "SNSNotifyLambdaEvaluateSQSEncryption" } + } + }, + "Description": "Lambda function for initiate to identify unencrypted SQS queues.", + "FunctionName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "InitiateSQSEncryptionLambdaFunctionName", "value"] } ] + ]}, + "Handler": "initiate_to_desc_sqs_unencrypted_queues.lambda_handler", + "MemorySize": 128, + "Timeout": "300", + "Role": {"Fn::Join" : ["", [ "arn:aws:iam::", + { "Ref": "AWS::AccountId" }, + ":role/", + { "Ref": "ResourcesPrefix" }, + { "Ref": "IdentificationIAMRole" } + ] ]}, + "Runtime": "python3.6" + } + }, + "LogGroupLambdaInitiateSQSEncryptionEvaluation": { + "Type" : "AWS::Logs::LogGroup", + "Properties" : { + "LogGroupName": {"Fn::Join": ["", [ "/aws/lambda/", + { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", + "InitiateSQSEncryptionLambdaFunctionName", + "value"] + } ] ] }, + "RetentionInDays": "7" + } + }, + "SubscriptionFilterLambdaInitiateSQSEncryptionEvaluation": { + "Type" : "AWS::Logs::SubscriptionFilter", + "DependsOn": ["LambdaLogsForwarder", + "PermissionToInvokeLambdaLogsForwarderCloudWatchLogs", + "LogGroupLambdaInitiateSQSEncryptionEvaluation"], + "Properties" : { + "DestinationArn" : { "Fn::GetAtt" : [ "LambdaLogsForwarder", "Arn" ] }, + "FilterPattern" : "[level != START && level != END && level != DEBUG, ...]", + "LogGroupName" : { "Ref": "LogGroupLambdaInitiateSQSEncryptionEvaluation" } + } + }, + + "LambdaEvaluateSQSEncryption": { + "Type": "AWS::Lambda::Function", + "DependsOn": ["LogGroupLambdaEvaluateSQSEncryption"], + "Properties": { + "Code": { + "S3Bucket": { "Ref": "SourceS3Bucket" }, + "S3Key": { "Ref": "SourceIdentificationSQSEncryption" } + }, + "Description": "Lambda function to describe un-encrypted SQS queues.", + "FunctionName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "IdentifySQSEncryptionLambdaFunctionName", "value"] } ] + ]}, + "Handler": "describe_sqs_unencrypted_queues.lambda_handler", + "MemorySize": 256, + "Timeout": "300", + "Role": {"Fn::Join" : ["", [ "arn:aws:iam::", + { "Ref": "AWS::AccountId" }, + ":role/", + { "Ref": "ResourcesPrefix" }, + { "Ref": "IdentificationIAMRole" } + ] ]}, + "Runtime": "python3.6" + } + }, + "LogGroupLambdaEvaluateSQSEncryption": { + "Type" : "AWS::Logs::LogGroup", + "Properties" : { + "LogGroupName": {"Fn::Join": ["", [ "/aws/lambda/", + { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", + "IdentifySQSEncryptionLambdaFunctionName", + "value"] + } ] ] }, + "RetentionInDays": "7" + } + }, + "SubscriptionFilterLambdaEvaluateSQSEncryption": { + "Type" : "AWS::Logs::SubscriptionFilter", + "DependsOn": ["LambdaLogsForwarder", + "PermissionToInvokeLambdaLogsForwarderCloudWatchLogs", + "LogGroupLambdaEvaluateSQSEncryption"], + "Properties" : { + "DestinationArn" : { "Fn::GetAtt" : [ "LambdaLogsForwarder", "Arn" ] }, + "FilterPattern" : "[level != START && level != END && level != DEBUG, ...]", + "LogGroupName" : { "Ref": "LogGroupLambdaEvaluateSQSEncryption" } + } + }, + "EventBackupDDB": { "Type": "AWS::Events::Rule", "DependsOn": ["LambdaBackupDDB"], @@ -2004,6 +2128,22 @@ ] } }, + "EventInitiateEvaluationSQSEncryption": { + "Type": "AWS::Events::Rule", + "DependsOn": ["LambdaInitiateSQSEncryptionEvaluation"], + "Properties": { + "Description": "Hammer ScheduledRule to initiate sqs queue encryption evaluations", + "Name": {"Fn::Join" : ["", [{ "Ref": "ResourcesPrefix" }, "InitiateEvaluationSQSEncryption"] ] }, + "ScheduleExpression": {"Fn::Join": ["", [ "cron(", "40 ", { "Ref": "IdentificationCheckRateExpression" }, ")" ] ]}, + "State": "ENABLED", + "Targets": [ + { + "Arn": { "Fn::GetAtt": ["LambdaInitiateSQSEncryptionEvaluation", "Arn"] }, + "Id": "LambdaInitiateSQSEncryptionEvaluation" + } + ] + } + }, "PermissionToInvokeLambdaLogsForwarderCloudWatchLogs": { "Type": "AWS::Lambda::Permission", @@ -2147,6 +2287,16 @@ "SourceArn": { "Fn::GetAtt": ["EventInitiateEvaluationRDSEncryption", "Arn"] } } }, + "PermissionToInvokeLambdaInitiateSQSEncryptionEvaluationCloudWatchEvents": { + "Type": "AWS::Lambda::Permission", + "DependsOn": ["LambdaInitiateSQSEncryptionEvaluation", "EventInitiateEvaluationSQSEncryption"], + "Properties": { + "FunctionName": { "Ref": "LambdaInitiateSQSEncryptionEvaluation" }, + "Action": "lambda:InvokeFunction", + "Principal": "events.amazonaws.com", + "SourceArn": { "Fn::GetAtt": ["EventInitiateEvaluationSQSEncryption", "Arn"] } + } + }, "SNSNotifyLambdaEvaluateSG": { "Type": "AWS::SNS::Topic", @@ -2364,6 +2514,24 @@ }] } }, + "SNSNotifyLambdaEvaluateSQSEncryption": { + "Type": "AWS::SNS::Topic", + "DependsOn": "LambdaEvaluateSQSEncryption", + "Properties": { + "DisplayName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "SNSDisplayNameSQSEncryption", "value"] } ] + ]}, + "TopicName": {"Fn::Join" : ["", [ { "Ref": "ResourcesPrefix" }, + { "Fn::FindInMap": ["NamingStandards", "SNSTopicNameSQSEncryption", "value"] } ] + ]}, + "Subscription": [{ + "Endpoint": { + "Fn::GetAtt": ["LambdaEvaluateSQSEncryption", "Arn"] + }, + "Protocol": "lambda" + }] + } + }, "PermissionToInvokeLambdaEvaluateSgSNS": { "Type": "AWS::Lambda::Permission", @@ -2485,6 +2653,16 @@ "FunctionName": { "Fn::GetAtt": ["LambdaEvaluateRDSEncryption", "Arn"] } } }, + "PermissionToInvokeLambdaEvaluateSQSEncryptionSNS": { + "Type": "AWS::Lambda::Permission", + "DependsOn": ["SNSNotifyLambdaEvaluateSQSEncryption", "LambdaEvaluateSQSEncryption"], + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "sns.amazonaws.com", + "SourceArn": { "Ref": "SNSNotifyLambdaEvaluateSQSEncryption" }, + "FunctionName": { "Fn::GetAtt": ["LambdaEvaluateSQSEncryption", "Arn"] } + } + }, "SNSIdentificationErrors": { "Type": "AWS::SNS::Topic", @@ -3088,6 +3266,52 @@ "Threshold": 0, "TreatMissingData": "notBreaching" } + }, + "AlarmErrorsLambdaInitiateSQSEncryptionEvaluation": { + "Type": "AWS::CloudWatch::Alarm", + "DependsOn": ["SNSIdentificationErrors", "LambdaInitiateSQSEncryptionEvaluation"], + "Properties": { + "AlarmActions": [ { "Ref": "SNSIdentificationErrors" } ], + "OKActions": [ { "Ref": "SNSIdentificationErrors" } ], + "AlarmName": {"Fn::Join": ["/", [ { "Ref": "LambdaInitiateSQSEncryptionEvaluation" }, "LambdaError" ] ]}, + "EvaluationPeriods": 1, + "Namespace": "AWS/Lambda", + "MetricName": "Errors", + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { "Ref": "LambdaInitiateSQSEncryptionEvaluation" } + } + ], + "Period": 3600, + "Statistic": "Maximum", + "ComparisonOperator" : "GreaterThanThreshold", + "Threshold": 0, + "TreatMissingData": "notBreaching" + } + }, + "AlarmErrorsLambdaSQSEncryptionEvaluation": { + "Type": "AWS::CloudWatch::Alarm", + "DependsOn": ["SNSIdentificationErrors", "LambdaEvaluateSQSEncryption"], + "Properties": { + "AlarmActions": [ { "Ref": "SNSIdentificationErrors" } ], + "OKActions": [ { "Ref": "SNSIdentificationErrors" } ], + "AlarmName": {"Fn::Join": ["/", [ { "Ref": "LambdaEvaluateSQSEncryption" }, "LambdaError" ] ]}, + "EvaluationPeriods": 1, + "Namespace": "AWS/Lambda", + "MetricName": "Errors", + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { "Ref": "LambdaEvaluateSQSEncryption" } + } + ], + "Period": 3600, + "Statistic": "Maximum", + "ComparisonOperator" : "GreaterThanThreshold", + "Threshold": 0, + "TreatMissingData": "notBreaching" + } } }, "Outputs": { diff --git a/deployment/configs/config.json b/deployment/configs/config.json index 74087148..b1154d53 100755 --- a/deployment/configs/config.json +++ b/deployment/configs/config.json @@ -135,6 +135,13 @@ "remediation": false, "remediation_retention_period": 0 }, + "sqs_encryption": { + "enabled": true, + "ddb.table_name": "hammer-sqs-unencrypted", + "reporting": true, + "remediation": false, + "remediation_retention_period": 21 + }, "rds_encryption": { "enabled": true, "ddb.table_name": "hammer-rds-unencrypted", diff --git a/deployment/configs/whitelist.json b/deployment/configs/whitelist.json index 6d31497a..aee46ccc 100755 --- a/deployment/configs/whitelist.json +++ b/deployment/configs/whitelist.json @@ -43,5 +43,8 @@ "s3_encryption": { }, "rds_encryption": { + }, + "sqs_encryption":{ + } } \ No newline at end of file diff --git a/deployment/terraform/modules/identification/identification.tf b/deployment/terraform/modules/identification/identification.tf index 654aaffa..a30793a5 100755 --- a/deployment/terraform/modules/identification/identification.tf +++ b/deployment/terraform/modules/identification/identification.tf @@ -14,7 +14,8 @@ resource "aws_cloudformation_stack" "identification" { "aws_s3_bucket_object.ebs-public-snapshots-identification", "aws_s3_bucket_object.sqs-public-policy-identification", "aws_s3_bucket_object.s3-unencrypted-bucket-issues-identification", - "aws_s3_bucket_object.rds-unencrypted-instance-identification" + "aws_s3_bucket_object.rds-unencrypted-instance-identification", + "aws_s3_bucket_object.sqs-unencrypted-queues-identification" ] tags = "${var.tags}" @@ -40,6 +41,7 @@ resource "aws_cloudformation_stack" "identification" { SourceIdentificationSQSPublicPolicy = "${aws_s3_bucket_object.sqs-public-policy-identification.id}" SourceIdentificationS3Encryption = "${aws_s3_bucket_object.s3-unencrypted-bucket-issues-identification.id}" SourceIdentificationRDSEncryption = "${aws_s3_bucket_object.rds-unencrypted-instance-identification.id}" + SourceIdentificationSQSEncryption = "${aws_s3_bucket_object.sqs-unencrypted-queues-identification.id}" } template_url = "https://${var.s3bucket}.s3.amazonaws.com/${aws_s3_bucket_object.identification-cfn.id}" diff --git a/deployment/terraform/modules/identification/sources.tf b/deployment/terraform/modules/identification/sources.tf index d0791d6d..7465c5c9 100755 --- a/deployment/terraform/modules/identification/sources.tf +++ b/deployment/terraform/modules/identification/sources.tf @@ -86,4 +86,11 @@ resource "aws_s3_bucket_object" "rds-unencrypted-instance-identification" { bucket = "${var.s3bucket}" 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" -} \ No newline at end of file +} + +resource "aws_s3_bucket_object" "sqs-unencrypted-queues-identification" { + bucket = "${var.s3bucket}" + key = "lambda/${format("sqs-unencrypted-queues-identification-%s.zip", "${md5(file("${path.module}/../../../packages/sqs-unencrypted-queues-identification.zip"))}")}" + source = "${path.module}/../../../packages/sqs-unencrypted-queues-identification.zip" +} + diff --git a/docs/_data/sidebars/mydoc_sidebar.yml b/docs/_data/sidebars/mydoc_sidebar.yml index 9b0b67ba..8080827a 100644 --- a/docs/_data/sidebars/mydoc_sidebar.yml +++ b/docs/_data/sidebars/mydoc_sidebar.yml @@ -115,3 +115,7 @@ entries: - title: RDS Unencrypted instances url: /playbook12_rds_unencryption.html output: web, pdf + + - title: SQS Unencrypted queues + url: /playbook13_sqs_unencryption.html + output: web, pdf diff --git a/docs/pages/deployment_cloudformation.md b/docs/pages/deployment_cloudformation.md index ce1bb23f..cf47aaa1 100644 --- a/docs/pages/deployment_cloudformation.md +++ b/docs/pages/deployment_cloudformation.md @@ -93,6 +93,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**. +* **SourceIdentificationSQSEncryption**: the relative path to the Lambda package that identifies SQS unencrypted queues. The default value is **sqs-unencrypted-queues-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..232db3c2 100644 --- a/docs/pages/editconfig.md +++ b/docs/pages/editconfig.md @@ -386,4 +386,17 @@ 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`; + +### 2.13. SQS Unencrypted Queues + +This section describes how to detect whether you have SQS queues that are not encrypted at rest. Refer to [issue-specific playbook](playbook13_sqs_unencryption.html) for further details. + +Edit the **sqs_encryption** 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-sqs-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 diff --git a/docs/pages/features.md b/docs/pages/features.md index 3b830f91..5b56d936 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 | +|[SQS Unencrypted queues](playbook13_sqs_unencryption.html) |Detects not encrypted at rest SQS queues |Any one of SQS queues is not encrypted at reset | 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/playbook13_sqs_unencryption.md b/docs/pages/playbook13_sqs_unencryption.md new file mode 100644 index 00000000..8f58daed --- /dev/null +++ b/docs/pages/playbook13_sqs_unencryption.md @@ -0,0 +1,175 @@ +--- +title: SQS unencrypted queues +keywords: playbook13 +sidebar: mydoc_sidebar +permalink: playbook13_sqs_unencryption.html +--- + +# Playbook 12: SQS unencrypted queues + +## Introduction + +This playbook describes how to configure Dow Jones Hammer to detect SQS queues that are not encrypted at rest. + +## 1. Issue Identification + +Dow Jones Hammer identifies those SQS queues for which ```Encrypted``` parameter value is ```false```. + +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/sqs-unencrypted-queues-identification/initiate_to_desc_sqs_unencrypted_queues.py`| +|Identification|`hammer/identification/lambdas/sqs-unencrypted-queues-identification/describe_sqs_unencrypted_queues.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 `owner` tag on affected SQS queue. In case when such tag **exists** and is **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_sqs_unencrypted_queue_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 **sqs_encryption** 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-sqs-unencrypted` | +|`reporting` |Toggle Dow Jones Hammer reporting functionality for this issue type |`false`| + +Sample **config.json** section: +``` +"sqs_encryption": { + "enabled": true, + "ddb.table_name": "hammer-sqs-unencrypted", + "reporting": true +} +``` + +### 3.2. The whitelist.json File + +You can define exceptions to the general automatic remediation settings for specific SQS queues. To configure such exceptions, you should edit the **sqs_encryption** section of the **whitelist.json** configuration file as follows: + +|Parameter Key | Parameter Value(s)| +|:------------:|:-----------------:| +|AWS Account ID|SQS Queue ARN(s)| + +Sample **whitelist.json** section: +``` +"sqs_encryption": { + "123456789012": ["queue_url1", "queue_url2"] +} +``` + +### 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/hammer-initiate-sqs-encryption`| +|Identification |`/aws/lambda/hammer-describe-sqs-encryption`| + +### 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_sqs_unencrypted_queue_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 | +|-------------|:----:|-------------------------------|---------------------------------| +|`name` |string|SQS queue name |`test-user` | +|`tags` |map |Tags associated with SQS queue |`{"Name": "TestQueue", "service": "archive"}`| diff --git a/docs/pages/remediation_backup_rollback.md b/docs/pages/remediation_backup_rollback.md index d05fe010..a5fc6d3a 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` | +|[SQS Unencrypted queues](playbook13_sqs_unencryption.html#3-issue-remediation) | `Yes` | `Yes` | ## 2. How Remediation Backup Works @@ -103,4 +104,11 @@ aws sqs set-queue-attributes --queue-url [queue_url] --attributes [backup_file_n To rollback a remediation of this issue, run the following command using the AWS CLI: ``` aws s3 put-bucket-encryption --bucket [bucket_name] --server-side-encryption-configuration [rules] +``` + +### 3.10. SQS Unencrypted Queues + +To rollback a remediation of this issue, run the following command using the AWS CLI: +``` +aws sqs set-queue-attributes --queue-url [queue_url] --attributes file://set-queue-attributes.json ``` \ No newline at end of file diff --git a/hammer/identification/lambdas/sqs-unencrypted-queues-identification/describe_sqs_unencrypted_queues.py b/hammer/identification/lambdas/sqs-unencrypted-queues-identification/describe_sqs_unencrypted_queues.py new file mode 100644 index 00000000..8714a1a2 --- /dev/null +++ b/hammer/identification/lambdas/sqs-unencrypted-queues-identification/describe_sqs_unencrypted_queues.py @@ -0,0 +1,83 @@ +import json +import logging + +from library.logger import set_logging +from library.config import Config +from library.aws.sqs import SQSEncryptionChecker +from library.aws.utility import Account +from library.ddb_issues import IssueStatus, SQSEncryptionIssue +from library.ddb_issues import Operations as IssueOperations +from library.aws.utility import Sns + + +def lambda_handler(event, context): + """ Lambda handler to evaluate unencrypted SQS queues """ + set_logging(level=logging.DEBUG) + + 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.sqsEncrypt.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 unencrypted SQS queues in {account}") + + # existing open issues for account to check if resolved + open_issues = IssueOperations.get_account_open_issues(ddb_table, account_id, SQSEncryptionIssue) + # 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"SQS in DDB:\n{open_issues.keys()}") + + checker = SQSEncryptionChecker(account=account) + if checker.check(): + for queue in checker.queues: + logging.debug(f"Checking {queue.name}") + if not queue.encrypted: + issue = SQSEncryptionIssue(account_id, queue.url) + issue.issue_details.tags = queue.tags + issue.issue_details.name = queue.name + issue.issue_details.region = queue.account.region + if config.sqsEncrypt.in_whitelist(account_id, queue.url): + issue.status = IssueStatus.Whitelisted + else: + issue.status = IssueStatus.Open + logging.debug(f"Setting {queue.name} 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(queue.url, None) + + logging.debug(f"SQS in DDB:\n{open_issues.keys()}") + # all other unresolved issues in DDB are for removed/remediated queues + for issue in open_issues.values(): + IssueOperations.set_status_resolved(ddb_table, issue) + except Exception: + logging.exception(f"Failed to check unencrypted SQS queues 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 insecure services checking") + + logging.debug(f"Checked unencrypted SQS queues for '{account_id} ({account_name})'") \ No newline at end of file diff --git a/hammer/identification/lambdas/sqs-unencrypted-queues-identification/initiate_to_desc_sqs_unencrypted_queues.py b/hammer/identification/lambdas/sqs-unencrypted-queues-identification/initiate_to_desc_sqs_unencrypted_queues.py new file mode 100644 index 00000000..87872b43 --- /dev/null +++ b/hammer/identification/lambdas/sqs-unencrypted-queues-identification/initiate_to_desc_sqs_unencrypted_queues.py @@ -0,0 +1,36 @@ +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 SQS unencrypted queues""" + set_logging(level=logging.INFO) + logging.debug("Initiating unencrypted SQS queues checking") + + try: + sns_arn = os.environ["SNS_SQS_ENCRYPT_ARN"] + config = Config() + + if not config.sqsEncrypt.enabled: + logging.debug("unencrypted SQS queues checking disabled") + return + + logging.debug("Iterating over each account to initiate unencrypted SQS queues check") + for account_id, account_name in config.sqsEncrypt.accounts.items(): + payload = {"account_id": account_id, + "account_name": account_name, + "regions": config.aws.regions, + "sns_arn": sns_arn + } + logging.debug(f"Initiating unencrypted SQS queues checking for '{account_name}'") + Sns.publish(sns_arn, payload) + + except Exception: + logging.exception("Error occurred while initiation of unencrypted SQS queues checking") + return + + logging.debug("Unencrypted SQS queues checking initiation done") diff --git a/hammer/library/aws/sqs.py b/hammer/library/aws/sqs.py index 6a577b0c..85e06df3 100644 --- a/hammer/library/aws/sqs.py +++ b/hammer/library/aws/sqs.py @@ -30,13 +30,23 @@ def put_queue_policy(sqs_client, queue_url, policy): } ) + @staticmethod + def set_queue_encryption(sqs_client, queue_url, kms_master_key_id): + + sqs_client.set_queue_attributes( + QueueUrl=queue_url, + Attributes={ + "KmsMasterKeyId": kms_master_key_id + } + ) + class SQSQueue(object): """ Basic class for SQS queue. Encapsulates `Tags`, dict with policy. """ - def __init__(self, account, url, tags, policy=None): + def __init__(self, account, url, tags, policy=None, encrypted=None): """ :param account: `Account` instance where SQS queue is present @@ -49,6 +59,7 @@ def __init__(self, account, url, tags, policy=None): self.name = os.path.basename(self.url) self.tags = tags self._policy = json.loads(policy) if policy else {} + self.encrypted = encrypted self.backup_filename = pathlib.Path(f"{self.name}.json") def __str__(self): @@ -105,6 +116,23 @@ def restrict_policy(self): return True + def encrypt_queue(self, kms_key_id=None): + """ + Encrypt queue with AWS KMS-managed keys (SSE-KMS). + :return: nothing + """ + try: + if kms_key_id: + SQSOperations.set_queue_encryption(self.account.client("sqs"), self.url, kms_key_id) + else: + logging.exception(f"Encryption key not found for {self.url} queue") + return False + except Exception: + logging.exception(f"Failed to encrypt {self.url} queue") + return False + + return True + class SQSPolicyChecker(object): """ @@ -187,3 +215,89 @@ def check(self, queues=None): ) self.queues.append(sqs_queue) return True + +class SQSEncryptionChecker(object): + """ + Basic class for checking unencrypted SQS queues in account. + Encapsulates discovered SQS queue. + """ + def __init__(self, account): + """ + :param account: `Account` instance with SQS queue to check + """ + self.account = account + self.queues = [] + + def get_queue(self, name): + """ + :return: `SQS Queue` by name + """ + for queue_url in self.queues: + if queue_url.name == name: + return queue_url + return None + + def check(self, queues=None): + """ + Walk through SQS queues in the account and check them (encrypted or not). + Put all gathered queues to `self.queues`. + + :param queues: list with SQS queue names to check, if it is not supplied - all queue must be checked + + :return: boolean. True - if check was successful, + False - otherwise + """ + try: + # AWS does not support filtering dirung list, so get all queues for account + queue_urls = self.account.client("sqs").list_queues().get("QueueUrls", []) + except ClientError as err: + if err.response['Error']['Code'] in ["AccessDenied", "UnauthorizedOperation"]: + logging.error(f"Access denied in {self.account} " + f"(sqs:{err.operation_name})") + else: + logging.exception(f"Failed to list queues in {self.account}") + return False + + for queue_url in queue_urls: + if queues is not None and queue_url not in queues: + continue + + # get queue policy + try: + response = self.account.client("sqs").get_queue_attributes( + QueueUrl=queue_url, + AttributeNames=['KmsMasterKeyId'] + ).get("Attributes", {}).get("KmsMasterKeyId", None) + if response: + queue_encrypted = True + else: + queue_encrypted = False + except ClientError as err: + if err.response['Error']['Code'] == "AccessDenied": + logging.error(f"Access denied in {self.account} " + f"(sqs:{err.operation_name}, " + f"resource='{queue_url}')") + continue + else: + logging.exception(f"Failed to get '{queue_url}' encyption details in {self.account}") + continue + # get queue tags + try: + tags = self.account.client("sqs").list_queue_tags(QueueUrl=queue_url).get("Tags", {}) + except ClientError as err: + tags = {} + if err.response['Error']['Code'] in ["AccessDenied", "UnauthorizedOperation"]: + logging.error(f"Access denied in {self.account} " + f"(sqs:{err.operation_name}, " + f"resource='{queue_url}')") + else: + logging.exception(f"Failed to get '{queue_url}' tags in {self.account}") + + sqs_queue = SQSQueue( + account=self.account, + url=queue_url, + tags=tags, + encrypted=queue_encrypted, + ) + self.queues.append(sqs_queue) + return True diff --git a/hammer/library/config.py b/hammer/library/config.py index 3e3ba1cc..613c7d5d 100755 --- a/hammer/library/config.py +++ b/hammer/library/config.py @@ -63,6 +63,9 @@ def __init__(self, # RDS encryption issue config self.rdsEncrypt = ModuleConfig(self._config, "rds_encryption") + # SQS encryption issue config + self.sqsEncrypt = ModuleConfig(self._config, "sqs_encryption") + self.bu_list = self._config.get("bu_list", []) self.whitelisting_procedure_url = self._config.get("whitelisting_procedure_url", None) diff --git a/hammer/library/ddb_issues.py b/hammer/library/ddb_issues.py index 433b9b5a..3cd7d19a 100755 --- a/hammer/library/ddb_issues.py +++ b/hammer/library/ddb_issues.py @@ -222,6 +222,10 @@ class RdsEncryptionIssue(Issue): def __init__(self, *args): super().__init__(*args) +class SQSEncryptionIssue(Issue): + def __init__(self, *args): + super().__init__(*args) + class Operations(object): @staticmethod diff --git a/hammer/reporting-remediation/remediation/clean_sqs_queue_unencrypted.py b/hammer/reporting-remediation/remediation/clean_sqs_queue_unencrypted.py new file mode 100644 index 00000000..0ae0807b --- /dev/null +++ b/hammer/reporting-remediation/remediation/clean_sqs_queue_unencrypted.py @@ -0,0 +1,145 @@ +""" +Class to remediate SQS unencrypted queue. +""" +import sys +import logging + + +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.ddb_issues import Operations as IssueOperations +from library.ddb_issues import SQSEncryptionIssue +from library.aws.sqs import SQSEncryptionChecker +from library.aws.utility import Account +from library.utility import SingletonInstance, SingletonInstanceException + + +class CleanSQSUnencryptedQueue: + """ Class to remediate SQS unencrypted queue """ + def __init__(self, config): + self.config = config + + def clean_sqs_unencrypted_queue(self): + """ Class method to clean SQS queues which are violating aws best practices """ + main_account = Account(region=config.aws.region) + ddb_table = main_account.resource("dynamodb").Table(self.config.sqsEncrypt.ddb_table_name) + + retention_period = self.config.sqsEncrypt.remediation_retention_period + remediation_warning_days = self.config.slack.remediation_warning_days + + #kms_key_id = self.config.sqsEncrypt.kms_key_id + kms_key_id = None + + 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_open_issues(ddb_table, account_id, SQSEncryptionIssue) + for issue in issues: + queue_url = issue.issue_id + queue_name = issue.issue_details.name + queue_region = issue.issue_details.region + + in_whitelist = self.config.sqsEncrypt.in_whitelist(account_id, queue_url) + + if in_whitelist: + logging.debug(f"Skipping {queue_name} (in whitelist)") + continue + + if issue.timestamps.reported is None: + logging.debug(f"Skipping '{queue_name}' (was not reported)") + continue + + if issue.timestamps.remediated is not None: + logging.debug(f"Skipping {queue_name} (has been already remediated)") + continue + + updated_date = issue.timestamp_as_datetime + no_of_days_issue_created = (self.config.now - updated_date).days + + owner = issue.jira_details.owner + bu = issue.jira_details.business_unit + product = issue.jira_details.product + + issue_remediation_days = retention_period - no_of_days_issue_created + if issue_remediation_days in remediation_warning_days: + slack.report_issue( + msg=f"SQS SQS unencrypted Queue '{queue_name}' issue is going to be remediated in " + f"{issue_remediation_days} days", + owner=owner, + account_id=account_id, + bu=bu, product=product, + ) + elif no_of_days_issue_created >= retention_period: + try: + 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 = SQSEncryptionChecker(account=account) + checker.check(queues=[queue_url]) + queue = checker.get_queue(queue_name) + if queue is None: + logging.debug(f"Queue {queue_name} was removed by user") + elif queue.encrypted: + logging.debug(f"Queue {queue.name} unencrypted issue was remediated by user") + else: + logging.debug(f"Remediating unencrypted '{queue.name}' ") + + remediation_succeed = True + if queue.encrypt_queue(kms_key_id): + comment = (f"Queue '{queue.name}' unencrypted issue " + f"in '{account_name} / {account_id}' account, '{queue_region}' region " + f"was remediated by hammer") + else: + remediation_succeed = False + comment = (f"Failed to remediate queue '{queue.name}' unencrypted issue " + f"in '{account_name} / {account_id}' account, '{queue_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 queue '{queue_url}' unencrypted issue " + f"in '{account_name} / {account_id}', '{queue_region}' region") + else: + logging.debug(f"Skipping '{queue_name}' " + f"({retention_period - no_of_days_issue_created} days before remediation)") + + +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: + class_object = CleanSQSUnencryptedQueue(config) + class_object.clean_sqs_unencrypted_queue() + except Exception: + logging.exception("Failed to clean SQS queue unencrypted issue") \ No newline at end of file diff --git a/hammer/reporting-remediation/reporting/create_sqs_unencrypted_queue_issue_tickets.py b/hammer/reporting-remediation/reporting/create_sqs_unencrypted_queue_issue_tickets.py new file mode 100644 index 00000000..dccfa658 --- /dev/null +++ b/hammer/reporting-remediation/reporting/create_sqs_unencrypted_queue_issue_tickets.py @@ -0,0 +1,179 @@ +""" +Class to create unencrypted SQS queue tickets. +""" +import sys +import logging + + +from library.logger import set_logging, add_cw_logging +from library.config import Config +from library.aws.utility import Account +from library.jiraoperations import JiraReporting, JiraOperations +from library.slack_utility import SlackNotification +from library.ddb_issues import IssueStatus, SQSEncryptionIssue +from library.ddb_issues import Operations as IssueOperations +from library.utility import SingletonInstance, SingletonInstanceException + + +class CreateSQSUnencryptedQueueIssueTickets: + """ Class to create unencrypted SQS queue tickets """ + def __init__(self, config): + self.config = config + + + def create_tickets_sqs_unencrypted_queues(self): + """ Class method to create jira tickets """ + table_name = self.config.sqsEncrypt.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, SQSEncryptionIssue) + for issue in issues: + queue_url = issue.issue_id + queue_name = issue.issue_details.name + queue_region = issue.issue_details.region + tags = issue.issue_details.tags + # issue has been already reported + if issue.timestamps.reported is not None: + owner = issue.issue_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} unencrypted SQS queue '{queue_name}' issue") + + comment = (f"Closing {issue.status.value} unencrypted SQS queue '{queue_name}' " + f"in '{account_name} / {account_id}' account, '{queue_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.debug(f"Updating unencrypted SQS queue '{queue_name}' issue") + + comment = "Issue details are changed, please check again.\n" + + comment += JiraOperations.build_tags_table(tags) + jira.update_issue( + ticket_id=issue.jira_details.ticket, + comment=comment + ) + slack.report_issue( + msg=f"Unencrypted SQS queue '{queue_name}' issue is changed " + f"in '{account_name} / {account_id}' account, '{queue_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 '{queue_name}'") + # issue has not been reported yet + else: + logging.debug(f"Reporting unencrypted SQS queue '{queue_name}' issue") + + owner = tags.get("owner", None) + bu = tags.get("bu", None) + product = tags.get("product", None) + + if bu is None: + bu = self.config.get_bu_by_name(queue_name) + + issue_summary = (f"SQS unencrypted queue '{queue_name}' " + f"in '{account_name} / {account_id}' account, '{queue_region}' region" + f"{' [' + bu + ']' if bu else ''}") + + issue_description = ( + f"SQS Queue is unencrypted.\n\n" + f"*Threat*: " + f"Based on data protection policies, data that is classified as sensitive information or " + f"intellectual property of the organization needs to be encrypted. Additionally, as part of the " + f"initiative of Encryption Everywhere, it is necessary to encrypt the data in order to ensure the " + f"confidentiality and integrity of the data.\n\n" + f"*Risk*: High\n\n" + f"*Account Name*: {account_name}\n" + f"*Account ID*: {account_id}\n" + f"*SQS queue url*: {queue_url}\n" + f"*SQS queue name*: {queue_name}\n" + f"*SQS queue region*: {queue_region}\n" + f"\n") + + auto_remediation_date = (self.config.now + self.config.sqsEncrypt.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 += f"\n" + issue_description += ( + f"*Recommendation*: " + f"Encrypt the Queue by enabling server-side encryption with AWS KMS-managed keys (SSE-KMS).") + + if self.config.whitelisting_procedure_url: + issue_description += (f"For any other exceptions, please follow the [whitelisting procedure|{self.config.whitelisting_procedure_url}] " + f"and provide a strong business reasoning. ") + + try: + response = jira.add_issue( + issue_summary=issue_summary, issue_description=issue_description, + priority="Major", labels=["sqs-unencrypted-queues"], + 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 = CreateSQSUnencryptedQueueIssueTickets(config) + obj.create_tickets_sqs_unencrypted_queues() + except Exception: + logging.exception("Failed to create unencrypted SQS queue tickets")