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.
- cyntrisec/__init__.py +3 -0
- cyntrisec/__main__.py +6 -0
- cyntrisec/aws/__init__.py +6 -0
- cyntrisec/aws/collectors/__init__.py +17 -0
- cyntrisec/aws/collectors/ec2.py +30 -0
- cyntrisec/aws/collectors/iam.py +116 -0
- cyntrisec/aws/collectors/lambda_.py +45 -0
- cyntrisec/aws/collectors/network.py +70 -0
- cyntrisec/aws/collectors/rds.py +38 -0
- cyntrisec/aws/collectors/s3.py +68 -0
- cyntrisec/aws/collectors/usage.py +188 -0
- cyntrisec/aws/credentials.py +153 -0
- cyntrisec/aws/normalizers/__init__.py +17 -0
- cyntrisec/aws/normalizers/ec2.py +115 -0
- cyntrisec/aws/normalizers/iam.py +182 -0
- cyntrisec/aws/normalizers/lambda_.py +83 -0
- cyntrisec/aws/normalizers/network.py +225 -0
- cyntrisec/aws/normalizers/rds.py +130 -0
- cyntrisec/aws/normalizers/s3.py +184 -0
- cyntrisec/aws/relationship_builder.py +1359 -0
- cyntrisec/aws/scanner.py +303 -0
- cyntrisec/cli/__init__.py +5 -0
- cyntrisec/cli/analyze.py +747 -0
- cyntrisec/cli/ask.py +412 -0
- cyntrisec/cli/can.py +307 -0
- cyntrisec/cli/comply.py +226 -0
- cyntrisec/cli/cuts.py +231 -0
- cyntrisec/cli/diff.py +332 -0
- cyntrisec/cli/errors.py +105 -0
- cyntrisec/cli/explain.py +348 -0
- cyntrisec/cli/main.py +114 -0
- cyntrisec/cli/manifest.py +893 -0
- cyntrisec/cli/output.py +117 -0
- cyntrisec/cli/remediate.py +643 -0
- cyntrisec/cli/report.py +462 -0
- cyntrisec/cli/scan.py +207 -0
- cyntrisec/cli/schemas.py +391 -0
- cyntrisec/cli/serve.py +164 -0
- cyntrisec/cli/setup.py +260 -0
- cyntrisec/cli/validate.py +101 -0
- cyntrisec/cli/waste.py +323 -0
- cyntrisec/core/__init__.py +31 -0
- cyntrisec/core/business_config.py +110 -0
- cyntrisec/core/business_logic.py +131 -0
- cyntrisec/core/compliance.py +437 -0
- cyntrisec/core/cost_estimator.py +301 -0
- cyntrisec/core/cuts.py +360 -0
- cyntrisec/core/diff.py +361 -0
- cyntrisec/core/graph.py +202 -0
- cyntrisec/core/paths.py +830 -0
- cyntrisec/core/schema.py +317 -0
- cyntrisec/core/simulator.py +371 -0
- cyntrisec/core/waste.py +309 -0
- cyntrisec/mcp/__init__.py +5 -0
- cyntrisec/mcp/server.py +862 -0
- cyntrisec/storage/__init__.py +7 -0
- cyntrisec/storage/filesystem.py +344 -0
- cyntrisec/storage/memory.py +113 -0
- cyntrisec/storage/protocol.py +92 -0
- cyntrisec-0.1.7.dist-info/METADATA +672 -0
- cyntrisec-0.1.7.dist-info/RECORD +65 -0
- cyntrisec-0.1.7.dist-info/WHEEL +4 -0
- cyntrisec-0.1.7.dist-info/entry_points.txt +2 -0
- cyntrisec-0.1.7.dist-info/licenses/LICENSE +190 -0
- cyntrisec-0.1.7.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compliance Mapping - Map findings to compliance frameworks.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- CIS AWS Foundations Benchmark v1.5
|
|
6
|
+
- SOC 2 Type II controls
|
|
7
|
+
|
|
8
|
+
Each finding type is mapped to relevant compliance controls,
|
|
9
|
+
allowing users to understand their compliance posture.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
from cyntrisec.core.schema import Asset, Finding
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Framework(str, Enum):
|
|
21
|
+
"""Supported compliance frameworks."""
|
|
22
|
+
|
|
23
|
+
CIS_AWS = "CIS-AWS"
|
|
24
|
+
SOC2 = "SOC2"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Control:
|
|
29
|
+
"""A compliance control."""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
framework: Framework
|
|
33
|
+
title: str
|
|
34
|
+
description: str
|
|
35
|
+
severity: str = "medium"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def full_id(self) -> str:
|
|
39
|
+
return f"{self.framework.value}:{self.id}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ControlMapping:
|
|
44
|
+
"""Mapping between a finding type and compliance controls."""
|
|
45
|
+
|
|
46
|
+
finding_type: str
|
|
47
|
+
controls: list[Control] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ComplianceResult:
|
|
52
|
+
"""Result of compliance check for a single control."""
|
|
53
|
+
|
|
54
|
+
control: Control
|
|
55
|
+
status: str # "pass", "fail", "unknown"
|
|
56
|
+
findings: list[Finding] = field(default_factory=list)
|
|
57
|
+
assets_affected: int = 0
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_passing(self) -> bool:
|
|
61
|
+
return self.status == "pass"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ComplianceReport:
|
|
66
|
+
"""Full compliance report for a framework."""
|
|
67
|
+
|
|
68
|
+
framework: Framework
|
|
69
|
+
results: list[ComplianceResult] = field(default_factory=list)
|
|
70
|
+
data_gaps: dict[str, dict] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def passing(self) -> int:
|
|
74
|
+
return sum(1 for r in self.results if r.status == "pass")
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def failing(self) -> int:
|
|
78
|
+
return sum(1 for r in self.results if r.status == "fail")
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def unknown(self) -> int:
|
|
82
|
+
return sum(1 for r in self.results if r.status not in {"pass", "fail"})
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def compliance_score(self) -> float:
|
|
86
|
+
"""Percentage of controls passing."""
|
|
87
|
+
total = self.passing + self.failing
|
|
88
|
+
return self.passing / total if total > 0 else 0.0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
CONTROL_ASSET_REQUIREMENTS: dict[str, list[str]] = {
|
|
92
|
+
# IAM
|
|
93
|
+
"CIS-AWS:1.4": ["iam:user"],
|
|
94
|
+
"CIS-AWS:1.5": ["iam:user"],
|
|
95
|
+
"CIS-AWS:1.10": ["iam:user"],
|
|
96
|
+
"CIS-AWS:1.12": ["iam:user"],
|
|
97
|
+
"CIS-AWS:1.16": ["iam:user"],
|
|
98
|
+
"CIS-AWS:1.17": ["iam:role"],
|
|
99
|
+
# S3
|
|
100
|
+
"CIS-AWS:2.1.1": ["s3:bucket"],
|
|
101
|
+
"CIS-AWS:2.1.2": ["s3:bucket"],
|
|
102
|
+
"CIS-AWS:2.1.5": ["s3:bucket"],
|
|
103
|
+
# EC2/VPC
|
|
104
|
+
"CIS-AWS:5.1": ["ec2:security-group"],
|
|
105
|
+
"CIS-AWS:5.2": ["ec2:security-group"],
|
|
106
|
+
"CIS-AWS:5.3": ["ec2:vpc"],
|
|
107
|
+
"CIS-AWS:5.4": ["ec2:instance"],
|
|
108
|
+
# SOC2
|
|
109
|
+
"SOC2:CC6.1": ["iam:user", "iam:role"],
|
|
110
|
+
"SOC2:CC6.2": ["iam:user"],
|
|
111
|
+
"SOC2:CC6.3": ["iam:role"],
|
|
112
|
+
"SOC2:CC6.6": ["s3:bucket"],
|
|
113
|
+
"SOC2:CC7.1": ["ec2:security-group", "s3:bucket"],
|
|
114
|
+
"SOC2:CC7.2": ["iam:role"],
|
|
115
|
+
"SOC2:CC6.7": ["s3:bucket"],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# CIS AWS Foundations Benchmark v1.5 Controls
|
|
120
|
+
CIS_CONTROLS = [
|
|
121
|
+
# IAM
|
|
122
|
+
Control(
|
|
123
|
+
"1.4",
|
|
124
|
+
Framework.CIS_AWS,
|
|
125
|
+
"Ensure no root account access key exists",
|
|
126
|
+
"The root account should not have access keys configured",
|
|
127
|
+
"critical",
|
|
128
|
+
),
|
|
129
|
+
Control(
|
|
130
|
+
"1.5",
|
|
131
|
+
Framework.CIS_AWS,
|
|
132
|
+
"Ensure MFA is enabled for root account",
|
|
133
|
+
"The root account should have MFA enabled",
|
|
134
|
+
"critical",
|
|
135
|
+
),
|
|
136
|
+
Control(
|
|
137
|
+
"1.10",
|
|
138
|
+
Framework.CIS_AWS,
|
|
139
|
+
"Ensure MFA is enabled for all IAM users with console password",
|
|
140
|
+
"All IAM users with console access should have MFA enabled",
|
|
141
|
+
"high",
|
|
142
|
+
),
|
|
143
|
+
Control(
|
|
144
|
+
"1.12",
|
|
145
|
+
Framework.CIS_AWS,
|
|
146
|
+
"Ensure credentials unused for 90 days are disabled",
|
|
147
|
+
"IAM credentials not used in 90 days should be disabled",
|
|
148
|
+
"medium",
|
|
149
|
+
),
|
|
150
|
+
Control(
|
|
151
|
+
"1.16",
|
|
152
|
+
Framework.CIS_AWS,
|
|
153
|
+
"Ensure IAM policies not attached directly to users",
|
|
154
|
+
"IAM policies should be attached to groups/roles, not users",
|
|
155
|
+
"medium",
|
|
156
|
+
),
|
|
157
|
+
Control(
|
|
158
|
+
"1.17",
|
|
159
|
+
Framework.CIS_AWS,
|
|
160
|
+
"Ensure wildcard (*) not used in IAM policies",
|
|
161
|
+
"IAM policies should not use wildcards for resources",
|
|
162
|
+
"high",
|
|
163
|
+
),
|
|
164
|
+
# S3
|
|
165
|
+
Control(
|
|
166
|
+
"2.1.1",
|
|
167
|
+
Framework.CIS_AWS,
|
|
168
|
+
"Ensure S3 bucket Block Public Access is enabled",
|
|
169
|
+
"All S3 buckets should have Block Public Access enabled",
|
|
170
|
+
"high",
|
|
171
|
+
),
|
|
172
|
+
Control(
|
|
173
|
+
"2.1.2",
|
|
174
|
+
Framework.CIS_AWS,
|
|
175
|
+
"Ensure S3 bucket Block Public Access at account level",
|
|
176
|
+
"Account-level S3 Block Public Access should be enabled",
|
|
177
|
+
"high",
|
|
178
|
+
),
|
|
179
|
+
Control(
|
|
180
|
+
"2.1.5",
|
|
181
|
+
Framework.CIS_AWS,
|
|
182
|
+
"Ensure S3 bucket access logging is enabled",
|
|
183
|
+
"S3 buckets should have access logging enabled",
|
|
184
|
+
"medium",
|
|
185
|
+
),
|
|
186
|
+
# EC2/VPC
|
|
187
|
+
Control(
|
|
188
|
+
"5.1",
|
|
189
|
+
Framework.CIS_AWS,
|
|
190
|
+
"Ensure no open Security Groups to 0.0.0.0/0",
|
|
191
|
+
"Security groups should not allow 0.0.0.0/0 ingress",
|
|
192
|
+
"high",
|
|
193
|
+
),
|
|
194
|
+
Control(
|
|
195
|
+
"5.2",
|
|
196
|
+
Framework.CIS_AWS,
|
|
197
|
+
"Ensure default security group restricts all traffic",
|
|
198
|
+
"VPC default security groups should not allow any traffic",
|
|
199
|
+
"medium",
|
|
200
|
+
),
|
|
201
|
+
Control(
|
|
202
|
+
"5.3",
|
|
203
|
+
Framework.CIS_AWS,
|
|
204
|
+
"Ensure VPC flow logging is enabled",
|
|
205
|
+
"All VPCs should have flow logging enabled",
|
|
206
|
+
"medium",
|
|
207
|
+
),
|
|
208
|
+
Control(
|
|
209
|
+
"5.4",
|
|
210
|
+
Framework.CIS_AWS,
|
|
211
|
+
"Ensure EC2 instances use IMDSv2",
|
|
212
|
+
"EC2 instances should use Instance Metadata Service v2",
|
|
213
|
+
"medium",
|
|
214
|
+
),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
# SOC 2 Type II Controls
|
|
218
|
+
SOC2_CONTROLS = [
|
|
219
|
+
Control(
|
|
220
|
+
"CC6.1",
|
|
221
|
+
Framework.SOC2,
|
|
222
|
+
"Logical and Physical Access Controls",
|
|
223
|
+
"Access to system components is controlled by access policies",
|
|
224
|
+
"high",
|
|
225
|
+
),
|
|
226
|
+
Control(
|
|
227
|
+
"CC6.2",
|
|
228
|
+
Framework.SOC2,
|
|
229
|
+
"Prior to Access",
|
|
230
|
+
"Users are authenticated before access is granted",
|
|
231
|
+
"high",
|
|
232
|
+
),
|
|
233
|
+
Control(
|
|
234
|
+
"CC6.3",
|
|
235
|
+
Framework.SOC2,
|
|
236
|
+
"Role-Based Access",
|
|
237
|
+
"Access is based on job function and least privilege",
|
|
238
|
+
"high",
|
|
239
|
+
),
|
|
240
|
+
Control(
|
|
241
|
+
"CC6.6",
|
|
242
|
+
Framework.SOC2,
|
|
243
|
+
"Encryption of Data",
|
|
244
|
+
"Data at rest and in transit is encrypted",
|
|
245
|
+
"high",
|
|
246
|
+
),
|
|
247
|
+
Control(
|
|
248
|
+
"CC6.7",
|
|
249
|
+
Framework.SOC2,
|
|
250
|
+
"Data Disposal",
|
|
251
|
+
"Data is disposed of securely when no longer needed",
|
|
252
|
+
"medium",
|
|
253
|
+
),
|
|
254
|
+
Control(
|
|
255
|
+
"CC7.1",
|
|
256
|
+
Framework.SOC2,
|
|
257
|
+
"Security Monitoring",
|
|
258
|
+
"Security events are detected and responded to",
|
|
259
|
+
"high",
|
|
260
|
+
),
|
|
261
|
+
Control(
|
|
262
|
+
"CC7.2",
|
|
263
|
+
Framework.SOC2,
|
|
264
|
+
"Incident Response",
|
|
265
|
+
"Security incidents are managed and resolved",
|
|
266
|
+
"high",
|
|
267
|
+
),
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
# Mapping from finding types to controls
|
|
271
|
+
FINDING_TO_CONTROLS: dict[str, list[str]] = {
|
|
272
|
+
# IAM findings
|
|
273
|
+
"iam_overly_permissive_trust": ["CIS-AWS:1.17", "SOC2:CC6.3"],
|
|
274
|
+
"iam_wildcard_policy": ["CIS-AWS:1.17", "SOC2:CC6.3"],
|
|
275
|
+
"iam_unused_credentials": ["CIS-AWS:1.12", "SOC2:CC6.1"],
|
|
276
|
+
"iam_user_direct_policy": ["CIS-AWS:1.16", "SOC2:CC6.3"],
|
|
277
|
+
"iam_no_mfa": ["CIS-AWS:1.10", "SOC2:CC6.2"],
|
|
278
|
+
# S3 findings
|
|
279
|
+
"s3_public_bucket": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.2", "SOC2:CC6.1"],
|
|
280
|
+
"s3-bucket-no-public-access-block": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.2", "SOC2:CC6.1"],
|
|
281
|
+
"s3-bucket-public-access-block": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.2", "SOC2:CC6.1"],
|
|
282
|
+
"s3-bucket-partial-public-access-block": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.5", "SOC2:CC6.1"],
|
|
283
|
+
"s3-bucket-public-acl": ["CIS-AWS:2.1.1", "SOC2:CC6.1"],
|
|
284
|
+
"s3-bucket-authenticated-users-acl": ["CIS-AWS:2.1.1", "SOC2:CC6.1"],
|
|
285
|
+
"s3_no_encryption": ["SOC2:CC6.6"],
|
|
286
|
+
"s3_no_logging": ["CIS-AWS:2.1.5", "SOC2:CC7.1"],
|
|
287
|
+
# EC2/Network findings
|
|
288
|
+
"security_group_open_to_world": ["CIS-AWS:5.1", "SOC2:CC6.1"],
|
|
289
|
+
"security-group-open-to-world": ["CIS-AWS:5.1", "CIS-AWS:5.2", "SOC2:CC6.1"],
|
|
290
|
+
"ec2-public-ip": ["CIS-AWS:5.1", "CIS-AWS:5.2", "SOC2:CC6.1"],
|
|
291
|
+
"vpc_default_sg_in_use": ["CIS-AWS:5.2", "SOC2:CC6.1"],
|
|
292
|
+
"vpc_no_flow_logs": ["CIS-AWS:5.3", "SOC2:CC7.1"],
|
|
293
|
+
"ec2_imdsv1": ["CIS-AWS:5.4", "SOC2:CC6.1"],
|
|
294
|
+
"ec2-imdsv1-enabled": ["CIS-AWS:5.4", "SOC2:CC6.1"],
|
|
295
|
+
"iam-role-trust-any-principal": ["CIS-AWS:1.17", "SOC2:CC6.3"],
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class ComplianceChecker:
|
|
300
|
+
"""
|
|
301
|
+
Check compliance against frameworks based on scan findings.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(self):
|
|
305
|
+
self._controls_by_id: dict[str, Control] = {}
|
|
306
|
+
for ctrl in CIS_CONTROLS + SOC2_CONTROLS:
|
|
307
|
+
self._controls_by_id[ctrl.full_id] = ctrl
|
|
308
|
+
|
|
309
|
+
def check(
|
|
310
|
+
self,
|
|
311
|
+
findings: list[Finding],
|
|
312
|
+
assets: list[Asset],
|
|
313
|
+
*,
|
|
314
|
+
framework: Framework | None = None,
|
|
315
|
+
collection_errors: list[dict] | None = None,
|
|
316
|
+
) -> ComplianceReport:
|
|
317
|
+
"""
|
|
318
|
+
Check compliance based on findings.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
findings: Security findings from scan
|
|
322
|
+
assets: Assets from scan
|
|
323
|
+
framework: Specific framework (default: CIS_AWS)
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
ComplianceReport with pass/fail status per control
|
|
327
|
+
"""
|
|
328
|
+
framework = framework or Framework.CIS_AWS
|
|
329
|
+
controls = CIS_CONTROLS if framework == Framework.CIS_AWS else SOC2_CONTROLS
|
|
330
|
+
|
|
331
|
+
# Build mapping: control_id -> findings that violate it
|
|
332
|
+
violations: dict[str, list[Finding]] = {}
|
|
333
|
+
for finding in findings:
|
|
334
|
+
control_ids = FINDING_TO_CONTROLS.get(finding.finding_type, [])
|
|
335
|
+
for ctrl_id in control_ids:
|
|
336
|
+
if ctrl_id not in violations:
|
|
337
|
+
violations[ctrl_id] = []
|
|
338
|
+
violations[ctrl_id].append(finding)
|
|
339
|
+
|
|
340
|
+
asset_types = {a.asset_type for a in assets}
|
|
341
|
+
error_services = {
|
|
342
|
+
err.get("service")
|
|
343
|
+
for err in (collection_errors or [])
|
|
344
|
+
if err.get("service")
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Build results
|
|
348
|
+
results = []
|
|
349
|
+
data_gaps: dict[str, dict] = {}
|
|
350
|
+
for ctrl in controls:
|
|
351
|
+
violating_findings = violations.get(ctrl.full_id, [])
|
|
352
|
+
required_assets = CONTROL_ASSET_REQUIREMENTS.get(ctrl.full_id, [])
|
|
353
|
+
|
|
354
|
+
if violating_findings:
|
|
355
|
+
status = "fail"
|
|
356
|
+
else:
|
|
357
|
+
has_assets = not required_assets or any(
|
|
358
|
+
asset_type in asset_types for asset_type in required_assets
|
|
359
|
+
)
|
|
360
|
+
if has_assets:
|
|
361
|
+
status = "pass"
|
|
362
|
+
else:
|
|
363
|
+
status = "unknown"
|
|
364
|
+
data_gaps[ctrl.full_id] = {
|
|
365
|
+
"reason": "missing_assets",
|
|
366
|
+
"required_assets": required_assets,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if status != "fail" and required_assets and error_services:
|
|
370
|
+
impacted = self._assets_impacted_by_errors(required_assets, error_services)
|
|
371
|
+
if impacted:
|
|
372
|
+
status = "unknown"
|
|
373
|
+
data_gaps[ctrl.full_id] = {
|
|
374
|
+
"reason": "collection_error",
|
|
375
|
+
"required_assets": required_assets,
|
|
376
|
+
"services": sorted(impacted),
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
results.append(
|
|
380
|
+
ComplianceResult(
|
|
381
|
+
control=ctrl,
|
|
382
|
+
status=status,
|
|
383
|
+
findings=violating_findings,
|
|
384
|
+
assets_affected=len(set(f.asset_id for f in violating_findings)),
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return ComplianceReport(
|
|
389
|
+
framework=framework,
|
|
390
|
+
results=results,
|
|
391
|
+
data_gaps=data_gaps,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def _assets_impacted_by_errors(
|
|
396
|
+
required_assets: list[str],
|
|
397
|
+
error_services: set[str],
|
|
398
|
+
) -> set[str]:
|
|
399
|
+
"""Map collection errors to affected control services."""
|
|
400
|
+
impacted: set[str] = set()
|
|
401
|
+
for service in error_services:
|
|
402
|
+
if service == "iam" and any(a.startswith("iam:") for a in required_assets):
|
|
403
|
+
impacted.add(service)
|
|
404
|
+
if service == "s3" and any(a.startswith("s3:") for a in required_assets):
|
|
405
|
+
impacted.add(service)
|
|
406
|
+
if service in {"ec2", "network"} and any(a.startswith("ec2:") for a in required_assets):
|
|
407
|
+
impacted.add(service)
|
|
408
|
+
if service == "lambda" and any(a.startswith("lambda:") for a in required_assets):
|
|
409
|
+
impacted.add(service)
|
|
410
|
+
if service == "rds" and any(a.startswith("rds:") for a in required_assets):
|
|
411
|
+
impacted.add(service)
|
|
412
|
+
return impacted
|
|
413
|
+
|
|
414
|
+
def get_control(self, control_id: str) -> Control | None:
|
|
415
|
+
"""Get a control by ID."""
|
|
416
|
+
return self._controls_by_id.get(control_id)
|
|
417
|
+
|
|
418
|
+
def summary(self, report: ComplianceReport) -> dict:
|
|
419
|
+
"""Generate summary statistics for a report."""
|
|
420
|
+
by_severity = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
421
|
+
failing_by_severity = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
422
|
+
|
|
423
|
+
for result in report.results:
|
|
424
|
+
sev = result.control.severity
|
|
425
|
+
by_severity[sev] = by_severity.get(sev, 0) + 1
|
|
426
|
+
if not result.is_passing:
|
|
427
|
+
failing_by_severity[sev] = failing_by_severity.get(sev, 0) + 1
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
"framework": report.framework.value,
|
|
431
|
+
"total_controls": len(report.results),
|
|
432
|
+
"passing": report.passing,
|
|
433
|
+
"failing": report.failing,
|
|
434
|
+
"compliance_score": report.compliance_score,
|
|
435
|
+
"by_severity": by_severity,
|
|
436
|
+
"failing_by_severity": failing_by_severity,
|
|
437
|
+
}
|