regscale-cli 6.27.3.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 (112) 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/compliance_integration.py +308 -38
  59. regscale/integrations/due_date_handler.py +3 -0
  60. regscale/integrations/scanner_integration.py +399 -84
  61. regscale/models/integration_models/cisa_kev_data.json +34 -4
  62. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  63. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  64. regscale/models/regscale_models/assessment.py +2 -1
  65. regscale/models/regscale_models/control_objective.py +74 -5
  66. regscale/models/regscale_models/file.py +2 -0
  67. regscale/models/regscale_models/issue.py +2 -5
  68. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
  70. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  71. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  73. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  86. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  87. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  89. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  90. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  91. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  92. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  94. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  96. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  98. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  99. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  100. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  101. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  103. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  104. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  105. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  106. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  108. tests/regscale/integrations/commercial/test_aws.py +55 -56
  109. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """AWS CloudTrail Evidence Integration for RegScale Compliance."""
4
+
5
+ import gzip
6
+ import json
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ import boto3
16
+
17
+ from regscale.core.app.api import Api
18
+ from regscale.integrations.commercial.aws.cloudtrail_control_mappings import CloudTrailControlMapper
19
+ from regscale.integrations.commercial.aws.inventory.resources.cloudtrail import CloudTrailCollector
20
+ from regscale.integrations.compliance_integration import ComplianceIntegration
21
+ from regscale.models.regscale_models.file import File
22
+
23
+ logger = logging.getLogger("regscale")
24
+
25
+
26
+ @dataclass
27
+ class CloudTrailEvidenceConfig:
28
+ """Configuration for AWS CloudTrail evidence collection."""
29
+
30
+ plan_id: int
31
+ region: str = "us-east-1"
32
+ framework: str = "NIST800-53R5"
33
+ create_issues: bool = False
34
+ update_control_status: bool = True
35
+ create_poams: bool = False
36
+ parent_module: str = "securityplans"
37
+ account_id: Optional[str] = None
38
+ tags: Optional[Dict[str, str]] = None
39
+ trail_name_filter: Optional[str] = None
40
+ create_evidence: bool = False
41
+ create_ssp_attachment: bool = True
42
+ evidence_control_ids: Optional[List[str]] = None
43
+ force_refresh: bool = False
44
+ aws_profile: Optional[str] = None
45
+ aws_access_key_id: Optional[str] = None
46
+ aws_secret_access_key: Optional[str] = None
47
+ aws_session_token: Optional[str] = None
48
+
49
+
50
+ class CloudTrailComplianceItem:
51
+ """Represents CloudTrail trail configuration for compliance assessment."""
52
+
53
+ def __init__(self, trail_data: Dict[str, Any]):
54
+ """
55
+ Initialize CloudTrail compliance item from trail data.
56
+
57
+ :param Dict trail_data: Trail configuration data from CloudTrailCollector
58
+ """
59
+ self.trail_name = trail_data.get("Name", "")
60
+ self.trail_arn = trail_data.get("TrailARN", "")
61
+ self.s3_bucket_name = trail_data.get("S3BucketName", "")
62
+ self.is_multi_region = trail_data.get("IsMultiRegionTrail", False)
63
+ self.is_organization_trail = trail_data.get("IsOrganizationTrail", False)
64
+ self.log_file_validation_enabled = trail_data.get("LogFileValidationEnabled", False)
65
+ self.kms_key_id = trail_data.get("KmsKeyId")
66
+ self.cloud_watch_logs_log_group_arn = trail_data.get("CloudWatchLogsLogGroupArn")
67
+ self.sns_topic_arn = trail_data.get("SnsTopicARN")
68
+ self.status = trail_data.get("Status", {})
69
+ self.event_selectors = trail_data.get("EventSelectors", [])
70
+ self.tags = trail_data.get("Tags", {})
71
+ self.region = trail_data.get("Region", "")
72
+ self.raw_data = trail_data
73
+
74
+ def to_dict(self) -> Dict[str, Any]:
75
+ """Convert to dictionary representation."""
76
+ return self.raw_data
77
+
78
+
79
+ class AWSCloudTrailEvidenceIntegration(ComplianceIntegration):
80
+ """AWS CloudTrail evidence integration for compliance data collection."""
81
+
82
+ def __init__(self, config: CloudTrailEvidenceConfig):
83
+ """
84
+ Initialize AWS CloudTrail evidence integration.
85
+
86
+ :param CloudTrailEvidenceConfig config: Configuration object containing all parameters
87
+ """
88
+ super().__init__(
89
+ plan_id=config.plan_id,
90
+ framework=config.framework,
91
+ create_issues=config.create_issues,
92
+ update_control_status=config.update_control_status,
93
+ create_poams=config.create_poams,
94
+ parent_module=config.parent_module,
95
+ )
96
+
97
+ self.plan_id = config.plan_id
98
+ self.region = config.region
99
+ self.title = "AWS CloudTrail"
100
+ self.account_id = config.account_id
101
+ self.tags = config.tags or {}
102
+ self.trail_name_filter = config.trail_name_filter
103
+ self.create_evidence = config.create_evidence
104
+ self.create_ssp_attachment = config.create_ssp_attachment
105
+ self.evidence_control_ids = config.evidence_control_ids or []
106
+ self.force_refresh = config.force_refresh
107
+
108
+ # Initialize control mapper
109
+ self.control_mapper = CloudTrailControlMapper(framework=config.framework)
110
+
111
+ # AWS credentials
112
+ self.aws_profile = config.aws_profile
113
+ self.aws_access_key_id = config.aws_access_key_id
114
+ self.aws_secret_access_key = config.aws_secret_access_key
115
+ self.aws_session_token = config.aws_session_token
116
+
117
+ # Initialize components
118
+ self.api = Api()
119
+ self.session = None
120
+ self.collector = None
121
+
122
+ # Cache configuration
123
+ self.cache_ttl_hours = 4
124
+ self.cache_dir = Path(tempfile.gettempdir()) / "regscale" / "aws_cloudtrail_cache"
125
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
126
+
127
+ # Data storage
128
+ self.raw_cloudtrail_data: Dict[str, Any] = {}
129
+ self.trails: List[CloudTrailComplianceItem] = []
130
+
131
+ def _get_cache_file_path(self) -> Path:
132
+ """Get cache file path for CloudTrail data."""
133
+ cache_key = f"{self.region}_{self.account_id or 'default'}"
134
+ return self.cache_dir / f"cloudtrail_trails_{cache_key}.json"
135
+
136
+ def _is_cache_valid(self) -> bool:
137
+ """Check if cache is valid and not expired."""
138
+ cache_file = self._get_cache_file_path()
139
+ if not cache_file.exists():
140
+ return False
141
+
142
+ cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
143
+ return cache_age < timedelta(hours=self.cache_ttl_hours)
144
+
145
+ def _save_cache(self, data: Dict[str, Any]) -> None:
146
+ """Save CloudTrail data to cache."""
147
+ cache_file = self._get_cache_file_path()
148
+ try:
149
+ with open(cache_file, "w", encoding="utf-8") as f:
150
+ json.dump(data, f, default=str)
151
+ logger.debug(f"Saved CloudTrail data to cache: {cache_file}")
152
+ except Exception as e:
153
+ logger.warning(f"Failed to save cache: {e}")
154
+
155
+ def _load_cached_data(self) -> Optional[List[Dict[str, Any]]]:
156
+ """Load CloudTrail data from cache."""
157
+ cache_file = self._get_cache_file_path()
158
+ try:
159
+ with open(cache_file, encoding="utf-8") as f:
160
+ data = json.load(f)
161
+
162
+ # Validate cache format - must be a list of dicts
163
+ if not isinstance(data, list):
164
+ logger.warning("Invalid cache format detected (not a list). Invalidating cache.")
165
+ return None
166
+
167
+ # Check if first item is a dict (trail configuration)
168
+ if data and not isinstance(data[0], dict):
169
+ logger.warning("Invalid cache format detected (items not dicts). Invalidating cache.")
170
+ return None
171
+
172
+ logger.info(f"Loaded CloudTrail data from cache (age: {self._get_cache_age_hours():.1f} hours)")
173
+ return data
174
+ except Exception as e:
175
+ logger.warning(f"Failed to load cache: {e}")
176
+ return None
177
+
178
+ def _get_cache_age_hours(self) -> float:
179
+ """Get cache age in hours."""
180
+ cache_file = self._get_cache_file_path()
181
+ if not cache_file.exists():
182
+ return float("inf")
183
+ cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
184
+ return cache_age.total_seconds() / 3600
185
+
186
+ def _initialize_aws_session(self) -> None:
187
+ """Initialize AWS session using provided credentials."""
188
+ if self.aws_access_key_id and self.aws_secret_access_key:
189
+ self.session = boto3.Session(
190
+ aws_access_key_id=self.aws_access_key_id,
191
+ aws_secret_access_key=self.aws_secret_access_key,
192
+ aws_session_token=self.aws_session_token,
193
+ region_name=self.region,
194
+ )
195
+ elif self.aws_profile:
196
+ self.session = boto3.Session(profile_name=self.aws_profile, region_name=self.region)
197
+ else:
198
+ self.session = boto3.Session(region_name=self.region)
199
+ logger.info(f"Initialized AWS session for region: {self.region}")
200
+
201
+ def fetch_compliance_data(self) -> List[Dict[str, Any]]:
202
+ """
203
+ Fetch CloudTrail trail configuration data from AWS.
204
+
205
+ :return: List of trail configurations
206
+ :rtype: List[Dict[str, Any]]
207
+ """
208
+ # Check cache first
209
+ if not self.force_refresh and self._is_cache_valid():
210
+ cached_data = self._load_cached_data()
211
+ if cached_data:
212
+ return cached_data
213
+
214
+ # Fetch fresh data
215
+ return self._fetch_fresh_cloudtrail_data()
216
+
217
+ def _fetch_fresh_cloudtrail_data(self) -> List[Dict[str, Any]]:
218
+ """
219
+ Fetch fresh CloudTrail trail data from AWS API.
220
+
221
+ :return: List of trail configurations
222
+ :rtype: List[Dict[str, Any]]
223
+ """
224
+ logger.info(f"Fetching CloudTrail trail configurations from AWS region: {self.region}")
225
+
226
+ # Initialize AWS session
227
+ if not self.session:
228
+ self._initialize_aws_session()
229
+
230
+ # Create CloudTrail collector
231
+ self.collector = CloudTrailCollector(
232
+ session=self.session, region=self.region, account_id=self.account_id, tags=self.tags
233
+ )
234
+
235
+ # Collect CloudTrail data
236
+ self.raw_cloudtrail_data = self.collector.collect()
237
+ trails = self.raw_cloudtrail_data.get("Trails", [])
238
+
239
+ # Apply trail name filter if specified
240
+ if self.trail_name_filter:
241
+ trails = [t for t in trails if self.trail_name_filter in t.get("Name", "")]
242
+ logger.info(f"Applied trail name filter '{self.trail_name_filter}': {len(trails)} trails match")
243
+
244
+ logger.info(f"Collected {len(trails)} CloudTrail trail(s) from region {self.region}")
245
+
246
+ # Save to cache
247
+ self._save_cache(trails)
248
+
249
+ return trails
250
+
251
+ def sync_compliance_data(self) -> None:
252
+ """Sync CloudTrail compliance data to RegScale."""
253
+ logger.info("Starting AWS CloudTrail compliance data sync to RegScale")
254
+
255
+ # Fetch trail data
256
+ trail_data = self.fetch_compliance_data()
257
+ if not trail_data:
258
+ logger.warning("No CloudTrail trail data to sync")
259
+ return
260
+
261
+ # Convert to compliance items
262
+ self.trails = [CloudTrailComplianceItem(trail) for trail in trail_data]
263
+ logger.info(f"Processing {len(self.trails)} CloudTrail trail(s) for compliance assessment")
264
+
265
+ # Assess compliance
266
+ compliance_results = self._assess_compliance()
267
+
268
+ # Populate control dictionaries for assessment creation
269
+ if self.update_control_status:
270
+ self._populate_control_results(compliance_results["overall"])
271
+ # Create control assessments and update implementation statuses
272
+ self._process_control_assessments()
273
+
274
+ # Create evidence artifacts
275
+ if self.create_evidence or self.create_ssp_attachment:
276
+ self._create_evidence_artifacts(compliance_results)
277
+
278
+ logger.info("AWS CloudTrail compliance sync completed successfully")
279
+
280
+ def create_compliance_item(self, raw_data: Dict[str, Any]):
281
+ """
282
+ Create a ComplianceItem from raw CloudTrail trail data.
283
+
284
+ :param Dict[str, Any] raw_data: Raw CloudTrail trail data
285
+ :return: CloudTrailComplianceItem instance
286
+ :rtype: CloudTrailComplianceItem
287
+ """
288
+ return CloudTrailComplianceItem(raw_data)
289
+
290
+ def _assess_compliance(self) -> Dict[str, Any]:
291
+ """
292
+ Assess CloudTrail compliance against NIST controls.
293
+
294
+ :return: Compliance assessment results
295
+ :rtype: Dict[str, Any]
296
+ """
297
+ logger.info("Assessing CloudTrail compliance against NIST 800-53 R5 controls")
298
+
299
+ # Assess each trail individually
300
+ trail_assessments = []
301
+ for trail_item in self.trails:
302
+ trail_result = self.control_mapper.assess_trail_compliance(trail_item.to_dict())
303
+ trail_assessments.append({"trail_name": trail_item.trail_name, "controls": trail_result})
304
+
305
+ # Get overall compliance results
306
+ trail_dicts = [t.to_dict() for t in self.trails]
307
+ overall_results = self.control_mapper.assess_all_trails_compliance(trail_dicts)
308
+
309
+ # Log summary
310
+ passed_controls = [ctrl for ctrl, result in overall_results.items() if result == "PASS"]
311
+ failed_controls = [ctrl for ctrl, result in overall_results.items() if result == "FAIL"]
312
+
313
+ logger.info("CloudTrail Compliance Assessment Summary:")
314
+ logger.info(f" Total Trails: {len(self.trails)}")
315
+ logger.info(f" Controls Passed: {len(passed_controls)} - {', '.join(passed_controls)}")
316
+ logger.info(f" Controls Failed: {len(failed_controls)} - {', '.join(failed_controls)}")
317
+
318
+ return {"overall": overall_results, "trails": trail_assessments}
319
+
320
+ def _populate_control_results(self, control_results: Dict[str, str]) -> None:
321
+ """
322
+ Populate passing_controls and failing_controls dictionaries from assessment results.
323
+
324
+ This method converts the control-level assessment results into the format expected
325
+ by the base class _process_control_assessments() method.
326
+
327
+ :param Dict[str, str] control_results: Control assessment results (e.g., {"AC-2": "PASS", "AC-3": "FAIL"})
328
+ :return: None
329
+ :rtype: None
330
+ """
331
+ for control_id, result in control_results.items():
332
+ # Normalize control ID to lowercase for consistent lookup
333
+ control_key = control_id.lower()
334
+
335
+ # Use first trail as placeholder for the base class
336
+ placeholder_item = self.trails[0] if self.trails else None
337
+ if not placeholder_item:
338
+ continue
339
+
340
+ # Create a simple compliance item placeholder for the base class
341
+ if result in self.PASS_STATUSES:
342
+ self.passing_controls[control_key] = placeholder_item
343
+ elif result in self.FAIL_STATUSES:
344
+ self.failing_controls[control_key] = placeholder_item
345
+
346
+ logger.debug(
347
+ f"Populated control results: {len(self.passing_controls)} passing, {len(self.failing_controls)} failing"
348
+ )
349
+
350
+ def _create_evidence_artifacts(self, compliance_results: Dict[str, Any]) -> None:
351
+ """
352
+ Create evidence artifacts in RegScale.
353
+
354
+ :param Dict compliance_results: Compliance assessment results
355
+ """
356
+ logger.info("Creating CloudTrail evidence artifacts in RegScale")
357
+
358
+ # Create comprehensive evidence file
359
+ evidence_file_path = self._create_evidence_file(compliance_results)
360
+
361
+ if self.create_ssp_attachment:
362
+ self._create_ssp_attachment_with_evidence(evidence_file_path)
363
+
364
+ # Clean up temporary file
365
+ if os.path.exists(evidence_file_path):
366
+ os.remove(evidence_file_path)
367
+ logger.debug(f"Cleaned up temporary evidence file: {evidence_file_path}")
368
+
369
+ def _create_evidence_file(self, compliance_results: Dict[str, Any]) -> str:
370
+ """
371
+ Create JSONL.GZ evidence file with CloudTrail configuration data.
372
+
373
+ :param Dict compliance_results: Compliance assessment results
374
+ :return: Path to created evidence file
375
+ :rtype: str
376
+ """
377
+ evidence_file = self._get_evidence_file_path()
378
+
379
+ try:
380
+ with gzip.open(evidence_file, "wt", encoding="utf-8") as f:
381
+ self._write_metadata(f)
382
+ self._write_compliance_summary(f, compliance_results)
383
+ self._write_trail_configurations(f)
384
+
385
+ logger.info(f"Created evidence file: {evidence_file}")
386
+ return evidence_file
387
+
388
+ except Exception as e:
389
+ logger.error(f"Failed to create evidence file: {e}", exc_info=True)
390
+ raise
391
+
392
+ def _get_evidence_file_path(self) -> str:
393
+ """Generate evidence file path with timestamp."""
394
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
395
+ return os.path.join(tempfile.gettempdir(), f"cloudtrail_evidence_{self.region}_{timestamp}.jsonl.gz")
396
+
397
+ def _write_metadata(self, file_handle) -> None:
398
+ """Write metadata record to evidence file."""
399
+ metadata = {
400
+ "type": "metadata",
401
+ "timestamp": datetime.now().isoformat(),
402
+ "region": self.region,
403
+ "account_id": self.account_id,
404
+ "trail_count": len(self.trails),
405
+ "compliance_framework": "NIST800-53R5",
406
+ }
407
+ file_handle.write(json.dumps(metadata) + "\n")
408
+
409
+ def _write_compliance_summary(self, file_handle, compliance_results: Dict[str, Any]) -> None:
410
+ """Write compliance summary to evidence file."""
411
+ summary = {"type": "compliance_summary", "results": compliance_results["overall"]}
412
+ file_handle.write(json.dumps(summary) + "\n")
413
+
414
+ def _write_trail_configurations(self, file_handle) -> None:
415
+ """Write trail configuration records to evidence file."""
416
+ for trail_item in self.trails:
417
+ trail_record = self._build_trail_record(trail_item)
418
+ file_handle.write(json.dumps(trail_record, default=str) + "\n")
419
+
420
+ def _build_trail_record(self, trail_item: CloudTrailComplianceItem) -> Dict[str, Any]:
421
+ """Build trail configuration record for evidence file."""
422
+ return {
423
+ "type": "trail_configuration",
424
+ "trail_name": trail_item.trail_name,
425
+ "trail_arn": trail_item.trail_arn,
426
+ "s3_bucket": trail_item.s3_bucket_name,
427
+ "multi_region": trail_item.is_multi_region,
428
+ "organization_trail": trail_item.is_organization_trail,
429
+ "log_validation": trail_item.log_file_validation_enabled,
430
+ "kms_encryption": bool(trail_item.kms_key_id),
431
+ "cloudwatch_logs": bool(trail_item.cloud_watch_logs_log_group_arn),
432
+ "sns_notifications": bool(trail_item.sns_topic_arn),
433
+ "status": trail_item.status,
434
+ "event_selectors": trail_item.event_selectors,
435
+ "tags": trail_item.tags,
436
+ }
437
+
438
+ def _create_ssp_attachment_with_evidence(self, evidence_file_path: str) -> None:
439
+ """
440
+ Create SSP attachment with CloudTrail evidence.
441
+
442
+ :param str evidence_file_path: Path to evidence file
443
+ """
444
+ try:
445
+ date_str = datetime.now().strftime("%Y%m%d")
446
+ file_name_pattern = f"cloudtrail_evidence_{self.region}_{date_str}"
447
+
448
+ # Check if evidence for today already exists using base class method
449
+ if self.check_for_existing_evidence(file_name_pattern):
450
+ logger.info(
451
+ f"Evidence file for CloudTrail in region {self.region} already exists for today. "
452
+ "Skipping upload to avoid duplicates."
453
+ )
454
+ return
455
+
456
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
457
+ file_name = f"cloudtrail_evidence_{self.region}_{timestamp}.jsonl.gz"
458
+
459
+ # Read the compressed file
460
+ with open(evidence_file_path, "rb") as f:
461
+ file_data = f.read()
462
+
463
+ # Upload file to RegScale
464
+ success = File.upload_file_to_regscale(
465
+ file_name=file_name,
466
+ parent_id=self.plan_id,
467
+ parent_module="securityplans",
468
+ api=self.api,
469
+ file_data=file_data,
470
+ tags="aws,cloudtrail,audit,logging,compliance,automated",
471
+ )
472
+
473
+ if success:
474
+ logger.info(f"Successfully uploaded CloudTrail evidence file: {file_name}")
475
+ # Link to controls if specified
476
+ if self.evidence_control_ids:
477
+ # Note: SSP attachments don't have IDs returned by upload_file_to_regscale
478
+ # This would need to be implemented if attachment-to-control linking is required
479
+ pass
480
+ else:
481
+ logger.error("Failed to upload CloudTrail evidence file")
482
+
483
+ except Exception as e:
484
+ logger.error(f"Failed to create SSP attachment: {e}", exc_info=True)
485
+
486
+ def _link_evidence_to_controls(self, evidence_id: int, is_attachment: bool = False) -> None:
487
+ """
488
+ Link evidence to specified control IDs.
489
+
490
+ :param int evidence_id: Evidence or attachment ID
491
+ :param bool is_attachment: True if linking attachment, False for evidence record
492
+ """
493
+ try:
494
+ for control_id in self.evidence_control_ids:
495
+ if is_attachment:
496
+ self.api.link_ssp_attachment_to_control(self.plan_id, evidence_id, control_id)
497
+ else:
498
+ self.api.link_evidence_to_control(evidence_id, control_id)
499
+ logger.info(f"Linked evidence {evidence_id} to control {control_id}")
500
+ except Exception as e:
501
+ logger.error(f"Failed to link evidence to controls: {e}", exc_info=True)