diff --git a/aws_quickstart/CHANGELOG.md b/aws_quickstart/CHANGELOG.md index c5b31e7..348ae53 100644 --- a/aws_quickstart/CHANGELOG.md +++ b/aws_quickstart/CHANGELOG.md @@ -1,3 +1,7 @@ +# 4.14.0 (June 2, 2026) + +- Add `datadog_agentless_saas.yaml` for SaaS-mode Agentless Scanning. The template provisions a single managed policy with the SaaS scanner permissions and attaches it to the local Datadog integration role — no scanner EC2/VPC/ASG resources and no delegate-role chaining. `SecurityAudit` is unconditionally attached to the integration role. `DatadogIntegrationRoleName` is required. Released alongside an entry added to `release.sh` so the new template participates in the standard placeholder substitution and upload pipeline. + # 4.13.0 (May 29, 2026) - Add `uk1.datadoghq.com` site support. Affects `main_v2.yaml`, `main_workflow.yaml`, `main_extended.yaml`, and `main_extended_workflow.yaml`. diff --git a/aws_quickstart/datadog_agentless_api_call.py b/aws_quickstart/datadog_agentless_api_call.py index 98295f6..63a1494 100644 --- a/aws_quickstart/datadog_agentless_api_call.py +++ b/aws_quickstart/datadog_agentless_api_call.py @@ -25,6 +25,7 @@ def call_datadog_agentless_api(context, event, method): instance_role_arn = event["ResourceProperties"].get("InstanceRoleArn") instance_profile_arn = event["ResourceProperties"].get("InstanceProfileArn") scanner_policy_arn = event["ResourceProperties"].get("ScannerPolicyArn") + saas_scanning_policy_arn = event["ResourceProperties"].get("SaaSScanningPolicyArn") orchestrator_policy_arn = event["ResourceProperties"].get("OrchestratorPolicyArn") worker_policy_arn = event["ResourceProperties"].get("WorkerPolicyArn") worker_dspm_policy_arn = event["ResourceProperties"].get("WorkerDSPMPolicyArn") @@ -65,6 +66,7 @@ def call_datadog_agentless_api(context, event, method): "instance_role_arn": instance_role_arn, "instance_profile_arn": instance_profile_arn, "scanner_policy_arn": scanner_policy_arn, + "saas_scanning_policy_arn": saas_scanning_policy_arn, "orchestrator_policy_arn": orchestrator_policy_arn, "worker_policy_arn": worker_policy_arn, "worker_dspm_policy_arn": worker_dspm_policy_arn, diff --git a/aws_quickstart/datadog_agentless_api_call_test.py b/aws_quickstart/datadog_agentless_api_call_test.py index 8c19ce3..2eb44c2 100644 --- a/aws_quickstart/datadog_agentless_api_call_test.py +++ b/aws_quickstart/datadog_agentless_api_call_test.py @@ -195,6 +195,9 @@ def test_post_request_payload_structure(self, mock_is_enabled, mock_urlopen): mock_is_enabled.return_value = False mock_response = self.create_mock_response(200) mock_urlopen.return_value = mock_response + self.base_event["ResourceProperties"]["SaaSScanningPolicyArn"] = ( + "arn:aws:iam::123456789012:policy/datadog-agentless-saas-scanning" + ) call_datadog_agentless_api(self.context, self.base_event, "POST") @@ -207,6 +210,10 @@ def test_post_request_payload_structure(self, mock_is_enabled, mock_urlopen): self.assertIn("data", payload) self.assertIn("type", payload["data"]) self.assertEqual(payload["data"]["type"], "aws_scan_options") + self.assertEqual( + payload["meta"]["resources"]["saas_scanning_policy_arn"], + "arn:aws:iam::123456789012:policy/datadog-agentless-saas-scanning", + ) class TestIsAgentlessScanningEnabled(unittest.TestCase): diff --git a/aws_quickstart/datadog_agentless_saas.yaml b/aws_quickstart/datadog_agentless_saas.yaml new file mode 100644 index 0000000..1513adc --- /dev/null +++ b/aws_quickstart/datadog_agentless_saas.yaml @@ -0,0 +1,293 @@ +# version: v +AWSTemplateFormatVersion: '2010-09-09' +Description: Attaches Datadog Agentless Scanning permissions for SaaS-mode deployments to the Datadog integration role. +Parameters: + AccountId: + Type: String + Description: The current AWS account ID. This parameter is for validation purposes only, and may be left empty. + AllowedPattern: "|[0-9]{12}" + Default: "" + + DatadogAPIKey: + Type: String + AllowedPattern: "[0-9a-f]{32}" + Description: API key for the Datadog account + NoEcho: true + + DatadogAPPKey: + Type: String + AllowedPattern: "([0-9a-f]{40})|(ddapp_[a-zA-Z0-9]{34})" + Description: Application key for the Datadog account + NoEcho: true + + DatadogSite: + Type: String + Description: >- + The Datadog site to use for the Datadog Agentless Scanner. + Allowed values: datadoghq.com, datadoghq.eu, us3.datadoghq.com, us5.datadoghq.com, + ap1.datadoghq.com, ap2.datadoghq.com. + Default: datadoghq.com + + AgentlessVulnerabilityScanning: + Type: String + AllowedValues: + - true + - false + Description: Enable Agentless Vulnerability Scanning (hosts, containers, and Lambda functions). + Default: false + + AgentlessSensitiveDataScanning: + Type: String + AllowedValues: + - true + - false + Description: Enable Agentless Scanning of datastores (S3 buckets). + Default: false + + AgentlessComplianceHostScanning: + Type: String + AllowedValues: + - true + - false + Description: Enable Agentless Compliance Scanning for hosts. + Default: false + + DatadogIntegrationRoleName: + Type: String + Description: >- + The name of the IAM role used by the Datadog AWS integration. In SaaS mode, Datadog assumes this + role directly to perform agentless scans. The SecurityAudit policy is also attached to this role. + AllowedPattern: '[\w+=,.@-]{1,64}' + +Rules: + MustMatchAccountId: + AssertDescription: 'Checking that AccountId matches the current AWS account ID' + Assertions: + - Assert: !Or [!Equals [!Ref AccountId, !Ref AWS::AccountId], !Equals [!Ref AccountId, ""]] + AssertDescription: >- + The current AWS account ID does not match the AWS account selected in Datadog. + Please log in to the AWS account where you want to set up Datadog Agentless Scanning and try again. + +Resources: + DatadogAgentlessSaaSScanningPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + Description: Policy for Datadog Agentless Scanning in SaaS mode. + Roles: + - !Ref DatadogIntegrationRoleName + PolicyDocument: + Version: '2012-10-17' + Statement: + - Action: 'ec2:CreateTags' + Effect: Allow + Resource: + - 'arn:aws:ec2:*:*:volume/*' + - 'arn:aws:ec2:*:*:snapshot/*' + - 'arn:aws:ec2:*:*:image/*' + Condition: + StringEquals: + 'ec2:CreateAction': + - CreateSnapshot + - CreateVolume + - CopySnapshot + - CopyImage + - Action: 'ec2:CreateSnapshot' + Effect: Allow + Resource: 'arn:aws:ec2:*:*:volume/*' + Condition: + StringNotEquals: + 'aws:ResourceTag/DatadogAgentlessScanner': 'false' + - Action: 'ec2:CopySnapshot' + Effect: Allow + Resource: 'arn:aws:ec2:*:*:snapshot/snap-*' + - Action: 'ec2:CopySnapshot' + Effect: Allow + Resource: 'arn:aws:ec2:*:*:snapshot/${*}' + Condition: + 'ForAllValues:StringLike': + 'aws:TagKeys': DatadogAgentlessScanner* + StringEquals: + 'aws:RequestTag/DatadogAgentlessScanner': 'true' + - Action: 'ec2:CreateSnapshot' + Effect: Allow + Resource: 'arn:aws:ec2:*:*:snapshot/*' + Condition: + 'ForAllValues:StringLike': + 'aws:TagKeys': DatadogAgentlessScanner* + StringEquals: + 'aws:RequestTag/DatadogAgentlessScanner': 'true' + - Action: 'ec2:DeleteSnapshot' + Effect: Allow + Resource: 'arn:aws:ec2:*:*:snapshot/*' + Condition: + StringEquals: + 'aws:ResourceTag/DatadogAgentlessScanner': 'true' + - Action: 'ec2:DescribeSnapshots' + Effect: Allow + Resource: '*' + - Action: 'kms:CreateGrant' + Effect: Allow + Resource: 'arn:aws:kms:*:*:key/*' + Condition: + 'ForAnyValue:StringEquals': + 'kms:EncryptionContextKeys': 'aws:ebs:id' + StringLike: + 'kms:ViaService': 'ec2.*.amazonaws.com' + Bool: + 'kms:GrantIsForAWSResource': true + - Action: 'kms:DescribeKey' + Effect: Allow + Resource: 'arn:aws:kms:*:*:key/*' + - Action: 'ec2:DeregisterImage' + Effect: Allow + Resource: 'arn:aws:ec2:*:*:image/*' + Condition: + StringEquals: + 'aws:ResourceTag/DatadogAgentlessScanner': 'true' + - Action: + - 'ebs:ListSnapshotBlocks' + - 'ebs:ListChangedBlocks' + - 'ebs:GetSnapshotBlock' + Effect: Allow + Resource: 'arn:aws:ec2:*:*:snapshot/*' + Condition: + StringEquals: + 'aws:ResourceTag/DatadogAgentlessScanner': 'true' + - Action: 'ec2:DescribeSnapshots' + Effect: Allow + Resource: '*' + - Action: 'ec2:DescribeVolumes' + Effect: Allow + Resource: '*' + - Action: 'kms:Decrypt' + Effect: Allow + Resource: 'arn:aws:kms:*:*:key/*' + Condition: + 'ForAnyValue:StringEquals': + 'kms:EncryptionContextKeys': 'aws:ebs:id' + StringLike: + 'kms:ViaService': 'ec2.*.amazonaws.com' + - Action: 'kms:DescribeKey' + Effect: Allow + Resource: 'arn:aws:kms:*:*:key/*' + - Action: 'lambda:GetFunction' + Effect: Allow + Resource: 'arn:aws:lambda:*:*:function:*' + Condition: + StringNotEquals: + 'aws:ResourceTag/DatadogAgentlessScanner': 'false' + - Action: 'lambda:GetLayerVersion' + Effect: Allow + Resource: 'arn:aws:lambda:*:*:layer:*:*' + Condition: + StringNotEquals: + 'aws:ResourceTag/DatadogAgentlessScanner': 'false' + - Action: + - "ecr:GetAuthorizationToken" + Effect: Allow + Resource: "*" + - Action: + - "ecr:GetDownloadUrlForLayer" + - "ecr:BatchGetImage" + Condition: + StringNotEquals: + "ecr:ResourceTag/DatadogAgentlessScanner": "false" + Effect: Allow + Resource: "arn:aws:ecr:*:*:repository/*" + - Action: 's3:GetObject' + Effect: Allow + Resource: 'arn:aws:s3:::*/*' + - Action: 's3:ListBucket' + Effect: Allow + Resource: 'arn:aws:s3:::*' + - Action: + - 'kms:Decrypt' + - 'kms:GenerateDataKey' + Effect: Allow + Resource: 'arn:aws:kms:*:*:key/*' + Condition: + StringLike: + 'kms:ViaService': 's3.*.amazonaws.com' + + LambdaExecutionRoleDatadogAgentlessAPICall: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + 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" + + DatadogAgentlessAPICall: + Type: "Custom::DatadogAgentlessAPICall" + Properties: + ServiceToken: !GetAtt "DatadogAgentlessAPICallFunction.Arn" + TemplateVersion: "" + APIKey: !Ref "DatadogAPIKey" + APPKey: !Ref "DatadogAPPKey" + DatadogSite: !Ref "DatadogSite" + AccountId: !Ref "AWS::AccountId" + VulnerabilityScanning: !Ref "AgentlessVulnerabilityScanning" + SensitiveData: !Ref "AgentlessSensitiveDataScanning" + ComplianceHost: !Ref "AgentlessComplianceHostScanning" + IntegrationRoleName: !Ref "DatadogIntegrationRoleName" + Partition: !Ref "AWS::Partition" + # Optional parameters + SaaSScanningPolicyArn: !Ref "DatadogAgentlessSaaSScanningPolicy" + + DatadogAgentlessAPICallFunction: + Type: "AWS::Lambda::Function" + Properties: + Description: A function to call the Datadog Agentless API. + Role: !GetAtt LambdaExecutionRoleDatadogAgentlessAPICall.Arn + Handler: "index.handler" + LoggingConfig: + ApplicationLogLevel: "INFO" + LogFormat: "JSON" + Runtime: "python3.13" + Timeout: 30 + Code: + ZipFile: | + + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Required" + Parameters: + - DatadogIntegrationRoleName + - Label: + default: "Advanced" + Parameters: + - AgentlessSensitiveDataScanning + - AgentlessComplianceHostScanning + - AccountId diff --git a/aws_quickstart/release.sh b/aws_quickstart/release.sh index 3b8728f..737f47f 100755 --- a/aws_quickstart/release.sh +++ b/aws_quickstart/release.sh @@ -122,7 +122,7 @@ for template in main_workflow.yaml main_extended_workflow.yaml main_v2.yaml main done # Process Agentless Scanning templates -for template in datadog_agentless_delegate_role.yaml datadog_agentless_scanning.yaml datadog_agentless_delegate_role_snapshot.yaml datadog_integration_autoscaling_policy.yaml datadog_integration_sds_policy.yaml datadog_agentless_delegate_role_stackset.yaml; do +for template in datadog_agentless_delegate_role.yaml datadog_agentless_scanning.yaml datadog_agentless_delegate_role_snapshot.yaml datadog_integration_autoscaling_policy.yaml datadog_integration_sds_policy.yaml datadog_agentless_delegate_role_stackset.yaml datadog_agentless_saas.yaml; do # Note: unlike above, here we remove the 'v' prefix from the version perl -pi -e "s//${VERSION#v}/g" "$template" diff --git a/aws_quickstart/version.txt b/aws_quickstart/version.txt index c4475d3..cabad0c 100644 --- a/aws_quickstart/version.txt +++ b/aws_quickstart/version.txt @@ -1 +1 @@ -v4.13.0 +v4.14.0