Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions aws_quickstart/datadog_agentless_api_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
97 changes: 96 additions & 1 deletion aws_quickstart/datadog_agentless_api_call_test.py
Original file line number Diff line number Diff line change
@@ -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,
)


Expand Down Expand Up @@ -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()
34 changes: 34 additions & 0 deletions aws_quickstart/datadog_agentless_delegate_role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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"
Expand Down Expand Up @@ -355,6 +388,7 @@ Metadata:
Parameters:
- ScannerInstanceRoleARN
- ScannerDelegateRoleName
- DatadogIntegrationRoleName
- Label:
default: "Advanced"
Parameters:
Expand Down
34 changes: 34 additions & 0 deletions aws_quickstart/datadog_agentless_delegate_role_stackset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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"
Expand Down Expand Up @@ -352,6 +385,7 @@ Metadata:
- DatadogAPIKey
- DatadogAPPKey
- DatadogSite
- DatadogIntegrationRoleName
- Label:
default: "Scanning Options"
Parameters:
Expand Down
23 changes: 23 additions & 0 deletions aws_quickstart/datadog_agentless_scanning.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion aws_quickstart/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v4.5.1
v4.6.0
Loading