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.

Files changed (113) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/utils/app_utils.py +11 -2
  3. regscale/dev/cli.py +26 -0
  4. regscale/dev/version.py +72 -0
  5. regscale/integrations/commercial/__init__.py +15 -1
  6. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  7. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  8. regscale/integrations/commercial/amazon/common.py +48 -58
  9. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  10. regscale/integrations/commercial/aws/cli.py +3093 -55
  11. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  12. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  13. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  14. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  15. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  16. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  17. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  18. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  19. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  20. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  21. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  22. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  23. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  24. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  25. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  26. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  27. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  28. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  29. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  30. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  31. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  32. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  33. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  34. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  35. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  36. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  37. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  38. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  39. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  40. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  41. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  42. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  43. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  44. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  45. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  46. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  47. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  48. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  49. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  50. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  51. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  52. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  53. regscale/integrations/commercial/aws/scanner.py +851 -206
  54. regscale/integrations/commercial/aws/security_hub.py +319 -0
  55. regscale/integrations/commercial/aws/session_manager.py +282 -0
  56. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  57. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  58. regscale/integrations/commercial/synqly/ticketing.py +27 -0
  59. regscale/integrations/compliance_integration.py +308 -38
  60. regscale/integrations/due_date_handler.py +3 -0
  61. regscale/integrations/scanner_integration.py +399 -84
  62. regscale/models/integration_models/cisa_kev_data.json +65 -5
  63. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  64. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  65. regscale/models/regscale_models/assessment.py +2 -1
  66. regscale/models/regscale_models/control_objective.py +74 -5
  67. regscale/models/regscale_models/file.py +2 -0
  68. regscale/models/regscale_models/issue.py +2 -5
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/METADATA +1 -1
  70. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
  71. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  73. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  86. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  87. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  89. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  90. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  91. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  92. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  94. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  96. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  98. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  99. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  100. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  101. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  103. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  104. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  105. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  106. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  108. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  109. tests/regscale/integrations/commercial/test_aws.py +55 -56
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/LICENSE +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
  113. {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
- matching_implementation = None
1265
- matching_security_control = None
1266
-
1267
- for implementation in implementations:
1268
- try:
1269
- security_control = SecurityControl.get_object(object_id=implementation.controlID)
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
- # Check if any variation matches (set intersection)
1288
- if search_variations & control_variations:
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> <span style="color: {result_color}; font-weight: bold;">{result}</span></p>
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():