feat: harden CIS 4.1 detection depth with M-of-N alarm tuning and CloudTrail Insights (#38)

Switch UnauthorizedApiCalls alarm from 3/3 consecutive to 3/6 M-of-N
so a single quiet 5-min window can't reset detection. The 3/3 setting
pre-dates #36 and was sized to suppress CFN/Config noise that #36 now
removes at the filter level, making a wider M-of-N evaluation window
safe from flap risk.

Enable CloudTrail Insights (ApiCallRateInsight + ApiErrorRateInsight)
on seahaven-org-trail as a compensating control for the residual risk
accepted in #36 — the CFN/Config-proxied denials intentionally excluded
from CIS 4.1 — and as a backstop for low-and-slow patterns the 5-min
alarm may miss. Cost ≈$35–$53/month at current org trail volume.

Refs: #37

Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
This commit is contained in:
seahaven-openswe[bot] 2026-07-07 15:47:41 -04:00 • committed by GitHub
parent 0d654edb35
commit 142e221c47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 22 additions and 6 deletions

View file

@ -30,7 +30,7 @@ The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This pr
| Resource | Logical ID | Notes |
|---|---|---|
| Multi-region trail | `Trail` (`seahaven-org-trail`) | Management events read+write, global service events, **log-file validation on** |
| Multi-region trail | `Trail` (`seahaven-org-trail`) | Management events read+write, global service events, **log-file validation on**, **CloudTrail Insights on** (ApiCallRate + ApiErrorRate, §37) |
| Log bucket | `TrailLogBucket` (`seahaven-cloudtrail-logs-328440206208`) | Private (Block Public Access all), SSE-KMS, versioned, **TLS-only**, **Object Lock GOVERNANCE 365d**, lifecycle (Glacier @90d, expire @365d), server access logging → `seahaven-s3-access-logs` |
| KMS CMK | `TrailKey` (`alias/cloudtrail-logs`) | Encrypts log files; **automatic rotation enabled** |
| CloudWatch Logs group | created by the L2 `Trail` | 365-day retention; this is the group the CIS Section 4 metric filters (H-1) attach to |

View file

@ -208,6 +208,19 @@ export class AccountBaselineStack extends cdk.Stack {
sendToCloudWatchLogs: true,
cloudWatchLogGroup: trailLogGroup,
managementEvents: cloudtrail.ReadWriteType.ALL,
// CloudTrail Insights (§37): compensating control for the residual risk
// accepted in #36 (CFN/Config-proxied denials excluded from CIS 4.1) and
// the low-and-slow evasion surface in the UnauthorizedApiCalls alarm.
// ApiCallRateInsight flags anomalous write-API spikes; ApiErrorRateInsight
// flags anomalous errored/denied call rates — including the denials CIS
// 4.1 intentionally filters out. Per-event cost (≈$0.35/100k management
// events); an org trail with 10–15M management events/month adds roughly
// $35–$53/month. CIS 4.1 alarm + GuardDuty + Security Hub (CIS v3.0) are
// already live, so this is defence-in-depth, not an urgent gap-fill.
insightTypes: [
cloudtrail.InsightType.API_CALL_RATE,
cloudtrail.InsightType.API_ERROR_RATE,
],
});
// ── Day 1 detective layer + governance toggles ──

View file

@ -72,11 +72,14 @@ const CIS_CONTROLS: CisControl[] = [
pattern:
'{ (($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied*")) && ($.sourceIPAddress != "*cloudformation.amazonaws.com") && ($.sourceIPAddress != "config.amazonaws.com") && ($.sourceIPAddress != "delivery.logs.amazonaws.com") && ($.eventName != "HeadBucket") }',
description: "CIS 4.1 — unauthorized API calls",
// Belt-and-suspenders after the service-noise exclusion above: still require
// 3 consecutive breaching 5-min periods so a one-off human fat-finger that
// self-recovers doesn't page, while sustained unauthorized activity (e.g. a
// misconfigured role failing every call) still trips within ~15 minutes.
evaluationPeriods: 3,
// M-of-N with N > M so a single quiet 5-min window cannot reset detection.
// Before #36, false positives from CFN/Config-proxied denials forced a
// conservative 3/3 consecutive; now that #36 scoped the metric-filter
// exclusion to those benign sources, we can widen the evaluation window
// while keeping the same alarm threshold. An attacker pacing denied calls to
// leave every third window empty would evade a 3/3 alarm but still trips a
// 3/6 — three breaching windows in any rolling 30-min span fire the alarm.
evaluationPeriods: 6,
datapointsToAlarm: 3,
},
{