Last active
June 18, 2024 18:32
-
-
Save atheiman/87d04578e80659ffab3dee67db82d6f5 to your computer and use it in GitHub Desktop.
AWS Config custom rule to evaluate AWS account tags
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# aws cloudformation deploy \ | |
# --profile mgmt \ | |
# --template-file ./template.yml \ | |
# --stack-name ConfigRuleAccountTags \ | |
# --capabilities CAPABILITY_IAM | |
Resources: | |
ConfigRule: | |
Type: AWS::Config::ConfigRule | |
DependsOn: EvaluationFunctionConfigPermission | |
Properties: | |
ConfigRuleName: aws-accounts-required-tags | |
Description: AWS Accounts must have required tags | |
Source: | |
Owner: CUSTOM_LAMBDA | |
SourceIdentifier: !Sub "${EvaluationFunction.Arn}" | |
SourceDetails: | |
- MessageType: ScheduledNotification | |
MaximumExecutionFrequency: One_Hour # One_Hour Three_Hours Six_Hours Twelve_Hours TwentyFour_Hours | |
EventSource: aws.config | |
EvaluationFunction: | |
Type: AWS::Lambda::Function | |
Properties: | |
Role: !Sub "${EvaluationFunctionRole.Arn}" | |
Handler: index.handler | |
Timeout: 300 | |
Runtime: python3.11 | |
Tags: | |
- Key: CfnStackId | |
Value: !Ref AWS::StackId | |
Code: | |
ZipFile: | | |
import boto3 | |
import os | |
import json | |
import re | |
config = boto3.client("config", region_name=os.environ["AWS_REGION"]) | |
orgs = boto3.client("organizations", region_name=os.environ["AWS_REGION"]) | |
# Mapping of required tag keys and optionally tag value requirements | |
required_tags = { | |
"Environment": {"AllowedValues": ["prod", "nonprod"]}, | |
"Application": {}, | |
"Owner": {"Regex": "^.+@example.com$"}, | |
} | |
def handler(event, context): | |
# print(json.dumps(event)) | |
# print("Listing accounts") | |
accts = [] | |
for pg in orgs.get_paginator("list_accounts").paginate(): | |
accts += pg["Accounts"] | |
# print(json.dumps(accts, default=str)) | |
evaluations = [] | |
orderingtime = json.loads(event["invokingEvent"])["notificationCreationTime"] | |
for acct in accts: | |
print("Evaluating account:", acct["Id"]) | |
tags = {} | |
for pg in orgs.get_paginator("list_tags_for_resource").paginate(ResourceId=acct['Id']): | |
for t in pg["Tags"]: | |
tags[t["Key"]] = t["Value"] | |
missing_requireds = [] | |
invalid_values = [] | |
evaluation = { | |
"ComplianceResourceType": "AWS::::Account", | |
"ComplianceResourceId": acct["Id"], | |
"ComplianceType": "COMPLIANT", | |
"Annotation": "", | |
"OrderingTimestamp": orderingtime, | |
} | |
for required_key, value_spec in required_tags.items(): | |
if required_key not in tags: | |
evaluation["ComplianceType"] = "NON_COMPLIANT" | |
missing_requireds.append(required_key) | |
continue | |
if "AllowedValues" in value_spec: | |
if tags[required_key] not in value_spec["AllowedValues"]: | |
evaluation["ComplianceType"] = "NON_COMPLIANT" | |
invalid_values.append(required_key) | |
if "Regex" in value_spec: | |
if not re.search(value_spec["Regex"], tags[required_key]): | |
evaluation["ComplianceType"] = "NON_COMPLIANT" | |
invalid_values.append(required_key) | |
evaluation["Annotation"] = f"Missing required tags: {missing_requireds}. Invalid value tags: {invalid_values}." | |
evaluations.append(evaluation) | |
batch_size = 100 | |
for i in range(0, len(evaluations), batch_size): | |
batch = evaluations[i : i + batch_size] | |
print(f"Submitting evaluations ({len(batch)}):") | |
print(json.dumps(batch, default=str)) | |
response = config.put_evaluations(Evaluations=batch, ResultToken=event["resultToken"]) | |
print("PutEvaluations response:") | |
print(json.dumps(response, default=str)) | |
EvaluationFunctionConfigPermission: | |
Type: AWS::Lambda::Permission | |
Properties: | |
FunctionName: !Sub "${EvaluationFunction.Arn}" | |
Action: lambda:InvokeFunction | |
Principal: config.amazonaws.com | |
SourceAccount: !Ref AWS::AccountId | |
EvaluationFunctionRole: | |
Type: AWS::IAM::Role | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: "2012-10-17" | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: lambda.amazonaws.com | |
Action: ["sts:AssumeRole"] | |
ManagedPolicyArns: | |
- !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" | |
Policies: | |
- PolicyName: Inline | |
PolicyDocument: | |
Version: "2012-10-17" | |
Statement: | |
- Effect: Allow | |
Resource: "*" | |
Action: | |
- config:PutEvaluations | |
- organizations:ListTagsForResource | |
- organizations:ListAccounts | |
EvaluationFunctionLogGroup: | |
Type: AWS::Logs::LogGroup | |
Properties: | |
LogGroupName: !Sub '/aws/lambda/${EvaluationFunction}' | |
RetentionInDays: 14 | |
Tags: | |
- Key: CfnStackId | |
Value: !Ref AWS::StackId |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment