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
@@ -0,0 +1,225 @@
1
+ """Network Normalizer - Transform network data to canonical schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any
7
+
8
+ from cyntrisec.core.schema import Asset, Finding, FindingSeverity, Relationship
9
+
10
+
11
+ class NetworkNormalizer:
12
+ """Normalize network data to canonical assets and relationships."""
13
+
14
+ def __init__(
15
+ self,
16
+ snapshot_id: uuid.UUID,
17
+ region: str,
18
+ account_id: str,
19
+ ):
20
+ self._snapshot_id = snapshot_id
21
+ self._region = region
22
+ self._account_id = account_id
23
+ self._asset_map: dict[str, Asset] = {}
24
+
25
+ def normalize(
26
+ self,
27
+ data: dict[str, Any],
28
+ ) -> tuple[list[Asset], list[Relationship], list[Finding]]:
29
+ """Normalize network data."""
30
+ assets: list[Asset] = []
31
+ relationships: list[Relationship] = []
32
+ findings: list[Finding] = []
33
+
34
+ # VPCs
35
+ for vpc in data.get("vpcs", []):
36
+ asset = self._normalize_vpc(vpc)
37
+ assets.append(asset)
38
+ self._asset_map[vpc["VpcId"]] = asset
39
+
40
+ # Subnets
41
+ for subnet in data.get("subnets", []):
42
+ asset = self._normalize_subnet(subnet)
43
+ assets.append(asset)
44
+ self._asset_map[subnet["SubnetId"]] = asset
45
+
46
+ # Create VPC -> Subnet relationship
47
+ vpc_id = subnet.get("VpcId")
48
+ if vpc_id and vpc_id in self._asset_map:
49
+ relationships.append(
50
+ Relationship(
51
+ snapshot_id=self._snapshot_id,
52
+ source_asset_id=self._asset_map[vpc_id].id,
53
+ target_asset_id=asset.id,
54
+ relationship_type="CONTAINS",
55
+ )
56
+ )
57
+
58
+ # Security Groups
59
+ for sg in data.get("security_groups", []):
60
+ asset, sg_findings = self._normalize_security_group(sg)
61
+ assets.append(asset)
62
+ self._asset_map[sg["GroupId"]] = asset
63
+ findings.extend(sg_findings)
64
+
65
+ # Load Balancers
66
+ for lb in data.get("load_balancers", []):
67
+ asset = self._normalize_load_balancer(lb)
68
+ assets.append(asset)
69
+
70
+ return assets, relationships, findings
71
+
72
+ def _normalize_vpc(self, vpc: dict[str, Any]) -> Asset:
73
+ """Normalize a VPC."""
74
+ vpc_id = vpc["VpcId"]
75
+
76
+ name = vpc_id
77
+ for tag in vpc.get("Tags", []):
78
+ if tag["Key"] == "Name":
79
+ name = tag["Value"]
80
+ break
81
+
82
+ return Asset(
83
+ snapshot_id=self._snapshot_id,
84
+ asset_type="ec2:vpc",
85
+ aws_region=self._region,
86
+ aws_resource_id=vpc_id,
87
+ arn=f"arn:aws:ec2:{self._region}:{self._account_id}:vpc/{vpc_id}",
88
+ name=name,
89
+ properties={
90
+ "cidr_block": vpc.get("CidrBlock"),
91
+ "is_default": vpc.get("IsDefault", False),
92
+ "state": vpc.get("State"),
93
+ },
94
+ )
95
+
96
+ def _normalize_subnet(self, subnet: dict[str, Any]) -> Asset:
97
+ """Normalize a subnet."""
98
+ subnet_id = subnet["SubnetId"]
99
+
100
+ name = subnet_id
101
+ for tag in subnet.get("Tags", []):
102
+ if tag["Key"] == "Name":
103
+ name = tag["Value"]
104
+ break
105
+
106
+ # Determine if public (has auto-assign public IP)
107
+ is_public = subnet.get("MapPublicIpOnLaunch", False)
108
+
109
+ return Asset(
110
+ snapshot_id=self._snapshot_id,
111
+ asset_type="ec2:subnet",
112
+ aws_region=self._region,
113
+ aws_resource_id=subnet_id,
114
+ arn=f"arn:aws:ec2:{self._region}:{self._account_id}:subnet/{subnet_id}",
115
+ name=name,
116
+ properties={
117
+ "vpc_id": subnet.get("VpcId"),
118
+ "cidr_block": subnet.get("CidrBlock"),
119
+ "availability_zone": subnet.get("AvailabilityZone"),
120
+ "map_public_ip_on_launch": is_public,
121
+ },
122
+ is_internet_facing=is_public,
123
+ )
124
+
125
+ def _normalize_security_group(
126
+ self,
127
+ sg: dict[str, Any],
128
+ ) -> tuple[Asset, list[Finding]]:
129
+ """Normalize a security group."""
130
+ sg_id = sg["GroupId"]
131
+ sg_name = sg.get("GroupName", sg_id)
132
+
133
+ asset = Asset(
134
+ snapshot_id=self._snapshot_id,
135
+ asset_type="ec2:security-group",
136
+ aws_region=self._region,
137
+ aws_resource_id=sg_id,
138
+ arn=f"arn:aws:ec2:{self._region}:{self._account_id}:security-group/{sg_id}",
139
+ name=sg_name,
140
+ properties={
141
+ "vpc_id": sg.get("VpcId"),
142
+ "description": sg.get("Description"),
143
+ "ingress_rules": sg.get("IpPermissions", []),
144
+ "egress_rules": sg.get("IpPermissionsEgress", []),
145
+ },
146
+ )
147
+
148
+ findings: list[Finding] = []
149
+
150
+ # Check for overly permissive ingress rules
151
+ for rule in sg.get("IpPermissions", []):
152
+ for ip_range in rule.get("IpRanges", []):
153
+ cidr = ip_range.get("CidrIp", "")
154
+ if cidr == "0.0.0.0/0":
155
+ from_port = rule.get("FromPort", "all")
156
+ to_port = rule.get("ToPort", "all")
157
+ protocol = rule.get("IpProtocol", "all")
158
+ severity = self._severity_for_open_rule(from_port, to_port, protocol)
159
+
160
+ findings.append(
161
+ Finding(
162
+ snapshot_id=self._snapshot_id,
163
+ asset_id=asset.id,
164
+ finding_type="security-group-open-to-world",
165
+ severity=severity,
166
+ title=f"Security group {sg_name} allows inbound from 0.0.0.0/0",
167
+ description=f"Ingress rule allows traffic from anywhere on port {from_port}-{to_port}",
168
+ remediation="Restrict the source IP range to known addresses",
169
+ evidence={"rule": rule, "cidr": cidr},
170
+ )
171
+ )
172
+ for ip_range in rule.get("Ipv6Ranges", []):
173
+ cidr = ip_range.get("CidrIpv6", "")
174
+ if cidr == "::/0":
175
+ from_port = rule.get("FromPort", "all")
176
+ to_port = rule.get("ToPort", "all")
177
+ protocol = rule.get("IpProtocol", "all")
178
+ severity = self._severity_for_open_rule(from_port, to_port, protocol)
179
+
180
+ findings.append(
181
+ Finding(
182
+ snapshot_id=self._snapshot_id,
183
+ asset_id=asset.id,
184
+ finding_type="security-group-open-to-world",
185
+ severity=severity,
186
+ title=f"Security group {sg_name} allows inbound from ::/0",
187
+ description=f"Ingress rule allows traffic from anywhere on port {from_port}-{to_port}",
188
+ remediation="Restrict the source IP range to known addresses",
189
+ evidence={"rule": rule, "cidr": cidr},
190
+ )
191
+ )
192
+
193
+ return asset, findings
194
+
195
+ @staticmethod
196
+ def _severity_for_open_rule(from_port, to_port, protocol) -> FindingSeverity:
197
+ """Determine severity for an open ingress rule."""
198
+ if from_port in [22, 3389] or to_port in [22, 3389]:
199
+ return FindingSeverity.critical
200
+ if protocol == "-1":
201
+ return FindingSeverity.critical
202
+ return FindingSeverity.high
203
+
204
+ def _normalize_load_balancer(self, lb: dict[str, Any]) -> Asset:
205
+ """Normalize a load balancer."""
206
+ lb_arn = lb["LoadBalancerArn"]
207
+ lb_name = lb.get("LoadBalancerName", lb_arn.split("/")[-1])
208
+ scheme = lb.get("Scheme", "internal")
209
+
210
+ return Asset(
211
+ snapshot_id=self._snapshot_id,
212
+ asset_type="elbv2:load-balancer",
213
+ aws_region=self._region,
214
+ aws_resource_id=lb_arn,
215
+ arn=lb_arn,
216
+ name=lb_name,
217
+ properties={
218
+ "type": lb.get("Type"),
219
+ "scheme": scheme,
220
+ "dns_name": lb.get("DNSName"),
221
+ "vpc_id": lb.get("VpcId"),
222
+ "security_groups": lb.get("SecurityGroups", []),
223
+ },
224
+ is_internet_facing=(scheme == "internet-facing"),
225
+ )
@@ -0,0 +1,130 @@
1
+ """RDS Normalizer - Transform RDS data to canonical schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any
7
+
8
+ from cyntrisec.core.schema import Asset, Finding, FindingSeverity, Relationship
9
+
10
+
11
+ class RdsNormalizer:
12
+ """Normalize RDS data to canonical assets."""
13
+
14
+ def __init__(
15
+ self,
16
+ snapshot_id: uuid.UUID,
17
+ region: str,
18
+ account_id: str,
19
+ ):
20
+ self._snapshot_id = snapshot_id
21
+ self._region = region
22
+ self._account_id = account_id
23
+
24
+ def normalize(
25
+ self,
26
+ data: dict[str, Any],
27
+ ) -> tuple[list[Asset], list[Relationship], list[Finding]]:
28
+ """Normalize RDS data."""
29
+ assets: list[Asset] = []
30
+ findings: list[Finding] = []
31
+
32
+ for instance in data.get("instances", []):
33
+ asset, instance_findings = self._normalize_instance(instance)
34
+ assets.append(asset)
35
+ findings.extend(instance_findings)
36
+
37
+ for cluster in data.get("clusters", []):
38
+ asset, cluster_findings = self._normalize_cluster(cluster)
39
+ assets.append(asset)
40
+ findings.extend(cluster_findings)
41
+
42
+ return assets, [], findings
43
+
44
+ def _normalize_instance(
45
+ self,
46
+ instance: dict[str, Any],
47
+ ) -> tuple[Asset, list[Finding]]:
48
+ """Normalize an RDS DB instance."""
49
+ db_id = instance["DBInstanceIdentifier"]
50
+ db_arn = instance["DBInstanceArn"]
51
+
52
+ asset = Asset(
53
+ snapshot_id=self._snapshot_id,
54
+ asset_type="rds:db-instance",
55
+ aws_region=self._region,
56
+ aws_resource_id=db_arn,
57
+ arn=db_arn,
58
+ name=db_id,
59
+ properties={
60
+ "engine": instance.get("Engine"),
61
+ "engine_version": instance.get("EngineVersion"),
62
+ "instance_class": instance.get("DBInstanceClass"),
63
+ "storage_encrypted": instance.get("StorageEncrypted"),
64
+ "publicly_accessible": instance.get("PubliclyAccessible"),
65
+ "multi_az": instance.get("MultiAZ"),
66
+ "vpc_security_groups": [
67
+ sg["VpcSecurityGroupId"] for sg in instance.get("VpcSecurityGroups", [])
68
+ ],
69
+ },
70
+ is_internet_facing=instance.get("PubliclyAccessible", False),
71
+ is_sensitive_target=True, # Databases are sensitive
72
+ )
73
+
74
+ findings: list[Finding] = []
75
+
76
+ # Check for public accessibility
77
+ if instance.get("PubliclyAccessible"):
78
+ findings.append(
79
+ Finding(
80
+ snapshot_id=self._snapshot_id,
81
+ asset_id=asset.id,
82
+ finding_type="rds-publicly-accessible",
83
+ severity=FindingSeverity.critical,
84
+ title=f"RDS instance {db_id} is publicly accessible",
85
+ description="Database is configured to be publicly accessible from the internet",
86
+ remediation="Disable public accessibility and use VPC endpoints or bastion hosts",
87
+ )
88
+ )
89
+
90
+ # Check for encryption
91
+ if not instance.get("StorageEncrypted"):
92
+ findings.append(
93
+ Finding(
94
+ snapshot_id=self._snapshot_id,
95
+ asset_id=asset.id,
96
+ finding_type="rds-not-encrypted",
97
+ severity=FindingSeverity.high,
98
+ title=f"RDS instance {db_id} is not encrypted",
99
+ description="Database storage is not encrypted at rest",
100
+ remediation="Enable storage encryption (requires database recreation)",
101
+ )
102
+ )
103
+
104
+ return asset, findings
105
+
106
+ def _normalize_cluster(
107
+ self,
108
+ cluster: dict[str, Any],
109
+ ) -> tuple[Asset, list[Finding]]:
110
+ """Normalize an RDS Aurora cluster."""
111
+ cluster_id = cluster["DBClusterIdentifier"]
112
+ cluster_arn = cluster["DBClusterArn"]
113
+
114
+ asset = Asset(
115
+ snapshot_id=self._snapshot_id,
116
+ asset_type="rds:db-cluster",
117
+ aws_region=self._region,
118
+ aws_resource_id=cluster_arn,
119
+ arn=cluster_arn,
120
+ name=cluster_id,
121
+ properties={
122
+ "engine": cluster.get("Engine"),
123
+ "engine_version": cluster.get("EngineVersion"),
124
+ "storage_encrypted": cluster.get("StorageEncrypted"),
125
+ "multi_az": cluster.get("MultiAZ"),
126
+ },
127
+ is_sensitive_target=True,
128
+ )
129
+
130
+ return asset, []
@@ -0,0 +1,184 @@
1
+ """S3 Normalizer - Transform S3 data to canonical schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import uuid
7
+ from typing import Any
8
+
9
+ from cyntrisec.core.schema import Asset, Finding, FindingSeverity, Relationship
10
+
11
+
12
+ class S3Normalizer:
13
+ """Normalize S3 data to canonical assets and findings."""
14
+
15
+ def __init__(self, snapshot_id: uuid.UUID):
16
+ self._snapshot_id = snapshot_id
17
+
18
+ def normalize(
19
+ self,
20
+ data: dict[str, Any],
21
+ ) -> tuple[list[Asset], list[Relationship], list[Finding]]:
22
+ """Normalize S3 data."""
23
+ assets: list[Asset] = []
24
+ findings: list[Finding] = []
25
+
26
+ for bucket in data.get("buckets", []):
27
+ asset, bucket_findings = self._normalize_bucket(bucket)
28
+ assets.append(asset)
29
+ findings.extend(bucket_findings)
30
+
31
+ return assets, [], findings
32
+
33
+ def _normalize_bucket(
34
+ self,
35
+ bucket: dict[str, Any],
36
+ ) -> tuple[Asset, list[Finding]]:
37
+ """Normalize an S3 bucket."""
38
+ bucket_name = bucket["Name"]
39
+ region = bucket.get("Location", "us-east-1")
40
+
41
+ # Check if this is a sensitive bucket (by name heuristic)
42
+ is_sensitive = any(
43
+ kw in bucket_name.lower()
44
+ for kw in ["backup", "secret", "credential", "key", "log", "audit"]
45
+ )
46
+
47
+ asset = Asset(
48
+ snapshot_id=self._snapshot_id,
49
+ asset_type="s3:bucket",
50
+ aws_region=region,
51
+ aws_resource_id=f"arn:aws:s3:::{bucket_name}",
52
+ arn=f"arn:aws:s3:::{bucket_name}",
53
+ name=bucket_name,
54
+ properties={
55
+ "creation_date": str(bucket.get("CreationDate")),
56
+ "region": region,
57
+ "has_policy": bucket.get("Policy") is not None,
58
+ "public_access_block": bucket.get("PublicAccessBlock"),
59
+ },
60
+ is_sensitive_target=is_sensitive,
61
+ )
62
+
63
+ findings: list[Finding] = []
64
+
65
+ # Check ACL for public access
66
+ acl = bucket.get("Acl", {})
67
+ for grant in acl.get("Grants", []):
68
+ grantee = grant.get("Grantee", {})
69
+ grantee_uri = grantee.get("URI", "")
70
+
71
+ if "AllUsers" in grantee_uri:
72
+ findings.append(
73
+ Finding(
74
+ snapshot_id=self._snapshot_id,
75
+ asset_id=asset.id,
76
+ finding_type="s3-bucket-public-acl",
77
+ severity=FindingSeverity.critical,
78
+ title=f"S3 bucket {bucket_name} has public ACL",
79
+ description="Bucket ACL grants access to all users (public)",
80
+ remediation="Remove public ACL grants and enable Block Public Access",
81
+ evidence={"grant": grant},
82
+ )
83
+ )
84
+ elif "AuthenticatedUsers" in grantee_uri:
85
+ findings.append(
86
+ Finding(
87
+ snapshot_id=self._snapshot_id,
88
+ asset_id=asset.id,
89
+ finding_type="s3-bucket-authenticated-users-acl",
90
+ severity=FindingSeverity.high,
91
+ title=f"S3 bucket {bucket_name} allows authenticated users",
92
+ description="Bucket ACL grants access to any authenticated AWS user",
93
+ remediation="Remove this grant - it allows any AWS account access",
94
+ evidence={"grant": grant},
95
+ )
96
+ )
97
+
98
+ # Check public access block
99
+ pab = bucket.get("PublicAccessBlock")
100
+ if not pab:
101
+ findings.append(
102
+ Finding(
103
+ snapshot_id=self._snapshot_id,
104
+ asset_id=asset.id,
105
+ finding_type="s3-bucket-no-public-access-block",
106
+ severity=FindingSeverity.medium,
107
+ title=f"S3 bucket {bucket_name} has no public access block",
108
+ description="Block Public Access is not configured for this bucket",
109
+ remediation="Enable Block Public Access at the bucket level",
110
+ )
111
+ )
112
+ elif not all(
113
+ [
114
+ pab.get("BlockPublicAcls"),
115
+ pab.get("IgnorePublicAcls"),
116
+ pab.get("BlockPublicPolicy"),
117
+ pab.get("RestrictPublicBuckets"),
118
+ ]
119
+ ):
120
+ findings.append(
121
+ Finding(
122
+ snapshot_id=self._snapshot_id,
123
+ asset_id=asset.id,
124
+ finding_type="s3-bucket-partial-public-access-block",
125
+ severity=FindingSeverity.low,
126
+ title=f"S3 bucket {bucket_name} has partial public access block",
127
+ description="Some Block Public Access settings are not enabled",
128
+ evidence={"public_access_block": pab},
129
+ )
130
+ )
131
+
132
+ # Check bucket policy for public access
133
+ policy = bucket.get("Policy")
134
+ if policy:
135
+ findings.extend(self._analyze_bucket_policy(asset, bucket_name, policy))
136
+
137
+ return asset, findings
138
+
139
+ def _analyze_bucket_policy(
140
+ self,
141
+ asset: Asset,
142
+ bucket_name: str,
143
+ policy: Any,
144
+ ) -> list[Finding]:
145
+ """Analyze bucket policy for public access."""
146
+ try:
147
+ policy_doc = json.loads(policy) if isinstance(policy, str) else policy
148
+ except (json.JSONDecodeError, TypeError):
149
+ return []
150
+
151
+ statements = policy_doc.get("Statement", [])
152
+ findings: list[Finding] = []
153
+ for statement in statements if isinstance(statements, list) else [statements]:
154
+ effect = statement.get("Effect", "")
155
+ principal = statement.get("Principal", {})
156
+ if effect != "Allow":
157
+ continue
158
+ if self._is_public_principal(principal):
159
+ findings.append(
160
+ Finding(
161
+ snapshot_id=self._snapshot_id,
162
+ asset_id=asset.id,
163
+ finding_type="s3-bucket-public-policy",
164
+ severity=FindingSeverity.critical,
165
+ title=f"S3 bucket {bucket_name} has public bucket policy",
166
+ description="Bucket policy allows public access via Principal: *",
167
+ remediation="Review and restrict the bucket policy",
168
+ evidence={"statement": statement},
169
+ )
170
+ )
171
+ return findings
172
+
173
+ @staticmethod
174
+ def _is_public_principal(principal: Any) -> bool:
175
+ """Return True when Principal implies public access."""
176
+ if principal == "*":
177
+ return True
178
+ if isinstance(principal, dict):
179
+ aws_principal = principal.get("AWS")
180
+ if aws_principal == "*" or aws_principal == ["*"]:
181
+ return True
182
+ if isinstance(aws_principal, list) and "*" in aws_principal:
183
+ return True
184
+ return False