cyntrisec 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. cyntrisec/__init__.py +3 -0
  2. cyntrisec/__main__.py +6 -0
  3. cyntrisec/aws/__init__.py +6 -0
  4. cyntrisec/aws/collectors/__init__.py +17 -0
  5. cyntrisec/aws/collectors/ec2.py +30 -0
  6. cyntrisec/aws/collectors/iam.py +116 -0
  7. cyntrisec/aws/collectors/lambda_.py +45 -0
  8. cyntrisec/aws/collectors/network.py +70 -0
  9. cyntrisec/aws/collectors/rds.py +38 -0
  10. cyntrisec/aws/collectors/s3.py +68 -0
  11. cyntrisec/aws/collectors/usage.py +188 -0
  12. cyntrisec/aws/credentials.py +153 -0
  13. cyntrisec/aws/normalizers/__init__.py +17 -0
  14. cyntrisec/aws/normalizers/ec2.py +115 -0
  15. cyntrisec/aws/normalizers/iam.py +182 -0
  16. cyntrisec/aws/normalizers/lambda_.py +83 -0
  17. cyntrisec/aws/normalizers/network.py +225 -0
  18. cyntrisec/aws/normalizers/rds.py +130 -0
  19. cyntrisec/aws/normalizers/s3.py +184 -0
  20. cyntrisec/aws/relationship_builder.py +1359 -0
  21. cyntrisec/aws/scanner.py +303 -0
  22. cyntrisec/cli/__init__.py +5 -0
  23. cyntrisec/cli/analyze.py +747 -0
  24. cyntrisec/cli/ask.py +412 -0
  25. cyntrisec/cli/can.py +307 -0
  26. cyntrisec/cli/comply.py +226 -0
  27. cyntrisec/cli/cuts.py +231 -0
  28. cyntrisec/cli/diff.py +332 -0
  29. cyntrisec/cli/errors.py +105 -0
  30. cyntrisec/cli/explain.py +348 -0
  31. cyntrisec/cli/main.py +114 -0
  32. cyntrisec/cli/manifest.py +893 -0
  33. cyntrisec/cli/output.py +117 -0
  34. cyntrisec/cli/remediate.py +643 -0
  35. cyntrisec/cli/report.py +462 -0
  36. cyntrisec/cli/scan.py +207 -0
  37. cyntrisec/cli/schemas.py +391 -0
  38. cyntrisec/cli/serve.py +164 -0
  39. cyntrisec/cli/setup.py +260 -0
  40. cyntrisec/cli/validate.py +101 -0
  41. cyntrisec/cli/waste.py +323 -0
  42. cyntrisec/core/__init__.py +31 -0
  43. cyntrisec/core/business_config.py +110 -0
  44. cyntrisec/core/business_logic.py +131 -0
  45. cyntrisec/core/compliance.py +437 -0
  46. cyntrisec/core/cost_estimator.py +301 -0
  47. cyntrisec/core/cuts.py +360 -0
  48. cyntrisec/core/diff.py +361 -0
  49. cyntrisec/core/graph.py +202 -0
  50. cyntrisec/core/paths.py +830 -0
  51. cyntrisec/core/schema.py +317 -0
  52. cyntrisec/core/simulator.py +371 -0
  53. cyntrisec/core/waste.py +309 -0
  54. cyntrisec/mcp/__init__.py +5 -0
  55. cyntrisec/mcp/server.py +862 -0
  56. cyntrisec/storage/__init__.py +7 -0
  57. cyntrisec/storage/filesystem.py +344 -0
  58. cyntrisec/storage/memory.py +113 -0
  59. cyntrisec/storage/protocol.py +92 -0
  60. cyntrisec-0.1.7.dist-info/METADATA +672 -0
  61. cyntrisec-0.1.7.dist-info/RECORD +65 -0
  62. cyntrisec-0.1.7.dist-info/WHEEL +4 -0
  63. cyntrisec-0.1.7.dist-info/entry_points.txt +2 -0
  64. cyntrisec-0.1.7.dist-info/licenses/LICENSE +190 -0
  65. cyntrisec-0.1.7.dist-info/licenses/NOTICE +5 -0
cyntrisec/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Cyntrisec - AWS capability graph analysis and attack path discovery."""
2
+
3
+ __version__ = "0.1.7"
cyntrisec/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m cyntrisec."""
2
+
3
+ from cyntrisec.cli.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,6 @@
1
+ """AWS scanning modules."""
2
+
3
+ from cyntrisec.aws.credentials import CredentialProvider
4
+ from cyntrisec.aws.scanner import AwsScanner
5
+
6
+ __all__ = ["CredentialProvider", "AwsScanner"]
@@ -0,0 +1,17 @@
1
+ """AWS resource collectors."""
2
+
3
+ from cyntrisec.aws.collectors.ec2 import Ec2Collector
4
+ from cyntrisec.aws.collectors.iam import IamCollector
5
+ from cyntrisec.aws.collectors.lambda_ import LambdaCollector
6
+ from cyntrisec.aws.collectors.network import NetworkCollector
7
+ from cyntrisec.aws.collectors.rds import RdsCollector
8
+ from cyntrisec.aws.collectors.s3 import S3Collector
9
+
10
+ __all__ = [
11
+ "Ec2Collector",
12
+ "IamCollector",
13
+ "S3Collector",
14
+ "LambdaCollector",
15
+ "RdsCollector",
16
+ "NetworkCollector",
17
+ ]
@@ -0,0 +1,30 @@
1
+ """EC2 Collector - Collect EC2 instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import boto3
8
+
9
+
10
+ class Ec2Collector:
11
+ """Collect EC2 resources."""
12
+
13
+ def __init__(self, session: boto3.Session, region: str):
14
+ self._ec2 = session.client("ec2", region_name=region)
15
+ self._region = region
16
+
17
+ def collect_all(self) -> dict[str, Any]:
18
+ """Collect all EC2 data."""
19
+ return {
20
+ "instances": self._collect_instances(),
21
+ }
22
+
23
+ def _collect_instances(self) -> list[dict]:
24
+ """Collect EC2 instances."""
25
+ instances = []
26
+ paginator = self._ec2.get_paginator("describe_instances")
27
+ for page in paginator.paginate():
28
+ for reservation in page.get("Reservations", []):
29
+ instances.extend(reservation.get("Instances", []))
30
+ return instances
@@ -0,0 +1,116 @@
1
+ """IAM Collector - Collect IAM users, roles, policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import boto3
8
+
9
+
10
+ class IamCollector:
11
+ """Collect IAM resources (global)."""
12
+
13
+ def __init__(self, session: boto3.Session):
14
+ self._iam = session.client("iam")
15
+
16
+ def collect_all(self) -> dict[str, Any]:
17
+ """Collect all IAM data."""
18
+ return {
19
+ "users": self._collect_users(),
20
+ "roles": self._collect_roles(),
21
+ "policies": self._collect_policies(),
22
+ "instance_profiles": self._collect_instance_profiles(),
23
+ }
24
+
25
+ def _collect_users(self) -> list[dict]:
26
+ """Collect IAM users."""
27
+ users = []
28
+ paginator = self._iam.get_paginator("list_users")
29
+ for page in paginator.paginate():
30
+ users.extend(page.get("Users", []))
31
+ return users
32
+
33
+ def _collect_roles(self) -> list[dict]:
34
+ """Collect IAM roles with trust policies."""
35
+ roles = []
36
+ paginator = self._iam.get_paginator("list_roles")
37
+ for page in paginator.paginate():
38
+ for role in page.get("Roles", []):
39
+ role_name = role.get("RoleName")
40
+ if role_name:
41
+ role["InlinePolicies"] = self._collect_inline_role_policies(role_name)
42
+ role["AttachedPolicies"] = self._collect_attached_role_policies(role_name)
43
+ # Trust policy is included in list_roles
44
+ roles.append(role)
45
+ return roles
46
+
47
+ def _collect_policies(self) -> list[dict]:
48
+ """Collect customer-managed policies."""
49
+ policies = []
50
+ paginator = self._iam.get_paginator("list_policies")
51
+ for page in paginator.paginate(Scope="Local"):
52
+ policies.extend(page.get("Policies", []))
53
+ return policies
54
+
55
+ def _collect_inline_role_policies(self, role_name: str) -> list[dict]:
56
+ """Collect inline policy documents for a role."""
57
+ policies: list[dict] = []
58
+ paginator = self._iam.get_paginator("list_role_policies")
59
+ for page in paginator.paginate(RoleName=role_name):
60
+ for policy_name in page.get("PolicyNames", []):
61
+ try:
62
+ response = self._iam.get_role_policy(
63
+ RoleName=role_name,
64
+ PolicyName=policy_name,
65
+ )
66
+ except Exception:
67
+ continue
68
+ policies.append(
69
+ {
70
+ "PolicyName": policy_name,
71
+ "Document": response.get("PolicyDocument"),
72
+ }
73
+ )
74
+ return policies
75
+
76
+ def _collect_attached_role_policies(self, role_name: str) -> list[dict]:
77
+ """Collect attached managed policy documents for a role."""
78
+ policies: list[dict] = []
79
+ paginator = self._iam.get_paginator("list_attached_role_policies")
80
+ for page in paginator.paginate(RoleName=role_name):
81
+ for policy in page.get("AttachedPolicies", []):
82
+ policy_arn = policy.get("PolicyArn")
83
+ if not policy_arn:
84
+ continue
85
+ document = self._get_managed_policy_document(policy_arn)
86
+ policies.append(
87
+ {
88
+ "PolicyName": policy.get("PolicyName"),
89
+ "PolicyArn": policy_arn,
90
+ "Document": document,
91
+ }
92
+ )
93
+ return policies
94
+
95
+ def _get_managed_policy_document(self, policy_arn: str) -> dict | None:
96
+ """Fetch the default policy document for a managed policy."""
97
+ try:
98
+ policy = self._iam.get_policy(PolicyArn=policy_arn)
99
+ version_id = policy.get("Policy", {}).get("DefaultVersionId")
100
+ if not version_id:
101
+ return None
102
+ version = self._iam.get_policy_version(
103
+ PolicyArn=policy_arn,
104
+ VersionId=version_id,
105
+ )
106
+ return version.get("PolicyVersion", {}).get("Document")
107
+ except Exception:
108
+ return None
109
+
110
+ def _collect_instance_profiles(self) -> list[dict]:
111
+ """Collect IAM instance profiles and attached roles."""
112
+ profiles = []
113
+ paginator = self._iam.get_paginator("list_instance_profiles")
114
+ for page in paginator.paginate():
115
+ profiles.extend(page.get("InstanceProfiles", []))
116
+ return profiles
@@ -0,0 +1,45 @@
1
+ """Lambda Collector - Collect Lambda functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import boto3
8
+ from botocore.exceptions import ClientError
9
+
10
+
11
+ class LambdaCollector:
12
+ """Collect Lambda resources."""
13
+
14
+ def __init__(self, session: boto3.Session, region: str):
15
+ self._lambda = session.client("lambda", region_name=region)
16
+ self._region = region
17
+
18
+ def collect_all(self) -> dict[str, Any]:
19
+ """Collect all Lambda data."""
20
+ functions = self._collect_functions()
21
+
22
+ # Enrich with policies
23
+ for func in functions:
24
+ name = func["FunctionName"]
25
+ func["Policy"] = self._get_function_policy(name)
26
+
27
+ return {"functions": functions}
28
+
29
+ def _collect_functions(self) -> list[dict]:
30
+ """List all Lambda functions."""
31
+ functions = []
32
+ paginator = self._lambda.get_paginator("list_functions")
33
+ for page in paginator.paginate():
34
+ functions.extend(page.get("Functions", []))
35
+ return functions
36
+
37
+ def _get_function_policy(self, function_name: str) -> dict | None:
38
+ """Get function resource policy."""
39
+ try:
40
+ response = self._lambda.get_policy(FunctionName=function_name)
41
+ return {"Policy": response.get("Policy")}
42
+ except ClientError as e:
43
+ if e.response["Error"]["Code"] == "ResourceNotFoundException":
44
+ return None
45
+ return {"Error": str(e)}
@@ -0,0 +1,70 @@
1
+ """Network Collector - Collect VPCs, subnets, security groups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import boto3
8
+
9
+
10
+ class NetworkCollector:
11
+ """Collect network resources."""
12
+
13
+ def __init__(self, session: boto3.Session, region: str):
14
+ self._session = session
15
+ self._ec2 = session.client("ec2", region_name=region)
16
+ self._region = region
17
+
18
+ def collect_all(self) -> dict[str, Any]:
19
+ """Collect all network data."""
20
+ return {
21
+ "vpcs": self._collect_vpcs(),
22
+ "subnets": self._collect_subnets(),
23
+ "security_groups": self._collect_security_groups(),
24
+ "route_tables": self._collect_route_tables(),
25
+ "internet_gateways": self._collect_internet_gateways(),
26
+ "nat_gateways": self._collect_nat_gateways(),
27
+ "load_balancers": self._collect_load_balancers(),
28
+ }
29
+
30
+ def _collect_vpcs(self) -> list[dict]:
31
+ """Collect VPCs."""
32
+ response = self._ec2.describe_vpcs()
33
+ return response.get("Vpcs", [])
34
+
35
+ def _collect_subnets(self) -> list[dict]:
36
+ """Collect subnets."""
37
+ response = self._ec2.describe_subnets()
38
+ return response.get("Subnets", [])
39
+
40
+ def _collect_security_groups(self) -> list[dict]:
41
+ """Collect security groups."""
42
+ sgs = []
43
+ paginator = self._ec2.get_paginator("describe_security_groups")
44
+ for page in paginator.paginate():
45
+ sgs.extend(page.get("SecurityGroups", []))
46
+ return sgs
47
+
48
+ def _collect_route_tables(self) -> list[dict]:
49
+ """Collect route tables."""
50
+ response = self._ec2.describe_route_tables()
51
+ return response.get("RouteTables", [])
52
+
53
+ def _collect_internet_gateways(self) -> list[dict]:
54
+ """Collect internet gateways."""
55
+ response = self._ec2.describe_internet_gateways()
56
+ return response.get("InternetGateways", [])
57
+
58
+ def _collect_nat_gateways(self) -> list[dict]:
59
+ """Collect NAT gateways."""
60
+ response = self._ec2.describe_nat_gateways()
61
+ return response.get("NatGateways", [])
62
+
63
+ def _collect_load_balancers(self) -> list[dict]:
64
+ """Collect ELBv2 load balancers."""
65
+ try:
66
+ elb = self._session.client("elbv2", region_name=self._region)
67
+ response = elb.describe_load_balancers()
68
+ return response.get("LoadBalancers", [])
69
+ except Exception:
70
+ return []
@@ -0,0 +1,38 @@
1
+ """RDS Collector - Collect RDS instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import boto3
8
+
9
+
10
+ class RdsCollector:
11
+ """Collect RDS resources."""
12
+
13
+ def __init__(self, session: boto3.Session, region: str):
14
+ self._rds = session.client("rds", region_name=region)
15
+ self._region = region
16
+
17
+ def collect_all(self) -> dict[str, Any]:
18
+ """Collect all RDS data."""
19
+ return {
20
+ "instances": self._collect_instances(),
21
+ "clusters": self._collect_clusters(),
22
+ }
23
+
24
+ def _collect_instances(self) -> list[dict]:
25
+ """Collect RDS DB instances."""
26
+ instances = []
27
+ paginator = self._rds.get_paginator("describe_db_instances")
28
+ for page in paginator.paginate():
29
+ instances.extend(page.get("DBInstances", []))
30
+ return instances
31
+
32
+ def _collect_clusters(self) -> list[dict]:
33
+ """Collect RDS Aurora clusters."""
34
+ clusters = []
35
+ paginator = self._rds.get_paginator("describe_db_clusters")
36
+ for page in paginator.paginate():
37
+ clusters.extend(page.get("DBClusters", []))
38
+ return clusters
@@ -0,0 +1,68 @@
1
+ """S3 Collector - Collect S3 buckets and policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import boto3
8
+ from botocore.exceptions import ClientError
9
+
10
+
11
+ class S3Collector:
12
+ """Collect S3 resources (global)."""
13
+
14
+ def __init__(self, session: boto3.Session):
15
+ self._s3 = session.client("s3")
16
+
17
+ def collect_all(self) -> dict[str, Any]:
18
+ """Collect all S3 data."""
19
+ buckets = self._collect_buckets()
20
+
21
+ # Enrich with policies and ACLs
22
+ for bucket in buckets:
23
+ name = bucket["Name"]
24
+ bucket["Policy"] = self._get_bucket_policy(name)
25
+ bucket["Acl"] = self._get_bucket_acl(name)
26
+ bucket["PublicAccessBlock"] = self._get_public_access_block(name)
27
+ bucket["Location"] = self._get_bucket_location(name)
28
+
29
+ return {"buckets": buckets}
30
+
31
+ def _collect_buckets(self) -> list[dict]:
32
+ """List all buckets."""
33
+ response = self._s3.list_buckets()
34
+ return response.get("Buckets", [])
35
+
36
+ def _get_bucket_policy(self, bucket_name: str) -> dict | None:
37
+ """Get bucket policy."""
38
+ try:
39
+ response = self._s3.get_bucket_policy(Bucket=bucket_name)
40
+ return {"Policy": response.get("Policy")}
41
+ except ClientError as e:
42
+ if e.response["Error"]["Code"] == "NoSuchBucketPolicy":
43
+ return None
44
+ return {"Error": str(e)}
45
+
46
+ def _get_bucket_acl(self, bucket_name: str) -> dict | None:
47
+ """Get bucket ACL."""
48
+ try:
49
+ return self._s3.get_bucket_acl(Bucket=bucket_name)
50
+ except ClientError:
51
+ return None
52
+
53
+ def _get_public_access_block(self, bucket_name: str) -> dict | None:
54
+ """Get public access block configuration."""
55
+ try:
56
+ response = self._s3.get_public_access_block(Bucket=bucket_name)
57
+ return response.get("PublicAccessBlockConfiguration")
58
+ except ClientError:
59
+ return None
60
+
61
+ def _get_bucket_location(self, bucket_name: str) -> str:
62
+ """Get bucket region."""
63
+ try:
64
+ response = self._s3.get_bucket_location(Bucket=bucket_name)
65
+ # None means us-east-1
66
+ return response.get("LocationConstraint") or "us-east-1"
67
+ except ClientError:
68
+ return "unknown"
@@ -0,0 +1,188 @@
1
+ """
2
+ Usage Collector - Collect IAM last-accessed data for waste analysis.
3
+
4
+ Uses AWS IAM's generate_service_last_accessed_details API to determine
5
+ which permissions have actually been used vs which are just granted.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime
14
+ from typing import Any
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class ServiceAccess:
21
+ """Service access record from IAM last-accessed data."""
22
+
23
+ service_name: str
24
+ service_namespace: str
25
+ last_authenticated: datetime | None = None
26
+ last_authenticated_entity: str | None = None
27
+ total_authenticated_entities: int = 0
28
+
29
+ @property
30
+ def is_unused(self) -> bool:
31
+ """Check if service was never accessed."""
32
+ return self.last_authenticated is None
33
+
34
+
35
+ @dataclass
36
+ class ActionAccess:
37
+ """Action-level access record."""
38
+
39
+ action_name: str
40
+ last_accessed: datetime | None = None
41
+
42
+ @property
43
+ def is_unused(self) -> bool:
44
+ return self.last_accessed is None
45
+
46
+
47
+ @dataclass
48
+ class RoleUsageReport:
49
+ """Usage report for an IAM role."""
50
+
51
+ role_arn: str
52
+ role_name: str
53
+ services: list[ServiceAccess] = field(default_factory=list)
54
+ actions: list[ActionAccess] = field(default_factory=list)
55
+ generated_at: datetime = field(default_factory=datetime.utcnow)
56
+
57
+ @property
58
+ def unused_services(self) -> list[ServiceAccess]:
59
+ return [s for s in self.services if s.is_unused]
60
+
61
+ @property
62
+ def used_services(self) -> list[ServiceAccess]:
63
+ return [s for s in self.services if not s.is_unused]
64
+
65
+
66
+ class UsageCollector:
67
+ """
68
+ Collect IAM usage data using AWS Access Advisor.
69
+
70
+ The last-accessed data shows which services a role has permissions for
71
+ and when those permissions were last used.
72
+ """
73
+
74
+ def __init__(self, session):
75
+ """
76
+ Initialize with a boto3 Session.
77
+
78
+ Args:
79
+ session: boto3.Session with IAM permissions
80
+ """
81
+ self._session = session
82
+ self._iam = session.client("iam")
83
+
84
+ def get_role_usage(
85
+ self,
86
+ role_arn: str,
87
+ *,
88
+ max_wait_seconds: int = 30,
89
+ ) -> RoleUsageReport | None:
90
+ """
91
+ Get usage report for an IAM role.
92
+
93
+ Args:
94
+ role_arn: ARN of the role to analyze
95
+ max_wait_seconds: Maximum time to wait for report generation
96
+
97
+ Returns:
98
+ RoleUsageReport with service access data, or None if failed
99
+ """
100
+ role_name = role_arn.split("/")[-1]
101
+ log.debug("Generating last-accessed report for %s", role_name)
102
+
103
+ try:
104
+ # Start the report generation
105
+ response = self._iam.generate_service_last_accessed_details(
106
+ Arn=role_arn,
107
+ Granularity="SERVICE_LEVEL",
108
+ )
109
+ job_id = response["JobId"]
110
+
111
+ # Poll for completion
112
+ start_time = time.time()
113
+ while time.time() - start_time < max_wait_seconds:
114
+ result = self._iam.get_service_last_accessed_details(JobId=job_id)
115
+ status = result.get("JobStatus")
116
+
117
+ if status == "COMPLETED":
118
+ return self._parse_report(role_arn, role_name, result)
119
+ elif status == "FAILED":
120
+ log.warning("Last-accessed report failed for %s", role_name)
121
+ return None
122
+
123
+ time.sleep(1)
124
+
125
+ log.warning("Timeout waiting for last-accessed report for %s", role_name)
126
+ return None
127
+
128
+ except Exception as e:
129
+ log.debug("Error getting usage for %s: %s", role_name, e)
130
+ return None
131
+
132
+ def _parse_report(
133
+ self,
134
+ role_arn: str,
135
+ role_name: str,
136
+ result: dict[str, Any],
137
+ ) -> RoleUsageReport:
138
+ """Parse the IAM last-accessed response."""
139
+ services = []
140
+
141
+ for svc in result.get("ServicesLastAccessed", []):
142
+ last_auth = svc.get("LastAuthenticated")
143
+ services.append(
144
+ ServiceAccess(
145
+ service_name=svc.get("ServiceName", ""),
146
+ service_namespace=svc.get("ServiceNamespace", ""),
147
+ last_authenticated=last_auth,
148
+ last_authenticated_entity=svc.get("LastAuthenticatedEntity"),
149
+ total_authenticated_entities=svc.get("TotalAuthenticatedEntities", 0),
150
+ )
151
+ )
152
+
153
+ return RoleUsageReport(
154
+ role_arn=role_arn,
155
+ role_name=role_name,
156
+ services=services,
157
+ )
158
+
159
+ def collect_all_roles(
160
+ self,
161
+ role_arns: list[str],
162
+ *,
163
+ max_roles: int = 50,
164
+ ) -> list[RoleUsageReport]:
165
+ """
166
+ Collect usage reports for multiple roles.
167
+
168
+ Args:
169
+ role_arns: List of role ARNs to analyze
170
+ max_roles: Maximum number of roles to analyze (API throttling)
171
+
172
+ Returns:
173
+ List of usage reports
174
+ """
175
+ reports = []
176
+
177
+ for i, arn in enumerate(role_arns[:max_roles]):
178
+ log.info(
179
+ "Analyzing role %d/%d: %s",
180
+ i + 1,
181
+ min(len(role_arns), max_roles),
182
+ arn.split("/")[-1],
183
+ )
184
+ report = self.get_role_usage(arn)
185
+ if report:
186
+ reports.append(report)
187
+
188
+ return reports