diff --git a/arch/templates/AuditAccountPreRequisitesPart1.yaml b/arch/templates/AuditAccountPreRequisitesPart1.yaml index 1543ba35..dee1356e 100644 --- a/arch/templates/AuditAccountPreRequisitesPart1.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart1.yaml @@ -32,6 +32,9 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. Conditions: GenerateEvidenceBucketName: !Equals @@ -408,6 +411,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC02 GC02CheckAccountManagementPlanLambda: @@ -425,6 +431,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC02CheckPasswordProtectionMechanismsLambda: Condition: IsAuditAccount @@ -441,3 +450,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/AuditAccountPreRequisitesPart2.yaml b/arch/templates/AuditAccountPreRequisitesPart2.yaml index 2b33ef36..88b56d7c 100644 --- a/arch/templates/AuditAccountPreRequisitesPart2.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart2.yaml @@ -17,6 +17,9 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. Conditions: IsAuditAccount: !Equals @@ -48,6 +51,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC01CheckDedicatedAdminAccountLambda: Condition: IsAuditAccount @@ -64,6 +70,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC02 GC02CheckIAMPasswordPolicyLambda: @@ -81,6 +90,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC02CheckGroupAccessConfigurationLambda: Condition: IsAuditAccount @@ -97,6 +109,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC02CheckPrivilegedRolesReviewLambda: Condition: IsAuditAccount @@ -113,3 +128,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/AuditAccountPreRequisitesPart3.yaml b/arch/templates/AuditAccountPreRequisitesPart3.yaml index 56c6dc9f..bb4301d2 100644 --- a/arch/templates/AuditAccountPreRequisitesPart3.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart3.yaml @@ -19,6 +19,9 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. Conditions: IsAuditAccount: !Equals @@ -54,6 +57,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC08 GC08CheckTargetNetworkArchitectureLambda: @@ -71,6 +77,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC08CheckCloudDeploymentGuideLambda: Condition: IsAuditAccount @@ -87,6 +96,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC08CheckCloudSegmentationDesignLambda: Condition: IsAuditAccount @@ -103,6 +115,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC09 GC09CheckNetworkSecurityArchitectureDocumentLambda: @@ -120,6 +135,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC09CheckNonPublicStorageAccountsLambda: Condition: IsAuditAccount @@ -136,6 +154,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC10 GC10CheckCyberCenterSensorsLambda: @@ -153,6 +174,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC10CheckSignedMOULambda: Condition: IsAuditAccount @@ -169,6 +193,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC11 GC11CheckSecurityContactLambda: @@ -186,3 +213,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/AuditAccountPreRequisitesPart4.yaml b/arch/templates/AuditAccountPreRequisitesPart4.yaml index c73ef7fe..95dc3c70 100644 --- a/arch/templates/AuditAccountPreRequisitesPart4.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart4.yaml @@ -19,6 +19,9 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. Conditions: IsAuditAccount: !Equals @@ -50,6 +53,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC11CheckMonitoringUseCasesLambda: Condition: IsAuditAccount @@ -66,6 +72,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC11CheckPolicyEventLoggingLambda: Condition: IsAuditAccount @@ -82,6 +91,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC11CheckTimezoneLambda: Condition: IsAuditAccount @@ -98,6 +110,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC11CheckTrailLoggingLambda: Condition: IsAuditAccount @@ -114,6 +129,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC12 GC12CheckMarketplacesLambda: @@ -131,6 +149,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC01 GC01CheckRootAccountMFAEnabledLambda: @@ -148,6 +169,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC01CheckFederatedUsersMFA: Condition: IsAuditAccount @@ -164,6 +188,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC01CheckMonitoringAndLoggingLambda: Condition: IsAuditAccount @@ -180,6 +207,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC01CheckAlertsFlagMisuseLambda: Condition: IsAuditAccount @@ -196,6 +226,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC01CheckMFADigitalPolicy: Condition: IsAuditAccount @@ -212,6 +245,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC03 GC03CheckEndpointAccessConfigLambda: @@ -229,6 +265,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC03CheckIAMCloudWatchAlarmsLambda: Condition: IsAuditAccount @@ -245,6 +284,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC03CheckTrustedDevicesAdminAccessLambda: Condition: IsAuditAccount @@ -261,6 +303,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC13 GC13EmergencyAccountManagementLambda: @@ -278,6 +323,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC13EmergencyAccountMgmtApprovalsLambda: Condition: IsAuditAccount @@ -294,6 +342,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC13EmergencyAccountAlertsLambda: Condition: IsAuditAccount @@ -310,6 +361,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC13EmergencyAccountTestingLambda: Condition: IsAuditAccount @@ -326,3 +380,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/AuditAccountPreRequisitesPart5.yaml b/arch/templates/AuditAccountPreRequisitesPart5.yaml index e1ce8ba7..31873969 100644 --- a/arch/templates/AuditAccountPreRequisitesPart5.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart5.yaml @@ -19,6 +19,10 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. + Conditions: IsAuditAccount: !Equals - !Ref AWS::AccountId @@ -49,6 +53,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC04CheckAlertsFlagMisuseLambda: Condition: IsAuditAccount @@ -65,6 +72,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile ## GC05 GC05CheckDataLocationLambda: @@ -82,3 +92,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/AuditAccountPreRequisitesPart6.yaml b/arch/templates/AuditAccountPreRequisitesPart6.yaml index 050f2f2c..755dba90 100644 --- a/arch/templates/AuditAccountPreRequisitesPart6.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart6.yaml @@ -19,6 +19,9 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. Conditions: IsAuditAccount: !Equals @@ -49,6 +52,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC07CheckCryptographicAlgorithmsLambda: Condition: IsAuditAccount @@ -65,6 +71,9 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile GC07CheckEncryptionInTransitLambda: Condition: IsAuditAccount @@ -81,3 +90,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/AuditAccountPreRequisitesPart7.yaml b/arch/templates/AuditAccountPreRequisitesPart7.yaml index c7c11080..cd2a34b1 100644 --- a/arch/templates/AuditAccountPreRequisitesPart7.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart7.yaml @@ -19,6 +19,9 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. Conditions: IsAuditAccount: !Equals @@ -49,3 +52,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/AuditAccountPreRequisitesPart8.yaml b/arch/templates/AuditAccountPreRequisitesPart8.yaml index 50ff0e44..f9eadf42 100644 --- a/arch/templates/AuditAccountPreRequisitesPart8.yaml +++ b/arch/templates/AuditAccountPreRequisitesPart8.yaml @@ -19,6 +19,9 @@ Parameters: Type: String Default: "python3.12" Description: The python runtime to use for the compliance dashboard + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. Conditions: IsAuditAccount: !Equals @@ -49,3 +52,6 @@ Resources: LoggingConfig: LogGroup: !Sub "${OrganizationName}gc_guardrails" LogFormat: "JSON" + Environment: + Variables: + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile diff --git a/arch/templates/EvidenceCollectionComponents.yaml b/arch/templates/EvidenceCollectionComponents.yaml index 27a3a5e6..fefc2caf 100644 --- a/arch/templates/EvidenceCollectionComponents.yaml +++ b/arch/templates/EvidenceCollectionComponents.yaml @@ -24,6 +24,9 @@ Parameters: Type: String DeployVersion: Type: String + DefaultCloudProfile: + Type: String + Description: The cloud profile to use when one is not provided by an account. AssessmentName: Type: String Default: "GC Guardrails Assessment" @@ -35,6 +38,15 @@ Conditions: - !Ref AuditAccountID Resources: + CloudGuardrailsCommonLayer: + Condition: IsAuditAccount + Type: AWS::Lambda::LayerVersion + Properties: + LayerName: CloudGuardrailsCommonLayerPartEvidenceCollection + CompatibleRuntimes: + - python3.12 + Content: "../../src/layer/cloud_guardrails/build/CloudGuardrailsCommonLayer/" + myKey: Condition: IsAuditAccount Type: "AWS::KMS::Key" @@ -175,6 +187,8 @@ Resources: Handler: app.lambda_handler Code: "../../src/lambda/aws_compile_audit_report/build/CronjobsLambda/" Role: !GetAtt CronLambdaExecutionRole.Arn + Layers: + - !Ref CloudGuardrailsCommonLayer Environment: Variables: source_target_bucket: !Sub "gc-fedclient-${AWS::AccountId}-${AWS::Region}" @@ -183,7 +197,8 @@ Resources: ORG_NAME: !Ref OrganizationName TENANT_ID: !Ref TenantId CAC_VERSION: !Ref DeployVersion - Runtime: python3.9 + DEFAULT_CLOUD_PROFILE: !Ref DefaultCloudProfile + Runtime: python3.12 Timeout: 360 CronLambdaExecutionRole: @@ -200,6 +215,14 @@ Resources: ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" Policies: + - PolicyName: AllowOrgList + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - organizations:ListTagsForResource + Resource: "*" - PolicyName: S3Policy PolicyDocument: Version: "2012-10-17" diff --git a/arch/templates/OrgRoleGenerator.yaml b/arch/templates/OrgRoleGenerator.yaml index 9ca5b6a3..ca674a97 100644 --- a/arch/templates/OrgRoleGenerator.yaml +++ b/arch/templates/OrgRoleGenerator.yaml @@ -54,6 +54,23 @@ Resources: - sts:AssumeRole Path: "/" Policies: + - PolicyName: self_invoke + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunctionUrl + - lambda:InvokeFunction + - lambda:GetFunctionEventInvokeConfig + - lambda:GetFunction + - lambda:InvokeAsync + - lambda:GetAlias + Resource: !Sub "arn:${AWS::Partition}:lambda:*:${AWS::AccountId}:function:${OrganizationName}aws_create_role" + - Effect: Allow + Action: + - lambda:ListFunctions + Resource: "*" - PolicyName: assume_role PolicyDocument: Version: "2012-10-17" @@ -83,38 +100,69 @@ Resources: """ Lambda function used to create roles in all organizational accounts. """ import json import logging + import threading + import time import boto3 - from boto3.session import Session + from botocore.exceptions import ClientError import urllib3 - import time - import random - import string - + SUCCESS = "SUCCESS" FAILED = "FAILED" - # cfn response replacement + + # cfnresponse replacement http = urllib3.PoolManager() + logger = logging.getLogger() logger.setLevel(logging.INFO) - + + # --- BEGIN REINVOCATION LOGIC CONFIG --- + MAX_REINVOCATIONS = 5 + TIMEOUT_MINUTES = 10 + stop_reinvocation = False # Flag used to cancel reinvocation once function completes + + def schedule_reinvocation(reinvoke_count, original_event, context, time_remaining_in_seconds): + """ + Sleeps for TIMEOUT_MINUTES. If stop_reinvocation is still False after that time, + re-invokes this Lambda function asynchronously with reinvoke_count + 1, + provided we haven't hit MAX_REINVOCATIONS. + """ + time.sleep(time_remaining_in_seconds) + if not stop_reinvocation: + if reinvoke_count < MAX_REINVOCATIONS: + logger.info( + f"{TIMEOUT_MINUTES} minutes passed without completion. " + f"Re-invoking (attempt {reinvoke_count + 1}/{MAX_REINVOCATIONS})." + ) + lambda_client = boto3.client('lambda') + # Make a shallow copy so we don't mutate the original event + new_event = dict(original_event) + new_event['ReinvokeCount'] = reinvoke_count + 1 + + lambda_client.invoke( + FunctionName=context.function_name, # lambda name + InvocationType='Event', # async invocation + Payload=json.dumps(new_event) + ) + else: + logger.info("Reached max reinvocations. No further attempts.") + # --- END REINVOCATION LOGIC CONFIG --- + def get_org_accounts(): """ - Returns a list of all accounts in the organization. + Returns a list of all active accounts in the organization. """ - - session = Session() - org_client = session.client('organizations') - + org_client = boto3.client('organizations') + active_accounts = [] paginator = org_client.get_paginator('list_accounts') - page_iterator = paginator.paginate() - - accounts = [] - for page in page_iterator: - accounts.extend(page['Accounts']) - - return accounts - - + + for page in paginator.paginate(): + for account in page['Accounts']: + if account['Status'] == "ACTIVE": + logger.info(f"Account added {account}") + active_accounts.append(account) + + return active_accounts + def create_iam_policy(session, policy_name, policy_document): """ Creates an IAM policy. @@ -126,8 +174,7 @@ Resources: PolicyDocument=policy_document ) return sts_response['Policy'] - - + def attach_iam_policy_to_role(session, role_name, policy_arn): """ Attaches an IAM policy to a role. @@ -139,8 +186,7 @@ Resources: PolicyArn=policy_arn ) return sts_response - - + def create_iam_role(session, role_name, principal): """ Creates an IAM role. @@ -164,8 +210,7 @@ Resources: AssumeRolePolicyDocument=json.dumps(assume_role_policy_document) ) return sts_response['Role'] - - + def delete_role(session, role_name): """ Deletes an IAM role. @@ -176,74 +221,41 @@ Resources: RoleName=role_name ) return sts_response - - - def delete_all_inline_policy(session,role_name): - """ - Deletes all inline policies from a role. - """ - logger.info(f"Deleting all inline policies from: {role_name}") - iam_client = session.client('iam') - sts_response = iam_client.list_role_policies( - RoleName=role_name - ) - logger.info(f"Deleting inline policies: {sts_response['PolicyNames']}") - for policy in sts_response['PolicyNames']: - sts_response = iam_client.delete_role_policy( - RoleName=role_name, - PolicyName=policy - ) - sts_response = iam_client.list_role_policies( - RoleName=role_name - ) - logger.info(f"current inline policies: {sts_response['PolicyNames']}") - return sts_response - - + def detach_all_policies_from_role(session, role_name): """ - Detaches all policies from a role. + Detaches all policies from a role and deletes any custom policies. """ - logger.info(f"Detaching all policies from: {role_name}") + logger.info(f"Detach all policies from {role_name}") iam_client = session.client('iam') - sts_response = iam_client.list_attached_role_policies( - RoleName=role_name - ) - logger.info(f"Detaching policies: {sts_response['AttachedPolicies']}") + sts_response = iam_client.list_attached_role_policies(RoleName=role_name) + + logger.info(f"Attached Policies to role {role_name} are {sts_response['AttachedPolicies']}") for policy in sts_response['AttachedPolicies']: - sts_response = iam_client.detach_role_policy( + logger.info(f"Deleting Policy: {policy}") + iam_client.detach_role_policy( RoleName=role_name, PolicyArn=policy['PolicyArn'] ) - delete_iam_policy(session, policy_arn=policy['PolicyArn']) - return sts_response - - def detach_policy_from_all_roles(session, policy_arn): - """ - Detaches a policy from all roles. - """ - logger.info(f"Detach Policy: {policy_arn} from all roles") - iam_client = session.client('iam') - sts_response = iam_client.list_roles() - for role in sts_response['Roles']: - try: - sts_response = iam_client.detach_role_policy( - RoleName=role['RoleName'], - PolicyArn=policy_arn - ) - except: - pass + delete_iam_policy(session=session, policy_arn=policy['PolicyArn']) + + sts_response = iam_client.list_role_policies(RoleName=role_name) + logger.info(f"Inline Policies attached to role {role_name} are {sts_response['PolicyNames']}") + for policy_name in sts_response['PolicyNames']: + logger.info(f"Deleting Policy: {policy_name}") + iam_client.delete_role_policy( + RoleName=role_name, + PolicyName=policy_name + ) return sts_response - - + def get_account_id(session): """ Returns the account ID. """ sts_client = session.client('sts') return sts_client.get_caller_identity()['Account'] - - + def assume_role(session, account_id, role_name): """ Assumes a role. @@ -253,130 +265,329 @@ Resources: RoleArn=f"arn:aws:iam::{account_id}:role/{role_name}", RoleSessionName=str(account_id) ) - sts_session = boto3.Session(aws_access_key_id=sts_response['Credentials']['AccessKeyId'], aws_secret_access_key=sts_response[ - 'Credentials']['SecretAccessKey'], aws_session_token=sts_response['Credentials']['SessionToken']) + sts_session = boto3.Session( + aws_access_key_id=sts_response['Credentials']['AccessKeyId'], + aws_secret_access_key=sts_response['Credentials']['SecretAccessKey'], + aws_session_token=sts_response['Credentials']['SessionToken'] + ) return sts_session - - + def delete_iam_policy(session, policy_arn): """ - Deletes an IAM policy. + Deletes an IAM policy, skipping AWS managed policies. """ - logger.info(f"Deleting IAM policy {policy_arn}") + # Check if the policy is an AWS managed policy + if policy_arn.startswith("arn:aws:iam::aws:policy/"): + logger.info(f"Skipping deletion of AWS managed policy: {policy_arn}") + return {"Status": "Skipped", "Reason": "AWS Managed Policy"} + iam_client = session.client('iam') - try: - sts_response = iam_client.delete_policy( - PolicyArn=policy_arn - ) - except Exception as err: - logger.info(f"{policy_arn} {err}") - # Policy is attached to something, detach it from everything, then delete it again - detach_policy_from_all_roles(session,policy_arn) - logger.info(f"Deleting IAM policy {policy_arn}") - sts_response = iam_client.delete_policy( - PolicyArn=policy_arn - ) + # List all versions of the policy + versions = iam_client.list_policy_versions(PolicyArn=policy_arn) + + # Loop through the versions and delete the non-default versions + for version in versions['Versions']: + if not version['IsDefaultVersion']: + iam_client.delete_policy_version( + PolicyArn=policy_arn, + VersionId=version['VersionId'] + ) + logger.info(f"Deleted non-default policy version {version['VersionId']}") + + sts_response = iam_client.delete_policy(PolicyArn=policy_arn) return sts_response - - - def get_random_string(length): - # choose from all lowercase letter - letters = string.ascii_lowercase - result_str = ''.join(random.choice(letters) for i in range(length)) - return result_str - - + + def send(event, context, response_status, response_data, physical_resource_id=None, no_echo=False, reason=None): + """ + Sends a response to CloudFormation. We skip this if the event + doesn't actually come from CloudFormation (i.e., no ResponseURL). + """ + if 'ResponseURL' not in event or not event['ResponseURL']: + logger.info("No CFN ResponseURL found. Skipping send() for non-CFN scenario.") + return + + response_url = event['ResponseURL'] + logger.info("Response URL: %s", response_url) + response_body = { + 'Status': response_status, + 'Reason': reason or f"See the details in CloudWatch Log Stream: {context.log_stream_name}", + 'PhysicalResourceId': physical_resource_id or context.log_stream_name, + 'StackId': event.get('StackId', 'NoStackId'), + 'RequestId': event.get('RequestId', 'NoRequestId'), + 'LogicalResourceId': event.get('LogicalResourceId', 'NoLogicalResourceId'), + 'NoEcho': no_echo, + 'Data': response_data + } + json_response_body = json.dumps(response_body) + logger.info("Response body:") + logger.info(json_response_body) + headers = {'content-type': '', 'content-length': str(len(json_response_body))} + try: + response = http.request('PUT', response_url, headers=headers, body=json_response_body) + logger.info("Status code: %s", response.status) + except (ValueError, TypeError, urllib3.exceptions.HTTPError) as err: + logger.error("send(..) failed executing http.request(..): %s", err) + + def normalize_event(event): + """ + If 'RequestType' is present, assume it's a CFN event and wrap it in a list. + If 'Roles' is present, assume it's an EventBridge event. Convert each role + into a CFN-like event so we can reuse the same logic. + If 'Roles' is present but empty, return an empty list of events so that + the main loop will skip any processing. + """ + + # CloudFormation event + if 'RequestType' in event and 'ResourceProperties' in event: + return [event] # It's already in CFN format + + # EventBridge event with a list of roles + if 'Roles' in event: + # If the roles array is empty, skip further processing + if not event['Roles']: + logger.info("No roles found in the 'Roles' array. Returning an empty list.") + return [] + + normalized = [] + for role in event['Roles']: + # Build a new "CFN-like" event + cfn_style_event = { + "RequestType": "Update", # We treat these as "Update" for creation logic + "ResourceProperties": { + "RoleName": role["Name"], + "TrustPrincipal": role["TrustPrincipal"], + "SwitchRole": role["SwitchRole"], + # The original code expects a string for PolicyPackage + "PolicyPackage": json.dumps(role["PolicyPackage"]) + }, + # Fake CFN required fields (since we won't actually call send() for these, + # or if we do, we skip if there's no real CFN ResponseURL). + "ResponseURL": None, + "StackId": "EventBridgeStack", + "RequestId": "EventBridgeRequest", + "LogicalResourceId": "EventBridgeLogicalId", + "PhysicalResourceId": "EventBridgePhysicalId" + } + normalized.append(cfn_style_event) + return normalized + + # Unknown event type + raise ValueError("Event does not contain 'RequestType' or 'Roles'. Cannot normalize.") + def lambda_handler(event, context): - """This function is the main entry point for Lambda. - Keyword arguments: - event -- the event variable given in the lambda handler - context -- the context variable given in the lambda handler """ - response_data = {} - logger.info('Event: {}'.format(event)) - roles = event['Roles'] - accounts_modified = [] - - try: # Get the Management account id - session = boto3.Session() - except Exception as err: - logger.error(f"Error getting session: {err}") - response_data['Error'] = f"Error getting session: {err}" - raise err + Main entry point for Lambda. We unify the handling of both CFN and EventBridge + events by first normalizing them into a list of CFN-like events and then using + the existing flow. + """ + global stop_reinvocation + + # --- REINVOCATION: read ReinvokeCount from event --- + reinvoke_count = event.get('ReinvokeCount', 0) + + # Start the background timer thread to automatically re-invoke after TIMEOUT_MINUTES + time_remaining_in_seconds = (context.get_remaining_time_in_millis() / 1000) - 10 + t = threading.Thread(target=schedule_reinvocation, args=(reinvoke_count, event, context, time_remaining_in_seconds)) + t.daemon = True + t.start() + # --- END REINVOCATION SETUP --- + try: - account_id = get_account_id(session) - except Exception as err: - logger.error(f"Error getting account ID: {err}") - response_data['Error'] = f"Error getting account ID: {err}" - raise err - try: # Get every account in the organization - accounts = get_org_accounts() - except Exception as err: - logger.error(f"Error getting accounts: {err}") - response_data['Error'] = f"Error getting accounts: {err}" - raise err - - # For every account in the organization - logger.info(f"Accounts {accounts}") - for account in accounts: - if account['Status'] != "ACTIVE": - logger.info(f"Found inactive account {account['Id']}") - continue # Skip non-active accounts - if account_id in account['Id']: # Skip the management account - logger.info(f"Skipping the management account {account['Id']}") - continue # do not skip the management account - logger.info(f"Account: {account['Id']}") - accounts_modified.append(account['Id']) + logger.info("Received Event: %s", json.dumps(event, indent=2)) - for role in roles: # For every role supplied - logger.info(f"Role: {role}") - # Get parameters from event - trust_principal = role['TrustPrincipal'] - switch_role = role['SwitchRole'] - role_name = role['Name'] - policy_string = role['PolicyPackage'] + try: + # Convert the incoming event to a CFN-like list + cfn_events = normalize_event(event) + except Exception as err: + logger.error(f"Error normalizing event: {err}") + response_data = {"Error": str(err)} + send(event, context, FAILED, response_data) + stop_reinvocation = True + return + + # If the list of cfn_events is empty (e.g., empty Roles), do nothing + if not cfn_events: + logger.info("No events to process after normalization. Exiting.") + stop_reinvocation = True + return + + # Process each "synthetic CFN event" in the same Lambda invocation + for cfn_event in cfn_events: + response_data = {} + + # Pull request type from the normalized event (Create/Update/Delete). + request_type = cfn_event['RequestType'] + + # Extract the ResourceProperties we need + resource_props = cfn_event['ResourceProperties'] + trust_principal = resource_props['TrustPrincipal'].split(',') + switch_role = resource_props['SwitchRole'] + role_name = resource_props['RoleName'] + policy_string = resource_props['PolicyPackage'].replace("'", '"') logger.info(f"Policy String: {policy_string}") - policy_package = policy_string #json.loads(policy_string, strict=False) - try: # Assume the supplied switch_role in the account we are deploying roles to - sts_session = assume_role( - session=session, account_id=account['Id'], role_name=switch_role) + policy_package = json.loads(policy_string, strict=False) + + # Get list of Org accounts and the current account_id + try: + accounts = get_org_accounts() + except Exception as err: + logger.error(f"Error getting accounts: {err}") + response_data['Error'] = f"Error getting accounts: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + try: + session = boto3.Session() except Exception as err: - logger.error(f"Error assuming role: {err}") - response_data['Error'] = f"Error assuming role: {err}" + logger.error(f"Error getting session: {err}") + response_data['Error'] = f"Error getting session: {err}" + send(cfn_event, context, FAILED, response_data) raise err - try: # Create the IAM role - iam_role = create_iam_role( - session=sts_session, role_name=role_name, principal=trust_principal) - except Exception as err: # On error, remove all the policies from the role, delete, then re-create it - logger.info( - f"Existing IAM Role {role_name} found, removing policies and deleting role. Exception: {err}") - delete_all_inline_policy( - session=sts_session, role_name=role_name) - detach_all_policies_from_role( - session=sts_session, role_name=role_name) - time.sleep(3) - delete_role(session=sts_session, role_name=role_name) - iam_role = create_iam_role( - session=sts_session, role_name=role_name, principal=trust_principal) - - for policy_doc in policy_package['Docs']: # For each policy in the supplied policy document - logger.info(f"Policy_Doc:{json.dumps(policy_doc)}") - try: - policy = create_iam_policy( - session=sts_session, policy_name=f"{policy_doc['Statement'][0]['Sid']}-{get_random_string(6)}", policy_document=json.dumps(policy_doc)) - except Exception as err: # The policy exists already, so attach it to the role - logger.info( - f"Existing Policy {policy_doc['Statement'][0]['Sid']} found. Exception: {err}") - response_data['Error'] = f"Existing Policy {policy_doc['Statement'][0]['Sid']} found. Exception: {err}" - policy = {} - policy['Arn'] = f"arn:aws:iam::{account['Id']}:policy/{policy_doc['Statement'][0]['Sid']}" - try: - attach_iam_policy_to_role( - session=sts_session, role_name=iam_role['RoleName'], policy_arn=policy['Arn']) - except Exception as err: - logger.info( - f"Error attaching {iam_role['RoleName']} to {policy['Arn']}. Exception: {err}") - logger.info(f"Accounts Modified {accounts_modified}") + + try: + account_id = get_account_id(session) + except Exception as err: + logger.error(f"Error getting account ID: {err}") + response_data['Error'] = f"Error getting account ID: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + # Handle Create/Update + if request_type == 'Create' or request_type == 'Update': + logger.info(f"CFN {request_type} request received") + for account in accounts: + # Skip the management account + if account_id in account['Id']: + continue + + try: + sts_session = assume_role(session=session, account_id=account['Id'], role_name=switch_role) + except Exception as err: + logger.error(f"Error assuming role: {err}") + response_data['Error'] = f"Error assuming role: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + # Create or recreate the IAM role + try: + iam_role = create_iam_role( + session=sts_session, + role_name=role_name, + principal=trust_principal + ) + except ClientError as e: + if e.response['Error']['Code'] == 'EntityAlreadyExists': + logger.info( + f"Existing IAM Role {role_name} found in account {account['Id']}, " + f"removing policies and deleting role." + ) + detach_all_policies_from_role(session=sts_session, role_name=role_name) + delete_role(session=sts_session, role_name=role_name) + iam_role = create_iam_role( + session=sts_session, + role_name=role_name, + principal=trust_principal + ) + else: + logger.info(f"Exception occurred while creating role {role_name} in {account['Id']}: {e}") + + # Create or recreate each policy in policy_package["Docs"], then attach + for policy_doc in policy_package["Docs"]: + logger.info(f"Policy_Doc: {json.dumps(policy_doc)}") + try: + policy = create_iam_policy( + session=sts_session, + policy_name=policy_doc['Statement'][0]['Sid'], + policy_document=json.dumps(policy_doc) + ) + except ClientError as e: + if e.response['Error']['Code'] == 'EntityAlreadyExists': + logger.info(f"Existing Policy {policy_doc['Statement'][0]['Sid']} found. Recreating it.") + try: + delete_iam_policy( + session=sts_session, + policy_arn=f"arn:aws:iam::{account['Id']}:policy/{policy_doc['Statement'][0]['Sid']}" + ) + policy = create_iam_policy( + session=sts_session, + policy_name=policy_doc['Statement'][0]['Sid'], + policy_document=json.dumps(policy_doc) + ) + except Exception as e2: + logger.info( + f"Error recreating {policy_doc['Statement'][0]['Sid']}. Exception: {e2}" + ) + else: + logger.info( + f"Exception occurred while creating policy {policy_doc['Statement'][0]['Sid']}: {e}" + ) + + try: + attach_iam_policy_to_role( + session=sts_session, + role_name=iam_role['RoleName'], + policy_arn=policy['Arn'] + ) + except Exception as err: + logger.info( + f"Error attaching {iam_role['RoleName']} to {policy['Arn']}. Exception: {err}" + ) + + # At this point, all operations completed successfully for this event + send(cfn_event, context, SUCCESS, response_data) + # Cancel further reinvocations + stop_reinvocation = True + + # Handle Delete + elif request_type == 'Delete': + logger.info(f"CFN {request_type} request received") + for account in accounts: + # Skip the management account + if account_id in account['Id']: + continue + + try: + sts_session = assume_role(session=session, account_id=account['Id'], role_name=switch_role) + except Exception as err: + logger.error(f"Error assuming role: {err}") + response_data['Error'] = f"Error assuming role: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + # Detach all policies, delete the role + try: + detach_all_policies_from_role(session=sts_session, role_name=role_name) + delete_role(session=sts_session, role_name=role_name) + except Exception as err: + logger.info(f"Error deleting role {role_name}. Exception: {err}") + + # Delete each custom policy in policy_package["Docs"] + for policy_doc in policy_package["Docs"]: + try: + delete_iam_policy( + session=sts_session, + policy_arn=f"arn:aws:iam::{account['Id']}:policy/{policy_doc['Statement'][0]['Sid']}" + ) + except Exception as err: + logger.info( + f"Deleting Policy {policy_doc} in {account['Id']} failed. Exception: {err}" + ) + + rs = cfn_event['PhysicalResourceId'] + response_data['lower'] = rs.lower() if rs else '' + send(cfn_event, context, SUCCESS, response_data) + + # Cancel further reinvocations on success + stop_reinvocation = True + + else: + # Unknown RequestType + send(cfn_event, context, FAILED, response_data, response_data.get('lower', None)) + stop_reinvocation = True + except Exception as err: + logger.Error(f"Error occurred: {err}") + t.join() + raise err EventBridgeCreateAccount: diff --git a/arch/templates/main.yaml b/arch/templates/main.yaml index 8885500f..6616a604 100644 --- a/arch/templates/main.yaml +++ b/arch/templates/main.yaml @@ -20,6 +20,7 @@ Metadata: - RootOUID - SecurityOUID - TenantId + - DefaultCloudProfile - Label: default: Optional Parameters Parameters: @@ -36,6 +37,8 @@ Metadata: default: The ID of the organization TenantId: default: The ID of the tenant + DefaultCloudProfile: + default: The cloud profile to use when one is not provided by an account ExecutionName: default: Execution name of the IAM role that is passed to GC permission's RootOUID: @@ -88,6 +91,10 @@ Parameters: Type: String Description: >- Tenant Id provided by root.yaml. + DefaultCloudProfile: + Type: String + Description: >- + The cloud profile to use when one is not provided by an account. ExecutionName: Type: String Description: >- @@ -198,6 +205,23 @@ Resources: - sts:AssumeRole Path: "/" Policies: + - PolicyName: self_invoke + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunctionUrl + - lambda:InvokeFunction + - lambda:GetFunctionEventInvokeConfig + - lambda:GetFunction + - lambda:InvokeAsync + - lambda:GetAlias + Resource: !Sub "arn:${AWS::Partition}:lambda:*:${AWS::AccountId}:function:${OrganizationName}aws_create_role" + - Effect: Allow + Action: + - lambda:ListFunctions + Resource: "*" - PolicyName: assume_role PolicyDocument: Version: "2012-10-17" @@ -266,6 +290,7 @@ Resources: "kinesis:DescribeStream", "kinesis:ListStreams", "memorydb:Describe*", + "organizations:ListTagsForResource", "organizations:Describe*", "organizations:List*", "qldb:DescribeLedger", @@ -391,6 +416,7 @@ Resources: "kinesis:DescribeStream", "kinesis:ListStreams", "memorydb:Describe*", + "organizations:List*", "qldb:DescribeLedger", "qldb:ListLedgers", "rds:Describe*", @@ -666,6 +692,8 @@ Resources: ParameterValue: !Ref RolePrefix - ParameterKey: AccelRolePrefix ParameterValue: !Ref AccelRolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -705,6 +733,8 @@ Resources: ParameterValue: !Ref AuditAccountID - ParameterKey: RolePrefix ParameterValue: !Ref RolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -744,6 +774,8 @@ Resources: ParameterValue: !Ref AuditAccountID - ParameterKey: RolePrefix ParameterValue: !Ref RolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -783,6 +815,8 @@ Resources: ParameterValue: !Ref AuditAccountID - ParameterKey: RolePrefix ParameterValue: !Ref RolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -823,6 +857,8 @@ Resources: ParameterValue: !Ref AuditAccountID - ParameterKey: RolePrefix ParameterValue: !Ref RolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -862,6 +898,8 @@ Resources: ParameterValue: !Ref AuditAccountID - ParameterKey: RolePrefix ParameterValue: !Ref RolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -901,6 +939,8 @@ Resources: ParameterValue: !Ref AuditAccountID - ParameterKey: RolePrefix ParameterValue: !Ref RolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -940,6 +980,8 @@ Resources: ParameterValue: !Ref AuditAccountID - ParameterKey: RolePrefix ParameterValue: !Ref RolePrefix + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile PermissionModel: SERVICE_MANAGED StackInstancesGroup: - DeploymentTargets: @@ -1384,6 +1426,7 @@ Resources: PolicyName: !Sub "${OrganizationName}GCLambdaExecutionRoleOrganizationsPolicy" Roles: - !Ref GCLambdaExecutionRole + - !Ref GCLambdaExecutionRole2 # IAM Access GCLambdaExecutionRoleCloudFrontPolicy: @@ -1735,6 +1778,8 @@ Resources: Parameters: - ParameterKey: AuditAccountID ParameterValue: !Ref AuditAccountID + - ParameterKey: DefaultCloudProfile + ParameterValue: !Ref DefaultCloudProfile - ParameterKey: DeployVersion ParameterValue: !Ref DeployVersion - ParameterKey: DestBucketName diff --git a/arch/templates/root.yaml b/arch/templates/root.yaml index bd53ecc2..4cfb23eb 100644 --- a/arch/templates/root.yaml +++ b/arch/templates/root.yaml @@ -12,6 +12,9 @@ Parameters: Type: String Default: "*CSPM_EMAIL*" Description: Subject to change, might not be used here + DefaultCloudProfile: + Type: String + AllowedValues: [ "1", "2", "3", "4", "5", "6"] OrganizationId: Type: String Default: "*ORG_ID*" @@ -86,6 +89,7 @@ Resources: OrganizationName: !Ref OrganizationName TenantId: !Ref TenantId RolePrefix: !Ref RolePrefix + DefaultCloudProfile: !Ref DefaultCloudProfile AccelRolePrefix: !Ref AccelRolePrefix AcceleratorRole: !Ref AcceleratorRole SecurityOUID: !Ref SecurityOUID diff --git a/src/lambda/aws_compile_audit_report/app.py b/src/lambda/aws_compile_audit_report/app.py index f078f850..95413b73 100644 --- a/src/lambda/aws_compile_audit_report/app.py +++ b/src/lambda/aws_compile_audit_report/app.py @@ -8,6 +8,9 @@ import os import boto3 +from utils import get_cloud_profile_from_tags +from boto_util.client import get_client +from boto_util.organizations import get_account_tags assessment_name = os.environ["ASSESSMENT_NAME"] cac_version = os.environ["CAC_VERSION"] @@ -28,9 +31,10 @@ def lambda_handler(event, context): logger.setLevel(logging.INFO) logger.info("start audit manager get assessments") logger.info("Received Event: %s", json.dumps(event, indent=2)) - + header = [ "accountId", + "accountCloudProfile", "dataSource", "guardrail", "controlName", @@ -61,12 +65,16 @@ def lambda_handler(event, context): control_id = folder["controlName"] evidences = get_evidence_by_evidence_folders(assessment_id, control_set_id, folder_id) for item in evidences: + aws_account_id = item["evidenceAwsAccountId"] + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) if item["time"] > (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(1)): rows = [] if len(item["resourcesIncluded"]) == 0: rows.append( [ - item["evidenceAwsAccountId"], + aws_account_id, + str(cloud_profile.value), item["dataSource"], control_set_id, control_id, @@ -84,7 +92,8 @@ def lambda_handler(event, context): for sub_evidence in item["resourcesIncluded"]: rows.append( [ - item["evidenceAwsAccountId"], + aws_account_id, + str(cloud_profile.value), item["dataSource"], control_set_id, control_id, @@ -102,7 +111,8 @@ def lambda_handler(event, context): if "value" not in item["resourcesIncluded"][0]: rows.append( [ - item["evidenceAwsAccountId"], + aws_account_id, + str(cloud_profile.value), item["dataSource"], control_set_id, control_id, @@ -119,7 +129,8 @@ def lambda_handler(event, context): else: rows.append( [ - item["evidenceAwsAccountId"], + aws_account_id, + str(cloud_profile.value), item["dataSource"], control_set_id, control_id, diff --git a/src/lambda/aws_create_role/app.py b/src/lambda/aws_create_role/app.py index 8e9bad6f..ab858298 100644 --- a/src/lambda/aws_create_role/app.py +++ b/src/lambda/aws_create_role/app.py @@ -1,4 +1,3 @@ -""" Lambda function used to assume a deployment role in all organizational accounts. """ import json import logging import threading @@ -6,28 +5,28 @@ import boto3 from botocore.exceptions import ClientError import urllib3 - + SUCCESS = "SUCCESS" FAILED = "FAILED" - + # cfnresponse replacement http = urllib3.PoolManager() - + logger = logging.getLogger() logger.setLevel(logging.INFO) - + # --- BEGIN REINVOCATION LOGIC CONFIG --- MAX_REINVOCATIONS = 5 TIMEOUT_MINUTES = 10 stop_reinvocation = False # Flag used to cancel reinvocation once function completes - -def schedule_reinvocation(reinvoke_count, original_event, context): + +def schedule_reinvocation(reinvoke_count, original_event, context, time_remaining_in_seconds): """ Sleeps for TIMEOUT_MINUTES. If stop_reinvocation is still False after that time, re-invokes this Lambda function asynchronously with reinvoke_count + 1, provided we haven't hit MAX_REINVOCATIONS. """ - time.sleep(TIMEOUT_MINUTES * 60) + time.sleep(time_remaining_in_seconds) if not stop_reinvocation: if reinvoke_count < MAX_REINVOCATIONS: logger.info( @@ -38,7 +37,7 @@ def schedule_reinvocation(reinvoke_count, original_event, context): # Make a shallow copy so we don't mutate the original event new_event = dict(original_event) new_event['ReinvokeCount'] = reinvoke_count + 1 - + lambda_client.invoke( FunctionName=context.function_name, # lambda name InvocationType='Event', # async invocation @@ -47,7 +46,7 @@ def schedule_reinvocation(reinvoke_count, original_event, context): else: logger.info("Reached max reinvocations. No further attempts.") # --- END REINVOCATION LOGIC CONFIG --- - + def get_org_accounts(): """ Returns a list of all active accounts in the organization. @@ -55,16 +54,15 @@ def get_org_accounts(): org_client = boto3.client('organizations') active_accounts = [] paginator = org_client.get_paginator('list_accounts') - + for page in paginator.paginate(): for account in page['Accounts']: if account['Status'] == "ACTIVE": logger.info(f"Account added {account}") active_accounts.append(account) - + return active_accounts - - + def create_iam_policy(session, policy_name, policy_document): """ Creates an IAM policy. @@ -76,8 +74,7 @@ def create_iam_policy(session, policy_name, policy_document): PolicyDocument=policy_document ) return sts_response['Policy'] - - + def attach_iam_policy_to_role(session, role_name, policy_arn): """ Attaches an IAM policy to a role. @@ -89,8 +86,7 @@ def attach_iam_policy_to_role(session, role_name, policy_arn): PolicyArn=policy_arn ) return sts_response - - + def create_iam_role(session, role_name, principal): """ Creates an IAM role. @@ -114,8 +110,7 @@ def create_iam_role(session, role_name, principal): AssumeRolePolicyDocument=json.dumps(assume_role_policy_document) ) return sts_response['Role'] - - + def delete_role(session, role_name): """ Deletes an IAM role. @@ -126,48 +121,41 @@ def delete_role(session, role_name): RoleName=role_name ) return sts_response - - + def detach_all_policies_from_role(session, role_name): """ - Detaches all policies from a role. + Detaches all policies from a role and deletes any custom policies. """ logger.info(f"Detach all policies from {role_name}") iam_client = session.client('iam') - sts_response = iam_client.list_attached_role_policies( - RoleName=role_name - ) - + sts_response = iam_client.list_attached_role_policies(RoleName=role_name) + logger.info(f"Attached Policies to role {role_name} are {sts_response['AttachedPolicies']}") for policy in sts_response['AttachedPolicies']: logger.info(f"Deleting Policy: {policy}") - sts_response = iam_client.detach_role_policy( + iam_client.detach_role_policy( RoleName=role_name, PolicyArn=policy['PolicyArn'] ) delete_iam_policy(session=session, policy_arn=policy['PolicyArn']) - - sts_response = iam_client.list_role_policies( - RoleName=role_name - ) + + sts_response = iam_client.list_role_policies(RoleName=role_name) logger.info(f"Inline Policies attached to role {role_name} are {sts_response['PolicyNames']}") for policy_name in sts_response['PolicyNames']: logger.info(f"Deleting Policy: {policy_name}") - sts_response = iam_client.delete_role_policy( + iam_client.delete_role_policy( RoleName=role_name, PolicyName=policy_name ) return sts_response - - + def get_account_id(session): """ Returns the account ID. """ sts_client = session.client('sts') return sts_client.get_caller_identity()['Account'] - - + def assume_role(session, account_id, role_name): """ Assumes a role. @@ -183,21 +171,20 @@ def assume_role(session, account_id, role_name): aws_session_token=sts_response['Credentials']['SessionToken'] ) return sts_session - - + def delete_iam_policy(session, policy_arn): """ - Deletes an IAM policy. + Deletes an IAM policy, skipping AWS managed policies. """ # Check if the policy is an AWS managed policy if policy_arn.startswith("arn:aws:iam::aws:policy/"): logger.info(f"Skipping deletion of AWS managed policy: {policy_arn}") return {"Status": "Skipped", "Reason": "AWS Managed Policy"} - + iam_client = session.client('iam') # List all versions of the policy versions = iam_client.list_policy_versions(PolicyArn=policy_arn) - + # Loop through the versions and delete the non-default versions for version in versions['Versions']: if not version['IsDefaultVersion']: @@ -206,24 +193,28 @@ def delete_iam_policy(session, policy_arn): VersionId=version['VersionId'] ) logger.info(f"Deleted non-default policy version {version['VersionId']}") - - sts_response = iam_client.delete_policy( - PolicyArn=policy_arn - ) + + sts_response = iam_client.delete_policy(PolicyArn=policy_arn) return sts_response - - + def send(event, context, response_status, response_data, physical_resource_id=None, no_echo=False, reason=None): - """Sends a response to CloudFormation""" + """ + Sends a response to CloudFormation. We skip this if the event + doesn't actually come from CloudFormation (i.e., no ResponseURL). + """ + if 'ResponseURL' not in event or not event['ResponseURL']: + logger.info("No CFN ResponseURL found. Skipping send() for non-CFN scenario.") + return + response_url = event['ResponseURL'] logger.info("Response URL: %s", response_url) response_body = { 'Status': response_status, 'Reason': reason or f"See the details in CloudWatch Log Stream: {context.log_stream_name}", 'PhysicalResourceId': physical_resource_id or context.log_stream_name, - 'StackId': event['StackId'], - 'RequestId': event['RequestId'], - 'LogicalResourceId': event['LogicalResourceId'], + 'StackId': event.get('StackId', 'NoStackId'), + 'RequestId': event.get('RequestId', 'NoRequestId'), + 'LogicalResourceId': event.get('LogicalResourceId', 'NoLogicalResourceId'), 'NoEcho': no_echo, 'Data': response_data } @@ -236,177 +227,264 @@ def send(event, context, response_status, response_data, physical_resource_id=No logger.info("Status code: %s", response.status) except (ValueError, TypeError, urllib3.exceptions.HTTPError) as err: logger.error("send(..) failed executing http.request(..): %s", err) - - + +def normalize_event(event): + """ + If 'RequestType' is present, assume it's a CFN event and wrap it in a list. + If 'Roles' is present, assume it's an EventBridge event. Convert each role + into a CFN-like event so we can reuse the same logic. + If 'Roles' is present but empty, return an empty list of events so that + the main loop will skip any processing. + """ + + # CloudFormation event + if 'RequestType' in event and 'ResourceProperties' in event: + return [event] # It's already in CFN format + + # EventBridge event with a list of roles + if 'Roles' in event: + # If the roles array is empty, skip further processing + if not event['Roles']: + logger.info("No roles found in the 'Roles' array. Returning an empty list.") + return [] + + normalized = [] + for role in event['Roles']: + # Build a new "CFN-like" event + cfn_style_event = { + "RequestType": "Update", # We treat these as "Update" for creation logic + "ResourceProperties": { + "RoleName": role["Name"], + "TrustPrincipal": role["TrustPrincipal"], + "SwitchRole": role["SwitchRole"], + # The original code expects a string for PolicyPackage + "PolicyPackage": json.dumps(role["PolicyPackage"]) + }, + # Fake CFN required fields (since we won't actually call send() for these, + # or if we do, we skip if there's no real CFN ResponseURL). + "ResponseURL": None, + "StackId": "EventBridgeStack", + "RequestId": "EventBridgeRequest", + "LogicalResourceId": "EventBridgeLogicalId", + "PhysicalResourceId": "EventBridgePhysicalId" + } + normalized.append(cfn_style_event) + return normalized + + # Unknown event type + raise ValueError("Event does not contain 'RequestType' or 'Roles'. Cannot normalize.") + def lambda_handler(event, context): """ - This function is the main entry point for Lambda. + Main entry point for Lambda. We unify the handling of both CFN and EventBridge + events by first normalizing them into a list of CFN-like events and then using + the existing flow. """ global stop_reinvocation - + # --- REINVOCATION: read ReinvokeCount from event --- reinvoke_count = event.get('ReinvokeCount', 0) - - # Start the background timer thread to automatically re-invoke after 10 minutes - t = threading.Thread(target=schedule_reinvocation, args=(reinvoke_count, event, context)) + + # Start the background timer thread to automatically re-invoke after TIMEOUT_MINUTES + time_remaining_in_seconds = (context.get_remaining_time_in_millis() / 1000) - 10 + t = threading.Thread(target=schedule_reinvocation, args=(reinvoke_count, event, context, time_remaining_in_seconds)) t.daemon = True t.start() # --- END REINVOCATION SETUP --- - - response_data = {} - logger.info("Received Event: %s", json.dumps(event, indent=2)) - - # Get parameters from event - trust_principal = event['ResourceProperties']['TrustPrincipal'].split(',') - switch_role = event['ResourceProperties']['SwitchRole'] - role_name = event['ResourceProperties']['RoleName'] - policy_string = event['ResourceProperties']['PolicyPackage'].replace("'", '"') - logger.info(f"Policy String: {policy_string}") - policy_package = json.loads(policy_string, strict=False) - - # Get list of Org accounts and the current account_id - try: - accounts = get_org_accounts() - except Exception as err: - logger.error(f"Error getting accounts: {err}") - response_data['Error'] = f"Error getting accounts: {err}" - send(event, context, FAILED, response_data) - raise err - - try: - session = boto3.Session() - except Exception as err: - logger.error(f"Error getting session: {err}") - response_data['Error'] = f"Error getting session: {err}" - send(event, context, FAILED, response_data) - raise err - + try: - account_id = get_account_id(session) - except Exception as err: - logger.error(f"Error getting account ID: {err}") - response_data['Error'] = f"Error getting account ID: {err}" - send(event, context, FAILED, response_data) - raise err - - if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': - logger.info(f"CFN {event['RequestType']} request received") - for account in accounts: - # Skip the management account - if account_id in account['Id']: - continue - + logger.info("Received Event: %s", json.dumps(event, indent=2)) + + try: + # Convert the incoming event to a CFN-like list + cfn_events = normalize_event(event) + except Exception as err: + logger.error(f"Error normalizing event: {err}") + response_data = {"Error": str(err)} + send(event, context, FAILED, response_data) + stop_reinvocation = True + return + + # If the list of cfn_events is empty (e.g., empty Roles), do nothing + if not cfn_events: + logger.info("No events to process after normalization. Exiting.") + stop_reinvocation = True + return + + # Process each "synthetic CFN event" in the same Lambda invocation + for cfn_event in cfn_events: + response_data = {} + + # Pull request type from the normalized event (Create/Update/Delete). + request_type = cfn_event['RequestType'] + + # Extract the ResourceProperties we need + resource_props = cfn_event['ResourceProperties'] + trust_principal = resource_props['TrustPrincipal'].split(',') + switch_role = resource_props['SwitchRole'] + role_name = resource_props['RoleName'] + policy_string = resource_props['PolicyPackage'].replace("'", '"') + logger.info(f"Policy String: {policy_string}") + policy_package = json.loads(policy_string, strict=False) + + # Get list of Org accounts and the current account_id try: - sts_session = assume_role(session=session, account_id=account['Id'], role_name=switch_role) + accounts = get_org_accounts() except Exception as err: - logger.error(f"Error assuming role: {err}") - response_data['Error'] = f"Error assuming role: {err}" - send(event, context, FAILED, response_data) + logger.error(f"Error getting accounts: {err}") + response_data['Error'] = f"Error getting accounts: {err}" + send(cfn_event, context, FAILED, response_data) raise err - + try: - iam_role = create_iam_role(session=sts_session, role_name=role_name, principal=trust_principal) - except ClientError as e: - if e.response['Error']['Code'] == 'EntityAlreadyExists': - logger.info( - f"Existing IAM Role {role_name} found in account {account['Id']}, " - f"removing policies and deleting role." - ) - detach_all_policies_from_role(session=sts_session, role_name=role_name) - delete_role(session=sts_session, role_name=role_name) - iam_role = create_iam_role(session=sts_session, role_name=role_name, principal=trust_principal) - else: - logger.info(f"Exception occurred while creating role {role_name} in {account['Id']}: {e}") - - for policy_doc in policy_package['Docs']: - logger.info(f"Policy_Doc: {json.dumps(policy_doc)}") - try: - policy = create_iam_policy( - session=sts_session, - policy_name=policy_doc['Statement'][0]['Sid'], - policy_document=json.dumps(policy_doc) - ) - except ClientError as e: - if e.response['Error']['Code'] == 'EntityAlreadyExists': - logger.info(f"Existing Policy {policy_doc['Statement'][0]['Sid']} found. Recreating it.") - try: - delete_iam_policy( + session = boto3.Session() + except Exception as err: + logger.error(f"Error getting session: {err}") + response_data['Error'] = f"Error getting session: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + try: + account_id = get_account_id(session) + except Exception as err: + logger.error(f"Error getting account ID: {err}") + response_data['Error'] = f"Error getting account ID: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + # Handle Create/Update + if request_type == 'Create' or request_type == 'Update': + logger.info(f"CFN {request_type} request received") + for account in accounts: + # Skip the management account + if account_id in account['Id']: + continue + + try: + sts_session = assume_role(session=session, account_id=account['Id'], role_name=switch_role) + except Exception as err: + logger.error(f"Error assuming role: {err}") + response_data['Error'] = f"Error assuming role: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + # Create or recreate the IAM role + try: + iam_role = create_iam_role( + session=sts_session, + role_name=role_name, + principal=trust_principal + ) + except ClientError as e: + if e.response['Error']['Code'] == 'EntityAlreadyExists': + logger.info( + f"Existing IAM Role {role_name} found in account {account['Id']}, " + f"removing policies and deleting role." + ) + detach_all_policies_from_role(session=sts_session, role_name=role_name) + delete_role(session=sts_session, role_name=role_name) + iam_role = create_iam_role( session=sts_session, - policy_arn=f"arn:aws:iam::{account['Id']}:policy/{policy_doc['Statement'][0]['Sid']}" + role_name=role_name, + principal=trust_principal ) + else: + logger.info(f"Exception occurred while creating role {role_name} in {account['Id']}: {e}") + + # Create or recreate each policy in policy_package["Docs"], then attach + for policy_doc in policy_package["Docs"]: + logger.info(f"Policy_Doc: {json.dumps(policy_doc)}") + try: policy = create_iam_policy( session=sts_session, policy_name=policy_doc['Statement'][0]['Sid'], policy_document=json.dumps(policy_doc) ) - except Exception as e2: + except ClientError as e: + if e.response['Error']['Code'] == 'EntityAlreadyExists': + logger.info(f"Existing Policy {policy_doc['Statement'][0]['Sid']} found. Recreating it.") + try: + delete_iam_policy( + session=sts_session, + policy_arn=f"arn:aws:iam::{account['Id']}:policy/{policy_doc['Statement'][0]['Sid']}" + ) + policy = create_iam_policy( + session=sts_session, + policy_name=policy_doc['Statement'][0]['Sid'], + policy_document=json.dumps(policy_doc) + ) + except Exception as e2: + logger.info( + f"Error recreating {policy_doc['Statement'][0]['Sid']}. Exception: {e2}" + ) + else: + logger.info( + f"Exception occurred while creating policy {policy_doc['Statement'][0]['Sid']}: {e}" + ) + + try: + attach_iam_policy_to_role( + session=sts_session, + role_name=iam_role['RoleName'], + policy_arn=policy['Arn'] + ) + except Exception as err: logger.info( - f"Error recreating {policy_doc['Statement'][0]['Sid']}. Exception: {e2}" + f"Error attaching {iam_role['RoleName']} to {policy['Arn']}. Exception: {err}" ) - else: - logger.info( - f"Exception occurred while creating policy {policy_doc['Statement'][0]['Sid']}: {e}" - ) - - try: - attach_iam_policy_to_role( - session=sts_session, - role_name=iam_role['RoleName'], - policy_arn=policy['Arn'] - ) - except Exception as err: - logger.info( - f"Error attaching {iam_role['RoleName']} to {policy['Arn']}. Exception: {err}" - ) - - # At this point, all operations completed successfully - send(event, context, SUCCESS, response_data) - # Cancel further reinvocations - stop_reinvocation = True - - elif event['RequestType'] == 'Delete': - logger.info(f"CFN {event['RequestType']} request received") - for account in accounts: - # Skip the management account - if account_id in account['Id']: - continue - - try: - sts_session = assume_role(session=session, account_id=account['Id'], role_name=switch_role) - except Exception as err: - logger.error(f"Error assuming role: {err}") - response_data['Error'] = f"Error assuming role: {err}" - send(event, context, FAILED, response_data) - raise err - - try: - detach_all_policies_from_role(session=sts_session, role_name=role_name) - delete_role(session=sts_session, role_name=role_name) - except Exception as err: - logger.info(f"Error deleting role {role_name}. Exception: {err}") - - # The original code in Delete references policy_package as a list of docs - # Ensure it aligns with the JSON structure you pass: - # In the original code, it was for policy_doc in policy_package (not policy_package['Docs']). - for policy_doc in policy_package: - try: - delete_iam_policy( - session=sts_session, - policy_arn=f"arn:aws:iam::{account['Id']}:policy/{policy_doc['Statement'][0]['Sid']}" - ) - except Exception as err: - logger.info( - f"Deleting Policy {policy_doc} in {account['Id']} failed. Exception: {err}" - ) - policy = {} - - rs = event['PhysicalResourceId'] - response_data['lower'] = rs.lower() - send(event, context, SUCCESS, response_data) - # Cancel further reinvocations on success - stop_reinvocation = True - - else: - # Unknown RequestType - send(event, context, FAILED, response_data, response_data.get('lower', None)) - stop_reinvocation = True + + # At this point, all operations completed successfully for this event + send(cfn_event, context, SUCCESS, response_data) + # Cancel further reinvocations + stop_reinvocation = True + + # Handle Delete + elif request_type == 'Delete': + logger.info(f"CFN {request_type} request received") + for account in accounts: + # Skip the management account + if account_id in account['Id']: + continue + + try: + sts_session = assume_role(session=session, account_id=account['Id'], role_name=switch_role) + except Exception as err: + logger.error(f"Error assuming role: {err}") + response_data['Error'] = f"Error assuming role: {err}" + send(cfn_event, context, FAILED, response_data) + raise err + + # Detach all policies, delete the role + try: + detach_all_policies_from_role(session=sts_session, role_name=role_name) + delete_role(session=sts_session, role_name=role_name) + except Exception as err: + logger.info(f"Error deleting role {role_name}. Exception: {err}") + + # Delete each custom policy in policy_package["Docs"] + for policy_doc in policy_package["Docs"]: + try: + delete_iam_policy( + session=sts_session, + policy_arn=f"arn:aws:iam::{account['Id']}:policy/{policy_doc['Statement'][0]['Sid']}" + ) + except Exception as err: + logger.info( + f"Deleting Policy {policy_doc} in {account['Id']} failed. Exception: {err}" + ) + + rs = cfn_event['PhysicalResourceId'] + response_data['lower'] = rs.lower() if rs else '' + send(cfn_event, context, SUCCESS, response_data) + + # Cancel further reinvocations on success + stop_reinvocation = True + + else: + # Unknown RequestType + send(cfn_event, context, FAILED, response_data, response_data.get('lower', None)) + stop_reinvocation = True + except Exception as err: + logger.Error(f"Error occurred: {err}") + t.join() + raise err \ No newline at end of file diff --git a/src/lambda/gc01_check_alerts_flag_misuse/app.py b/src/lambda/gc01_check_alerts_flag_misuse/app.py index 93ff3e67..7f3cd615 100644 --- a/src/lambda/gc01_check_alerts_flag_misuse/app.py +++ b/src/lambda/gc01_check_alerts_flag_misuse/app.py @@ -8,7 +8,8 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists, read_s3_object @@ -100,82 +101,52 @@ def check_rule_sns_target_is_setup(sns_client, event_bridge_client, rule, event) resource_type=resource_type, annotation="An Event rule that has a SNS topic and subscription to send notification emails is not setup or confirmed.", ) - - -def lambda_handler(event, context): - """ - This function is the main entry point for Lambda. - - Keyword arguments: - - event -- the event variable given in the lambda handler - - context -- the context variable given in the lambda handler - """ - logger.info("Received Event: %s", json.dumps(event, indent=2)) - - invoking_event = json.loads(event["invokingEvent"]) - if not is_scheduled_notification(invoking_event["messageType"]): - logger.error("Skipping assessments as this is not a scheduled invocation") - return - - rule_parameters = check_required_parameters(json.loads(event.get("ruleParameters", "{}")), ["ExecutionRoleName"]) - execution_role_name = rule_parameters.get("ExecutionRoleName") - audit_account_id = rule_parameters.get("AuditAccountID", "") - aws_account_id = event["accountId"] - is_not_audit_account = aws_account_id != audit_account_id - evaluations = [] - - aws_config_client = get_client("config", aws_account_id, execution_role_name) - aws_s3_client = get_client("s3") - aws_guard_duty_client = get_client("guardduty", aws_account_id, execution_role_name) - aws_event_bridge_client = get_client("events", aws_account_id, execution_role_name) - aws_sns_client = get_client("sns", aws_account_id, execution_role_name) - + +def check_alerts_flag_misuse(event, rule_parameters, aws_account_id, evaluations, aws_s3_client, aws_guard_duty_client, aws_event_bridge_client, aws_sns_client): rules = list_all_event_bridge_rules(aws_event_bridge_client) guard_duty_is_setup = False if guard_duty_is_enabled(aws_guard_duty_client): - # yes, filter for rules that target GuardDuty findings + # yes, filter for rules that target GuardDuty findings logger.info("GuardDuty is enabled.") guardduty_rules = [r for r in rules if rule_event_pattern_matches_guard_duty_findings(r.get("EventPattern"))] logger.info("GuardDuty rules count: %d", len(guardduty_rules)) - # are there any rules that target GuardDuty findings + # are there any rules that target GuardDuty findings if len(guardduty_rules) > 0: - # yes, check that an SNS target is setup that sends an email notification to authorized personnel + # yes, check that an SNS target is setup that sends an email notification to authorized personnel for rule in guardduty_rules: eval = check_rule_sns_target_is_setup(aws_sns_client, aws_event_bridge_client, rule, event) if eval.get("ComplianceType") == "COMPLIANT": guard_duty_is_setup = True evaluations.append(eval) logger.info( - "GuardDuty is setup and rules are setup to notify authorized personnel: %s", guard_duty_is_setup - ) + "GuardDuty is setup and rules are setup to notify authorized personnel: %s", guard_duty_is_setup + ) - # are the GuardDuty rules found to be COMPLIANT? + # are the GuardDuty rules found to be COMPLIANT? if guard_duty_is_setup: - # yes, add compliance evaluation for account + # yes, add compliance evaluation for account evaluations.append( - build_evaluation( - aws_account_id, - "COMPLIANT", - event, - annotation="GuardDuty is enabled, and a rule is setup to notify authorized personnel of GuardDuty findings.", + build_evaluation( + aws_account_id, + "COMPLIANT", + event, + annotation="GuardDuty is enabled, and a rule is setup to notify authorized personnel of GuardDuty findings.", + ) ) - ) else: - # no, check for EventBridge rules with naming convention + # no, check for EventBridge rules with naming convention rule_naming_convention_file_path = rule_parameters.get("RuleNamingConventionFilePath", "") if not check_s3_object_exists(aws_s3_client, rule_naming_convention_file_path): evaluations.append( - build_evaluation( - aws_account_id, - "NON_COMPLIANT", - event, - annotation="No RuleNamingConventionFilePath input provided.", + build_evaluation( + aws_account_id, + "NON_COMPLIANT", + event, + annotation="No RuleNamingConventionFilePath input provided.", + ) ) - ) else: evaluations = [] is_compliant = False @@ -184,36 +155,94 @@ def lambda_handler(event, context): logger.info("Filtering rules by rule_naming_convention: %s", rule_naming_convention) filtered_rules = [r for r in rules if reg.search(r.get("Name", ""))] - # are there any rules matching the naming convention? + # are there any rules matching the naming convention? if len(filtered_rules) > 0: - # yes, check that an SNS target is setup that sends an email notification to authorized personnel + # yes, check that an SNS target is setup that sends an email notification to authorized personnel for rule in filtered_rules: eval = check_rule_sns_target_is_setup(aws_sns_client, aws_event_bridge_client, rule, event) if eval.get("ComplianceType") == "COMPLIANT": is_compliant = True evaluations.append(eval) - # are EventBridge rules setup to notify authorized personnel of misuse? + # are EventBridge rules setup to notify authorized personnel of misuse? if is_compliant: - # yes, append COMPLIANT results for account + # yes, append COMPLIANT results for account evaluations.append( - build_evaluation( - aws_account_id, - "COMPLIANT", - event, - annotation="EventBridge rules have been setup to notify authorized personnel of misuse or suspicious activity.", + build_evaluation( + aws_account_id, + "COMPLIANT", + event, + annotation="EventBridge rules have been setup to notify authorized personnel of misuse or suspicious activity.", + ) ) - ) else: - # no, append to NON_COMPLIANT results for account + # no, append to NON_COMPLIANT results for account evaluations.append( - build_evaluation( - aws_account_id, - "NON_COMPLIANT", - event, - annotation="GuardDuty is not enabled and there are no EventBridge rules setup to notify authorized personnel of misuse or suspicious activity.", + build_evaluation( + aws_account_id, + "NON_COMPLIANT", + event, + annotation="GuardDuty is not enabled and there are no EventBridge rules setup to notify authorized personnel of misuse or suspicious activity.", + ) ) - ) + + return evaluations + + +def lambda_handler(event, context): + """ + This function is the main entry point for Lambda. + + Keyword arguments: + + event -- the event variable given in the lambda handler + + context -- the context variable given in the lambda handler + """ + logger.info("Received Event: %s", json.dumps(event, indent=2)) + + invoking_event = json.loads(event["invokingEvent"]) + if not is_scheduled_notification(invoking_event["messageType"]): + logger.error("Skipping assessments as this is not a scheduled invocation") + return + + rule_parameters = check_required_parameters(json.loads(event.get("ruleParameters", "{}")), ["ExecutionRoleName"]) + execution_role_name = rule_parameters.get("ExecutionRoleName") + audit_account_id = rule_parameters.get("AuditAccountID", "") + aws_account_id = event["accountId"] + is_not_audit_account = aws_account_id != audit_account_id + evaluations = [] + + aws_config_client = get_client("config", aws_account_id, execution_role_name) + aws_s3_client = get_client("s3") + aws_guard_duty_client = get_client("guardduty", aws_account_id, execution_role_name) + aws_event_bridge_client = get_client("events", aws_account_id, execution_role_name) + aws_sns_client = get_client("sns", aws_account_id, execution_role_name) + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + + evaluations = check_alerts_flag_misuse(event, rule_parameters, aws_account_id, evaluations, aws_s3_client, aws_guard_duty_client, aws_event_bridge_client, aws_sns_client) logger.info("AWS Config updating evaluations: %s", evaluations) submit_evaluations(aws_config_client, event["resultToken"], evaluations) + diff --git a/src/lambda/gc01_check_attestation_letter/app.py b/src/lambda/gc01_check_attestation_letter/app.py index 41bbb339..27448f8f 100644 --- a/src/lambda/gc01_check_attestation_letter/app.py +++ b/src/lambda/gc01_check_attestation_letter/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -45,9 +46,31 @@ def lambda_handler(event, context): if is_not_audit_account: logger.info("Attestation letter not checked in account %s - not the Audit account", aws_account_id) return - + aws_config_client = get_client("config") aws_s3_client = get_client("s3") + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" @@ -57,5 +80,6 @@ def lambda_handler(event, context): annotation = "Attestation letter NOT found" logger.info(f"{compliance_type}: {annotation}") + evaluations.append(build_evaluation(aws_account_id, compliance_type, event, annotation=annotation)) submit_evaluations(aws_config_client, event["resultToken"], evaluations) diff --git a/src/lambda/gc01_check_dedicated_admin_account/app.py b/src/lambda/gc01_check_dedicated_admin_account/app.py index 72b1e58d..a4d3d440 100644 --- a/src/lambda/gc01_check_dedicated_admin_account/app.py +++ b/src/lambda/gc01_check_dedicated_admin_account/app.py @@ -7,11 +7,11 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags, get_organizations_mgmt_account_id, organizations_list_all_accounts from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists, get_lines_from_s3_file -from boto_util.organizations import get_organizations_mgmt_account_id, organizations_list_all_accounts from boto_util.iam import list_all_iam_users from boto_util.sso_admin import list_all_sso_admin_instances @@ -453,8 +453,31 @@ def lambda_handler(event, context): aws_sso_admin_client = get_client("sso-admin", aws_account_id, execution_role_name, is_not_audit_account) aws_identity_store_client = get_client("identitystore", aws_account_id, execution_role_name, is_not_audit_account) aws_s3_client = get_client("s3") - + evaluations = [] + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + privileged_users_file_path = rule_parameters.get("PrivilegedUsersFilePath", "") non_privileged_users_file_path = rule_parameters.get("NonPrivilegedUsersFilePath", "") diff --git a/src/lambda/gc01_check_federated_users_mfa/app.py b/src/lambda/gc01_check_federated_users_mfa/app.py index 284cef23..b2834b2f 100644 --- a/src/lambda/gc01_check_federated_users_mfa/app.py +++ b/src/lambda/gc01_check_federated_users_mfa/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.iam import account_has_federated_users @@ -40,8 +41,29 @@ def lambda_handler(event, context): evaluations = [] aws_config_client = get_client("config", aws_account_id, execution_role_name) - aws_iam_client = get_client("iam", aws_account_id, execution_role_name) - + aws_iam_client = get_client("iam", aws_account_id, execution_role_name) + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + compliance_type = "COMPLIANT" annotation = ( "Dependent on the compliance of the Federated IdP." @@ -50,5 +72,6 @@ def lambda_handler(event, context): ) logger.info(f"{compliance_type}: {annotation}") + evaluations.append(build_evaluation(aws_account_id, compliance_type, event, annotation=annotation)) submit_evaluations(aws_config_client, event["resultToken"], evaluations) diff --git a/src/lambda/gc01_check_iam_users_mfa/app.py b/src/lambda/gc01_check_iam_users_mfa/app.py index 2777b022..f6d40873 100644 --- a/src/lambda/gc01_check_iam_users_mfa/app.py +++ b/src/lambda/gc01_check_iam_users_mfa/app.py @@ -7,7 +7,8 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.iam import list_all_iam_users @@ -89,12 +90,35 @@ def lambda_handler(event, context): audit_account_id = rule_parameters.get("AuditAccountID", "") aws_account_id = event["accountId"] is_not_audit_account = aws_account_id != audit_account_id + evaluations = [] bg_accounts = [rule_parameters["BgUser1"], rule_parameters["BgUser2"]] aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) aws_iam_client = get_client("iam", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + evaluations = check_iam_users_mfa(aws_iam_client, aws_account_id, event, bg_accounts) logger.info("AWS Config updating evaluations: %s", evaluations) diff --git a/src/lambda/gc01_check_mfa_digital_policy/app.py b/src/lambda/gc01_check_mfa_digital_policy/app.py index be1e0c90..fc0ca819 100644 --- a/src/lambda/gc01_check_mfa_digital_policy/app.py +++ b/src/lambda/gc01_check_mfa_digital_policy/app.py @@ -4,7 +4,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.iam import account_has_federated_users @@ -40,7 +41,30 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name) aws_iam_client = get_client("iam", aws_account_id, execution_role_name) - + aws_organizations_client = get_client("organizations", aws_account_id, execution_role_name) + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + compliance_type = "COMPLIANT" annotation = ( "Dependent on the compliance of the Federated IdP." diff --git a/src/lambda/gc01_check_monitoring_and_logging/app.py b/src/lambda/gc01_check_monitoring_and_logging/app.py index c2ea271e..353ab96c 100644 --- a/src/lambda/gc01_check_monitoring_and_logging/app.py +++ b/src/lambda/gc01_check_monitoring_and_logging/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.cloud_trail import list_all_cloud_trails @@ -68,7 +69,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name) aws_cloudtrail_client = get_client("cloudtrail", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + trails = list_all_cloud_trails(aws_cloudtrail_client) if trails: diff --git a/src/lambda/gc01_check_root_mfa/app.py b/src/lambda/gc01_check_root_mfa/app.py index 97294e3b..70689236 100644 --- a/src/lambda/gc01_check_root_mfa/app.py +++ b/src/lambda/gc01_check_root_mfa/app.py @@ -8,10 +8,10 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags, get_organizations_mgmt_account_id from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations -from boto_util.organizations import get_organizations_mgmt_account_id # Logging setup logger = logging.getLogger() @@ -137,7 +137,29 @@ def lambda_handler(event, context): evaluations = [] aws_organizations_client = get_client("organizations", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail1, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if aws_account_id != get_organizations_mgmt_account_id(aws_organizations_client): logger.info("Root Account MFA not checked in account %s as this is not the Management Account", aws_account_id) return @@ -153,5 +175,6 @@ def lambda_handler(event, context): annotation = "Root Account MFA NOT enabled." logger.info(f"{compliance_type}: {annotation}") + evaluations.append(build_evaluation(aws_account_id, compliance_type, event, annotation=annotation)) submit_evaluations(aws_config_client, event["resultToken"], evaluations) diff --git a/src/lambda/gc02_check_account_mgmt_plan/app.py b/src/lambda/gc02_check_account_mgmt_plan/app.py index f89a73e0..88bf6900 100644 --- a/src/lambda/gc02_check_account_mgmt_plan/app.py +++ b/src/lambda/gc02_check_account_mgmt_plan/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -48,7 +49,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail2, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Account management plan document found" diff --git a/src/lambda/gc02_check_group_access_configuration/app.py b/src/lambda/gc02_check_group_access_configuration/app.py index 08a0fe72..2a92d17d 100644 --- a/src/lambda/gc02_check_group_access_configuration/app.py +++ b/src/lambda/gc02_check_group_access_configuration/app.py @@ -8,7 +8,8 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists, get_lines_from_s3_file @@ -370,7 +371,29 @@ def lambda_handler(event, context): aws_identity_store_client = get_client("identitystore", aws_account_id, execution_role_name, is_not_audit_account) aws_sso_admin_client = get_client("sso-admin", aws_account_id, execution_role_name, is_not_audit_account) aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail2, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + admin_accounts_s3_path = rule_parameters.get("s3ObjectPath", "") if not check_s3_object_exists(aws_s3_client, admin_accounts_s3_path): compliance_type = "NON_COMPLIANT" diff --git a/src/lambda/gc02_check_iam_password_policy/app.py b/src/lambda/gc02_check_iam_password_policy/app.py index 73cc9d29..335ad0ad 100644 --- a/src/lambda/gc02_check_iam_password_policy/app.py +++ b/src/lambda/gc02_check_iam_password_policy/app.py @@ -7,7 +7,8 @@ import botocore -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.iam import account_has_federated_users @@ -168,7 +169,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) aws_iam_client = get_client("iam", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail2, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + assessment_result = assess_iam_password_policy(aws_iam_client, password_assessment_policy) logger.info(f"{assessment_result["status"]}: {assessment_result["annotation"]}") diff --git a/src/lambda/gc02_check_password_protection_mechanisms/app.py b/src/lambda/gc02_check_password_protection_mechanisms/app.py index fa5b7a7f..86558c5f 100644 --- a/src/lambda/gc02_check_password_protection_mechanisms/app.py +++ b/src/lambda/gc02_check_password_protection_mechanisms/app.py @@ -7,7 +7,8 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.iam import account_has_federated_users @@ -75,7 +76,29 @@ def lambda_handler(event, context): aws_iam_client = get_client("iam", aws_account_id, execution_role_name) aws_guard_duty_client = get_client("guardduty", aws_account_id, execution_role_name) aws_cloudtrail_client = get_client("cloudtrail", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail2, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if guard_duty_is_enabled(aws_guard_duty_client) or is_cloudtrail_enabled(aws_cloudtrail_client): compliance_type = "COMPLIANT" annotation = ( diff --git a/src/lambda/gc02_check_privileged_roles_review/app.py b/src/lambda/gc02_check_privileged_roles_review/app.py index 699021ea..dd9065db 100644 --- a/src/lambda/gc02_check_privileged_roles_review/app.py +++ b/src/lambda/gc02_check_privileged_roles_review/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -50,7 +51,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail2, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Privileged Roles Reviews document found" diff --git a/src/lambda/gc03_check_endpoint_access_config/app.py b/src/lambda/gc03_check_endpoint_access_config/app.py index 639c0fe8..594a3578 100644 --- a/src/lambda/gc03_check_endpoint_access_config/app.py +++ b/src/lambda/gc03_check_endpoint_access_config/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.iam import account_has_federated_users @@ -45,7 +46,29 @@ def lambda_handler(event, context): aws_account_id = event["accountId"] aws_config_client = get_client("config", aws_account_id, execution_role_name) aws_iam_client = get_client("iam", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail3, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + compliance_type = "COMPLIANT" annotation = "Configuration for devices and policies are implemented." if account_has_federated_users(aws_iam_client): diff --git a/src/lambda/gc03_check_iam_cloudwatch_alarms/app.py b/src/lambda/gc03_check_iam_cloudwatch_alarms/app.py index 64b4be8b..8cac9358 100644 --- a/src/lambda/gc03_check_iam_cloudwatch_alarms/app.py +++ b/src/lambda/gc03_check_iam_cloudwatch_alarms/app.py @@ -5,10 +5,10 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags, get_organizations_mgmt_account_id from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations -from boto_util.organizations import get_organizations_mgmt_account_id import botocore.exceptions @@ -159,17 +159,40 @@ def lambda_handler(event, context): return aws_organizations_client = get_client("organizations", aws_account_id, execution_role_name) - + if aws_account_id != get_organizations_mgmt_account_id(aws_organizations_client): logger.info( "CloudWatch Alarms not checked in account %s as this is not the Management Account", aws_account_id, ) return + aws_config_client = get_client("config", aws_account_id, execution_role_name) aws_cloudwatch_client = get_client("cloudwatch", aws_account_id, execution_role_name) + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail3, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + results = check_cloudwatch_alarms( aws_cloudwatch_client, alarm_names=str(rule_parameters["AlarmList"]).split(",") ) diff --git a/src/lambda/gc03_check_trusted_devices_admin_access/app.py b/src/lambda/gc03_check_trusted_devices_admin_access/app.py index 10034f5f..647dd623 100644 --- a/src/lambda/gc03_check_trusted_devices_admin_access/app.py +++ b/src/lambda/gc03_check_trusted_devices_admin_access/app.py @@ -6,11 +6,11 @@ import logging import ipaddress -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags, get_organizations_mgmt_account_id from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.iam import account_has_federated_users -from boto_util.organizations import get_organizations_mgmt_account_id from boto_util.s3 import check_s3_object_exists, get_lines_from_s3_file from boto_util.cloud_trail import lookup_cloud_trail_events @@ -68,6 +68,28 @@ def lambda_handler(event, context): aws_cloudtrail_client = get_client("cloudtrail", aws_account_id, execution_role_name) aws_iam_client = get_client("iam", aws_account_id, execution_role_name) + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail3, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + file_param_name = "s3ObjectPath" vpn_ip_ranges_file_path = rule_parameters.get(file_param_name, "") diff --git a/src/lambda/gc04_check_alerts_flag_misuse/app.py b/src/lambda/gc04_check_alerts_flag_misuse/app.py index 0d02b534..48a25cde 100644 --- a/src/lambda/gc04_check_alerts_flag_misuse/app.py +++ b/src/lambda/gc04_check_alerts_flag_misuse/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters, flatten_dict +from utils import is_scheduled_notification, check_required_parameters, flatten_dict, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.event_bridge import list_all_event_bridge_rules, list_all_event_bridge_rule_targets @@ -176,7 +177,29 @@ def lambda_handler(event, context): aws_sns_client = get_client("sns", aws_account_id, execution_role_name) aws_cloud_trail_client = get_client("cloudtrail", aws_account_id, execution_role_name) aws_iam_client = get_client("iam", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail4, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + rules_are_compliant = False rules = list_all_event_bridge_rules(aws_event_bridge_client) cb_role = rule_parameters["IAM_Role_Name"] diff --git a/src/lambda/gc04_check_enterprise_monitoring/app.py b/src/lambda/gc04_check_enterprise_monitoring/app.py index 01ae65c4..84e0a79d 100644 --- a/src/lambda/gc04_check_enterprise_monitoring/app.py +++ b/src/lambda/gc04_check_enterprise_monitoring/app.py @@ -5,10 +5,10 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags, get_organizations_mgmt_account_id from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations -from boto_util.organizations import get_organizations_mgmt_account_id import botocore.exceptions @@ -92,7 +92,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name) aws_iam_client = get_client("iam", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail4, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + trusted_principal = rule_parameters.get("IAM_Trusted_Principal") role_name = rule_parameters.get("IAM_Role_Name") results = check_enterprise_monitoring_accounts(aws_iam_client, trusted_principal, role_name) diff --git a/src/lambda/gc05_check_data_location/app.py b/src/lambda/gc05_check_data_location/app.py index 1db4ba33..b1b68d63 100644 --- a/src/lambda/gc05_check_data_location/app.py +++ b/src/lambda/gc05_check_data_location/app.py @@ -9,7 +9,8 @@ import sys import re -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.ec2 import get_enabled_regions @@ -393,7 +394,29 @@ def lambda_handler(event, context): aws_ec2_client = get_client("ec2", aws_account_id, execution_role_name) aws_resource_explorer_client = get_client("resource-explorer-2", aws_account_id, execution_role_name) aws_s3_client = get_client("s3", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail5, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + EnabledRegionsList = get_enabled_regions(aws_ec2_client) logger.info("Regions enabled or that do not require opt-in: {}".format(EnabledRegionsList)) UnauthorizedRegionsList = [] diff --git a/src/lambda/gc06_check_encryption_at_rest_part1/app.py b/src/lambda/gc06_check_encryption_at_rest_part1/app.py index 981fc72d..6982707b 100644 --- a/src/lambda/gc06_check_encryption_at_rest_part1/app.py +++ b/src/lambda/gc06_check_encryption_at_rest_part1/app.py @@ -6,7 +6,8 @@ import logging import time -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client, is_throttling_exception from boto_util.config import build_evaluation, submit_evaluations from boto_util.api_gateway import list_all_api_gateway_deployments, list_all_api_gateway_rest_apis @@ -618,7 +619,29 @@ def lambda_handler(event, context): aws_dax_client = get_client("dax", aws_account_id, execution_role_name, is_not_audit_account) aws_dynamo_db_client = get_client("dynamodb", aws_account_id, execution_role_name, is_not_audit_account) aws_ec2_client = get_client("ec2", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail6, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + # API Gateway evaluations.extend(assess_api_gw_encryption_at_rest(aws_api_gw_client, event)) diff --git a/src/lambda/gc06_check_encryption_at_rest_part2/app.py b/src/lambda/gc06_check_encryption_at_rest_part2/app.py index 6bed250c..ce4abcf2 100644 --- a/src/lambda/gc06_check_encryption_at_rest_part2/app.py +++ b/src/lambda/gc06_check_encryption_at_rest_part2/app.py @@ -6,7 +6,8 @@ import logging import time -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client, is_throttling_exception from boto_util.config import build_evaluation, submit_evaluations from boto_util.efs import describe_all_efs_file_systems @@ -517,7 +518,29 @@ def lambda_handler(event, context): aws_rds_client = get_client("rds", aws_account_id, execution_role_name, is_not_audit_account) aws_s3_client = get_client("s3", aws_account_id, execution_role_name, is_not_audit_account) aws_sns_client = get_client("sns", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail6, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + # EFS evaluations.extend(assess_efs_encryption_at_rest(aws_efs_client, event)) diff --git a/src/lambda/gc07_check_certificate_authorities/app.py b/src/lambda/gc07_check_certificate_authorities/app.py index ad7bf961..30a9e79f 100644 --- a/src/lambda/gc07_check_certificate_authorities/app.py +++ b/src/lambda/gc07_check_certificate_authorities/app.py @@ -4,7 +4,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists, get_lines_from_s3_file @@ -75,7 +76,29 @@ def lambda_handler(event, context): # Get the S3 client for the current (Audit) account where this lambda runs from aws_s3_client = get_client("s3") aws_acm_client = get_client("acm", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail7, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + cas_currently_in_use_file_path = rule_parameters.get(file_param_name, "") if not check_s3_object_exists(aws_s3_client, cas_currently_in_use_file_path): diff --git a/src/lambda/gc07_check_cryptographic_algorithms/app.py b/src/lambda/gc07_check_cryptographic_algorithms/app.py index 09a04f2f..1fcc32ba 100644 --- a/src/lambda/gc07_check_cryptographic_algorithms/app.py +++ b/src/lambda/gc07_check_cryptographic_algorithms/app.py @@ -4,7 +4,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters, flat +from utils import is_scheduled_notification, check_required_parameters, flat, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.elb import describe_elb_load_balancer_policies, describe_all_elb_load_balancers @@ -163,7 +164,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) # Using older 'elb' api instead of 'elbv2' so that we get the 'Classic' load balancers aws_elb_client = get_client("elb", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail7, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + load_balancers_details = [ { "Description": x, diff --git a/src/lambda/gc07_check_encryption_in_transit/app.py b/src/lambda/gc07_check_encryption_in_transit/app.py index 655c3e4c..f2770334 100644 --- a/src/lambda/gc07_check_encryption_in_transit/app.py +++ b/src/lambda/gc07_check_encryption_in_transit/app.py @@ -6,7 +6,8 @@ import logging import time -from utils import is_scheduled_notification, check_required_parameters, flat +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client, is_throttling_exception from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import list_all_s3_buckets @@ -625,7 +626,29 @@ def lambda_handler(event, context): aws_api_gw_client = get_client("apigateway", aws_account_id, execution_role_name, is_not_audit_account) aws_open_search_client = get_client("opensearch", aws_account_id, execution_role_name, is_not_audit_account) aws_cloud_front_client = get_client("cloudfront", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail7, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + evaluations.extend(assess_s3_buckets_ssl_enforcement(aws_s3_client, event)) evaluations.extend(assess_redshift_clusters_ssl_enforcement(aws_redshift_client, event)) evaluations.extend(assess_elb_v2_ssl_enforcement(aws_elb_v2_client, event)) diff --git a/src/lambda/gc07_check_secure_network_transmission_policy/app.py b/src/lambda/gc07_check_secure_network_transmission_policy/app.py index 03d3626f..f67f6b5a 100644 --- a/src/lambda/gc07_check_secure_network_transmission_policy/app.py +++ b/src/lambda/gc07_check_secure_network_transmission_policy/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters, flat +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -50,7 +51,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail7, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Secure Network Transmission Policy found." diff --git a/src/lambda/gc08_check_cloud_deployment_guide/app.py b/src/lambda/gc08_check_cloud_deployment_guide/app.py index ea9a6be4..9761f97f 100644 --- a/src/lambda/gc08_check_cloud_deployment_guide/app.py +++ b/src/lambda/gc08_check_cloud_deployment_guide/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -51,7 +52,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail8, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Target Cloud Deployment Guide document found" diff --git a/src/lambda/gc08_check_cloud_segmentation_design/app.py b/src/lambda/gc08_check_cloud_segmentation_design/app.py index f5fa1e35..f2a71f96 100644 --- a/src/lambda/gc08_check_cloud_segmentation_design/app.py +++ b/src/lambda/gc08_check_cloud_segmentation_design/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -52,7 +53,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail8, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Target Cloud Segmentation Design document found" diff --git a/src/lambda/gc08_check_target_network_architecture/app.py b/src/lambda/gc08_check_target_network_architecture/app.py index 75d916fb..3e5dbfab 100644 --- a/src/lambda/gc08_check_target_network_architecture/app.py +++ b/src/lambda/gc08_check_target_network_architecture/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -51,7 +52,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail8, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Target Network Architecture document found" diff --git a/src/lambda/gc09_check_netsec_architecture/app.py b/src/lambda/gc09_check_netsec_architecture/app.py index 94bff160..2b1d714d 100644 --- a/src/lambda/gc09_check_netsec_architecture/app.py +++ b/src/lambda/gc09_check_netsec_architecture/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -51,7 +52,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail9, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Network Security Architecture document found" diff --git a/src/lambda/gc09_check_non_public_storage_accounts/app.py b/src/lambda/gc09_check_non_public_storage_accounts/app.py index 25d45c56..68b6ee2a 100644 --- a/src/lambda/gc09_check_non_public_storage_accounts/app.py +++ b/src/lambda/gc09_check_non_public_storage_accounts/app.py @@ -4,7 +4,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import list_all_s3_buckets @@ -67,7 +68,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name) aws_s3_client = get_client("s3", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail9, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + buckets = list_all_s3_buckets(aws_s3_client) for b in buckets: b_eval = check_bucket_acls(aws_s3_client, b.get("Name", ""), event) diff --git a/src/lambda/gc10_check_cyber_center_sensors/app.py b/src/lambda/gc10_check_cyber_center_sensors/app.py index 6ae37ab7..8c4330f9 100644 --- a/src/lambda/gc10_check_cyber_center_sensors/app.py +++ b/src/lambda/gc10_check_cyber_center_sensors/app.py @@ -6,10 +6,10 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_organizations_mgmt_account_id, organizations_list_all_accounts, get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations -from boto_util.organizations import get_organizations_mgmt_account_id, organizations_list_all_accounts from boto_util.s3 import list_all_s3_buckets, get_lines_from_s3_file, check_s3_object_exists from boto_util.iam import list_all_iam_roles @@ -113,7 +113,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) # Get the S3 client for the current (Audit) account where this lambda runs from aws_s3_client_for_audit_account = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail10, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if not check_s3_object_exists(aws_s3_client_for_audit_account, log_buckets_s3_path): annotation = f"No file found for s3 path '{log_buckets_s3_path}' via '{file_param_name}' input parameter." logger.info(f"NON_COMPLIANT: {annotation}") diff --git a/src/lambda/gc10_confirmation_of_mou/app.py b/src/lambda/gc10_confirmation_of_mou/app.py index f90afc9e..da0de70d 100644 --- a/src/lambda/gc10_confirmation_of_mou/app.py +++ b/src/lambda/gc10_confirmation_of_mou/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -49,7 +50,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail10, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Signed MOU found" diff --git a/src/lambda/gc11_check_monitoring_all_users/app.py b/src/lambda/gc11_check_monitoring_all_users/app.py index 9a489cf2..613251df 100644 --- a/src/lambda/gc11_check_monitoring_all_users/app.py +++ b/src/lambda/gc11_check_monitoring_all_users/app.py @@ -7,7 +7,8 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations @@ -49,7 +50,29 @@ def lambda_handler(event: dict, context): evaluations = [] aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail11, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + conformance_pack_id = extract_conformance_pack_id(event.get("configRuleName", "")) config_rule_name = f"gc01_check_monitoring_and_logging-conformance-pack-{conformance_pack_id}" diff --git a/src/lambda/gc11_check_monitoring_use_cases/app.py b/src/lambda/gc11_check_monitoring_use_cases/app.py index a49014ab..8c1e26b5 100644 --- a/src/lambda/gc11_check_monitoring_use_cases/app.py +++ b/src/lambda/gc11_check_monitoring_use_cases/app.py @@ -4,7 +4,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -51,7 +52,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail11, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Monitoring Use Cases document found" diff --git a/src/lambda/gc11_check_policy_event_logging/app.py b/src/lambda/gc11_check_policy_event_logging/app.py index 66b58f43..4b521d49 100644 --- a/src/lambda/gc11_check_policy_event_logging/app.py +++ b/src/lambda/gc11_check_policy_event_logging/app.py @@ -4,7 +4,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations, describe_all_config_rules @@ -74,6 +75,29 @@ def lambda_handler(event, context): is_not_audit_account = aws_account_id != audit_account_id aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail11, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + evaluations, all_aws_managed_rules_are_compliant = assess_aws_managed_rules(aws_config_client, event) if not all_aws_managed_rules_are_compliant: diff --git a/src/lambda/gc11_check_security_contact/app.py b/src/lambda/gc11_check_security_contact/app.py index d45cac1d..d69f8a0f 100644 --- a/src/lambda/gc11_check_security_contact/app.py +++ b/src/lambda/gc11_check_security_contact/app.py @@ -7,7 +7,8 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations @@ -67,7 +68,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) aws_account_client = get_client("account", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail11, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_security_contact(aws_account_client): compliance_type = "COMPLIANT" annotation = "Security contact registered" diff --git a/src/lambda/gc11_check_timezone/app.py b/src/lambda/gc11_check_timezone/app.py index d2efd8e7..b5d15189 100644 --- a/src/lambda/gc11_check_timezone/app.py +++ b/src/lambda/gc11_check_timezone/app.py @@ -4,7 +4,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations @@ -37,7 +38,29 @@ def lambda_handler(event: dict, context): is_not_audit_account = aws_account_id != audit_account_id aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail11, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + compliance_type = "COMPLIANT" annotation = "Logs in AWS are timestamped in UTC." diff --git a/src/lambda/gc11_check_trail_logging/app.py b/src/lambda/gc11_check_trail_logging/app.py index 936163ea..6a510a2d 100644 --- a/src/lambda/gc11_check_trail_logging/app.py +++ b/src/lambda/gc11_check_trail_logging/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations, aws_config_is_enabled from boto_util.cloud_trail import list_all_cloud_trails @@ -95,7 +96,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) aws_cloudtrail_client = get_client("cloudtrail", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail11, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + evaluations, all_cloudtrail_resources_are_compliant = assess_cloudtrail_configurations(aws_cloudtrail_client, event) if not evaluations: diff --git a/src/lambda/gc12_check_marketplace/app.py b/src/lambda/gc12_check_marketplace/app.py index 4c3f775a..e22be005 100644 --- a/src/lambda/gc12_check_marketplace/app.py +++ b/src/lambda/gc12_check_marketplace/app.py @@ -8,7 +8,7 @@ import botocore -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.organizations import ( @@ -16,6 +16,7 @@ get_organizations_mgmt_account_id, organizations_list_all_policies_for_target, organizations_list_all_organizational_units, + get_account_tags ) # Logging setup @@ -180,7 +181,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) aws_iam_client = get_client("iam", aws_account_id, execution_role_name, is_not_audit_account) aws_organizations_client = get_client("organizations", aws_account_id, execution_role_name, is_not_audit_account) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail12, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + selected_policy_summaries = get_policies_that_restrict_marketplace_access( aws_organizations_client, aws_iam_client, interval_between_calls ) diff --git a/src/lambda/gc13_emergency_account_alerts/app.py b/src/lambda/gc13_emergency_account_alerts/app.py index ba7d40ef..fc30068e 100644 --- a/src/lambda/gc13_emergency_account_alerts/app.py +++ b/src/lambda/gc13_emergency_account_alerts/app.py @@ -7,7 +7,8 @@ import botocore.exceptions -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists, get_lines_from_s3_file @@ -91,7 +92,29 @@ def lambda_handler(event, context): aws_s3_client = get_client("s3") aws_event_bridge_client = get_client("events", aws_account_id, execution_role_name) aws_sns_client = get_client("sns", aws_account_id, execution_role_name) - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail13, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + rule_resource_type = "AWS::Events::Rule" file_param_name = "s3ObjectPath" rule_names_file_path = rule_parameters.get(file_param_name, "") diff --git a/src/lambda/gc13_emergency_account_management/app.py b/src/lambda/gc13_emergency_account_management/app.py index b8888f6f..fb5d7c9a 100644 --- a/src/lambda/gc13_emergency_account_management/app.py +++ b/src/lambda/gc13_emergency_account_management/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -51,7 +52,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail13, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Emergency Account Management Procedure found" diff --git a/src/lambda/gc13_emergency_account_mgmt_approvals/app.py b/src/lambda/gc13_emergency_account_mgmt_approvals/app.py index 4bf3b185..75545c9a 100644 --- a/src/lambda/gc13_emergency_account_mgmt_approvals/app.py +++ b/src/lambda/gc13_emergency_account_mgmt_approvals/app.py @@ -5,7 +5,8 @@ import json import logging -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType +from boto_util.organizations import get_account_tags from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations from boto_util.s3 import check_s3_object_exists @@ -52,7 +53,29 @@ def lambda_handler(event, context): aws_config_client = get_client("config") aws_s3_client = get_client("s3") - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail13, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + if check_s3_object_exists(aws_s3_client, rule_parameters["s3ObjectPath"]): compliance_type = "COMPLIANT" annotation = "Emergency Account Management Procedure Approval found" diff --git a/src/lambda/gc13_emergency_account_testing/app.py b/src/lambda/gc13_emergency_account_testing/app.py index 8e77e62b..32bad42f 100644 --- a/src/lambda/gc13_emergency_account_testing/app.py +++ b/src/lambda/gc13_emergency_account_testing/app.py @@ -6,12 +6,10 @@ import logging from datetime import datetime, timedelta, timezone -import botocore.exceptions - -from utils import is_scheduled_notification, check_required_parameters +from utils import is_scheduled_notification, check_required_parameters, check_guardrail_requirement_by_cloud_usage_profile, get_cloud_profile_from_tags, GuardrailType, GuardrailRequirementType from boto_util.client import get_client from boto_util.config import build_evaluation, submit_evaluations -from boto_util.organizations import get_organizations_mgmt_account_id +from boto_util.organizations import get_organizations_mgmt_account_id, get_account_tags from boto_util.iam import get_iam_user # Logging setup @@ -56,7 +54,7 @@ def lambda_handler(event, context): aws_config_client = get_client("config", aws_account_id, execution_role_name, is_not_audit_account) aws_iam_client = get_client("iam", aws_account_id, execution_role_name, is_not_audit_account) aws_organizations_client = get_client("organizations", aws_account_id, execution_role_name, is_not_audit_account) - + if aws_account_id != get_organizations_mgmt_account_id(aws_organizations_client): # We're not in the Management Account logger.info( @@ -64,7 +62,29 @@ def lambda_handler(event, context): aws_account_id, ) return - + + # Check cloud profile + tags = get_account_tags(get_client("organizations", assume_role=False), aws_account_id) + cloud_profile = get_cloud_profile_from_tags(tags) + gr_requirement_type = check_guardrail_requirement_by_cloud_usage_profile(GuardrailType.Guardrail13, cloud_profile) + + # If the guardrail is recommended + if gr_requirement_type == GuardrailRequirementType.Recommended: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "COMPLIANT", + event, + gr_requirement_type=gr_requirement_type + )]) + # If the guardrail is not required + elif gr_requirement_type == GuardrailRequirementType.Not_Required: + return submit_evaluations(aws_config_client, event["resultToken"], [build_evaluation( + aws_account_id, + "NOT_APPLICABLE", + event, + gr_requirement_type=gr_requirement_type + )]) + iam_user_resource_type = "AWS::IAM::User" bg_account_names = [rule_parameters["BgUser1"], rule_parameters["BgUser2"]] num_compliant = 0 diff --git a/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/config.py b/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/config.py index 8bfd3bec..81c6f045 100644 --- a/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/config.py +++ b/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/config.py @@ -2,6 +2,7 @@ import time import botocore.exceptions +from utils import GuardrailRequirementType def build_evaluation( @@ -10,6 +11,7 @@ def build_evaluation( event: dict, resource_type: str = "AWS::::Account", annotation: str | None = None, + gr_requirement_type: GuardrailRequirementType = GuardrailRequirementType.Required ) -> dict: """ Form an evaluation as a dictionary. Usually suited to report on scheduled rules. @@ -29,12 +31,20 @@ def build_evaluation( evaluation = { "ComplianceResourceId": resource_id, "ComplianceResourceType": resource_type, - "ComplianceType": compliance_type, "OrderingTimestamp": str(json.loads(event["invokingEvent"])["notificationCreationTime"]), } - if annotation: - evaluation["Annotation"] = annotation + match gr_requirement_type: + case GuardrailRequirementType.Required: + evaluation["ComplianceType"] = compliance_type + if annotation: + evaluation["Annotation"] = annotation + case GuardrailRequirementType.Recommended: + evaluation["ComplianceType"] = "COMPLIANT" + evaluation["Annotation"] = "This guardrail is not required for the accounts cloud profile level." + case GuardrailRequirementType.Not_Required: + evaluation["ComplianceType"] = "NOT_APPLICABLE" + return evaluation diff --git a/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/organizations.py b/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/organizations.py index 351e0251..aac2d02a 100644 --- a/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/organizations.py +++ b/src/layer/cloud_guardrails/lib/python3.12/site-packages/boto_util/organizations.py @@ -6,6 +6,17 @@ logger = logging.getLogger() logger.setLevel(logging.INFO) +def get_account_tags(organizations_client, account_id): + tags = [] + next_token = None + while True: + response = organizations_client.list_tags_for_resource(ResourceId=account_id, NextToken=next_token) if next_token else organizations_client.list_tags_for_resource(ResourceId=account_id) + tags = tags + response.get("Tags") + next_token = response.get("NextToken") + if not next_token: + break + return tags + def get_organizations_mgmt_account_id(organizations_client): """Calls the AWS Organizations API to obtain the Management Account ID""" diff --git a/src/layer/cloud_guardrails/lib/python3.12/site-packages/utils/__init__.py b/src/layer/cloud_guardrails/lib/python3.12/site-packages/utils/__init__.py index ca268c25..a2bf1983 100644 --- a/src/layer/cloud_guardrails/lib/python3.12/site-packages/utils/__init__.py +++ b/src/layer/cloud_guardrails/lib/python3.12/site-packages/utils/__init__.py @@ -1,7 +1,43 @@ import logging +import os +from enum import Enum logger = logging.getLogger() -logger.setLevel(logging.INFO) +logger.setLevel(logging.INFO) + +class GuardrailType(Enum): + Guardrail1 = "Protect_User_Accounts_And_Identities" + Guardrail2 = "Manage_Access" + Guardrail3 = "Secure_Endpoints" + Guardrail4 = "Enterprise_Monitoring_Accounts" + Guardrail5 = "Data_Locations" + Guardrail6 = "Protection_Of_Data_At_Rest" + Guardrail7 = "Protection_Of_Data_In_Transit" + Guardrail8 = "Segment_And_Separate" + Guardrail9 = "Network_Security_Services" + Guardrail10 = "Cyber_Defense_Services" + Guardrail11 = "Logging_And_Monitoring" + Guardrail12 = "Configuration_Of_Cloud_Marketplace" + Guardrail13 = "Plan_For_Continuity" + +class CloudProfileType(Enum): + Profile1 = "1" + Profile2 = "2" + Profile3 = "3" + Profile4 = "4" + Profile5 = "5" + Profile6 = "6" + +class GuardrailRequirementType(Enum): + Required = "Required" + Recommended = "Recommended" + Not_Required = "Not_Applicable" + +class UtilLibraryError(Exception): + pass + +class GuardrailMissingProfileLevelRequirementType(UtilLibraryError): + pass def is_scheduled_notification(message_type: str) -> bool: @@ -51,3 +87,51 @@ def flatten_dict(d: dict, parent_key: str = "", sep: str = "."): else: items.append((new_key, v)) return dict(items) + +def check_guardrail_requirement_by_cloud_usage_profile(guardrail: GuardrailType, account_cloud_profile: CloudProfileType) -> GuardrailRequirementType: + guardrails_required_by_cloud_profile = { + CloudProfileType.Profile1: [GuardrailType.Guardrail1, GuardrailType.Guardrail2, GuardrailType.Guardrail4, GuardrailType.Guardrail8, GuardrailType.Guardrail12], + CloudProfileType.Profile2: [GuardrailType.Guardrail1, GuardrailType.Guardrail2, GuardrailType.Guardrail3, GuardrailType.Guardrail4, GuardrailType.Guardrail7, GuardrailType.Guardrail8, GuardrailType.Guardrail9, GuardrailType.Guardrail10, GuardrailType.Guardrail11, GuardrailType.Guardrail12, GuardrailType.Guardrail13], + CloudProfileType.Profile3: [GuardrailType.Guardrail1, GuardrailType.Guardrail2, GuardrailType.Guardrail3, GuardrailType.Guardrail4, GuardrailType.Guardrail5, GuardrailType.Guardrail6, GuardrailType.Guardrail7, GuardrailType.Guardrail8, GuardrailType.Guardrail9, GuardrailType.Guardrail10, GuardrailType.Guardrail11, GuardrailType.Guardrail12, GuardrailType.Guardrail13], + CloudProfileType.Profile4: [GuardrailType.Guardrail1, GuardrailType.Guardrail2, GuardrailType.Guardrail3, GuardrailType.Guardrail4, GuardrailType.Guardrail5, GuardrailType.Guardrail6, GuardrailType.Guardrail7, GuardrailType.Guardrail8, GuardrailType.Guardrail9, GuardrailType.Guardrail10, GuardrailType.Guardrail11, GuardrailType.Guardrail12, GuardrailType.Guardrail13], + CloudProfileType.Profile5: [GuardrailType.Guardrail1, GuardrailType.Guardrail2, GuardrailType.Guardrail3, GuardrailType.Guardrail4, GuardrailType.Guardrail5, GuardrailType.Guardrail6, GuardrailType.Guardrail7, GuardrailType.Guardrail8, GuardrailType.Guardrail9, GuardrailType.Guardrail10, GuardrailType.Guardrail11, GuardrailType.Guardrail12, GuardrailType.Guardrail13], + CloudProfileType.Profile6: [GuardrailType.Guardrail1, GuardrailType.Guardrail2, GuardrailType.Guardrail3, GuardrailType.Guardrail4, GuardrailType.Guardrail5, GuardrailType.Guardrail6, GuardrailType.Guardrail7, GuardrailType.Guardrail8, GuardrailType.Guardrail9, GuardrailType.Guardrail10, GuardrailType.Guardrail11, GuardrailType.Guardrail12, GuardrailType.Guardrail13], + } + + guardrails_recommended_by_cloud_profile = { + CloudProfileType.Profile1: [GuardrailType.Guardrail3, GuardrailType.Guardrail5, GuardrailType.Guardrail7, GuardrailType.Guardrail9, GuardrailType.Guardrail11], + CloudProfileType.Profile2: [GuardrailType.Guardrail5, GuardrailType.Guardrail6] + } + + guardrails_not_required_by_cloud_profile = { + CloudProfileType.Profile1: [GuardrailType.Guardrail6, GuardrailType.Guardrail10, GuardrailType.Guardrail13] + } + + if guardrail in guardrails_required_by_cloud_profile.get(account_cloud_profile, []): + return GuardrailRequirementType.Required + elif guardrail in guardrails_recommended_by_cloud_profile.get(account_cloud_profile, []): + return GuardrailRequirementType.Recommended + elif guardrail in guardrails_not_required_by_cloud_profile.get(account_cloud_profile, []): + return GuardrailRequirementType.Not_Required + else: + raise GuardrailMissingProfileLevelRequirementType + +def get_cloud_profile_from_tags(tags: list[dict]) -> CloudProfileType: + tagKey = "CloudProfile" + default = CloudProfileType.Profile3 + try: + default = CloudProfileType(os.environ.get("DEFAULT_CLOUD_PROFILE", CloudProfileType.Profile3)) + except: + logger.info("Failed to cast environment variable 'DEFAULT_CLOUD_PROFILE', invalid CloudProfileType. Using Cloud Profile 6 as default.") + + for t in tags: + if t.get("Key", "") == tagKey: + logger.info(f"Account cloud profile found: '{t.get("Value")}'") + try: + return CloudProfileType(t.get("Value")) + except: + logger.info(f"Failed to cast tag '{tagKey}', invalid CloudProfileType. Returning default '{default}'") + return CloudProfileType(default) + + logger.info(f"No tag with key '{tagKey}' found. Returning default '{default}'") + return default