regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.1.0__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.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/dev/cli.py +26 -0
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +15 -1
- regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/amazon/common.py +204 -0
- regscale/integrations/commercial/amazon/common.py +48 -58
- regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
- regscale/integrations/commercial/aws/cli.py +3093 -55
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +851 -206
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/synqly/ticketing.py +27 -0
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/scanner_integration.py +399 -84
- regscale/models/integration_models/cisa_kev_data.json +65 -5
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/issue.py +2 -5
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/test_aws.py +55 -56
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/top_level.txt +0 -0
|
@@ -8,6 +8,7 @@ that follow common patterns across different compliance tools (Wiz, Tenable, Sic
|
|
|
8
8
|
"""
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
|
+
import time
|
|
11
12
|
from abc import ABC, abstractmethod
|
|
12
13
|
from collections import defaultdict
|
|
13
14
|
from typing import Dict, List, Optional, Any, Iterator
|
|
@@ -170,6 +171,10 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
170
171
|
# Initialize control matcher for robust control ID matching
|
|
171
172
|
self._control_matcher = ControlMatcher()
|
|
172
173
|
|
|
174
|
+
# Performance optimization: cache for control lookups
|
|
175
|
+
# Key: control ID variation (e.g., 'ac-2(1)') -> (ControlImplementation, SecurityControl)
|
|
176
|
+
self._control_lookup_cache: Dict[str, tuple[ControlImplementation, SecurityControl]] = {}
|
|
177
|
+
|
|
173
178
|
def is_poam(self, finding: IntegrationFinding) -> bool: # type: ignore[override]
|
|
174
179
|
"""
|
|
175
180
|
Determines if an issue should be considered a POAM for compliance integrations.
|
|
@@ -384,6 +389,50 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
384
389
|
cache_key = f"{implementation_id}_{day_key}"
|
|
385
390
|
return self._existing_assessments_cache.get(cache_key)
|
|
386
391
|
|
|
392
|
+
def check_for_existing_evidence(self, file_name_pattern: str) -> bool:
|
|
393
|
+
"""
|
|
394
|
+
Check if an evidence file matching the pattern already exists in RegScale.
|
|
395
|
+
|
|
396
|
+
This method fetches existing files for the plan and checks if any match
|
|
397
|
+
the provided pattern, helping prevent duplicate evidence uploads.
|
|
398
|
+
|
|
399
|
+
:param str file_name_pattern: Pattern to match against existing file names
|
|
400
|
+
:return: True if a matching file exists, False otherwise
|
|
401
|
+
:rtype: bool
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
# Import here to avoid circular dependency
|
|
405
|
+
from regscale.models.regscale_models import File
|
|
406
|
+
|
|
407
|
+
# Get all existing files for the plan
|
|
408
|
+
existing_files = File.get_files_for_parent_from_regscale(
|
|
409
|
+
parent_id=self.plan_id, parent_module=self.parent_module
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Check if any file matches the pattern
|
|
413
|
+
for file_obj in existing_files:
|
|
414
|
+
if hasattr(file_obj, "trustedDisplayName") and file_obj.trustedDisplayName:
|
|
415
|
+
# Check if the pattern is in the file name
|
|
416
|
+
if file_name_pattern in file_obj.trustedDisplayName:
|
|
417
|
+
logger.debug(
|
|
418
|
+
"Found existing evidence file matching pattern '%s': %s",
|
|
419
|
+
file_name_pattern,
|
|
420
|
+
file_obj.trustedDisplayName,
|
|
421
|
+
)
|
|
422
|
+
return True
|
|
423
|
+
|
|
424
|
+
logger.debug("No existing evidence files found matching pattern '%s'", file_name_pattern)
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logger.warning(
|
|
429
|
+
"Unable to check for existing evidence files (pattern: '%s'): %s. Proceeding with upload.",
|
|
430
|
+
file_name_pattern,
|
|
431
|
+
e,
|
|
432
|
+
)
|
|
433
|
+
# Return False to allow upload to proceed if check fails
|
|
434
|
+
return False
|
|
435
|
+
|
|
387
436
|
@abstractmethod
|
|
388
437
|
def fetch_compliance_data(self) -> List[Any]:
|
|
389
438
|
"""
|
|
@@ -604,6 +653,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
604
653
|
pass_count = 0
|
|
605
654
|
|
|
606
655
|
for result, count in result_counts.items():
|
|
656
|
+
if result is None: # Skip None results (controls without evidence)
|
|
657
|
+
continue
|
|
607
658
|
result_lower = result.lower()
|
|
608
659
|
if result_lower in fail_statuses_lower:
|
|
609
660
|
fail_count += count
|
|
@@ -1131,6 +1182,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1131
1182
|
logger.warning("No control implementations found for assessment processing")
|
|
1132
1183
|
return
|
|
1133
1184
|
|
|
1185
|
+
# Build control lookup cache for fast O(1) matching
|
|
1186
|
+
self._build_control_lookup_cache(implementations)
|
|
1187
|
+
|
|
1134
1188
|
all_control_ids = set(self.passing_controls.keys()) | set(self.failing_controls.keys())
|
|
1135
1189
|
logger.info(f"Processing assessments for {len(all_control_ids)} controls with compliance data")
|
|
1136
1190
|
logger.info(f"Control IDs with data: {sorted(list(all_control_ids))}")
|
|
@@ -1171,6 +1225,56 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1171
1225
|
logger.info(f"Found {len(implementations)} control implementations")
|
|
1172
1226
|
return implementations
|
|
1173
1227
|
|
|
1228
|
+
def _build_control_lookup_cache(self, implementations: List[ControlImplementation]) -> None:
|
|
1229
|
+
"""
|
|
1230
|
+
Build a lookup cache mapping control ID variations to implementations and security controls.
|
|
1231
|
+
|
|
1232
|
+
This dramatically improves performance by:
|
|
1233
|
+
1. Fetching all SecurityControl objects once (instead of once per match attempt)
|
|
1234
|
+
2. Pre-computing all control ID variations
|
|
1235
|
+
3. Creating a dictionary for O(1) lookup instead of O(n) iteration
|
|
1236
|
+
|
|
1237
|
+
For 1011 implementations x 71 controls = 71,781 iterations in the old code.
|
|
1238
|
+
New code: 1011 fetches + 71 dictionary lookups = ~1082 operations (67x faster!)
|
|
1239
|
+
|
|
1240
|
+
:param List[ControlImplementation] implementations: List of control implementations to cache
|
|
1241
|
+
:return: None
|
|
1242
|
+
:rtype: None
|
|
1243
|
+
"""
|
|
1244
|
+
if self._control_lookup_cache:
|
|
1245
|
+
# Cache already built
|
|
1246
|
+
return
|
|
1247
|
+
|
|
1248
|
+
logger.debug(f"Building control lookup cache for {len(implementations)} implementations...")
|
|
1249
|
+
start_time = time.time()
|
|
1250
|
+
|
|
1251
|
+
for implementation in implementations:
|
|
1252
|
+
try:
|
|
1253
|
+
security_control = SecurityControl.get_object(object_id=implementation.controlID)
|
|
1254
|
+
if not security_control or not security_control.controlId:
|
|
1255
|
+
continue
|
|
1256
|
+
|
|
1257
|
+
# Generate all variations of this control ID for flexible matching
|
|
1258
|
+
control_variations = self._control_matcher._get_control_id_variations(security_control.controlId)
|
|
1259
|
+
|
|
1260
|
+
# Map each variation to this implementation + security control pair
|
|
1261
|
+
for variation in control_variations:
|
|
1262
|
+
# Store the first implementation found for each variation
|
|
1263
|
+
# (if multiple implementations have the same control, use the first one)
|
|
1264
|
+
if variation not in self._control_lookup_cache:
|
|
1265
|
+
self._control_lookup_cache[variation] = (implementation, security_control)
|
|
1266
|
+
|
|
1267
|
+
except Exception as e: # noqa: BLE001
|
|
1268
|
+
logger.error(
|
|
1269
|
+
f"Error caching implementation {implementation.id} with controlID {implementation.controlID}: {e}"
|
|
1270
|
+
)
|
|
1271
|
+
continue
|
|
1272
|
+
|
|
1273
|
+
elapsed = time.time() - start_time
|
|
1274
|
+
logger.info(
|
|
1275
|
+
f"Built control lookup cache with {len(self._control_lookup_cache)} control ID variations in {elapsed:.2f}s"
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1174
1278
|
def _log_sample_controls(self, implementations: List[ControlImplementation]) -> None:
|
|
1175
1279
|
"""
|
|
1176
1280
|
Log sample control IDs for debugging purposes.
|
|
@@ -1249,9 +1353,10 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1249
1353
|
Find matching implementation and security control for a control ID.
|
|
1250
1354
|
|
|
1251
1355
|
Uses ControlMatcher for robust control ID matching with leading zero normalization.
|
|
1356
|
+
Performance optimized with pre-built lookup cache for O(1) matching.
|
|
1252
1357
|
|
|
1253
1358
|
:param str control_id: Control identifier to match
|
|
1254
|
-
:param List[ControlImplementation] implementations: Available implementations
|
|
1359
|
+
:param List[ControlImplementation] implementations: Available implementations (used for fallback only)
|
|
1255
1360
|
:return: Tuple of matching implementation and security control, or (None, None)
|
|
1256
1361
|
:rtype: tuple[Optional[ControlImplementation], Optional[SecurityControl]]
|
|
1257
1362
|
"""
|
|
@@ -1261,43 +1366,17 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1261
1366
|
logger.debug(f"Could not generate control ID variations for: {control_id}")
|
|
1262
1367
|
return None, None
|
|
1263
1368
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
if not security_control:
|
|
1271
|
-
logger.debug(
|
|
1272
|
-
f"No security control found for implementation {implementation.id} with controlID: {implementation.controlID}"
|
|
1273
|
-
)
|
|
1274
|
-
continue
|
|
1275
|
-
security_control_id = security_control.controlId
|
|
1276
|
-
if not security_control_id:
|
|
1277
|
-
logger.debug(f"Security control {security_control.id} has no controlId")
|
|
1278
|
-
continue
|
|
1279
|
-
|
|
1280
|
-
# Get variations of the security control ID
|
|
1281
|
-
control_variations = self._control_matcher._get_control_id_variations(security_control_id)
|
|
1282
|
-
|
|
1283
|
-
logger.debug(
|
|
1284
|
-
f"Comparing control '{control_id}' variations {list(search_variations)[:3]} with RegScale control '{security_control_id}' variations {list(control_variations)[:3]} (impl: {implementation.id})"
|
|
1369
|
+
# Try to find a match using the pre-built lookup cache (O(1) lookup)
|
|
1370
|
+
for variation in search_variations:
|
|
1371
|
+
if variation in self._control_lookup_cache:
|
|
1372
|
+
implementation, security_control = self._control_lookup_cache[variation]
|
|
1373
|
+
logger.info(
|
|
1374
|
+
f"✅ MATCH FOUND: '{security_control.controlId}' == '{control_id}' (implementation: {implementation.id})"
|
|
1285
1375
|
)
|
|
1376
|
+
return implementation, security_control
|
|
1286
1377
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
matching_implementation = implementation
|
|
1290
|
-
matching_security_control = security_control
|
|
1291
|
-
logger.info(
|
|
1292
|
-
f"✅ MATCH FOUND: '{security_control_id}' == '{control_id}' (implementation: {implementation.id})"
|
|
1293
|
-
)
|
|
1294
|
-
break
|
|
1295
|
-
except Exception as e: # noqa: BLE001
|
|
1296
|
-
logger.error(
|
|
1297
|
-
f"Error processing implementation {implementation.id} with controlID {implementation.controlID}: {e}"
|
|
1298
|
-
)
|
|
1299
|
-
continue
|
|
1300
|
-
return matching_implementation, matching_security_control
|
|
1378
|
+
# No match found in cache
|
|
1379
|
+
return None, None
|
|
1301
1380
|
|
|
1302
1381
|
def _log_no_match(self, control_id: str, implementations: List[ControlImplementation]) -> None:
|
|
1303
1382
|
"""
|
|
@@ -1527,8 +1606,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1527
1606
|
pass
|
|
1528
1607
|
else:
|
|
1529
1608
|
# Create new assessment
|
|
1609
|
+
# leadAssessorId will be set automatically from the token via the Assessment model's default_factory
|
|
1530
1610
|
assessment = Assessment(
|
|
1531
|
-
leadAssessorId=implementation.createdById,
|
|
1532
1611
|
title=f"{self.title} compliance assessment for {control_id.upper()}",
|
|
1533
1612
|
assessmentType="Control Testing",
|
|
1534
1613
|
plannedStart=get_current_datetime(),
|
|
@@ -1631,13 +1710,204 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1631
1710
|
<p><strong>Unique Resources Assessed:</strong> {len(unique_resources)}</p>
|
|
1632
1711
|
<p><strong>Passing Assessments:</strong> <span style="color: #2e7d32;">{pass_count}</span></p>
|
|
1633
1712
|
<p><strong>Failing Assessments:</strong> <span style="color: #d32f2f;">{fail_count}</span></p>
|
|
1634
|
-
<p><strong>Overall Control Result:</strong>
|
|
1713
|
+
<p><strong>Overall Control Result:</strong>
|
|
1714
|
+
<span style="color: {result_color}; font-weight: bold;">{result}</span></p>
|
|
1635
1715
|
</div>
|
|
1636
1716
|
"""
|
|
1637
1717
|
)
|
|
1638
1718
|
|
|
1719
|
+
# Add detailed failure information for AWS Audit Manager
|
|
1720
|
+
if result == "Fail" and fail_count > 0:
|
|
1721
|
+
failed_items = [item for item in compliance_items if item.compliance_result not in self.PASS_STATUSES]
|
|
1722
|
+
html_parts.append(self._create_failure_details_section(failed_items))
|
|
1723
|
+
|
|
1639
1724
|
return "\n".join(html_parts)
|
|
1640
1725
|
|
|
1726
|
+
def _create_failure_details_section(self, failed_items: List[ComplianceItem]) -> str:
|
|
1727
|
+
"""
|
|
1728
|
+
Create detailed failure information section for failed compliance items.
|
|
1729
|
+
|
|
1730
|
+
:param List[ComplianceItem] failed_items: List of failed compliance items
|
|
1731
|
+
:return: HTML section with detailed failure information
|
|
1732
|
+
:rtype: str
|
|
1733
|
+
"""
|
|
1734
|
+
html_parts = [self._get_failure_section_header()]
|
|
1735
|
+
|
|
1736
|
+
for idx, item in enumerate(failed_items, 1):
|
|
1737
|
+
html_parts.append(self._process_failed_item(idx, item))
|
|
1738
|
+
|
|
1739
|
+
html_parts.append("</div>")
|
|
1740
|
+
return "\n".join(html_parts)
|
|
1741
|
+
|
|
1742
|
+
def _get_failure_section_header(self) -> str:
|
|
1743
|
+
"""Get the HTML header for failure details section."""
|
|
1744
|
+
return """
|
|
1745
|
+
<div style="margin-top: 20px; padding: 15px; background-color: #fff3e0;
|
|
1746
|
+
border-left: 4px solid #ff9800; border-radius: 5px;">
|
|
1747
|
+
<h4 style="color: #e65100; margin-top: 0;">Failed Evidence Details</h4>
|
|
1748
|
+
"""
|
|
1749
|
+
|
|
1750
|
+
def _process_failed_item(self, idx: int, item: ComplianceItem) -> str:
|
|
1751
|
+
"""
|
|
1752
|
+
Process a single failed item and return HTML.
|
|
1753
|
+
|
|
1754
|
+
:param int idx: The index of the failed item
|
|
1755
|
+
:param ComplianceItem item: The failed compliance item
|
|
1756
|
+
:return: HTML for the failed item
|
|
1757
|
+
:rtype: str
|
|
1758
|
+
"""
|
|
1759
|
+
if self._has_aws_evidence(item):
|
|
1760
|
+
return self._process_aws_item_with_evidence(idx, item)
|
|
1761
|
+
return self._process_non_aws_item(idx, item)
|
|
1762
|
+
|
|
1763
|
+
def _has_aws_evidence(self, item: ComplianceItem) -> bool:
|
|
1764
|
+
"""Check if item has AWS Audit Manager evidence."""
|
|
1765
|
+
return hasattr(item, "evidence_items") and item.evidence_items
|
|
1766
|
+
|
|
1767
|
+
def _process_aws_item_with_evidence(self, idx: int, item: ComplianceItem) -> str:
|
|
1768
|
+
"""Process AWS item with evidence details."""
|
|
1769
|
+
evidence_categories = self._categorize_evidence(item)
|
|
1770
|
+
|
|
1771
|
+
if not evidence_categories["failed"]:
|
|
1772
|
+
return ""
|
|
1773
|
+
|
|
1774
|
+
html_parts = []
|
|
1775
|
+
html_parts.append(self._create_failed_check_header(idx, item, evidence_categories))
|
|
1776
|
+
html_parts.append(self._create_failed_evidence_details(evidence_categories["failed"]))
|
|
1777
|
+
html_parts.append(self._add_remediation_guidance(item))
|
|
1778
|
+
html_parts.append("</div>")
|
|
1779
|
+
|
|
1780
|
+
return "\n".join(html_parts)
|
|
1781
|
+
|
|
1782
|
+
def _categorize_evidence(self, item: ComplianceItem) -> Dict[str, List[Any]]:
|
|
1783
|
+
"""
|
|
1784
|
+
Categorize evidence items by compliance status.
|
|
1785
|
+
|
|
1786
|
+
:param ComplianceItem item: The compliance item with evidence
|
|
1787
|
+
:return: Dictionary with categorized evidence
|
|
1788
|
+
:rtype: Dict[str, List[Any]]
|
|
1789
|
+
"""
|
|
1790
|
+
categories = {"failed": [], "compliant": [], "inconclusive": []}
|
|
1791
|
+
|
|
1792
|
+
for evidence in item.evidence_items:
|
|
1793
|
+
compliance_check = self._get_evidence_compliance_check(item, evidence)
|
|
1794
|
+
|
|
1795
|
+
if compliance_check == "FAILED":
|
|
1796
|
+
categories["failed"].append(evidence)
|
|
1797
|
+
elif compliance_check == "COMPLIANT":
|
|
1798
|
+
categories["compliant"].append(evidence)
|
|
1799
|
+
else:
|
|
1800
|
+
categories["inconclusive"].append(evidence)
|
|
1801
|
+
|
|
1802
|
+
return categories
|
|
1803
|
+
|
|
1804
|
+
def _get_evidence_compliance_check(self, item: ComplianceItem, evidence: Any) -> Optional[str]:
|
|
1805
|
+
"""Get compliance check result for evidence."""
|
|
1806
|
+
if hasattr(item, "_get_evidence_compliance"):
|
|
1807
|
+
return item._get_evidence_compliance(evidence)
|
|
1808
|
+
return None
|
|
1809
|
+
|
|
1810
|
+
def _create_failed_check_header(
|
|
1811
|
+
self, idx: int, item: ComplianceItem, evidence_categories: Dict[str, List[Any]]
|
|
1812
|
+
) -> str:
|
|
1813
|
+
"""Create HTML header for failed check."""
|
|
1814
|
+
return f"""
|
|
1815
|
+
<div style="margin-top: 15px; padding: 10px; background-color: #ffebee; border-radius: 3px;">
|
|
1816
|
+
<h5 style="color: #c62828; margin-top: 0;">
|
|
1817
|
+
Failed Check #{idx}: {item.control_id}
|
|
1818
|
+
</h5>
|
|
1819
|
+
<p><strong>Resource:</strong> {getattr(item, 'resource_name', item.resource_id)}</p>
|
|
1820
|
+
<p><strong>Evidence Summary:</strong>
|
|
1821
|
+
{len(evidence_categories["failed"])} failed, {len(evidence_categories["compliant"])} compliant,
|
|
1822
|
+
{len(evidence_categories["inconclusive"])} inconclusive</p>
|
|
1823
|
+
"""
|
|
1824
|
+
|
|
1825
|
+
def _create_failed_evidence_details(self, failed_evidence: List[Any]) -> str:
|
|
1826
|
+
"""Create HTML for failed evidence details."""
|
|
1827
|
+
html_parts = ['<div style="margin-top: 10px;"><strong>Failed Evidence:</strong><ul>']
|
|
1828
|
+
|
|
1829
|
+
# Limit to 10 failed evidence items
|
|
1830
|
+
for evidence in failed_evidence[:10]:
|
|
1831
|
+
html_parts.append(self._format_single_evidence(evidence))
|
|
1832
|
+
|
|
1833
|
+
html_parts.append("</ul></div>")
|
|
1834
|
+
return "\n".join(html_parts)
|
|
1835
|
+
|
|
1836
|
+
def _format_single_evidence(self, evidence: Dict[str, Any]) -> str:
|
|
1837
|
+
"""Format a single evidence item as HTML."""
|
|
1838
|
+
evidence_html = []
|
|
1839
|
+
evidence_source = evidence.get("dataSource", "Unknown source")
|
|
1840
|
+
evidence_id = evidence.get("id", "")[:50]
|
|
1841
|
+
|
|
1842
|
+
evidence_html.append(f"<li><strong>Source:</strong> {evidence_source}")
|
|
1843
|
+
|
|
1844
|
+
if evidence_id:
|
|
1845
|
+
evidence_html.append(f"<br><strong>Evidence ID:</strong> {evidence_id}")
|
|
1846
|
+
|
|
1847
|
+
resources_info = self._get_resources_info(evidence)
|
|
1848
|
+
if resources_info:
|
|
1849
|
+
evidence_html.append(f'<br><strong>Resources:</strong><ul><li>{"</li><li>".join(resources_info)}</li></ul>')
|
|
1850
|
+
|
|
1851
|
+
evidence_html.append("</li>")
|
|
1852
|
+
return "\n".join(evidence_html)
|
|
1853
|
+
|
|
1854
|
+
def _get_resources_info(self, evidence: Dict[str, Any]) -> List[str]:
|
|
1855
|
+
"""Extract resource information from evidence."""
|
|
1856
|
+
resources_info = []
|
|
1857
|
+
resources_included = evidence.get("resourcesIncluded", [])
|
|
1858
|
+
|
|
1859
|
+
# Limit to 5 resources per evidence
|
|
1860
|
+
for resource in resources_included[:5]:
|
|
1861
|
+
resource_str = self._format_resource(resource)
|
|
1862
|
+
if resource_str:
|
|
1863
|
+
resources_info.append(resource_str)
|
|
1864
|
+
|
|
1865
|
+
return resources_info
|
|
1866
|
+
|
|
1867
|
+
def _format_resource(self, resource: Dict[str, Any]) -> Optional[str]:
|
|
1868
|
+
"""Format a single resource as a string."""
|
|
1869
|
+
resource_type = resource.get("type", "Unknown")
|
|
1870
|
+
resource_value = resource.get("value", "")[:100]
|
|
1871
|
+
resource_check = resource.get("complianceCheck", "N/A")
|
|
1872
|
+
|
|
1873
|
+
if resource_value:
|
|
1874
|
+
return f"{resource_type}: {resource_value} (Status: {resource_check})"
|
|
1875
|
+
return None
|
|
1876
|
+
|
|
1877
|
+
def _add_remediation_guidance(self, item: ComplianceItem) -> str:
|
|
1878
|
+
"""Add remediation guidance if available."""
|
|
1879
|
+
if not (hasattr(item, "action_plan_instructions") and item.action_plan_instructions):
|
|
1880
|
+
return ""
|
|
1881
|
+
|
|
1882
|
+
instructions = item.action_plan_instructions[:500]
|
|
1883
|
+
truncated = "..." if len(item.action_plan_instructions) > 500 else ""
|
|
1884
|
+
|
|
1885
|
+
return f"""
|
|
1886
|
+
<div style="margin-top: 10px; padding: 8px; background-color: #e3f2fd;
|
|
1887
|
+
border-left: 3px solid #1976d2; border-radius: 3px;">
|
|
1888
|
+
<strong>Remediation Guidance:</strong><br>
|
|
1889
|
+
{instructions}{truncated}
|
|
1890
|
+
</div>
|
|
1891
|
+
"""
|
|
1892
|
+
|
|
1893
|
+
def _process_non_aws_item(self, idx: int, item: ComplianceItem) -> str:
|
|
1894
|
+
"""Process non-AWS items or items without evidence."""
|
|
1895
|
+
description = self._get_item_description(item)
|
|
1896
|
+
|
|
1897
|
+
return f"""
|
|
1898
|
+
<div style="margin-top: 15px; padding: 10px; background-color: #ffebee; border-radius: 3px;">
|
|
1899
|
+
<h5 style="color: #c62828; margin-top: 0;">Failed Check #{idx}: {item.control_id}</h5>
|
|
1900
|
+
<p><strong>Resource:</strong> {getattr(item, 'resource_name', item.resource_id)}</p>
|
|
1901
|
+
<p><strong>Description:</strong> {description}</p>
|
|
1902
|
+
</div>
|
|
1903
|
+
"""
|
|
1904
|
+
|
|
1905
|
+
def _get_item_description(self, item: ComplianceItem) -> str:
|
|
1906
|
+
"""Get truncated description from item."""
|
|
1907
|
+
if hasattr(item, "description"):
|
|
1908
|
+
return item.description[:200]
|
|
1909
|
+
return "N/A"
|
|
1910
|
+
|
|
1641
1911
|
def _get_security_plan(self) -> Optional[regscale_models.SecurityPlan]:
|
|
1642
1912
|
"""
|
|
1643
1913
|
Get the security plan for this integration.
|
|
@@ -60,6 +60,8 @@ class DueDateHandler:
|
|
|
60
60
|
"moderate": 120,
|
|
61
61
|
regscale_models.IssueSeverity.Low: 364,
|
|
62
62
|
"low": 364,
|
|
63
|
+
regscale_models.IssueSeverity.NotAssigned: 364, # Default to Low severity timeline
|
|
64
|
+
"notassigned": 364,
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
# Load integration-specific timelines from config
|
|
@@ -90,6 +92,7 @@ class DueDateHandler:
|
|
|
90
92
|
"moderate": regscale_models.IssueSeverity.Moderate,
|
|
91
93
|
"medium": regscale_models.IssueSeverity.Moderate, # Some integrations use 'medium'
|
|
92
94
|
"low": regscale_models.IssueSeverity.Low,
|
|
95
|
+
"notassigned": regscale_models.IssueSeverity.NotAssigned, # Handle unassigned severities
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
for config_key, severity in severity_mapping.items():
|