Skip to content

Commit c96d211

Browse files
Route security findings to dedicated Slack channel (#122)
## Summary - Routes Security Hub and GuardDuty findings to new `#platform-security-alerts` Slack channel (webhook stored in SSM as SecureString) - Adds `Workflow.Status = NEW` filter to EventBridge rule so suppressed findings no longer trigger Lambda invocations on re-import - Deduplicates GuardDuty root credential usage alerts (one per hour instead of one per API call) - 50 known/accepted findings suppressed directly in Security Hub (S3 public access for website buckets, Elastic Beanstalk legacy apps, default VPC security groups, SSM public sharing) — active count reduced from 80 to 29 ## Changes - `handler.py` — read new `SECURITY_WEBHOOK_PARAM`, route security findings to it, hourly dedup for root credential GuardDuty findings - `lambdas/main.tf` — add `SECURITY_WEBHOOK_PARAM` env var to `slack-alert` and `securityhub-summary` Lambdas - `monitoring/main.tf` — add `Workflow.Status = ["NEW"]` to Security Hub EventBridge rule pattern - `CLAUDE.md` — document new SSM parameter and updated alert routing ## Test plan - [ ] Verify `terraform plan` shows only expected changes (Lambda env vars + EventBridge rule pattern) - [ ] After apply, confirm Security Hub findings post to `#platform-security-alerts` - [ ] Confirm IAM/resource/login events still post to `#javabin-infra-alerts` - [ ] Confirm suppressed findings do not trigger new alerts on next Security Hub re-evaluation cycle
1 parent 2e46def commit c96d211

4 files changed

Lines changed: 52 additions & 19 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,15 @@ terraform/state/
229229
## Alert Routing
230230

231231
```
232-
EventBridge ──► javabin-security SNS ──► slack-alert Lambda ──► #javabin-infra-alerts
233-
GuardDuty ──► Security Hub ──► SNS ──► slack-alert Lambda ──► #javabin-infra-alerts
232+
EventBridge ──► javabin-security SNS ──► slack-alert Lambda:
233+
Security Hub findings (NEW only) ──► #platform-security-alerts
234+
GuardDuty findings ──► #platform-security-alerts
235+
IAM / resource / login events ──► #javabin-infra-alerts
234236
Cost Anomaly ──► javabin-alerts SNS ──► slack-alert Lambda ──► #javabin-cost-alerts
235237
236238
Scheduled:
237239
Monday 08:00 UTC ──► cost-report ──► #javabin-cost-alerts
238-
Monday 08:00 UTC ──► securityhub-summary ──► #javabin-infra-alerts
240+
Monday 08:00 UTC ──► securityhub-summary ──► #platform-security-alerts
239241
Daily 08:00 UTC ──► daily-cost-check ──► #javabin-cost-alerts (only on spikes)
240242
241243
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
252254
| Path | Type | Used By |
253255
|------|------|---------|
254256
| `/javabin/slack/platform-resource-alerts-webhook` | SecureString | slack-alert, compliance-reporter, platform-ci |
257+
| `/javabin/slack/platform-security-alerts-webhook` | SecureString | slack-alert (Security Hub + GuardDuty), securityhub-summary |
255258
| `/javabin/slack/platform-cost-alerts-webhook` | String | slack-alert (cost), cost-report, daily-cost-check |
256259
| `/javabin/slack/platform-override-alerts-webhook` | SecureString | tf-apply (block notification), approve-override |
257260
| `/javabin/platform/google-admin-sa` | SecureString | team-provisioner (GCP SA JSON key, domain-wide delegation) |

terraform/lambda-src/slack_alert/handler.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# SSM parameter names passed via environment
2222
INFRA_WEBHOOK_PARAM = os.environ["INFRA_WEBHOOK_PARAM"]
2323
COST_WEBHOOK_PARAM = os.environ["COST_WEBHOOK_PARAM"]
24+
SECURITY_WEBHOOK_PARAM = os.environ.get("SECURITY_WEBHOOK_PARAM", "")
2425
SECURITY_TOPIC_ARN = os.environ["SECURITY_TOPIC_ARN"]
2526
PROJECT_PREFIX = os.environ.get("PROJECT_PREFIX", "javabin")
2627
GITHUB_ORG_URL = os.environ.get("GITHUB_ORG_URL", "https://github.com/javaBin")
@@ -932,6 +933,16 @@ def format_guardduty_finding(parsed):
932933
region = parsed.get("region", detail.get("region", "unknown"))
933934
account = parsed.get("account", detail.get("accountId", "unknown"))
934935

936+
# Dedup root credential usage — GuardDuty fires per API call (ConsoleLogin,
937+
# Search, GetIdentityMetadata, etc.). One alert per hour is enough.
938+
if finding_type == "Policy:IAMUser/RootCredentialUsage":
939+
hour_key = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H")
940+
dedup_key = f"guardduty:root:{hour_key}"
941+
if is_finding_already_alerted(dedup_key):
942+
logger.info("GuardDuty root credential finding suppressed (hourly dedup): %s", title)
943+
return None
944+
record_finding_alert(dedup_key)
945+
935946
# Suppress findings for resources recently managed by CI
936947
resource = detail.get("resource", {})
937948
for s3_detail in resource.get("S3BucketDetails", []):
@@ -1293,7 +1304,7 @@ def format_securityhub_summary():
12931304

12941305
def summary_handler(event, context):
12951306
"""Lambda handler for the weekly Security Hub summary."""
1296-
webhook_url = _get_webhook(INFRA_WEBHOOK_PARAM)
1307+
webhook_url = _get_webhook(SECURITY_WEBHOOK_PARAM if SECURITY_WEBHOOK_PARAM else INFRA_WEBHOOK_PARAM)
12971308
try:
12981309
result = format_securityhub_summary()
12991310
if result:
@@ -1310,21 +1321,35 @@ def summary_handler(event, context):
13101321
# ---------------------------------------------------------------------------
13111322
# Main handler
13121323
# ---------------------------------------------------------------------------
1324+
def _is_security_finding(parsed):
1325+
"""Check if the event is a Security Hub or GuardDuty finding."""
1326+
detail_type = parsed.get("detail-type", "")
1327+
return detail_type in (
1328+
"Security Hub Findings - Imported",
1329+
"GuardDuty Finding",
1330+
)
1331+
1332+
13131333
def handler(event, context):
13141334
for record in event["Records"]:
13151335
sns_message = record["Sns"]
13161336
topic_arn = sns_message.get("TopicArn", "")
13171337
subject = sns_message.get("Subject", "AWS Alert")
13181338
raw_message = sns_message["Message"]
13191339

1320-
# Route to correct webhook based on SNS topic
1340+
# Default webhook based on SNS topic
13211341
if topic_arn == SECURITY_TOPIC_ARN:
13221342
webhook_url = _get_webhook(INFRA_WEBHOOK_PARAM)
13231343
else:
13241344
webhook_url = _get_webhook(COST_WEBHOOK_PARAM)
13251345

13261346
try:
13271347
parsed = json.loads(raw_message)
1348+
# Route Security Hub + GuardDuty findings to dedicated security channel
1349+
if (topic_arn == SECURITY_TOPIC_ARN
1350+
and SECURITY_WEBHOOK_PARAM
1351+
and _is_security_finding(parsed)):
1352+
webhook_url = _get_webhook(SECURITY_WEBHOOK_PARAM)
13281353
result = format_structured_alert(subject, parsed)
13291354
except (json.JSONDecodeError, TypeError):
13301355
result = format_plain_alert(subject, raw_message)

terraform/platform/lambdas/main.tf

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -773,13 +773,14 @@ resource "aws_lambda_function" "slack_alert" {
773773

774774
environment {
775775
variables = {
776-
INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook"
777-
COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook"
778-
SECURITY_TOPIC_ARN = var.security_topic_arn
779-
PROJECT_PREFIX = var.project
780-
GITHUB_ORG_URL = local.github_org_url
781-
DEPLOY_REGION = var.region
782-
DEDUP_TABLE_NAME = var.alert_dedup_table_name
776+
INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook"
777+
COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook"
778+
SECURITY_WEBHOOK_PARAM = "/javabin/slack/platform-security-alerts-webhook"
779+
SECURITY_TOPIC_ARN = var.security_topic_arn
780+
PROJECT_PREFIX = var.project
781+
GITHUB_ORG_URL = local.github_org_url
782+
DEPLOY_REGION = var.region
783+
DEDUP_TABLE_NAME = var.alert_dedup_table_name
783784
}
784785
}
785786
}
@@ -1349,13 +1350,14 @@ resource "aws_lambda_function" "securityhub_summary" {
13491350

13501351
environment {
13511352
variables = {
1352-
INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook"
1353-
COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook"
1354-
SECURITY_TOPIC_ARN = var.security_topic_arn
1355-
PROJECT_PREFIX = var.project
1356-
GITHUB_ORG_URL = local.github_org_url
1357-
DEPLOY_REGION = var.region
1358-
DEDUP_TABLE_NAME = var.alert_dedup_table_name
1353+
INFRA_WEBHOOK_PARAM = "/javabin/slack/platform-resource-alerts-webhook"
1354+
COST_WEBHOOK_PARAM = "/javabin/slack/platform-cost-alerts-webhook"
1355+
SECURITY_WEBHOOK_PARAM = "/javabin/slack/platform-security-alerts-webhook"
1356+
SECURITY_TOPIC_ARN = var.security_topic_arn
1357+
PROJECT_PREFIX = var.project
1358+
GITHUB_ORG_URL = local.github_org_url
1359+
DEPLOY_REGION = var.region
1360+
DEDUP_TABLE_NAME = var.alert_dedup_table_name
13591361
}
13601362
}
13611363
}

terraform/platform/monitoring/main.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,9 @@ resource "aws_cloudwatch_event_rule" "securityhub_findings" {
464464
Severity = {
465465
Label = ["HIGH", "CRITICAL"]
466466
}
467+
Workflow = {
468+
Status = ["NEW"]
469+
}
467470
}
468471
}
469472
})

0 commit comments

Comments
 (0)