diff --git a/CLAUDE.md b/CLAUDE.md index 9108181..22b3ea4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -229,13 +229,15 @@ terraform/state/ ## Alert Routing ``` -EventBridge ──► javabin-security SNS ──► slack-alert Lambda ──► #javabin-infra-alerts -GuardDuty ──► Security Hub ──► SNS ──► slack-alert Lambda ──► #javabin-infra-alerts +EventBridge ──► javabin-security SNS ──► slack-alert Lambda: + Security Hub findings (NEW only) ──► #platform-security-alerts + GuardDuty findings ──► #platform-security-alerts + IAM / resource / login events ──► #javabin-infra-alerts Cost Anomaly ──► javabin-alerts SNS ──► slack-alert Lambda ──► #javabin-cost-alerts Scheduled: Monday 08:00 UTC ──► cost-report ──► #javabin-cost-alerts - Monday 08:00 UTC ──► securityhub-summary ──► #javabin-infra-alerts + Monday 08:00 UTC ──► securityhub-summary ──► #platform-security-alerts Daily 08:00 UTC ──► daily-cost-check ──► #javabin-cost-alerts (only on spikes) EventBridge (Create/Run) ──► compliance-reporter (report to Slack, no auto-fix) @@ -252,6 +254,7 @@ All parameters are in `eu-central-1`. Use `--profile javabin --region eu-central | Path | Type | Used By | |------|------|---------| | `/javabin/slack/platform-resource-alerts-webhook` | SecureString | slack-alert, compliance-reporter, platform-ci | +| `/javabin/slack/platform-security-alerts-webhook` | SecureString | slack-alert (Security Hub + GuardDuty), securityhub-summary | | `/javabin/slack/platform-cost-alerts-webhook` | String | slack-alert (cost), cost-report, daily-cost-check | | `/javabin/slack/platform-override-alerts-webhook` | SecureString | tf-apply (block notification), approve-override | | `/javabin/platform/google-admin-sa` | SecureString | team-provisioner (GCP SA JSON key, domain-wide delegation) | diff --git a/terraform/lambda-src/slack_alert/handler.py b/terraform/lambda-src/slack_alert/handler.py index d8d9d3e..3916086 100644 --- a/terraform/lambda-src/slack_alert/handler.py +++ b/terraform/lambda-src/slack_alert/handler.py @@ -21,6 +21,7 @@ # SSM parameter names passed via environment INFRA_WEBHOOK_PARAM = os.environ["INFRA_WEBHOOK_PARAM"] COST_WEBHOOK_PARAM = os.environ["COST_WEBHOOK_PARAM"] +SECURITY_WEBHOOK_PARAM = os.environ.get("SECURITY_WEBHOOK_PARAM", "") SECURITY_TOPIC_ARN = os.environ["SECURITY_TOPIC_ARN"] PROJECT_PREFIX = os.environ.get("PROJECT_PREFIX", "javabin") GITHUB_ORG_URL = os.environ.get("GITHUB_ORG_URL", "https://github.com/javaBin") @@ -932,6 +933,16 @@ def format_guardduty_finding(parsed): region = parsed.get("region", detail.get("region", "unknown")) account = parsed.get("account", detail.get("accountId", "unknown")) + # Dedup root credential usage — GuardDuty fires per API call (ConsoleLogin, + # Search, GetIdentityMetadata, etc.). One alert per hour is enough. + if finding_type == "Policy:IAMUser/RootCredentialUsage": + hour_key = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H") + dedup_key = f"guardduty:root:{hour_key}" + if is_finding_already_alerted(dedup_key): + logger.info("GuardDuty root credential finding suppressed (hourly dedup): %s", title) + return None + record_finding_alert(dedup_key) + # Suppress findings for resources recently managed by CI resource = detail.get("resource", {}) for s3_detail in resource.get("S3BucketDetails", []): @@ -1293,7 +1304,7 @@ def format_securityhub_summary(): def summary_handler(event, context): """Lambda handler for the weekly Security Hub summary.""" - webhook_url = _get_webhook(INFRA_WEBHOOK_PARAM) + webhook_url = _get_webhook(SECURITY_WEBHOOK_PARAM if SECURITY_WEBHOOK_PARAM else INFRA_WEBHOOK_PARAM) try: result = format_securityhub_summary() if result: @@ -1310,6 +1321,15 @@ def summary_handler(event, context): # --------------------------------------------------------------------------- # Main handler # --------------------------------------------------------------------------- +def _is_security_finding(parsed): + """Check if the event is a Security Hub or GuardDuty finding.""" + detail_type = parsed.get("detail-type", "") + return detail_type in ( + "Security Hub Findings - Imported", + "GuardDuty Finding", + ) + + def handler(event, context): for record in event["Records"]: sns_message = record["Sns"] @@ -1317,7 +1337,7 @@ def handler(event, context): subject = sns_message.get("Subject", "AWS Alert") raw_message = sns_message["Message"] - # Route to correct webhook based on SNS topic + # Default webhook based on SNS topic if topic_arn == SECURITY_TOPIC_ARN: webhook_url = _get_webhook(INFRA_WEBHOOK_PARAM) else: @@ -1325,6 +1345,11 @@ def handler(event, context): try: parsed = json.loads(raw_message) + # Route Security Hub + GuardDuty findings to dedicated security channel + if (topic_arn == SECURITY_TOPIC_ARN + and SECURITY_WEBHOOK_PARAM + and _is_security_finding(parsed)): + webhook_url = _get_webhook(SECURITY_WEBHOOK_PARAM) result = format_structured_alert(subject, parsed) except (json.JSONDecodeError, TypeError): result = format_plain_alert(subject, raw_message) diff --git a/terraform/platform/lambdas/main.tf b/terraform/platform/lambdas/main.tf index d4723f8..c900c83 100644 --- a/terraform/platform/lambdas/main.tf +++ b/terraform/platform/lambdas/main.tf @@ -773,13 +773,14 @@ resource "aws_lambda_function" "slack_alert" { environment { variables = { - INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook" - COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook" - SECURITY_TOPIC_ARN = var.security_topic_arn - PROJECT_PREFIX = var.project - GITHUB_ORG_URL = local.github_org_url - DEPLOY_REGION = var.region - DEDUP_TABLE_NAME = var.alert_dedup_table_name + INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook" + COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook" + SECURITY_WEBHOOK_PARAM = "/javabin/slack/platform-security-alerts-webhook" + SECURITY_TOPIC_ARN = var.security_topic_arn + PROJECT_PREFIX = var.project + GITHUB_ORG_URL = local.github_org_url + DEPLOY_REGION = var.region + DEDUP_TABLE_NAME = var.alert_dedup_table_name } } } @@ -1349,13 +1350,14 @@ resource "aws_lambda_function" "securityhub_summary" { environment { variables = { - INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook" - COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook" - SECURITY_TOPIC_ARN = var.security_topic_arn - PROJECT_PREFIX = var.project - GITHUB_ORG_URL = local.github_org_url - DEPLOY_REGION = var.region - DEDUP_TABLE_NAME = var.alert_dedup_table_name + INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook" + COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook" + SECURITY_WEBHOOK_PARAM = "/javabin/slack/platform-security-alerts-webhook" + SECURITY_TOPIC_ARN = var.security_topic_arn + PROJECT_PREFIX = var.project + GITHUB_ORG_URL = local.github_org_url + DEPLOY_REGION = var.region + DEDUP_TABLE_NAME = var.alert_dedup_table_name } } } diff --git a/terraform/platform/monitoring/main.tf b/terraform/platform/monitoring/main.tf index 5638691..4b527f8 100644 --- a/terraform/platform/monitoring/main.tf +++ b/terraform/platform/monitoring/main.tf @@ -464,6 +464,9 @@ resource "aws_cloudwatch_event_rule" "securityhub_findings" { Severity = { Label = ["HIGH", "CRITICAL"] } + Workflow = { + Status = ["NEW"] + } } } })