Repository: cloud-custodian/cloud-custodian

Issue:
Add support for Access Analyzer
### Describe the feature

It would be great if we could use custodian to check access issues of various AWS resources.

### Extra information or context

_No response_


Discussion:
Afaics cross-account does cover most of the same use cases.

Resolving patch:
diff --git a/c7n/resources/accessanalyzer.py b/c7n/resources/accessanalyzer.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/accessanalyzer.py
@@ -0,0 +1,58 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+import logging
+from c7n.manager import resources
+from c7n import query
+from c7n.utils import local_session
+
+log = logging.getLogger('custodian.access-analyzer')
+
+class DescribeAccessanalyzerFinding(query.DescribeSource):
+
+    def resources(self, query):
+        analyzer_arn = self.get_analyzer_arn()
+        if not analyzer_arn:
+            return ()
+        if not query:
+            query = {}
+        query['analyzerArn'] = analyzer_arn
+        return super().resources(query)
+
+    def get_analyzer_arn(self):
+        """ Find Active Access Analyzer ARN
+        """
+        client = local_session(self.manager.session_factory).client('accessanalyzer')
+        analyzers = client.list_analyzers().get('analyzers', ())
+        found = False
+        for analyzer in analyzers:
+            if analyzer['status'] != 'ACTIVE':
+                continue
+            # If this account is the Management/delegated administrator for IAM Access Analyzer,
+            #  organization analyzer is prefered
+            if analyzer['type'] == 'ORGANIZATION':
+                found = analyzer
+                break
+            found = analyzer
+        if not found:
+            log.warning("policy: {} no active access analyzer found in account".format(
+                self.manager.ctx.policy.name))
+            return None
+        return found['arn']
+
+
+@resources.register("access-analyzer-finding")
+class AccessanalyzerFinding(query.QueryResourceManager):
+    """AWS IAM Access Analyzer Findings resource
+    """
+    class resource_type(query.TypeInfo):
+        service = "accessanalyzer"
+        enum_spec = ('list_findings', 'findings', None)
+        id = "id"
+        arn_type = ""
+        name = "resourceType"
+        permissions_enum = ('access-analyzer:ListAnalyzers','access-analyzer:ListFindings')
+        permission_prefix = "access-analyzer"
+
+    source_mapping = {
+        "describe": DescribeAccessanalyzerFinding,
+    }
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -1,6 +1,7 @@
 # Copyright The Cloud Custodian Authors.
 # SPDX-License-Identifier: Apache-2.0
 ResourceMap = {
+  "aws.access-analyzer-finding": "c7n.resources.accessanalyzer.AccessanalyzerFinding",
   "aws.account": "c7n.resources.account.Account",
   "aws.acm-certificate": "c7n.resources.acm.Certificate",
   "aws.advisor-check": "c7n.resources.support.AdvisorCheck",


Test patch:
diff --git a/tests/data/placebo/test_access_analyzer_finding/access-analyzer.ListAnalyzers_1.json b/tests/data/placebo/test_access_analyzer_finding/access-analyzer.ListAnalyzers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_access_analyzer_finding/access-analyzer.ListAnalyzers_1.json
@@ -0,0 +1,36 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "analyzers": [
+            {
+                "arn": "arn:aws:access-analyzer:us-east-1:644160558196:analyzer/ConsoleAnalyzer-iamaa-2b9ec1f2-xxxx-4e7d-8c0c-12340954e789",
+                "name": "ConsoleAnalyzer-iamaa-2b9ec1f2-xxxx-4e7d-8c0c-12340954e789",
+                "type": "ACCOUNT",
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 3,
+                    "day": 29,
+                    "hour": 13,
+                    "minute": 53,
+                    "second": 8,
+                    "microsecond": 0
+                },
+                "lastResourceAnalyzed": "arn:aws:ecr:us-east-1:644160558196:repository/container-assets-644160558196-us-east-1",
+                "lastResourceAnalyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 9,
+                    "day": 2,
+                    "hour": 11,
+                    "minute": 15,
+                    "second": 5,
+                    "microsecond": 964000
+                },
+                "tags": {},
+                "status": "ACTIVE"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_access_analyzer_finding/access-analyzer.ListFindings_1.json b/tests/data/placebo/test_access_analyzer_finding/access-analyzer.ListFindings_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_access_analyzer_finding/access-analyzer.ListFindings_1.json
@@ -0,0 +1,188 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "findings": [
+            {
+                "id": "6d1a2533-bfac-4e07-82b2-7540f68cad26",
+                "principal": {
+                    "AWS": "arn:aws:iam::644160558196:role/service-role/resource-check-role-6qxf8l2j"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-RecorderRoleC4E33A-TSABEY6UXXXX",
+                "isPublic": false,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 1,
+                    "hour": 16,
+                    "minute": 17,
+                    "second": 22,
+                    "microsecond": 667000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "status": "RESOLVED",
+                "resourceOwnerAccount": "644160558196"
+            },
+            {
+                "id": "de234567-65aa-4290-9699-50d59ec5d8ae",
+                "principal": {
+                    "AWS": "arn:aws:sts::644160558196:assumed-role/resource-check-role-6qxf8l2j/resource-check"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-RecorderRoleC4E33C-TSABEY6UXXXX",
+                "isPublic": false,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 1,
+                    "hour": 16,
+                    "minute": 15,
+                    "second": 9,
+                    "microsecond": 48000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "status": "RESOLVED",
+                "resourceOwnerAccount": "644160558196"
+            },
+            {
+                "id": "532ef930-f3f7-4389-a9f0-6ae323bfa899",
+                "principal": {
+                    "AWS": "644160558196"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-RecorderRoleC4E33B-TSABEY6UXXXX",
+                "isPublic": true,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 9,
+                    "day": 2,
+                    "hour": 11,
+                    "minute": 15,
+                    "second": 5,
+                    "microsecond": 967000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 13,
+                    "minute": 48,
+                    "second": 10,
+                    "microsecond": 546000
+                },
+                "status": "ACTIVE",
+                "resourceOwnerAccount": "644160558196"
+            },
+            {
+                "id": "56a4bd74-f5d1-4fa0-968c-111f1cdeeaf8",
+                "principal": {
+                    "AWS": "644160558196"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-pe-RecorderRoleC4E33A-3B9W2DM0TNW",
+                "isPublic": false,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 8,
+                    "day": 10,
+                    "hour": 9,
+                    "minute": 20,
+                    "second": 18,
+                    "microsecond": 792000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 9,
+                    "day": 2,
+                    "hour": 11,
+                    "minute": 15,
+                    "second": 5,
+                    "microsecond": 967000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 8,
+                    "day": 10,
+                    "hour": 9,
+                    "minute": 24,
+                    "second": 46,
+                    "microsecond": 267000
+                },
+                "status": "ACTIVE",
+                "resourceOwnerAccount": "644160558196"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_access_analyzer_finding_no_analyzer/access-analyzer.ListAnalyzers_1.json b/tests/data/placebo/test_access_analyzer_finding_no_analyzer/access-analyzer.ListAnalyzers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_access_analyzer_finding_no_analyzer/access-analyzer.ListAnalyzers_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "analyzers": []
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_access_analyzer_finding_org_analyzer/access-analyzer.ListAnalyzers_1.json b/tests/data/placebo/test_access_analyzer_finding_org_analyzer/access-analyzer.ListAnalyzers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_access_analyzer_finding_org_analyzer/access-analyzer.ListAnalyzers_1.json
@@ -0,0 +1,64 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "analyzers": [
+            {
+                "arn": "arn:aws:access-analyzer:us-east-1:644160558196:analyzer/ConsoleAnalyzer-iamaa-2b9ec1f2-xxxx-4e7d-8c0c-12340954e789",
+                "name": "ConsoleAnalyzer-iamaa-2b9ec1f2-xxxx-4e7d-8c0c-12340954e789",
+                "type": "ACCOUNT",
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 3,
+                    "day": 29,
+                    "hour": 13,
+                    "minute": 53,
+                    "second": 8,
+                    "microsecond": 0
+                },
+                "lastResourceAnalyzed": "arn:aws:ecr:us-east-1:644160558196:repository/container-assets-644160558196-us-east-1",
+                "lastResourceAnalyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 9,
+                    "day": 2,
+                    "hour": 11,
+                    "minute": 15,
+                    "second": 5,
+                    "microsecond": 964000
+                },
+                "tags": {},
+                "status": "ACTIVE"
+            },
+            {
+                "arn": "arn:aws:access-analyzer:us-east-1:644160558196:analyzer/ORG-ConsoleAnalyzer-iamaa-2b9ec1f2-xxxx-4e7d-8c0c-67890b1234",
+                "name": "ORG-ConsoleAnalyzer-iamaa-2b9ec1f2-xxxx-4e7d-abcd-67890b1234",
+                "type": "ORGANIZATION",
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 3,
+                    "day": 29,
+                    "hour": 13,
+                    "minute": 53,
+                    "second": 8,
+                    "microsecond": 0
+                },
+                "lastResourceAnalyzed": "arn:aws:ecr:us-east-1:644160558196:repository/container-assets-644160558196-us-east-1",
+                "lastResourceAnalyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 9,
+                    "day": 2,
+                    "hour": 11,
+                    "minute": 15,
+                    "second": 5,
+                    "microsecond": 964000
+                },
+                "tags": {},
+                "status": "ACTIVE"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_access_analyzer_finding_org_analyzer/access-analyzer.ListFindings_1.json b/tests/data/placebo/test_access_analyzer_finding_org_analyzer/access-analyzer.ListFindings_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_access_analyzer_finding_org_analyzer/access-analyzer.ListFindings_1.json
@@ -0,0 +1,188 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "findings": [
+            {
+                "id": "3545aa07-7291-4e6e-8f12-ec1079808bce",
+                "principal": {
+                    "AWS": "arn:aws:iam::644160558196:role/service-role/resource-check-role-6qxf8l2j"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-RecorderRoleC4E33A-TSABEY6UXXXX",
+                "isPublic": false,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 1,
+                    "hour": 16,
+                    "minute": 17,
+                    "second": 22,
+                    "microsecond": 667000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "status": "RESOLVED",
+                "resourceOwnerAccount": "644160558196"
+            },
+            {
+                "id": "c8b465c4-d3fb-415e-b7af-2fb228d85942",
+                "principal": {
+                    "AWS": "arn:aws:sts::644160558196:assumed-role/resource-check-role-6qxf8l2j/resource-check"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-RecorderRoleC4E33C-TSABEY6UXXXX",
+                "isPublic": false,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 1,
+                    "hour": 16,
+                    "minute": 15,
+                    "second": 9,
+                    "microsecond": 48000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "status": "RESOLVED",
+                "resourceOwnerAccount": "644160558196"
+            },
+            {
+                "id": "ba83897f-f9d4-4722-844a-3e423df6d7d3",
+                "principal": {
+                    "AWS": "644160558196"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-RecorderRoleC4E33B-TSABEY6UXXXX",
+                "isPublic": true,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 32,
+                    "second": 24,
+                    "microsecond": 886000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 9,
+                    "day": 2,
+                    "hour": 11,
+                    "minute": 15,
+                    "second": 5,
+                    "microsecond": 967000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 5,
+                    "hour": 13,
+                    "minute": 48,
+                    "second": 10,
+                    "microsecond": 546000
+                },
+                "status": "ACTIVE",
+                "resourceOwnerAccount": "644160558196"
+            },
+            {
+                "id": "8ad2f7a9-e237-4b5b-a8f3-858273e2597a",
+                "principal": {
+                    "AWS": "644160558196"
+                },
+                "action": [
+                    "sts:AssumeRole"
+                ],
+                "resource": "arn:aws:iam::644160558196:role/aws-pe-RecorderRoleC4E33A-3B9W2DM0TNW",
+                "isPublic": false,
+                "resourceType": "AWS::IAM::Role",
+                "condition": {},
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 8,
+                    "day": 10,
+                    "hour": 9,
+                    "minute": 20,
+                    "second": 18,
+                    "microsecond": 792000
+                },
+                "analyzedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 9,
+                    "day": 2,
+                    "hour": 11,
+                    "minute": 15,
+                    "second": 5,
+                    "microsecond": 967000
+                },
+                "updatedAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 8,
+                    "day": 10,
+                    "hour": 9,
+                    "minute": 24,
+                    "second": 46,
+                    "microsecond": 267000
+                },
+                "status": "ACTIVE",
+                "resourceOwnerAccount": "644160558196"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/test_accessanalyzer.py b/tests/test_accessanalyzer.py
new file mode 100644
--- /dev/null
+++ b/tests/test_accessanalyzer.py
@@ -0,0 +1,60 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+from .common import BaseTest
+
+
+class AccessanalyzerFindingTest(BaseTest):
+    """ IAM Access Analyzer Tests"""
+    def test_access_analyzer_finding(self):
+        """
+        Test IAM AA Finding resource with Generic Value filter
+        """
+        session_factory = self.replay_flight_data('test_access_analyzer_finding')
+        policy = {
+            'name': 'list-access-analyzer-findings',
+            'resource': 'aws.access-analyzer-finding',
+            'filters': [{'type': 'value',
+                 'key': 'status',
+                 'value': 'ACTIVE'}]
+        }
+
+        policy = self.load_policy(
+            policy,
+            session_factory=session_factory
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 2)
+
+    def test_access_analyzer_finding_no_analyzer(self):
+        """
+        Test IAM AA Finding resource when there is no active analyzer configured in the account
+        """
+        session_factory = self.replay_flight_data('test_access_analyzer_finding_no_analyzer')
+        policy = {
+            'name': 'list-access-analyzer-findings',
+            'resource': 'aws.access-analyzer-finding'
+        }
+
+        policy = self.load_policy(
+            policy,
+            session_factory=session_factory
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 0)
+
+    def test_access_analyzer_finding_org_analyzer(self):
+        """
+        Test IAM AA Finding resource when there is Org analyzer configured in the account
+        """
+        session_factory = self.replay_flight_data('test_access_analyzer_finding_org_analyzer')
+        policy = {
+            'name': 'list-access-analyzer-findings',
+            'resource': 'aws.access-analyzer-finding'
+        }
+
+        policy = self.load_policy(
+            policy,
+            session_factory=session_factory
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 4)