diff --git a/aws_quickstart/CHANGELOG.md b/aws_quickstart/CHANGELOG.md index dad0674d..b2b9b9ad 100644 --- a/aws_quickstart/CHANGELOG.md +++ b/aws_quickstart/CHANGELOG.md @@ -1,3 +1,7 @@ +# 4.20.0 (August 27, 2026) + +- Add AWS Lambda as a managed instrumentation resource type and forward Lambda lifecycle, configuration, and tag changes for event-driven reconciliation. + # 4.19.1 (August 25, 2026) - Fix Datadog Operator Marketplace agreement discovery and acceptance for Lambda runtimes that lack required agreement operations or paginators. diff --git a/aws_quickstart/attach_integration_permissions.py b/aws_quickstart/attach_integration_permissions.py index 39be6110..4d28b4d2 100644 --- a/aws_quickstart/attach_integration_permissions.py +++ b/aws_quickstart/attach_integration_permissions.py @@ -283,8 +283,18 @@ def _role_arn_patterns_overlap(left, right): def _validate_permissions_boundary_policy_documents(boundary_documents, account_id, partition): - if not isinstance(boundary_documents, list) or not boundary_documents: - raise DatadogAPIError("Datadog API returned no permissions boundary policy documents") + # An absent field means a response predating the boundary contract, so it stays fatal. + # An explicit empty list is a valid answer: resource types with no helper role, such as + # aws:lambda:function, need no boundary, and any boundary left from a previous selection + # is then correctly cleaned up. + if boundary_documents is None: + raise DatadogAPIError( + "Datadog API response omitted permissions_boundary_policy_documents" + ) + if not isinstance(boundary_documents, list): + raise DatadogAPIError("Datadog API returned invalid permissions boundary policy documents") + if not boundary_documents: + return [] if len(boundary_documents) > MAX_BOUNDARY_POLICIES: raise DatadogAPIError( f"Datadog API returned {len(boundary_documents)} permissions boundaries; " diff --git a/aws_quickstart/attach_integration_permissions_test.py b/aws_quickstart/attach_integration_permissions_test.py index 35d6fca0..937157a0 100644 --- a/aws_quickstart/attach_integration_permissions_test.py +++ b/aws_quickstart/attach_integration_permissions_test.py @@ -15,6 +15,7 @@ sys.modules["cfnresponse"] = MagicMock() from attach_integration_permissions import ( + DatadogAPIError, parse_resource_types, build_instrumentation_permissions_url, attach_instrumentation_permissions, @@ -80,7 +81,7 @@ def test_release_embeds_tested_source(self): def test_custom_resource_has_policy_attachment_schema_version(self): template_path = Path(__file__).with_name("datadog_integration_permissions.yaml") - self.assertIn(' PolicyAttachmentSchemaVersion: "5"', template_path.read_text()) + self.assertIn(' PolicyAttachmentSchemaVersion: "6"', template_path.read_text()) def test_execution_role_scopes_boundary_lifecycle_to_namespace(self): template_path = Path(__file__).with_name("datadog_integration_permissions.yaml") @@ -479,6 +480,28 @@ def test_missing_boundary_documents_preserves_existing_policies(self, mock_urlop self.iam.create_policy.assert_not_called() self.iam.detach_role_policy.assert_not_called() + @patch("attach_integration_permissions.urllib.request.urlopen") + def test_lambda_without_boundaries_attaches_instrumentation_policy(self, mock_urlopen): + document = {"Version": "2012-10-17", "Statement": []} + mock_urlopen.return_value = self._mock_response( + policy_documents=[document], + permissions_boundary_policy_documents=[], + ) + + self._attach(["aws:lambda:function"], fail_on_error=True) + + self.assertEqual(self._created_policy_documents(), [document]) + self.manage_boundaries.assert_called_once_with( + self.iam, + [], + self.owner_id, + ) + self.cleanup_boundaries.assert_called_once_with( + self.iam, + self.owner_id, + retained_policy_arns=set(), + ) + @patch("attach_integration_permissions._replace_instrumentation_policies") @patch("attach_integration_permissions.urllib.request.urlopen") def test_manages_boundaries_before_replacing_instrumentation_policies( @@ -839,6 +862,20 @@ def test_validates_dynamic_boundary_contract(self): sorted(BOUNDARY_POLICY_NAMES), ) + def test_rejects_omitted_boundary_documents(self): + with self.assertRaises(DatadogAPIError): + _validate_permissions_boundary_policy_documents(None, "123456789012", "aws") + + def test_accepts_explicit_empty_boundary_documents(self): + self.assertEqual( + _validate_permissions_boundary_policy_documents( + [], + "123456789012", + "aws", + ), + [], + ) + def test_accepts_new_boundary_name_without_quickstart_change(self): documents = permissions_boundary_documents(policy_names=("datadog-new-boundary",)) diff --git a/aws_quickstart/cfn_common_test.py b/aws_quickstart/cfn_common_test.py index c70afb08..88dc4b55 100644 --- a/aws_quickstart/cfn_common_test.py +++ b/aws_quickstart/cfn_common_test.py @@ -72,5 +72,64 @@ def test_shared_helper_composes_with_each_handler(self): compile(source, filename, "exec") +class TestForwardingConditions(unittest.TestCase): + def test_parent_templates_gate_forwarding_on_supported_resource_types(self): + directory = Path(__file__).parent + condition = """ IncludeEC2: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:ec2:instance," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeEKS: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:eks:cluster," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeLambda: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:lambda:function," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + ShouldForwardEvents: + Fn::Or: + - Condition: IncludeEC2 + - Condition: IncludeEKS + - Condition: IncludeLambda +""" + + for filename in ( + "main_agent_installation.yaml", + "main_workflow.yaml", + "main_extended_workflow.yaml", + ): + with self.subTest(filename=filename): + template = (directory / filename).read_text() + self.assertIn(condition, template) + + if __name__ == "__main__": unittest.main() diff --git a/aws_quickstart/datadog_agent_resource_update_forwarding.yaml b/aws_quickstart/datadog_agent_resource_update_forwarding.yaml index 80cb5141..603ce696 100644 --- a/aws_quickstart/datadog_agent_resource_update_forwarding.yaml +++ b/aws_quickstart/datadog_agent_resource_update_forwarding.yaml @@ -24,8 +24,8 @@ Parameters: Default: "" Description: >- Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, - aws:eks:cluster) to forward CloudTrail events for. Only rules for the listed - types are deployed. + aws:eks:cluster, aws:lambda:function) to forward CloudTrail events for. Only + rules for the listed types are deployed. Conditions: IncludeEC2: Fn::Not: @@ -37,6 +37,11 @@ Conditions: - Fn::Equals: - !Join ["", !Split [",aws:eks:cluster,", !Sub ",${InstrumentationResourceTypes},"]] - !Sub ",${InstrumentationResourceTypes}," + IncludeLAMBDA: + Fn::Not: + - Fn::Equals: + - !Join ["", !Split [",aws:lambda:function,", !Sub ",${InstrumentationResourceTypes},"]] + - !Sub ",${InstrumentationResourceTypes}," Resources: DDIntakeConnection: Type: AWS::Events::Connection @@ -165,3 +170,32 @@ Resources: - Id: datadog-intake Arn: !GetAtt DDIntakeApiDestination.Arn RoleArn: !GetAtt DDEventBridgeInvocationRole.Arn + DDEventForwardingRuleLAMBDA: + Type: AWS::Events::Rule + Condition: IncludeLAMBDA + Properties: + Name: datadog-agent-resource-update-rule-lambda + Description: Forward LAMBDA CloudTrail events to the Datadog resource update intake + State: ENABLED + EventPattern: + source: + - aws.lambda + detail-type: + - "AWS API Call via CloudTrail" + detail: + errorCode: + - exists: false + $or: + - eventName: + - CreateFunction20150331 + - UpdateFunctionConfiguration20150331v2 + - eventName: + - TagResource20170331v2 + - UntagResource20170331v2 + requestParameters: + resource: + - wildcard: "*:function:*" + Targets: + - Id: datadog-intake + Arn: !GetAtt DDIntakeApiDestination.Arn + RoleArn: !GetAtt DDEventBridgeInvocationRole.Arn diff --git a/aws_quickstart/datadog_integration_permissions.yaml b/aws_quickstart/datadog_integration_permissions.yaml index 45792f71..fc5cb549 100644 --- a/aws_quickstart/datadog_integration_permissions.yaml +++ b/aws_quickstart/datadog_integration_permissions.yaml @@ -18,9 +18,9 @@ Parameters: Default: "" Description: >- Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, - aws:eks:cluster) that the Datadog integration role should be granted the IAM permissions - required to instrument with the Datadog Agent. In the commercial AWS partition, selecting - EKS also accepts the free Datadog Operator AWS Marketplace agreement. Leave blank to skip. + aws:eks:cluster, aws:lambda:function) that the Datadog integration role should be granted the + IAM permissions required to instrument. In the commercial AWS partition, selecting EKS also + accepts the free Datadog Operator AWS Marketplace agreement. Leave blank to skip. DatadogSite: Type: String Default: "datadoghq.com" @@ -157,8 +157,8 @@ Resources: Type: Custom::DatadogAttachIntegrationPermissionsFunctionTrigger Properties: ServiceToken: !GetAtt DatadogAttachIntegrationPermissionsFunction.Arn - # Bump this value when Lambda behavior changes so existing stacks invoke the custom resource. - PolicyAttachmentSchemaVersion: "5" + # Change only for migrations that must rerun policy attachment on existing stacks. + PolicyAttachmentSchemaVersion: "6" DatadogIntegrationRole: !Ref IAMRoleName AccountId: !Ref AWS::AccountId Partition: !Sub "${AWS::Partition}" diff --git a/aws_quickstart/datadog_integration_role.yaml b/aws_quickstart/datadog_integration_role.yaml index f3285ed0..d08d6c6a 100644 --- a/aws_quickstart/datadog_integration_role.yaml +++ b/aws_quickstart/datadog_integration_role.yaml @@ -23,8 +23,8 @@ Parameters: Default: "" Description: >- Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, - aws:eks:cluster) that the Datadog integration role should be granted the IAM permissions - required to instrument with the Datadog Agent. Leave blank to skip. + aws:eks:cluster, aws:lambda:function) that the Datadog integration role should be granted the IAM permissions + required to instrument. Leave blank to skip. DatadogSite: Type: String Default: "datadoghq.com" diff --git a/aws_quickstart/main_agent_installation.yaml b/aws_quickstart/main_agent_installation.yaml index 9ea6a8eb..c84faa9b 100644 --- a/aws_quickstart/main_agent_installation.yaml +++ b/aws_quickstart/main_agent_installation.yaml @@ -1,12 +1,6 @@ # version: -# -# Post-setup Agent installation add-on. Lets customers who declined the Agent installation option -# during initial AWS integration setup enable it later, against an existing integration role: it -# attaches the instrumentation IAM permissions and deploys the EventBridge forwarding pipeline, -# without touching the standard or resource-collection policies owned by the role stack. -# AWSTemplateFormatVersion: 2010-09-09 -Description: Datadog AWS Integration - Agent installation add-on +Description: Datadog AWS Integration - instrumentation add-on Parameters: APIKey: Description: >- @@ -35,21 +29,60 @@ Parameters: InstrumentationResourceTypes: Type: CommaDelimitedList Description: >- - Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:eks:cluster) to enable - Datadog Agent installation for. The integration role is granted the IAM permissions required to instrument - these resources. CloudTrail update events are forwarded to Datadog for the supported resource types - (currently aws:ec2:instance and aws:eks:cluster); other types receive IAM permissions but no event forwarding. + Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:eks:cluster, + aws:lambda:function) to enable Datadog instrumentation for. The integration role is granted the IAM + permissions required to instrument these resources. CloudTrail update events are forwarded to Datadog + for supported resource types; other types receive IAM permissions but no event forwarding. Rules: ValidateAccountId: Assertions: - Assert: !Equals [!Ref AccountId, !Ref "AWS::AccountId"] AssertDescription: "The AWS Account Id of the account integrated in Datadog does not match the AWS Account Id of the account where this stack will be created." Conditions: - ShouldForwardEvents: + IncludeEC2: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:ec2:instance," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeEKS: Fn::Not: - Fn::Equals: - - !Join ["", !Ref InstrumentationResourceTypes] - - "" + - !Join + - "" + - !Split + - ",aws:eks:cluster," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeLambda: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:lambda:function," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + ShouldForwardEvents: + Fn::Or: + - Condition: IncludeEC2 + - Condition: IncludeEKS + - Condition: IncludeLambda Resources: # Attaches only the instrumentation IAM policies to the existing integration role. ManageBasePermissions # is false so the standard and resource-collection policies owned by the role stack are left untouched. @@ -64,9 +97,6 @@ Resources: DatadogSite: !Ref DatadogSite ManageBasePermissions: false FailOnInstrumentationError: true - # EventBridge pipeline forwarding CloudTrail events to the Datadog resource update intake. - # Deployed only when at least one InstrumentationResourceTypes value is set; single-region - # (covers the region this stack is deployed in). DatadogAgentResourceUpdateForwardingStack: Type: AWS::CloudFormation::Stack Condition: ShouldForwardEvents diff --git a/aws_quickstart/main_extended.yaml b/aws_quickstart/main_extended.yaml index d9b92f2e..abeb77fa 100644 --- a/aws_quickstart/main_extended.yaml +++ b/aws_quickstart/main_extended.yaml @@ -85,9 +85,9 @@ Parameters: InstrumentationResourceTypes: Type: CommaDelimitedList Description: >- - Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, aws:eks:cluster) - that the Datadog integration role should be granted the IAM permissions required to instrument with the Datadog - Agent. Leave blank to skip granting any extra instrumentation permissions. + Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, + aws:eks:cluster, aws:lambda:function) that the Datadog integration role should be granted the IAM permissions + required to instrument. Leave blank to skip granting any extra instrumentation permissions. Default: "" AgentlessVulnerabilityScanning: Type: String diff --git a/aws_quickstart/main_extended_workflow.yaml b/aws_quickstart/main_extended_workflow.yaml index abf702f1..968f5764 100644 --- a/aws_quickstart/main_extended_workflow.yaml +++ b/aws_quickstart/main_extended_workflow.yaml @@ -95,9 +95,9 @@ Parameters: InstrumentationResourceTypes: Type: CommaDelimitedList Description: >- - Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, aws:eks:cluster) - that the Datadog integration role should be granted the IAM permissions required to instrument with the Datadog - Agent. Leave blank to skip granting any extra instrumentation permissions. + Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, + aws:eks:cluster, aws:lambda:function) that the Datadog integration role should be granted the IAM permissions + required to instrument. Leave blank to skip granting any extra instrumentation permissions. Default: "" AgentlessVulnerabilityScanning: Type: String @@ -222,11 +222,50 @@ Conditions: Fn::Equals: - !Ref DisableResourceCollection - false - ShouldForwardEvents: + IncludeEC2: Fn::Not: - Fn::Equals: - - !Join ["", !Ref InstrumentationResourceTypes] - - "" + - !Join + - "" + - !Split + - ",aws:ec2:instance," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeEKS: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:eks:cluster," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeLambda: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:lambda:function," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + ShouldForwardEvents: + Fn::Or: + - Condition: IncludeEC2 + - Condition: IncludeEKS + - Condition: IncludeLambda EnableAgentlessScanning: Fn::And: - Fn::Not: @@ -564,9 +603,6 @@ Resources: stack_id: !Ref AWS::StackId forwarder_arn: !GetAtt ForwarderStack.Outputs.DatadogForwarderArn - # EventBridge pipeline forwarding CloudTrail events to the Datadog resource update - # intake. Deployed only when at least one InstrumentationResourceTypes value is set; - # single-region (covers the region this stack is deployed in). DatadogAgentResourceUpdateForwardingStack: Type: AWS::CloudFormation::Stack Condition: ShouldForwardEvents diff --git a/aws_quickstart/main_v2.yaml b/aws_quickstart/main_v2.yaml index 68efddc6..107d8e60 100644 --- a/aws_quickstart/main_v2.yaml +++ b/aws_quickstart/main_v2.yaml @@ -72,9 +72,9 @@ Parameters: InstrumentationResourceTypes: Type: CommaDelimitedList Description: >- - Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, aws:eks:cluster) - that the Datadog integration role should be granted the IAM permissions required to instrument with the Datadog - Agent. Leave blank to skip granting any extra instrumentation permissions. + Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, + aws:eks:cluster, aws:lambda:function) that the Datadog integration role should be granted the IAM permissions + required to instrument. Leave blank to skip granting any extra instrumentation permissions. Default: "" Rules: ResourceCollectionValidState: diff --git a/aws_quickstart/main_workflow.yaml b/aws_quickstart/main_workflow.yaml index a31d10c1..63249b4d 100644 --- a/aws_quickstart/main_workflow.yaml +++ b/aws_quickstart/main_workflow.yaml @@ -61,9 +61,9 @@ Parameters: InstrumentationResourceTypes: Type: CommaDelimitedList Description: >- - Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, aws:eks:cluster) - that the Datadog integration role should be granted the IAM permissions required to instrument with the Datadog - Agent. Leave blank to skip granting any extra instrumentation permissions. + Comma-separated list of AWS resource types (UDM form, e.g. aws:ec2:instance, aws:ecs:cluster, + aws:eks:cluster, aws:lambda:function) that the Datadog integration role should be granted the IAM permissions + required to instrument. Leave blank to skip granting any extra instrumentation permissions. Default: "" WorkflowId: Description: >- @@ -122,11 +122,50 @@ Conditions: Fn::Equals: - !Ref DisableResourceCollection - false - ShouldForwardEvents: + IncludeEC2: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:ec2:instance," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeEKS: Fn::Not: - Fn::Equals: - - !Join ["", !Ref InstrumentationResourceTypes] - - "" + - !Join + - "" + - !Split + - ",aws:eks:cluster," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + IncludeLambda: + Fn::Not: + - Fn::Equals: + - !Join + - "" + - !Split + - ",aws:lambda:function," + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + - !Sub + - ",${NormalizedResourceTypes}," + - NormalizedResourceTypes: !Join [",", !Ref InstrumentationResourceTypes] + ShouldForwardEvents: + Fn::Or: + - Condition: IncludeEC2 + - Condition: IncludeEKS + - Condition: IncludeLambda IsGov: Fn::Or: - Fn::Equals: @@ -420,9 +459,6 @@ Resources: stack_id: !Ref AWS::StackId forwarder_arn: !GetAtt ForwarderStack.Outputs.DatadogForwarderArn - # EventBridge pipeline forwarding CloudTrail events to the Datadog resource update - # intake. Deployed only when at least one InstrumentationResourceTypes value is set; - # single-region (covers the region this stack is deployed in). DatadogAgentResourceUpdateForwardingStack: Type: AWS::CloudFormation::Stack Condition: ShouldForwardEvents diff --git a/aws_quickstart/version.txt b/aws_quickstart/version.txt index 9b56abd2..1beef590 100644 --- a/aws_quickstart/version.txt +++ b/aws_quickstart/version.txt @@ -1 +1 @@ -v4.19.1 +v4.20.0