-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathbuildspec.yml
More file actions
423 lines (382 loc) · 21 KB
/
buildspec.yml
File metadata and controls
423 lines (382 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
version: 0.2
phases:
install:
runtime-versions:
python: 3.12
commands:
- echo "Installing dependencies..."
- |
if ! pip install -q --upgrade pip; then
echo "ERROR: Failed to upgrade pip"
exit 1
fi
- |
if ! pip install -q --upgrade aws-sam-cli; then
echo "ERROR: Failed to install aws-sam-cli"
exit 1
fi
- |
# Verify jq is available (used for parsing JSON credentials)
if ! command -v jq &> /dev/null; then
echo "ERROR: jq is not installed"
exit 1
fi
build:
commands:
- echo "Starting AIML Security Assessment"
- |
# Validate required environment variables
missing_vars=()
[[ -z "$AWS_DEFAULT_REGION" ]] && missing_vars+=("AWS_DEFAULT_REGION")
if [[ $MULTI_ACCOUNT_SCAN == 'true' ]]; then
[[ -z "$BUCKET_REPORT" ]] && missing_vars+=("BUCKET_REPORT")
[[ -z "$AWS_PARTITION" ]] && missing_vars+=("AWS_PARTITION")
[[ -z "$AWS_ACCOUNT_ID" ]] && missing_vars+=("AWS_ACCOUNT_ID")
[[ -z "$MEMBER_ROLE" ]] && missing_vars+=("MEMBER_ROLE")
fi
if [[ ${#missing_vars[@]} -gt 0 ]]; then
echo "ERROR: Missing required environment variables: ${missing_vars[*]}"
exit 1
fi
- echo "Multi-account scan is $MULTI_ACCOUNT_SCAN"
- cd aiml-security-assessment
- ls -la
- |
if [[ $MULTI_ACCOUNT_SCAN = 'true' ]]; then
echo "Building multi-account template"
if ! sam build --template template-multi-account.yaml; then
echo "ERROR: SAM build failed for multi-account template"
exit 1
fi
else
echo "Building single-account template"
if ! sam build --template template.yaml; then
echo "ERROR: SAM build failed for single-account template"
exit 1
fi
fi
- echo "Build completed, checking build directory:"
- ls -la .aws-sam/build/
- |
if [[ $MULTI_ACCOUNT_SCAN = 'true' ]]; then
echo "Getting list of accounts to scan"
if [[ $MULTI_ACCOUNT_LIST_OVERRIDE != '' ]]; then
echo "Using account overrides."
account_list=$MULTI_ACCOUNT_LIST_OVERRIDE
else
echo "Using accounts from AWS Organizations."
if ! account_list=$(aws organizations list-accounts --query 'Accounts[?Status==`ACTIVE`].Id' --output text); then
echo "ERROR: Failed to retrieve account list from AWS Organizations"
exit 1
fi
fi
if [[ -z "$account_list" ]]; then
echo "ERROR: No accounts found to scan"
exit 1
fi
echo "Will scan accounts: $account_list"
# Track failed accounts
failed_accounts=()
# Create directory to store execution ARNs for post-build phase
mkdir -p /tmp/execution_arns
for accountId in $account_list; do
echo "Processing account $accountId"
if [[ $accountId == $AWS_ACCOUNT_ID ]]; then
echo "Skipping management account $accountId - will be handled separately"
continue
fi
# Assume role with error handling - keep credentials in memory only
CREDS=$(aws sts assume-role --role-arn arn:$AWS_PARTITION:iam::$accountId:role/service-role/$MEMBER_ROLE --role-session-name AIMLSecurityAssessment --output json 2>/dev/null)
if [[ $? -ne 0 || -z "$CREDS" ]]; then
echo "ERROR: Failed to assume role in account $accountId"
failed_accounts+=($accountId)
continue
fi
# Extract credentials from memory
export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r '.Credentials.SessionToken')
unset CREDS
if [[ -z "$AWS_ACCESS_KEY_ID" || "$AWS_ACCESS_KEY_ID" == "null" ]]; then
echo "ERROR: Failed to extract credentials for account $accountId"
failed_accounts+=($accountId)
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
continue
fi
echo "Deploying to account $accountId"
if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name aiml-security-$accountId --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3 --region $AWS_DEFAULT_REGION; then
echo "ERROR: Deploy failed for account $accountId"
failed_accounts+=($accountId)
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
continue
fi
STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name aiml-security-$accountId --query 'Stacks[0].Outputs[?OutputKey==`AIMLAssessmentStateMachineArn`].OutputValue' --output text 2>/dev/null)
if [[ $STATE_MACHINE_ARN != "" && $STATE_MACHINE_ARN != "None" ]]; then
if EXECUTION_ARN=$(aws stepfunctions start-execution --state-machine-arn $STATE_MACHINE_ARN --input "{\"accountId\":\"$accountId\"}" --query 'executionArn' --output text); then
echo "Started execution: $EXECUTION_ARN"
# Save execution ARN for post-build phase
echo "$EXECUTION_ARN" > /tmp/execution_arns/$accountId.txt
else
echo "WARNING: Failed to start Step Function execution for account $accountId"
fi
else
echo "WARNING: No State Machine ARN found for account $accountId"
fi
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
done
echo "Deploying to management account $AWS_ACCOUNT_ID"
# Clean up any leftover SAM managed stack from previous failed runs
SAM_STATUS=$(aws cloudformation describe-stacks --stack-name aws-sam-cli-managed-default --query 'Stacks[0].StackStatus' --output text 2>/dev/null)
if [[ "$SAM_STATUS" == "ROLLBACK_COMPLETE" || "$SAM_STATUS" == "DELETE_FAILED" ]]; then
echo "Cleaning up leftover SAM managed stack (status: $SAM_STATUS)"
aws cloudformation delete-stack --stack-name aws-sam-cli-managed-default
aws cloudformation wait stack-delete-complete --stack-name aws-sam-cli-managed-default 2>/dev/null || true
fi
if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name aiml-security-mgmt --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3 --region $AWS_DEFAULT_REGION; then
echo "ERROR: Deploy failed for management account"
failed_accounts+=($AWS_ACCOUNT_ID)
else
STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name aiml-security-mgmt --query 'Stacks[0].Outputs[?OutputKey==`AIMLAssessmentStateMachineArn`].OutputValue' --output text 2>/dev/null)
if [[ $STATE_MACHINE_ARN != "" && $STATE_MACHINE_ARN != "None" ]]; then
if MGMT_EXECUTION_ARN=$(aws stepfunctions start-execution --state-machine-arn $STATE_MACHINE_ARN --input "{\"accountId\":\"$AWS_ACCOUNT_ID\"}" --query 'executionArn' --output text); then
echo "Started management account execution: $MGMT_EXECUTION_ARN"
# Save execution ARN for post-build phase
echo "$MGMT_EXECUTION_ARN" > /tmp/execution_arns/$AWS_ACCOUNT_ID.txt
else
echo "WARNING: Failed to start Step Function execution for management account"
fi
fi
fi
# Report failed accounts
if [[ ${#failed_accounts[@]} -gt 0 ]]; then
echo "WARNING: Deployments failed for the following accounts: ${failed_accounts[*]}"
echo "Check logs above for specific error details"
fi
else
echo "Single account deployment"
STACK_NAME="aiml-sec-${AWS_ACCOUNT_ID}"
if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3; then
echo "ERROR: SAM deploy failed for single account"
exit 1
fi
STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`AIMLAssessmentStateMachineArn`].OutputValue' --output text)
if [[ -z "$STATE_MACHINE_ARN" || "$STATE_MACHINE_ARN" == "None" ]]; then
echo "ERROR: Failed to retrieve State Machine ARN"
exit 1
fi
# Capture execution ARN for post-build phase to wait on
EXECUTION_ARN=$(aws stepfunctions start-execution --state-machine-arn $STATE_MACHINE_ARN --input "{\"accountId\":\"$AWS_ACCOUNT_ID\"}" --query 'executionArn' --output text)
if [[ -z "$EXECUTION_ARN" || "$EXECUTION_ARN" == "None" ]]; then
echo "ERROR: Failed to start Step Function execution"
exit 1
fi
echo "Started Step Function execution: $EXECUTION_ARN"
# Save execution ARN to file for post-build phase
echo "$EXECUTION_ARN" > /tmp/single_account_execution_arn.txt
fi
post_build:
commands:
- echo "CODEBUILD_BUILD_SUCCEEDING status is $CODEBUILD_BUILD_SUCCEEDING"
- |
if [ $CODEBUILD_BUILD_SUCCEEDING -eq 0 ]; then
echo "ERROR: The previous build stage failed. Exiting post_build without processing results."
exit 1
fi
echo "Assessment completed. Results in S3:$BUCKET_REPORT"
if [[ $MULTI_ACCOUNT_SCAN = 'true' ]]; then
echo "Getting list of accounts for post-build processing"
if [[ $MULTI_ACCOUNT_LIST_OVERRIDE != '' ]]; then
account_list=$MULTI_ACCOUNT_LIST_OVERRIDE
else
if ! account_list=$(aws organizations list-accounts --query 'Accounts[?Status==`ACTIVE`].Id' --output text); then
echo "ERROR: Failed to retrieve account list from AWS Organizations"
exit 1
fi
fi
echo "Account list for post-build processing: $account_list"
# Track failures
upload_failed=false
failed_accounts=()
# Clean up any existing account-files directory to ensure fresh start
echo "Cleaning up existing account-files directory"
rm -rf /tmp/account-files
mkdir -p /tmp/account-files
echo "Copying files from respective account S3 buckets to CodeBuild environment"
for accountId in $account_list; do
echo "Processing account $accountId"
if [[ $accountId == $AWS_ACCOUNT_ID ]]; then
# Management account - use mgmt stack name
STACK_NAME="aiml-security-mgmt"
else
# Member account - assume role and use account-specific stack name
CREDS=$(aws sts assume-role --role-arn arn:$AWS_PARTITION:iam::$accountId:role/service-role/$MEMBER_ROLE --role-session-name AIMLSecurityAssessment --output json 2>/dev/null)
if [[ $? -ne 0 || -z "$CREDS" ]]; then
echo "ERROR: Failed to assume role for account $accountId in post-build"
failed_accounts+=($accountId)
continue
fi
export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r '.Credentials.SessionToken')
unset CREDS
if [[ -z "$AWS_ACCESS_KEY_ID" || "$AWS_ACCESS_KEY_ID" == "null" ]]; then
echo "ERROR: Failed to extract credentials for account $accountId in post-build"
failed_accounts+=($accountId)
continue
fi
STACK_NAME="aiml-security-$accountId"
fi
# Wait for Step Function to complete before copying files
# Read the execution ARN saved from build phase
if [[ -f /tmp/execution_arns/$accountId.txt ]]; then
EXECUTION_ARN=$(cat /tmp/execution_arns/$accountId.txt)
echo "Waiting for Step Function execution in account $accountId: $EXECUTION_ARN"
if [[ $EXECUTION_ARN != "" && $EXECUTION_ARN != "None" ]]; then
timeout=${SF_POLL_TIMEOUT:-600}
elapsed=0
while [[ $elapsed -lt $timeout ]]; do
STATUS=$(aws stepfunctions describe-execution --execution-arn $EXECUTION_ARN --query 'status' --output text 2>/dev/null)
if [[ $STATUS == "SUCCEEDED" || $STATUS == "FAILED" || $STATUS == "TIMED_OUT" || $STATUS == "ABORTED" ]]; then
echo "Step Function for account $accountId completed with status: $STATUS"
if [[ $STATUS == "FAILED" || $STATUS == "TIMED_OUT" || $STATUS == "ABORTED" ]]; then
echo "WARNING: Step Function did not succeed for account $accountId"
fi
break
fi
echo "Step Function for account $accountId still running (status: $STATUS)... waiting 30 seconds"
sleep 30
elapsed=$((elapsed + 30))
done
if [[ $elapsed -ge $timeout ]]; then
echo "WARNING: Timeout waiting for Step Function completion in account $accountId"
fi
fi
else
echo "WARNING: No execution ARN file found for account $accountId - execution may not have started"
fi
# Now copy files from the completed assessment
ACCOUNT_BUCKET=$(aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`AssessmentBucketName`].OutputValue' --output text 2>/dev/null)
if [[ $ACCOUNT_BUCKET != "" && $ACCOUNT_BUCKET != "None" ]]; then
echo "Copying files from $ACCOUNT_BUCKET to local storage"
mkdir -p /tmp/account-files/$accountId
# List bucket contents first for debugging
echo "Bucket contents for $accountId:"
if ! aws s3 ls s3://$ACCOUNT_BUCKET/; then
echo "WARNING: Failed to list bucket contents for $accountId"
fi
if ! aws s3 cp s3://$ACCOUNT_BUCKET/ /tmp/account-files/$accountId/ --recursive --exclude "*" --include "*.csv" --include "*.html" --exclude "*/"; then
echo "WARNING: No files to copy or copy failed from $accountId"
fi
# Show what was actually copied
echo "Files copied for $accountId:"
ls -la /tmp/account-files/$accountId/ 2>/dev/null || echo "No files in directory"
# Flatten any nested directories
find /tmp/account-files/$accountId -type f \( -name "*.csv" -o -name "*.html" \) -exec mv {} /tmp/account-files/$accountId/ \; 2>/dev/null || true
find /tmp/account-files/$accountId -type d -empty -delete 2>/dev/null || true
else
echo "WARNING: No assessment bucket found for $accountId (STACK_NAME: $STACK_NAME)"
fi
if [[ $accountId != $AWS_ACCOUNT_ID ]]; then
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
fi
done
echo "Uploading files to management account S3 bucket and creating consolidated report"
# Debug: Show which directories exist
echo "Available account directories:"
ls -la /tmp/account-files/ || echo "No account-files directory"
# Upload files from local storage to management account bucket organized by account (overwrite)
echo "Uploading account reports to S3..."
for accountId in $account_list; do
if [[ -d /tmp/account-files/$accountId ]]; then
echo "Uploading files for account $accountId (overwriting existing)"
if ! aws s3 sync /tmp/account-files/$accountId/ s3://$BUCKET_REPORT/$accountId/; then
echo "ERROR: Failed to upload files for $accountId"
upload_failed=true
fi
else
echo "WARNING: No directory found for account $accountId"
fi
done
# Create consolidated HTML report using separate script
echo "Creating consolidated HTML report..."
if ! python3 ../consolidate_html_reports.py; then
echo "ERROR: Failed to create consolidated HTML report"
upload_failed=true
fi
# Report failures
if [[ ${#failed_accounts[@]} -gt 0 ]]; then
echo "WARNING: Post-build processing failed for the following accounts: ${failed_accounts[*]}"
fi
if [[ $upload_failed == true ]]; then
echo "ERROR: One or more uploads or report generation failed. Check logs above for details."
exit 1
fi
echo "==========================================="
echo "Multi-account assessment completed successfully!"
echo "==========================================="
echo "ACCESS YOUR RESULTS HERE: s3://$BUCKET_REPORT/"
echo ""
echo "Note: Other S3 buckets created during deployment (e.g., aiml-sec-*-aimlassessmentbucket-*)"
echo "are for internal use. Use the bucket above from the AssessmentBucket stack output."
echo "==========================================="
else
# Single account post-build: wait for Step Function and report status
echo "Single account post-build processing"
STACK_NAME="aiml-sec-${AWS_ACCOUNT_ID}"
# Read the execution ARN saved from build phase
if [[ -f /tmp/single_account_execution_arn.txt ]]; then
EXECUTION_ARN=$(cat /tmp/single_account_execution_arn.txt)
echo "Waiting for Step Function execution: $EXECUTION_ARN"
if [[ $EXECUTION_ARN != "" && $EXECUTION_ARN != "None" ]]; then
timeout=${SF_POLL_TIMEOUT:-600}
elapsed=0
while [[ $elapsed -lt $timeout ]]; do
STATUS=$(aws stepfunctions describe-execution --execution-arn $EXECUTION_ARN --query 'status' --output text 2>/dev/null)
if [[ $STATUS == "SUCCEEDED" || $STATUS == "FAILED" || $STATUS == "TIMED_OUT" || $STATUS == "ABORTED" ]]; then
echo "Step Function completed with status: $STATUS"
if [[ $STATUS != "SUCCEEDED" ]]; then
echo "WARNING: Step Function did not succeed"
fi
break
fi
echo "Step Function still running (status: $STATUS)... waiting 30 seconds"
sleep 30
elapsed=$((elapsed + 30))
done
if [[ $elapsed -ge $timeout ]]; then
echo "WARNING: Timeout waiting for Step Function completion"
fi
fi
else
echo "WARNING: No execution ARN file found from build phase"
fi
# Get assessment bucket and sync results to BUCKET_REPORT for consistency
ACCOUNT_BUCKET=$(aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`AssessmentBucketName`].OutputValue' --output text 2>/dev/null)
if [[ $ACCOUNT_BUCKET != "" && $ACCOUNT_BUCKET != "None" ]]; then
echo "Assessment results generated in S3 bucket: $ACCOUNT_BUCKET"
echo "Bucket contents:"
aws s3 ls s3://$ACCOUNT_BUCKET/ --recursive || echo "Failed to list bucket contents"
# Sync results to BUCKET_REPORT for consistent access
if [[ $ACCOUNT_BUCKET != $BUCKET_REPORT ]]; then
echo "Syncing results to consolidated bucket: $BUCKET_REPORT"
if ! aws s3 sync s3://$ACCOUNT_BUCKET/ s3://$BUCKET_REPORT/$AWS_ACCOUNT_ID/ --exclude "*" --include "*.csv" --include "*.html"; then
echo "WARNING: Failed to sync results to $BUCKET_REPORT"
else
echo "Results synced successfully to s3://$BUCKET_REPORT/$AWS_ACCOUNT_ID/"
fi
else
echo "Assessment bucket and report bucket are the same, no sync needed"
fi
else
echo "WARNING: Could not determine assessment bucket from stack outputs"
fi
fi
- |
echo "==========================================="
echo "Assessment completed successfully!"
echo "==========================================="
echo "ACCESS YOUR RESULTS HERE: s3://$BUCKET_REPORT/"
echo ""
echo "Note: Other S3 buckets created during deployment (e.g., aiml-sec-*-aimlassessmentbucket-*)"
echo "are for internal use. Use the bucket above from the AssessmentBucket stack output."
echo "==========================================="