diff --git a/aws_quickstart/datadog_agentless_api_call.py b/aws_quickstart/datadog_agentless_api_call.py index 79f04c6..2aced84 100644 --- a/aws_quickstart/datadog_agentless_api_call.py +++ b/aws_quickstart/datadog_agentless_api_call.py @@ -112,11 +112,36 @@ def is_agentless_scanning_enabled(url_account, headers): return True +def ensure_security_audit_policy(role_name, partition): + """Ensure the SecurityAudit policy is attached to the integration role.""" + if not role_name: + LOGGER.info("No integration role name provided, skipping SecurityAudit policy attachment.") + return + + import boto3 + + policy_arn = f"arn:{partition}:iam::aws:policy/SecurityAudit" + iam = boto3.client("iam") + + paginator = iam.get_paginator("list_attached_role_policies") + for page in paginator.paginate(RoleName=role_name): + for policy in page["AttachedPolicies"]: + if policy["PolicyArn"] == policy_arn: + LOGGER.info("SecurityAudit policy is already attached to role %s.", role_name) + return + + LOGGER.info("Attaching SecurityAudit policy to role %s.", role_name) + iam.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn) + + def handler(event, context): """Handle Lambda event from AWS""" try: if event["RequestType"] == "Create": LOGGER.info("Received Create request.") + role_name = event["ResourceProperties"].get("IntegrationRoleName", "") + partition = event["ResourceProperties"].get("Partition", "aws") + ensure_security_audit_policy(role_name, partition) response = call_datadog_agentless_api(context, event, "POST") send_response( event, diff --git a/aws_quickstart/datadog_agentless_api_call_test.py b/aws_quickstart/datadog_agentless_api_call_test.py index c7a05cc..384e214 100644 --- a/aws_quickstart/datadog_agentless_api_call_test.py +++ b/aws_quickstart/datadog_agentless_api_call_test.py @@ -1,15 +1,22 @@ #!/usr/bin/env python3 import json +import sys import unittest from types import SimpleNamespace -from unittest.mock import patch, Mock +from unittest.mock import patch, Mock, MagicMock from urllib.error import HTTPError +# boto3 is available in Lambda runtime but not necessarily in CI. +# Insert a mock module so the lazy import inside ensure_security_audit_policy works. +if "boto3" not in sys.modules: + sys.modules["boto3"] = MagicMock() + # Import the functions to test from datadog_agentless_api_call import ( call_datadog_agentless_api, is_agentless_scanning_enabled, + ensure_security_audit_policy, ) @@ -246,5 +253,93 @@ def test_other_error_raises_exception(self, mock_urlopen): is_agentless_scanning_enabled(self.url, self.headers) +class TestEnsureSecurityAuditPolicy(unittest.TestCase): + """Test cases for ensure_security_audit_policy function""" + + def setUp(self): + """Set up test fixtures""" + self.role_name = "DatadogIntegrationRole" + self.partition = "aws" + self.policy_arn = "arn:aws:iam::aws:policy/SecurityAudit" + + @patch("boto3.client") + def test_policy_already_attached(self, mock_boto3_client): + """Test that function skips attachment when SecurityAudit is already attached""" + mock_iam = Mock() + mock_boto3_client.return_value = mock_iam + mock_paginator = Mock() + mock_iam.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + { + "AttachedPolicies": [ + {"PolicyName": "SecurityAudit", "PolicyArn": self.policy_arn}, + ] + } + ] + + ensure_security_audit_policy(self.role_name, self.partition) + + mock_iam.attach_role_policy.assert_not_called() + + @patch("boto3.client") + def test_policy_not_attached(self, mock_boto3_client): + """Test that function attaches SecurityAudit when it is not present""" + mock_iam = Mock() + mock_boto3_client.return_value = mock_iam + mock_paginator = Mock() + mock_iam.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + { + "AttachedPolicies": [ + {"PolicyName": "OtherPolicy", "PolicyArn": "arn:aws:iam::aws:policy/OtherPolicy"}, + ] + } + ] + + ensure_security_audit_policy(self.role_name, self.partition) + + mock_iam.attach_role_policy.assert_called_once_with( + RoleName=self.role_name, + PolicyArn=self.policy_arn, + ) + + @patch("boto3.client") + def test_empty_role_name_skips(self, mock_boto3_client): + """Test that function skips when role name is empty""" + ensure_security_audit_policy("", self.partition) + + mock_boto3_client.assert_not_called() + + @patch("boto3.client") + def test_error_propagates(self, mock_boto3_client): + """Test that IAM errors propagate to the caller""" + mock_iam = Mock() + mock_boto3_client.return_value = mock_iam + mock_paginator = Mock() + mock_iam.get_paginator.return_value = mock_paginator + mock_paginator.paginate.side_effect = Exception("IAM error") + + with self.assertRaises(Exception): + ensure_security_audit_policy(self.role_name, self.partition) + + @patch("boto3.client") + def test_govcloud_partition(self, mock_boto3_client): + """Test that function uses the correct partition for GovCloud""" + mock_iam = Mock() + mock_boto3_client.return_value = mock_iam + mock_paginator = Mock() + mock_iam.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + {"AttachedPolicies": []} + ] + + ensure_security_audit_policy(self.role_name, "aws-us-gov") + + mock_iam.attach_role_policy.assert_called_once_with( + RoleName=self.role_name, + PolicyArn="arn:aws-us-gov:iam::aws:policy/SecurityAudit", + ) + + if __name__ == "__main__": unittest.main() diff --git a/aws_quickstart/datadog_agentless_delegate_role.yaml b/aws_quickstart/datadog_agentless_delegate_role.yaml index d7c0c76..c943909 100644 --- a/aws_quickstart/datadog_agentless_delegate_role.yaml +++ b/aws_quickstart/datadog_agentless_delegate_role.yaml @@ -74,10 +74,19 @@ Parameters: Description: The name of the role assumed by the Datadog Agentless Scanner Default: DatadogAgentlessScannerDelegateRole + DatadogIntegrationRoleName: + Type: String + Description: The name of IAM role used by the Datadog AWS integration. If provided, the SecurityAudit policy will be attached to this role. + Default: '' + Conditions: DSPMEnabled: !Equals - !Ref 'AgentlessSensitiveDataScanning' - 'true' + AttachSecurityAuditPolicy: !Not + - !Equals + - !Ref 'DatadogIntegrationRoleName' + - '' Rules: MustMatchAccountId: @@ -313,6 +322,28 @@ Resources: ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + SecurityAuditPolicyAttachmentPermissions: + Type: AWS::IAM::RolePolicy + Condition: AttachSecurityAuditPolicy + Properties: + RoleName: !Ref LambdaExecutionRoleDatadogAgentlessAPICall + PolicyName: SecurityAuditPolicyAttachmentPermissions + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - iam:ListAttachedRolePolicies + Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DatadogIntegrationRoleName}" + - Effect: Allow + Action: + - iam:AttachRolePolicy + Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DatadogIntegrationRoleName}" + Condition: + ArnEquals: + 'iam:PolicyARN': + - !Sub "arn:${AWS::Partition}:iam::aws:policy/SecurityAudit" + DatadogAgentlessAPICall: Type: "Custom::DatadogAgentlessAPICall" Properties: @@ -326,6 +357,8 @@ Resources: Containers: !Ref "AgentlessContainerScanning" Lambdas: !Ref "AgentlessLambdaScanning" SensitiveData: !Ref "AgentlessSensitiveDataScanning" + IntegrationRoleName: !Ref "DatadogIntegrationRoleName" + Partition: !Ref "AWS::Partition" # Optional parameters DelegateRoleArn: !GetAtt "ScannerDelegateRole.Arn" OrchestratorPolicyArn: !Ref "ScannerDelegateRoleOrchestratorPolicy" @@ -355,6 +388,7 @@ Metadata: Parameters: - ScannerInstanceRoleARN - ScannerDelegateRoleName + - DatadogIntegrationRoleName - Label: default: "Advanced" Parameters: diff --git a/aws_quickstart/datadog_agentless_delegate_role_stackset.yaml b/aws_quickstart/datadog_agentless_delegate_role_stackset.yaml index dcf9499..951f2b7 100644 --- a/aws_quickstart/datadog_agentless_delegate_role_stackset.yaml +++ b/aws_quickstart/datadog_agentless_delegate_role_stackset.yaml @@ -52,10 +52,19 @@ Parameters: Description: Enable Agentless Scanning of datastores (S3 buckets). Default: false + DatadogIntegrationRoleName: + Type: String + Description: The name of IAM role used by the Datadog AWS integration. If provided, the SecurityAudit policy will be attached to this role. + Default: '' + Conditions: DSPMEnabled: !Equals - !Ref 'AgentlessSensitiveDataScanning' - 'true' + AttachSecurityAuditPolicy: !Not + - !Equals + - !Ref 'DatadogIntegrationRoleName' + - '' Resources: ScannerDelegateRoleOrchestratorPolicy: @@ -281,6 +290,28 @@ Resources: ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + SecurityAuditPolicyAttachmentPermissions: + Type: AWS::IAM::RolePolicy + Condition: AttachSecurityAuditPolicy + Properties: + RoleName: !Ref LambdaExecutionRoleDatadogAgentlessAPICall + PolicyName: SecurityAuditPolicyAttachmentPermissions + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - iam:ListAttachedRolePolicies + Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DatadogIntegrationRoleName}" + - Effect: Allow + Action: + - iam:AttachRolePolicy + Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DatadogIntegrationRoleName}" + Condition: + ArnEquals: + 'iam:PolicyARN': + - !Sub "arn:${AWS::Partition}:iam::aws:policy/SecurityAudit" + DatadogAgentlessAPICall: Type: "Custom::DatadogAgentlessAPICall" Properties: @@ -294,6 +325,8 @@ Resources: Containers: !Ref "AgentlessVulnerabilityScanning" Lambdas: !Ref "AgentlessVulnerabilityScanning" SensitiveData: !Ref "AgentlessSensitiveDataScanning" + IntegrationRoleName: !Ref "DatadogIntegrationRoleName" + Partition: !Ref "AWS::Partition" # Optional parameters DelegateRoleArn: !GetAtt "ScannerDelegateRole.Arn" OrchestratorPolicyArn: !Ref "ScannerDelegateRoleOrchestratorPolicy" @@ -352,6 +385,7 @@ Metadata: - DatadogAPIKey - DatadogAPPKey - DatadogSite + - DatadogIntegrationRoleName - Label: default: "Scanning Options" Parameters: diff --git a/aws_quickstart/datadog_agentless_scanning.yaml b/aws_quickstart/datadog_agentless_scanning.yaml index 3b51386..c63af2d 100644 --- a/aws_quickstart/datadog_agentless_scanning.yaml +++ b/aws_quickstart/datadog_agentless_scanning.yaml @@ -1043,6 +1043,27 @@ Resources: ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + SecurityAuditPolicyAttachmentPermissions: + Type: AWS::IAM::RolePolicy + Properties: + RoleName: !Ref LambdaExecutionRoleDatadogAgentlessAPICall + PolicyName: SecurityAuditPolicyAttachmentPermissions + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - iam:ListAttachedRolePolicies + Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DatadogIntegrationRoleName}" + - Effect: Allow + Action: + - iam:AttachRolePolicy + Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DatadogIntegrationRoleName}" + Condition: + ArnEquals: + 'iam:PolicyARN': + - !Sub "arn:${AWS::Partition}:iam::aws:policy/SecurityAudit" + # Retrieving secrets passed in via SecretsManager Arn DatadogAgentlessAPICall: Type: "Custom::DatadogAgentlessAPICall" @@ -1057,6 +1078,8 @@ Resources: Containers: !Ref "AgentlessContainerScanning" Lambdas: !Ref "AgentlessLambdaScanning" SensitiveData: !Ref "AgentlessSensitiveDataScanning" + IntegrationRoleName: !Ref "DatadogIntegrationRoleName" + Partition: !Ref "AWS::Partition" # Optional parameters LaunchTemplateId: !Ref "ScannerLaunchTemplate" AutoScalingGroupArn: !GetAtt "ScannerAutoScalingGroup.AutoScalingGroupARN" diff --git a/aws_quickstart/version.txt b/aws_quickstart/version.txt index af6f175..2707569 100644 --- a/aws_quickstart/version.txt +++ b/aws_quickstart/version.txt @@ -1 +1 @@ -v4.5.1 +v4.6.0