Skip to content

Commit 28cd770

Browse files
committed
feat: add CloudTrail security alerts resource type (CIS CloudWatch.1-14)
New resource type 'aws-cloudtrail-security-alerts' creates CloudWatch metric filters and alarms aligned with AWS Security Hub/CIS controls. 14 alerts covering the full CIS CloudWatch control set: - CloudWatch.1: Root account usage - CloudWatch.2: Unauthorized API calls (AccessDenied/UnauthorizedAccess) - CloudWatch.3: Console login without MFA (successful only) - CloudWatch.4: IAM policy changes (incl group/user/version operations) - CloudWatch.5: CloudTrail config changes (incl Create/Start/Stop) - CloudWatch.6: Failed console logins - CloudWatch.7: KMS key deletion/disable - CloudWatch.8: S3 bucket policy changes (incl CORS/lifecycle/replication) - CloudWatch.9: AWS Config recorder/delivery channel changes - CloudWatch.10: Security group changes - CloudWatch.11: NACL changes - CloudWatch.12: Network gateway changes - CloudWatch.13: Route table changes - CloudWatch.14: VPC changes Covers: SOC2 CC6.1/CC6.5/CC6.6/CC7.1/CC7.2/CC7.3 ISO 27001 A.5.16/A.5.18/A.8.2/A.8.5/A.8.15/A.8.16/A.8.20/A.8.22/A.8.24/A.8.32 NIST 800-53 AU-6, AC-2, AC-6, SI-4
1 parent 53aa8c4 commit 28cd770

8 files changed

Lines changed: 794 additions & 1 deletion

File tree

docs/docs/reference/supported-resources.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,58 @@ When this resource is used in a client stack via the `uses` section, Simple Cont
345345

346346
📖 **For complete details on environment variables and template placeholders, see:** [Template Placeholders Advanced - AWS RDS MySQL](../concepts/template-placeholders-advanced.md#rds-mysql)
347347

348+
#### **CloudTrail Security Alerts** (`aws-cloudtrail-security-alerts`)
349+
350+
Creates CloudWatch metric filters and alarms for security-relevant CloudTrail events, aligned with the AWS Security Hub/CIS CloudWatch controls (CloudWatch.1 through CloudWatch.14).
351+
352+
**Golang Struct Reference:** `pkg/clouds/aws/cloudtrail_security_alerts.go:CloudTrailSecurityAlertsConfig`
353+
354+
```yaml
355+
# server.yaml - Parent Stack
356+
resources:
357+
resources:
358+
production:
359+
resources:
360+
cloudtrail-security:
361+
type: aws-cloudtrail-security-alerts
362+
config:
363+
# AWS account configuration (inherited from AccountConfig)
364+
credentials: "${auth:aws-us}"
365+
account: "${auth:aws-us.projectId}"
366+
367+
# CloudTrail log group (required)
368+
logGroupName: "aws-cloudtrail-logs-s3-buckets"
369+
logGroupRegion: "us-west-2" # Optional: if different from default region
370+
371+
# Notification channels
372+
email:
373+
addresses:
374+
- security@company.com
375+
# slack:
376+
# webhookUrl: "${secret:security-slack-webhook}" # TODO: not yet wired
377+
378+
# Alert selectors (all default to false)
379+
alerts:
380+
rootAccountUsage: true # CIS CloudWatch.1
381+
unauthorizedApiCalls: true # CIS CloudWatch.2 (threshold: 5)
382+
consoleLoginWithoutMfa: true # CIS CloudWatch.3
383+
iamPolicyChanges: true # CIS CloudWatch.4
384+
cloudTrailTampering: true # CIS CloudWatch.5
385+
failedConsoleLogins: true # CIS CloudWatch.6
386+
kmsKeyDeletion: true # CIS CloudWatch.7
387+
s3BucketPolicyChanges: true # CIS CloudWatch.8
388+
configChanges: true # CIS CloudWatch.9
389+
securityGroupChanges: true # CIS CloudWatch.10
390+
naclChanges: true # CIS CloudWatch.11
391+
networkGatewayChanges: true # CIS CloudWatch.12
392+
routeTableChanges: true # CIS CloudWatch.13
393+
vpcChanges: true # CIS CloudWatch.14
394+
```
395+
396+
**Compliance:** SOC 2 (CC6/CC7), ISO 27001:2022 (A.5/A.8), NIST 800-53 (AU-6, AC-2, SI-4)
397+
398+
**Note:** This resource is provisioned once per AWS account (not per service). It monitors the CloudTrail log group and does not require `uses` in client stacks.
399+
348400
### **Authentication** (`AuthType` → `auth` section in `secrets.yaml`)
349401

350402
#### **AWS Token Authentication** (`aws-token`)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# CloudTrail Security Alerts Implementation Summary
2+
3+
## Implementation Complete
4+
5+
New resource type `aws-cloudtrail-security-alerts` that creates CloudWatch metric filters and alarms for security-relevant CloudTrail events. Covers the full AWS Security Hub/CIS CloudWatch control set (CloudWatch.1-14).
6+
7+
## What Was Implemented
8+
9+
### New Resource Type
10+
11+
Users can define security alerts in their `server.yaml`:
12+
13+
```yaml
14+
resources:
15+
resources:
16+
prod:
17+
resources:
18+
cloudtrail-security:
19+
type: aws-cloudtrail-security-alerts
20+
config:
21+
logGroupName: aws-cloudtrail-logs-s3-buckets
22+
logGroupRegion: us-west-2
23+
email:
24+
addresses:
25+
- security@company.com
26+
alerts:
27+
rootAccountUsage: true
28+
unauthorizedApiCalls: true
29+
consoleLoginWithoutMfa: true
30+
iamPolicyChanges: true
31+
cloudTrailTampering: true
32+
failedConsoleLogins: true
33+
kmsKeyDeletion: true
34+
s3BucketPolicyChanges: true
35+
configChanges: true
36+
securityGroupChanges: true
37+
naclChanges: true
38+
networkGatewayChanges: true
39+
routeTableChanges: true
40+
vpcChanges: true
41+
```
42+
43+
### 14 CIS-Aligned Alerts
44+
45+
| CIS Control | Alert | Default Threshold |
46+
|-------------|-------|-------------------|
47+
| CloudWatch.1 | Root account usage | >= 1 / 5min |
48+
| CloudWatch.2 | Unauthorized API calls | >= 5 / 5min |
49+
| CloudWatch.3 | Console login without MFA (success only) | >= 1 / 5min |
50+
| CloudWatch.4 | IAM policy changes | >= 1 / 5min |
51+
| CloudWatch.5 | CloudTrail config changes | >= 1 / 5min |
52+
| CloudWatch.6 | Failed console logins | >= 1 / 5min |
53+
| CloudWatch.7 | KMS key deletion/disable | >= 1 / 5min |
54+
| CloudWatch.8 | S3 bucket policy changes | >= 1 / 5min |
55+
| CloudWatch.9 | AWS Config changes | >= 1 / 5min |
56+
| CloudWatch.10 | Security group changes | >= 1 / 5min |
57+
| CloudWatch.11 | NACL changes | >= 1 / 5min |
58+
| CloudWatch.12 | Network gateway changes | >= 1 / 5min |
59+
| CloudWatch.13 | Route table changes | >= 1 / 5min |
60+
| CloudWatch.14 | VPC changes | >= 1 / 5min |
61+
62+
### Code Changes Summary
63+
64+
| File | Change | Lines |
65+
|------|--------|-------|
66+
| `pkg/clouds/aws/cloudtrail_security_alerts.go` | Config struct, selectors, reader | +41 |
67+
| `pkg/clouds/aws/cloudtrail_security_alerts_test.go` | Config parsing tests | +151 |
68+
| `pkg/clouds/aws/init.go` | Register config reader | +3 |
69+
| `pkg/clouds/pulumi/aws/cloudtrail_security_alerts.go` | Pulumi provisioner (metric filters + alarms) | +310 |
70+
| `pkg/clouds/pulumi/aws/cloudtrail_security_alerts_test.go` | Alert selection + definition tests | +120 |
71+
| `pkg/clouds/pulumi/aws/init.go` | Register provisioner | +2 |
72+
73+
**Total:** 6 files, ~627 lines
74+
75+
## Architecture
76+
77+
Each enabled alert creates:
78+
1. A CloudWatch LogMetricFilter on the specified CloudTrail log group
79+
2. A CloudWatch MetricAlarm (Sum >= threshold in 5 min period)
80+
3. Optional SNS topic + email subscriptions for notifications
81+
82+
### Cross-Region Support
83+
84+
CloudTrail log groups may be in a different region than the main SC deployment. When `logGroupRegion` is specified, a region-specific AWS provider is created with credentials from `AccountConfig`.
85+
86+
### Resource Naming
87+
88+
Resource names are derived from the descriptor name (not hardcoded), supporting multiple instances per stack.
89+
90+
### Notification Channels
91+
92+
- Email: via SNS topic (implemented)
93+
- Slack/Discord/Telegram: via SC alert Lambda (TODO — requires wiring alarm actions to existing Lambda infrastructure)
94+
95+
## Compliance Coverage
96+
97+
- SOC 2: CC6.1, CC6.5, CC6.6, CC7.1, CC7.2, CC7.3
98+
- ISO 27001:2022: A.5.16, A.5.18, A.8.2, A.8.5, A.8.15, A.8.16, A.8.20, A.8.22, A.8.24, A.8.32
99+
- NIST 800-53: AU-6, AC-2, AC-6, SI-4
100+
- AWS CIS Benchmark: CloudWatch.1 through CloudWatch.14
101+
102+
## Testing Results
103+
104+
All tests passing (config parsing, alert selection, deterministic ordering, definition validation, unique names).
105+
106+
## Review History
107+
108+
- Self-review: 3 rounds
109+
- OpenAI Codex code review: fixed credential pass-through, naming collisions, MFA filter scope
110+
- OpenAI Codex compliance gap analysis: expanded 7 -> 14 alerts, corrected filter patterns to match CIS reference
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package aws
2+
3+
import "github.com/simple-container-com/api/pkg/api"
4+
5+
const ResourceTypeCloudTrailSecurityAlerts = "aws-cloudtrail-security-alerts"
6+
7+
// CloudTrailSecurityAlertsConfig defines CloudWatch metric filters and alarms
8+
// for security-relevant CloudTrail events (SOC2 CC6.1/CC7.1, ISO 27001 A.8.2/A.8.15/A.8.16).
9+
type CloudTrailSecurityAlertsConfig struct {
10+
AccountConfig `json:",inline" yaml:",inline"`
11+
LogGroupName string `json:"logGroupName" yaml:"logGroupName"`
12+
LogGroupRegion string `json:"logGroupRegion,omitempty" yaml:"logGroupRegion,omitempty"`
13+
Slack *api.SlackCfg `json:"slack,omitempty" yaml:"slack,omitempty"`
14+
Discord *api.DiscordCfg `json:"discord,omitempty" yaml:"discord,omitempty"`
15+
Telegram *api.TelegramCfg `json:"telegram,omitempty" yaml:"telegram,omitempty"`
16+
Email *api.EmailCfg `json:"email,omitempty" yaml:"email,omitempty"`
17+
Alerts CloudTrailAlertSelectors `json:"alerts" yaml:"alerts"`
18+
}
19+
20+
// CloudTrailAlertSelectors controls which security alerts are enabled.
21+
// Each field maps to a CloudWatch metric filter + alarm on the CloudTrail log group.
22+
// Alert names reference AWS Security Hub/CIS CloudWatch controls (CloudWatch.1-14).
23+
type CloudTrailAlertSelectors struct {
24+
RootAccountUsage bool `json:"rootAccountUsage,omitempty" yaml:"rootAccountUsage,omitempty"` // CIS CloudWatch.1
25+
UnauthorizedApiCalls bool `json:"unauthorizedApiCalls,omitempty" yaml:"unauthorizedApiCalls,omitempty"` // CIS CloudWatch.2
26+
ConsoleLoginWithoutMfa bool `json:"consoleLoginWithoutMfa,omitempty" yaml:"consoleLoginWithoutMfa,omitempty"` // CIS CloudWatch.3
27+
IamPolicyChanges bool `json:"iamPolicyChanges,omitempty" yaml:"iamPolicyChanges,omitempty"` // CIS CloudWatch.4
28+
CloudTrailTampering bool `json:"cloudTrailTampering,omitempty" yaml:"cloudTrailTampering,omitempty"` // CIS CloudWatch.5
29+
FailedConsoleLogins bool `json:"failedConsoleLogins,omitempty" yaml:"failedConsoleLogins,omitempty"` // CIS CloudWatch.6
30+
KmsKeyDeletion bool `json:"kmsKeyDeletion,omitempty" yaml:"kmsKeyDeletion,omitempty"` // CIS CloudWatch.7
31+
S3BucketPolicyChanges bool `json:"s3BucketPolicyChanges,omitempty" yaml:"s3BucketPolicyChanges,omitempty"` // CIS CloudWatch.8
32+
ConfigChanges bool `json:"configChanges,omitempty" yaml:"configChanges,omitempty"` // CIS CloudWatch.9
33+
SecurityGroupChanges bool `json:"securityGroupChanges,omitempty" yaml:"securityGroupChanges,omitempty"` // CIS CloudWatch.10
34+
NaclChanges bool `json:"naclChanges,omitempty" yaml:"naclChanges,omitempty"` // CIS CloudWatch.11
35+
NetworkGatewayChanges bool `json:"networkGatewayChanges,omitempty" yaml:"networkGatewayChanges,omitempty"` // CIS CloudWatch.12
36+
RouteTableChanges bool `json:"routeTableChanges,omitempty" yaml:"routeTableChanges,omitempty"` // CIS CloudWatch.13
37+
VpcChanges bool `json:"vpcChanges,omitempty" yaml:"vpcChanges,omitempty"` // CIS CloudWatch.14
38+
}
39+
40+
func ReadCloudTrailSecurityAlertsConfig(config *api.Config) (api.Config, error) {
41+
return api.ConvertConfig(config, &CloudTrailSecurityAlertsConfig{})
42+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package aws
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/gomega"
7+
8+
"github.com/simple-container-com/api/pkg/api"
9+
)
10+
11+
func TestReadCloudTrailSecurityAlertsConfig(t *testing.T) {
12+
RegisterTestingT(t)
13+
14+
tests := []struct {
15+
name string
16+
config *api.Config
17+
wantErr bool
18+
}{
19+
{
20+
name: "valid config with all CIS alerts enabled",
21+
config: &api.Config{
22+
Config: map[string]any{
23+
"logGroupName": "aws-cloudtrail-logs-s3-buckets",
24+
"logGroupRegion": "us-west-2",
25+
"slack": map[string]any{
26+
"webhookUrl": "https://hooks.slack.com/services/xxx",
27+
},
28+
"alerts": map[string]any{
29+
"rootAccountUsage": true,
30+
"unauthorizedApiCalls": true,
31+
"consoleLoginWithoutMfa": true,
32+
"iamPolicyChanges": true,
33+
"cloudTrailTampering": true,
34+
"failedConsoleLogins": true,
35+
"kmsKeyDeletion": true,
36+
"s3BucketPolicyChanges": true,
37+
"configChanges": true,
38+
"securityGroupChanges": true,
39+
"naclChanges": true,
40+
"networkGatewayChanges": true,
41+
"routeTableChanges": true,
42+
"vpcChanges": true,
43+
},
44+
},
45+
},
46+
wantErr: false,
47+
},
48+
{
49+
name: "valid config with partial alerts",
50+
config: &api.Config{
51+
Config: map[string]any{
52+
"logGroupName": "my-trail-logs",
53+
"alerts": map[string]any{
54+
"rootAccountUsage": true,
55+
"cloudTrailTampering": true,
56+
},
57+
},
58+
},
59+
wantErr: false,
60+
},
61+
{
62+
name: "valid config with no alerts",
63+
config: &api.Config{
64+
Config: map[string]any{
65+
"logGroupName": "my-trail-logs",
66+
"alerts": map[string]any{},
67+
},
68+
},
69+
wantErr: false,
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
RegisterTestingT(t)
76+
result, err := ReadCloudTrailSecurityAlertsConfig(tt.config)
77+
if tt.wantErr {
78+
Expect(err).ToNot(BeNil())
79+
return
80+
}
81+
Expect(err).To(BeNil())
82+
cfg, ok := result.Config.(*CloudTrailSecurityAlertsConfig)
83+
Expect(ok).To(BeTrue())
84+
Expect(cfg).ToNot(BeNil())
85+
})
86+
}
87+
}
88+
89+
func TestReadCloudTrailSecurityAlertsConfig_FieldValues(t *testing.T) {
90+
RegisterTestingT(t)
91+
92+
config := &api.Config{
93+
Config: map[string]any{
94+
"logGroupName": "aws-cloudtrail-logs-s3-buckets",
95+
"logGroupRegion": "us-west-2",
96+
"slack": map[string]any{
97+
"webhookUrl": "https://hooks.slack.com/services/xxx",
98+
},
99+
"email": map[string]any{
100+
"addresses": []any{"security@example.com", "ops@example.com"},
101+
},
102+
"alerts": map[string]any{
103+
"rootAccountUsage": true,
104+
"unauthorizedApiCalls": false,
105+
"consoleLoginWithoutMfa": true,
106+
"iamPolicyChanges": true,
107+
"cloudTrailTampering": true,
108+
"failedConsoleLogins": true,
109+
"kmsKeyDeletion": true,
110+
"s3BucketPolicyChanges": false,
111+
"configChanges": true,
112+
"securityGroupChanges": true,
113+
"naclChanges": true,
114+
"networkGatewayChanges": false,
115+
"routeTableChanges": false,
116+
"vpcChanges": true,
117+
},
118+
},
119+
}
120+
121+
result, err := ReadCloudTrailSecurityAlertsConfig(config)
122+
Expect(err).To(BeNil())
123+
124+
cfg := result.Config.(*CloudTrailSecurityAlertsConfig)
125+
Expect(cfg.LogGroupName).To(Equal("aws-cloudtrail-logs-s3-buckets"))
126+
Expect(cfg.LogGroupRegion).To(Equal("us-west-2"))
127+
Expect(cfg.Slack).ToNot(BeNil())
128+
Expect(cfg.Slack.WebhookUrl).To(Equal("https://hooks.slack.com/services/xxx"))
129+
Expect(cfg.Email).ToNot(BeNil())
130+
Expect(cfg.Email.Addresses).To(HaveLen(2))
131+
132+
Expect(cfg.Alerts.RootAccountUsage).To(BeTrue())
133+
Expect(cfg.Alerts.UnauthorizedApiCalls).To(BeFalse())
134+
Expect(cfg.Alerts.ConsoleLoginWithoutMfa).To(BeTrue())
135+
Expect(cfg.Alerts.IamPolicyChanges).To(BeTrue())
136+
Expect(cfg.Alerts.CloudTrailTampering).To(BeTrue())
137+
Expect(cfg.Alerts.FailedConsoleLogins).To(BeTrue())
138+
Expect(cfg.Alerts.KmsKeyDeletion).To(BeTrue())
139+
Expect(cfg.Alerts.S3BucketPolicyChanges).To(BeFalse())
140+
Expect(cfg.Alerts.ConfigChanges).To(BeTrue())
141+
Expect(cfg.Alerts.SecurityGroupChanges).To(BeTrue())
142+
Expect(cfg.Alerts.NaclChanges).To(BeTrue())
143+
Expect(cfg.Alerts.NetworkGatewayChanges).To(BeFalse())
144+
Expect(cfg.Alerts.RouteTableChanges).To(BeFalse())
145+
Expect(cfg.Alerts.VpcChanges).To(BeTrue())
146+
}

pkg/clouds/aws/init.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ func init() {
2525
// rds
2626
ResourceTypeRdsPostgres: ReadRdsPostgresConfig,
2727
ResourceTypeRdsMysql: ReadRdsMysqlConfig,
28+
29+
// security
30+
ResourceTypeCloudTrailSecurityAlerts: ReadCloudTrailSecurityAlertsConfig,
2831
})
2932

3033
api.RegisterProvisionerFieldConfig(api.ProvisionerFieldConfigRegister{

0 commit comments

Comments
 (0)