regscale-cli 6.27.2.0__py3-none-any.whl → 6.28.0.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 (140) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +11 -2
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/dev/cli.py +26 -0
  10. regscale/dev/version.py +72 -0
  11. regscale/integrations/commercial/__init__.py +15 -1
  12. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  13. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  14. regscale/integrations/commercial/amazon/common.py +48 -58
  15. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  16. regscale/integrations/commercial/aws/cli.py +3093 -55
  17. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  18. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  19. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  20. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  21. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  22. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  23. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  24. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  25. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  26. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  27. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  28. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  29. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  30. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  31. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  32. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  33. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  34. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  35. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  36. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  37. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  38. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  39. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  40. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  41. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  42. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  43. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  44. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  45. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  46. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  47. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  48. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  49. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  50. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  51. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  52. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  53. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  54. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  55. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  56. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  57. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  58. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  59. regscale/integrations/commercial/aws/scanner.py +853 -205
  60. regscale/integrations/commercial/aws/security_hub.py +319 -0
  61. regscale/integrations/commercial/aws/session_manager.py +282 -0
  62. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  63. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  64. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  65. regscale/integrations/compliance_integration.py +308 -38
  66. regscale/integrations/control_matcher.py +78 -23
  67. regscale/integrations/due_date_handler.py +3 -0
  68. regscale/integrations/public/csam/csam.py +572 -763
  69. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  70. regscale/integrations/public/csam/csam_common.py +154 -0
  71. regscale/integrations/public/csam/csam_controls.py +432 -0
  72. regscale/integrations/public/csam/csam_poam.py +124 -0
  73. regscale/integrations/public/fedramp/click.py +17 -4
  74. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  75. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  76. regscale/integrations/scanner_integration.py +415 -85
  77. regscale/models/integration_models/cisa_kev_data.json +80 -20
  78. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  79. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
  80. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  81. regscale/models/platform.py +3 -0
  82. regscale/models/regscale_models/__init__.py +5 -0
  83. regscale/models/regscale_models/assessment.py +2 -1
  84. regscale/models/regscale_models/component.py +1 -1
  85. regscale/models/regscale_models/control_implementation.py +55 -24
  86. regscale/models/regscale_models/control_objective.py +74 -5
  87. regscale/models/regscale_models/file.py +2 -0
  88. regscale/models/regscale_models/issue.py +2 -5
  89. regscale/models/regscale_models/organization.py +3 -0
  90. regscale/models/regscale_models/regscale_model.py +17 -5
  91. regscale/models/regscale_models/security_plan.py +1 -0
  92. regscale/regscale.py +11 -1
  93. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  94. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
  95. tests/regscale/core/test_login.py +171 -4
  96. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  97. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  98. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  99. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  100. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  101. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  102. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  103. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  104. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  105. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  106. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  107. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  108. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  109. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  110. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  111. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  112. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  113. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  114. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  115. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  116. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  117. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  118. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  119. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  120. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  121. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  122. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  123. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  124. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  125. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  126. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  127. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  128. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  129. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  130. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  131. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  132. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  133. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  134. tests/regscale/integrations/commercial/test_aws.py +55 -56
  135. tests/regscale/integrations/test_control_matcher.py +24 -0
  136. tests/regscale/models/test_control_implementation.py +118 -3
  137. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  138. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  139. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  140. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """AWS Config Conformance Pack to Control Mappings."""
4
+
5
+ import logging
6
+ import re
7
+ from typing import Dict, List, Optional
8
+
9
+ logger = logging.getLogger("regscale")
10
+
11
+ # Control ID constants
12
+ CONTROL_IA_2_1 = "IA-2(1)"
13
+
14
+ # NIST 800-53 R5 Conformance Pack Control Mappings
15
+ # Maps AWS Config rule names to NIST 800-53 R5 control IDs
16
+ NIST_80053_R5_MAPPINGS = {
17
+ # Access Control (AC) Family
18
+ "iam-password-policy": ["AC-2", "IA-5"],
19
+ "iam-user-mfa-enabled": ["AC-2", CONTROL_IA_2_1],
20
+ "iam-root-access-key-check": ["AC-2", "AC-6"],
21
+ "iam-user-unused-credentials-check": ["AC-2"],
22
+ "iam-user-group-membership-check": ["AC-2"],
23
+ "iam-policy-no-statements-with-admin-access": ["AC-6"],
24
+ "iam-policy-no-statements-with-full-access": ["AC-6"],
25
+ "s3-bucket-public-read-prohibited": ["AC-3", "AC-4"],
26
+ "s3-bucket-public-write-prohibited": ["AC-3", "AC-4"],
27
+ "ec2-security-group-attached-to-eni": ["AC-4"],
28
+ "restricted-ssh": ["AC-4", "AC-17"],
29
+ "restricted-common-ports": ["AC-4"],
30
+ # Audit and Accountability (AU) Family
31
+ "cloudtrail-enabled": ["AU-2", "AU-3", "AU-6", "AU-12"],
32
+ "cloud-trail-cloud-watch-logs-enabled": ["AU-6"],
33
+ "cloudtrail-log-file-validation-enabled": ["AU-9"],
34
+ "cloudtrail-encryption-enabled": ["AU-9"],
35
+ "cloudtrail-s3-dataevents-enabled": ["AU-2"],
36
+ "multi-region-cloudtrail-enabled": ["AU-2"],
37
+ "s3-bucket-logging-enabled": ["AU-2"],
38
+ "rds-logging-enabled": ["AU-2"],
39
+ "elb-logging-enabled": ["AU-2"],
40
+ "cloudwatch-alarm-action-check": ["AU-6"],
41
+ # Configuration Management (CM) Family
42
+ "ec2-instance-managed-by-systems-manager": ["CM-2", "CM-6"],
43
+ "ec2-managedinstance-patch-compliance-status-check": ["CM-6", "SI-2"], # Maps to both CM and SI families
44
+ "approved-amis-by-tag": ["CM-2"],
45
+ # Identification and Authentication (IA) Family
46
+ "mfa-enabled-for-iam-console-access": [CONTROL_IA_2_1],
47
+ "root-account-mfa-enabled": [CONTROL_IA_2_1],
48
+ # System and Communications Protection (SC) Family
49
+ "s3-bucket-ssl-requests-only": ["SC-8", "SC-13"],
50
+ "alb-http-to-https-redirection-check": ["SC-8"],
51
+ "elb-tls-https-listeners-only": ["SC-8"],
52
+ "rds-snapshot-encrypted": ["SC-13"],
53
+ "encrypted-volumes": ["SC-13"],
54
+ "s3-bucket-server-side-encryption-enabled": ["SC-13"],
55
+ "ec2-ebs-encryption-by-default": ["SC-13"],
56
+ "rds-storage-encrypted": ["SC-13"],
57
+ "dynamodb-table-encrypted-kms": ["SC-13"],
58
+ # System and Information Integrity (SI) Family
59
+ "guardduty-enabled-centralized": ["SI-4"],
60
+ "securityhub-enabled": ["SI-4"],
61
+ "access-keys-rotated": ["SI-4"],
62
+ "vpc-flow-logs-enabled": ["SI-4"],
63
+ # Risk Assessment (RA) Family
64
+ "security-account-information-provided": ["RA-5"],
65
+ }
66
+
67
+
68
+ def extract_control_ids_from_rule_name(rule_name: str) -> List[str]:
69
+ """
70
+ Extract control IDs from AWS Config rule name using pattern matching.
71
+
72
+ Supports patterns like:
73
+ - "ac-2-iam-user-mfa-enabled"
74
+ - "nist-800-53-r5-ac-2"
75
+ - "iam-password-policy-ac-2-ia-5"
76
+
77
+ :param str rule_name: Config rule name
78
+ :return: List of extracted control IDs
79
+ :rtype: List[str]
80
+ """
81
+ control_ids = []
82
+
83
+ # Pattern for NIST control IDs: AC-2, SI-3(1), etc.
84
+ pattern = r"\b([A-Z]{2}-\d+(?:\(\d+\))?)\b"
85
+
86
+ matches = re.findall(pattern, rule_name.upper())
87
+ control_ids.extend(matches)
88
+
89
+ return list(set(control_ids)) # Remove duplicates
90
+
91
+
92
+ def extract_control_ids_from_tags(tags: Dict[str, str]) -> List[str]:
93
+ """
94
+ Extract control IDs from AWS Config rule tags.
95
+
96
+ Expected tag format:
97
+ - ControlID=AC-2
98
+ - ControlID=AC-2,AU-3,SI-2
99
+ - ControlIDs=AC-2,AU-3
100
+
101
+ :param Dict[str, str] tags: Dictionary of tag key-value pairs
102
+ :return: List of extracted control IDs
103
+ :rtype: List[str]
104
+ """
105
+ control_ids = []
106
+
107
+ # Check for ControlID or ControlIDs tags
108
+ for tag_key in ["ControlID", "ControlIDs", "Control-ID", "Control-IDs"]:
109
+ if tag_key in tags:
110
+ tag_value = tags[tag_key]
111
+ # Split by comma and clean up
112
+ ids = [cid.strip().upper() for cid in tag_value.split(",") if cid.strip()]
113
+ control_ids.extend(ids)
114
+
115
+ return list(set(control_ids)) # Remove duplicates
116
+
117
+
118
+ def get_control_mappings_for_framework(framework: str) -> Dict[str, List[str]]:
119
+ """
120
+ Get control mappings for a specific framework.
121
+
122
+ :param str framework: Framework name (e.g., "NIST800-53R5")
123
+ :return: Dictionary mapping rule names to control IDs
124
+ :rtype: Dict[str, List[str]]
125
+ """
126
+ framework_upper = framework.upper().replace("-", "").replace("_", "")
127
+
128
+ if "NIST80053" in framework_upper or "NIST800" in framework_upper:
129
+ return NIST_80053_R5_MAPPINGS
130
+
131
+ # Add more framework mappings as needed
132
+ # elif "PCI" in framework_upper:
133
+ # return PCI_DSS_MAPPINGS
134
+ # elif "CIS" in framework_upper:
135
+ # return CIS_MAPPINGS
136
+
137
+ logger.warning(f"No built-in control mappings available for framework: {framework}")
138
+ return {}
139
+
140
+
141
+ def map_rule_to_controls(
142
+ rule_name: str,
143
+ rule_description: Optional[str] = None,
144
+ rule_tags: Optional[Dict[str, str]] = None,
145
+ framework: str = "NIST800-53R5",
146
+ ) -> List[str]:
147
+ """
148
+ Map an AWS Config rule to control IDs using multiple strategies.
149
+
150
+ Priority order:
151
+ 1. Framework-specific mappings (conformance pack)
152
+ 2. Rule tags (ControlID tag)
153
+ 3. Pattern matching in rule name
154
+ 4. Pattern matching in rule description
155
+
156
+ :param str rule_name: Config rule name
157
+ :param Optional[str] rule_description: Config rule description
158
+ :param Optional[Dict[str, str]] rule_tags: Config rule tags
159
+ :param str framework: Target framework
160
+ :return: List of mapped control IDs
161
+ :rtype: List[str]
162
+ """
163
+ control_ids = []
164
+
165
+ # Strategy 1: Check framework-specific mappings
166
+ framework_mappings = get_control_mappings_for_framework(framework)
167
+ if rule_name in framework_mappings:
168
+ control_ids.extend(framework_mappings[rule_name])
169
+ logger.debug(f"Rule '{rule_name}' mapped to controls via framework mappings: {control_ids}")
170
+
171
+ # Strategy 2: Check rule tags
172
+ if rule_tags:
173
+ tag_control_ids = extract_control_ids_from_tags(rule_tags)
174
+ if tag_control_ids:
175
+ control_ids.extend(tag_control_ids)
176
+ logger.debug(f"Rule '{rule_name}' mapped to controls via tags: {tag_control_ids}")
177
+
178
+ # Strategy 3: Pattern matching in rule name
179
+ if not control_ids:
180
+ name_control_ids = extract_control_ids_from_rule_name(rule_name)
181
+ if name_control_ids:
182
+ control_ids.extend(name_control_ids)
183
+ logger.debug(f"Rule '{rule_name}' mapped to controls via name pattern: {name_control_ids}")
184
+
185
+ # Strategy 4: Pattern matching in rule description
186
+ if not control_ids and rule_description:
187
+ desc_control_ids = extract_control_ids_from_rule_name(rule_description)
188
+ if desc_control_ids:
189
+ control_ids.extend(desc_control_ids)
190
+ logger.debug(f"Rule '{rule_name}' mapped to controls via description pattern: {desc_control_ids}")
191
+
192
+ # Remove duplicates and sort
193
+ control_ids = sorted(set(control_ids))
194
+
195
+ if not control_ids:
196
+ logger.debug(f"Rule '{rule_name}' could not be mapped to any controls")
197
+
198
+ return control_ids
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """AWS Evidence Generator for SSP compliance documentation"""
4
+
5
+ import gzip
6
+ import json
7
+ import logging
8
+ from datetime import datetime, timedelta
9
+ from io import BytesIO
10
+ from typing import List, Optional
11
+
12
+ from regscale.core.app.api import Api
13
+ from regscale.models.regscale_models.evidence import Evidence
14
+ from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
15
+ from regscale.models.regscale_models.file import File
16
+
17
+ logger = logging.getLogger("regscale")
18
+
19
+
20
+ class AWSEvidenceGenerator:
21
+ """Generate compliance evidence from AWS security findings"""
22
+
23
+ def __init__(self, api: Api, ssp_id: Optional[int] = None):
24
+ """
25
+ Initialize evidence generator
26
+
27
+ :param Api api: RegScale API instance
28
+ :param Optional[int] ssp_id: Security Plan ID to link evidence
29
+ """
30
+ self.api = api
31
+ self.ssp_id = ssp_id
32
+
33
+ def create_evidence_from_scan(
34
+ self,
35
+ service_name: str,
36
+ findings: List[dict],
37
+ ocsf_data: Optional[List[dict]] = None,
38
+ update_frequency: int = 30,
39
+ control_ids: Optional[List[int]] = None,
40
+ ) -> Optional[Evidence]:
41
+ """
42
+ Create evidence record from AWS service scan
43
+
44
+ :param str service_name: AWS service name (GuardDuty, SecurityHub, etc.)
45
+ :param List[dict] findings: List of AWS findings (native format)
46
+ :param Optional[List[dict]] ocsf_data: OCSF-formatted findings
47
+ :param int update_frequency: Evidence update frequency in days (default: 30)
48
+ :param Optional[List[int]] control_ids: Control IDs to link evidence
49
+ :return: Created Evidence object or None
50
+ :rtype: Optional[Evidence]
51
+ """
52
+ if not findings:
53
+ logger.warning("No findings provided, skipping evidence creation")
54
+ return None
55
+
56
+ # Generate evidence title with timestamp
57
+ scan_date = datetime.now().strftime("%Y-%m-%d")
58
+ title = f"{service_name} Findings Scan - {scan_date}"
59
+
60
+ # Create description with finding summary
61
+ total_findings = len(findings)
62
+ severity_counts = self._count_severities(findings, service_name)
63
+ description = self._build_evidence_description(service_name, total_findings, severity_counts, ocsf_data)
64
+
65
+ # Calculate due date based on update frequency
66
+ due_date = (datetime.now() + timedelta(days=update_frequency)).isoformat()
67
+
68
+ try:
69
+ # Create evidence record
70
+ evidence = Evidence(
71
+ title=title,
72
+ description=description,
73
+ status="Collected",
74
+ updateFrequency=update_frequency,
75
+ dueDate=due_date,
76
+ )
77
+
78
+ # Create evidence in RegScale
79
+ created_evidence = evidence.create()
80
+ if not created_evidence or not created_evidence.id:
81
+ logger.error("Failed to create evidence record")
82
+ return None
83
+
84
+ logger.info("Created evidence record %s: %s", created_evidence.id, title)
85
+
86
+ # Upload findings as file attachments
87
+ self._attach_findings_files(
88
+ created_evidence.id,
89
+ findings,
90
+ ocsf_data,
91
+ service_name,
92
+ )
93
+
94
+ # Link evidence to SSP if provided
95
+ if self.ssp_id:
96
+ self._link_to_ssp(created_evidence.id)
97
+
98
+ # Link evidence to specific controls if provided
99
+ if control_ids:
100
+ self._link_to_controls(created_evidence.id, control_ids)
101
+
102
+ return created_evidence
103
+
104
+ except Exception as ex:
105
+ logger.error("Failed to create evidence: %s", ex)
106
+ return None
107
+
108
+ def _count_severities(self, findings: List[dict], service_name: str) -> dict:
109
+ """
110
+ Count findings by severity level
111
+
112
+ :param List[dict] findings: AWS findings
113
+ :param str service_name: AWS service name for severity field mapping
114
+ :return: Dictionary with severity counts
115
+ :rtype: dict
116
+ """
117
+ severity_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
118
+
119
+ for finding in findings:
120
+ # Map service-specific severity fields
121
+ if service_name == "GuardDuty":
122
+ severity = finding.get("Severity", 0)
123
+ # GuardDuty uses numeric severity (1.0-8.9)
124
+ if severity >= 7.0:
125
+ severity_counts["HIGH"] += 1
126
+ elif severity >= 4.0:
127
+ severity_counts["MEDIUM"] += 1
128
+ else:
129
+ severity_counts["LOW"] += 1
130
+
131
+ elif service_name == "SecurityHub":
132
+ # Security Hub uses normalized severity
133
+ severity_label = finding.get("Severity", {}).get("Label", "INFO")
134
+ severity_counts[severity_label] = severity_counts.get(severity_label, 0) + 1
135
+
136
+ elif service_name == "CloudTrail":
137
+ # CloudTrail events don't have severity, count as INFO
138
+ severity_counts["INFO"] += 1
139
+
140
+ return severity_counts
141
+
142
+ def _build_evidence_description(
143
+ self,
144
+ service_name: str,
145
+ total_findings: int,
146
+ severity_counts: dict,
147
+ ocsf_data: Optional[List[dict]],
148
+ ) -> str:
149
+ """
150
+ Build evidence description with finding summary
151
+
152
+ :param str service_name: AWS service name
153
+ :param int total_findings: Total number of findings
154
+ :param dict severity_counts: Severity breakdown
155
+ :param Optional[List[dict]] ocsf_data: OCSF-formatted findings
156
+ :return: Evidence description text
157
+ :rtype: str
158
+ """
159
+ description_parts = [
160
+ f"Automated evidence collection from AWS {service_name}.",
161
+ f"Total findings: {total_findings}",
162
+ "",
163
+ "Severity Breakdown:",
164
+ ]
165
+
166
+ for severity, count in severity_counts.items():
167
+ if count > 0:
168
+ description_parts.append(f" - {severity}: {count}")
169
+
170
+ description_parts.extend(["", "Files attached:"])
171
+ description_parts.append(f" - {service_name.lower()}_findings_native.jsonl.gz (AWS native format, compressed)")
172
+
173
+ if ocsf_data:
174
+ description_parts.append(
175
+ f" - {service_name.lower()}_findings_ocsf.jsonl.gz (OCSF normalized format, compressed)"
176
+ )
177
+
178
+ return "\n".join(description_parts)
179
+
180
+ def _attach_findings_files(
181
+ self,
182
+ evidence_id: int,
183
+ findings: List[dict],
184
+ ocsf_data: Optional[List[dict]],
185
+ service_name: str,
186
+ ) -> None:
187
+ """
188
+ Upload findings as file attachments to evidence
189
+
190
+ :param int evidence_id: Evidence record ID
191
+ :param List[dict] findings: Native AWS findings
192
+ :param Optional[List[dict]] ocsf_data: OCSF-formatted findings
193
+ :param str service_name: AWS service name
194
+ """
195
+ # Upload native findings as compressed JSONL
196
+ native_jsonl = "\n".join([json.dumps(f) for f in findings])
197
+
198
+ # Compress the JSONL data
199
+ compressed_buffer = BytesIO()
200
+ with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
201
+ gz_file.write(native_jsonl)
202
+
203
+ compressed_data = compressed_buffer.getvalue()
204
+
205
+ success = File.upload_file_to_regscale(
206
+ file_name=f"{service_name.lower()}_findings_native.jsonl.gz",
207
+ parent_id=evidence_id,
208
+ parent_module="evidence",
209
+ api=self.api,
210
+ file_data=compressed_data,
211
+ tags=f"aws,{service_name.lower()},native,compressed",
212
+ )
213
+
214
+ if success:
215
+ logger.info("Uploaded compressed native findings file for evidence %s", evidence_id)
216
+ else:
217
+ logger.warning("Failed to upload compressed native findings file for evidence %s", evidence_id)
218
+
219
+ # Upload OCSF findings if available
220
+ if ocsf_data:
221
+ ocsf_jsonl = "\n".join([json.dumps(f) for f in ocsf_data])
222
+
223
+ # Compress the OCSF JSONL data
224
+ compressed_buffer = BytesIO()
225
+ with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
226
+ gz_file.write(ocsf_jsonl)
227
+
228
+ compressed_data = compressed_buffer.getvalue()
229
+
230
+ success = File.upload_file_to_regscale(
231
+ file_name=f"{service_name.lower()}_findings_ocsf.jsonl.gz",
232
+ parent_id=evidence_id,
233
+ parent_module="evidence",
234
+ api=self.api,
235
+ file_data=compressed_data,
236
+ tags=f"aws,{service_name.lower()},ocsf,compressed",
237
+ )
238
+
239
+ if success:
240
+ logger.info("Uploaded compressed OCSF findings file for evidence %s", evidence_id)
241
+ else:
242
+ logger.warning("Failed to upload compressed OCSF findings file for evidence %s", evidence_id)
243
+
244
+ def _link_to_ssp(self, evidence_id: int) -> None:
245
+ """
246
+ Link evidence to Security Plan
247
+
248
+ :param int evidence_id: Evidence record ID
249
+ """
250
+ if not self.ssp_id:
251
+ return
252
+
253
+ mapping = EvidenceMapping(
254
+ evidenceID=evidence_id,
255
+ mappedID=self.ssp_id,
256
+ mappingType="securityplans",
257
+ )
258
+
259
+ try:
260
+ mapping.create()
261
+ logger.info("Linked evidence %s to SSP %s", evidence_id, self.ssp_id)
262
+ except Exception as ex:
263
+ logger.warning("Failed to link evidence to SSP: %s", ex)
264
+
265
+ def _link_to_controls(self, evidence_id: int, control_ids: List[int]) -> None:
266
+ """
267
+ Link evidence to specific security controls
268
+
269
+ :param int evidence_id: Evidence record ID
270
+ :param List[int] control_ids: List of control IDs
271
+ """
272
+ for control_id in control_ids:
273
+ mapping = EvidenceMapping(
274
+ evidenceID=evidence_id,
275
+ mappedID=control_id,
276
+ mappingType="securityControlAssessments",
277
+ )
278
+
279
+ try:
280
+ mapping.create()
281
+ logger.info("Linked evidence %s to control %s", evidence_id, control_id)
282
+ except Exception as ex:
283
+ logger.warning("Failed to link evidence to control %s: %s", control_id, ex)