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,492 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """AWS Systems Manager 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.inventory.resources.systems_manager import SystemsManagerCollector
19
+ from regscale.integrations.commercial.aws.ssm_control_mappings import SSMControlMapper
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 SSMEvidenceConfig:
28
+ """Configuration for AWS Systems Manager 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
+ create_evidence: bool = False
40
+ create_ssp_attachment: bool = True
41
+ evidence_control_ids: Optional[List[str]] = None
42
+ force_refresh: bool = False
43
+ aws_profile: Optional[str] = None
44
+ aws_access_key_id: Optional[str] = None
45
+ aws_secret_access_key: Optional[str] = None
46
+ aws_session_token: Optional[str] = None
47
+
48
+
49
+ class SSMComplianceItem:
50
+ """Represents Systems Manager configuration for compliance assessment."""
51
+
52
+ def __init__(self, ssm_data: Dict[str, Any]):
53
+ """
54
+ Initialize SSM compliance item from configuration data.
55
+
56
+ :param Dict ssm_data: SSM configuration data from SystemsManagerCollector
57
+ """
58
+ self.managed_instances = ssm_data.get("ManagedInstances", [])
59
+ self.parameters = ssm_data.get("Parameters", [])
60
+ self.documents = ssm_data.get("Documents", [])
61
+ self.patch_baselines = ssm_data.get("PatchBaselines", [])
62
+ self.maintenance_windows = ssm_data.get("MaintenanceWindows", [])
63
+ self.associations = ssm_data.get("Associations", [])
64
+ self.inventory_entries = ssm_data.get("InventoryEntries", [])
65
+ self.compliance_summary = ssm_data.get("ComplianceSummary", {})
66
+ self.raw_data = ssm_data
67
+
68
+ def to_dict(self) -> Dict[str, Any]:
69
+ """Convert to dictionary representation."""
70
+ return self.raw_data
71
+
72
+
73
+ class AWSSSMEvidenceIntegration(ComplianceIntegration):
74
+ """AWS Systems Manager evidence integration for compliance data collection."""
75
+
76
+ def __init__(self, config: SSMEvidenceConfig):
77
+ """
78
+ Initialize AWS Systems Manager evidence integration.
79
+
80
+ :param SSMEvidenceConfig config: Configuration object containing all parameters
81
+ """
82
+ super().__init__(
83
+ plan_id=config.plan_id,
84
+ framework=config.framework,
85
+ create_issues=config.create_issues,
86
+ update_control_status=config.update_control_status,
87
+ create_poams=config.create_poams,
88
+ parent_module=config.parent_module,
89
+ )
90
+
91
+ self.plan_id = config.plan_id
92
+ self.region = config.region
93
+ self.title = "AWS Systems Manager"
94
+ self.account_id = config.account_id
95
+ self.tags = config.tags or {}
96
+ self.create_evidence = config.create_evidence
97
+ self.create_ssp_attachment = config.create_ssp_attachment
98
+ self.evidence_control_ids = config.evidence_control_ids or []
99
+ self.force_refresh = config.force_refresh
100
+
101
+ # AWS credentials
102
+ self.aws_profile = config.aws_profile
103
+ self.aws_access_key_id = config.aws_access_key_id
104
+ self.aws_secret_access_key = config.aws_secret_access_key
105
+ self.aws_session_token = config.aws_session_token
106
+
107
+ # Initialize components
108
+ self.api = Api()
109
+ self.control_mapper = SSMControlMapper(framework=config.framework)
110
+ self.session = None
111
+ self.collector = None
112
+
113
+ # Cache configuration
114
+ self.cache_ttl_hours = 4
115
+ self.cache_dir = Path(tempfile.gettempdir()) / "regscale" / "aws_ssm_cache"
116
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
117
+
118
+ # Data storage
119
+ self.raw_ssm_data: Dict[str, Any] = {}
120
+ self.ssm_item: Optional[SSMComplianceItem] = None
121
+
122
+ def _get_cache_file_path(self) -> Path:
123
+ """Get cache file path for SSM data."""
124
+ cache_key = f"{self.region}_{self.account_id or 'default'}"
125
+ return self.cache_dir / f"ssm_data_{cache_key}.json"
126
+
127
+ def _is_cache_valid(self) -> bool:
128
+ """Check if cache is valid and not expired."""
129
+ cache_file = self._get_cache_file_path()
130
+ if not cache_file.exists():
131
+ return False
132
+
133
+ cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
134
+ return cache_age < timedelta(hours=self.cache_ttl_hours)
135
+
136
+ def _save_cache(self, data: Dict[str, Any]) -> None:
137
+ """Save SSM data to cache."""
138
+ cache_file = self._get_cache_file_path()
139
+ try:
140
+ with open(cache_file, "w", encoding="utf-8") as f:
141
+ json.dump(data, f, default=str)
142
+ logger.debug(f"Saved SSM data to cache: {cache_file}")
143
+ except Exception as e:
144
+ logger.warning(f"Failed to save cache: {e}")
145
+
146
+ def _load_cached_data(self) -> Optional[Dict[str, Any]]:
147
+ """Load SSM data from cache."""
148
+ cache_file = self._get_cache_file_path()
149
+ try:
150
+ with open(cache_file, encoding="utf-8") as f:
151
+ data = json.load(f)
152
+
153
+ # Validate cache format - must be a dict
154
+ if not isinstance(data, dict):
155
+ logger.warning("Invalid cache format detected (not a dict). Invalidating cache.")
156
+ return None
157
+
158
+ logger.info(f"Loaded SSM data from cache (age: {self._get_cache_age_hours():.1f} hours)")
159
+ return data
160
+ except Exception as e:
161
+ logger.warning(f"Failed to load cache: {e}")
162
+ return None
163
+
164
+ def _get_cache_age_hours(self) -> float:
165
+ """Get cache age in hours."""
166
+ cache_file = self._get_cache_file_path()
167
+ if not cache_file.exists():
168
+ return float("inf")
169
+ cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
170
+ return cache_age.total_seconds() / 3600
171
+
172
+ def _initialize_aws_session(self) -> None:
173
+ """Initialize AWS session using provided credentials."""
174
+ if self.aws_access_key_id and self.aws_secret_access_key:
175
+ self.session = boto3.Session(
176
+ aws_access_key_id=self.aws_access_key_id,
177
+ aws_secret_access_key=self.aws_secret_access_key,
178
+ aws_session_token=self.aws_session_token,
179
+ region_name=self.region,
180
+ )
181
+ elif self.aws_profile:
182
+ self.session = boto3.Session(profile_name=self.aws_profile, region_name=self.region)
183
+ else:
184
+ self.session = boto3.Session(region_name=self.region)
185
+ logger.info(f"Initialized AWS session for region: {self.region}")
186
+
187
+ def fetch_compliance_data(self) -> Dict[str, Any]:
188
+ """
189
+ Fetch Systems Manager configuration data from AWS.
190
+
191
+ :return: SSM configuration data
192
+ :rtype: Dict[str, Any]
193
+ """
194
+ # Check cache first
195
+ if not self.force_refresh and self._is_cache_valid():
196
+ cached_data = self._load_cached_data()
197
+ if cached_data:
198
+ return cached_data
199
+
200
+ # Fetch fresh data
201
+ return self._fetch_fresh_ssm_data()
202
+
203
+ def _fetch_fresh_ssm_data(self) -> Dict[str, Any]:
204
+ """
205
+ Fetch fresh SSM data from AWS API.
206
+
207
+ :return: SSM configuration data
208
+ :rtype: Dict[str, Any]
209
+ """
210
+ logger.info(f"Fetching Systems Manager configurations from AWS region: {self.region}")
211
+
212
+ # Initialize AWS session
213
+ if not self.session:
214
+ self._initialize_aws_session()
215
+
216
+ # Create SSM collector
217
+ self.collector = SystemsManagerCollector(
218
+ session=self.session, region=self.region, account_id=self.account_id, tags=self.tags
219
+ )
220
+
221
+ # Collect SSM data
222
+ self.raw_ssm_data = self.collector.collect()
223
+
224
+ managed_instances = len(self.raw_ssm_data.get("ManagedInstances", []))
225
+ parameters = len(self.raw_ssm_data.get("Parameters", []))
226
+ documents = len(self.raw_ssm_data.get("Documents", []))
227
+ patch_baselines = len(self.raw_ssm_data.get("PatchBaselines", []))
228
+
229
+ logger.info(
230
+ f"Collected SSM data: {managed_instances} instances, {parameters} parameters, "
231
+ f"{documents} documents, {patch_baselines} patch baselines"
232
+ )
233
+
234
+ # Save to cache
235
+ self._save_cache(self.raw_ssm_data)
236
+
237
+ return self.raw_ssm_data
238
+
239
+ def sync_compliance_data(self) -> None:
240
+ """Sync Systems Manager compliance data to RegScale."""
241
+ logger.info("Starting AWS Systems Manager compliance data sync to RegScale")
242
+
243
+ # Fetch SSM data
244
+ ssm_data = self.fetch_compliance_data()
245
+ if not ssm_data:
246
+ logger.warning("No Systems Manager data to sync")
247
+ return
248
+
249
+ # Convert to compliance item
250
+ self.ssm_item = SSMComplianceItem(ssm_data)
251
+ logger.info("Processing Systems Manager configuration for compliance assessment")
252
+
253
+ # Assess compliance
254
+ compliance_results = self._assess_compliance()
255
+
256
+ # Populate control dictionaries for assessment creation
257
+ if self.update_control_status:
258
+ self._populate_control_results(compliance_results["overall"])
259
+ # Create control assessments and update implementation statuses
260
+ self._process_control_assessments()
261
+
262
+ # Create evidence artifacts
263
+ if self.create_evidence or self.create_ssp_attachment:
264
+ self._create_evidence_artifacts(compliance_results)
265
+
266
+ logger.info("AWS Systems Manager compliance sync completed successfully")
267
+
268
+ def create_compliance_item(self, raw_data: Dict[str, Any]):
269
+ """
270
+ Create a ComplianceItem from raw SSM data.
271
+
272
+ :param Dict[str, Any] raw_data: Raw SSM configuration data
273
+ :return: SSMComplianceItem instance
274
+ :rtype: SSMComplianceItem
275
+ """
276
+ return SSMComplianceItem(raw_data)
277
+
278
+ def _assess_compliance(self) -> Dict[str, Any]:
279
+ """
280
+ Assess Systems Manager compliance against NIST controls.
281
+
282
+ :return: Compliance assessment results
283
+ :rtype: Dict[str, Any]
284
+ """
285
+ logger.info("Assessing Systems Manager compliance against NIST 800-53 R5 controls")
286
+
287
+ # Assess overall compliance
288
+ overall_results = self.control_mapper.assess_ssm_compliance(self.ssm_item.to_dict())
289
+
290
+ # Log summary
291
+ passed_controls = [ctrl for ctrl, result in overall_results.items() if result == "PASS"]
292
+ failed_controls = [ctrl for ctrl, result in overall_results.items() if result == "FAIL"]
293
+
294
+ logger.info("Systems Manager Compliance Assessment Summary:")
295
+ logger.info(f" Managed Instances: {len(self.ssm_item.managed_instances)}")
296
+ logger.info(f" Parameters: {len(self.ssm_item.parameters)}")
297
+ logger.info(f" Documents: {len(self.ssm_item.documents)}")
298
+ logger.info(f" Patch Baselines: {len(self.ssm_item.patch_baselines)}")
299
+ logger.info(f" Controls Passed: {len(passed_controls)} - {', '.join(passed_controls)}")
300
+ logger.info(f" Controls Failed: {len(failed_controls)} - {', '.join(failed_controls)}")
301
+
302
+ return {"overall": overall_results}
303
+
304
+ def _populate_control_results(self, control_results: Dict[str, str]) -> None:
305
+ """
306
+ Populate passing_controls and failing_controls dictionaries from assessment results.
307
+
308
+ This method converts the control-level assessment results into the format expected
309
+ by the base class _process_control_assessments() method.
310
+
311
+ :param Dict[str, str] control_results: Control assessment results (e.g., {"AC-2": "PASS", "AC-3": "FAIL"})
312
+ :return: None
313
+ :rtype: None
314
+ """
315
+ for control_id, result in control_results.items():
316
+ # Normalize control ID to lowercase for consistent lookup
317
+ control_key = control_id.lower()
318
+
319
+ # Create a simple compliance item placeholder for the base class
320
+ if result in self.PASS_STATUSES:
321
+ self.passing_controls[control_key] = self.ssm_item
322
+ elif result in self.FAIL_STATUSES:
323
+ self.failing_controls[control_key] = self.ssm_item
324
+
325
+ logger.debug(
326
+ f"Populated control results: {len(self.passing_controls)} passing, {len(self.failing_controls)} failing"
327
+ )
328
+
329
+ def _create_evidence_artifacts(self, compliance_results: Dict[str, Any]) -> None:
330
+ """
331
+ Create evidence artifacts in RegScale.
332
+
333
+ :param Dict compliance_results: Compliance assessment results
334
+ """
335
+ logger.info("Creating Systems Manager evidence artifacts in RegScale")
336
+
337
+ # Create comprehensive evidence file
338
+ evidence_file_path = self._create_evidence_file(compliance_results)
339
+
340
+ if self.create_ssp_attachment:
341
+ self._create_ssp_attachment_with_evidence(evidence_file_path)
342
+
343
+ # Clean up temporary file
344
+ if os.path.exists(evidence_file_path):
345
+ os.remove(evidence_file_path)
346
+ logger.debug(f"Cleaned up temporary evidence file: {evidence_file_path}")
347
+
348
+ def _create_evidence_file(self, compliance_results: Dict[str, Any]) -> str:
349
+ """
350
+ Create JSONL.GZ evidence file with Systems Manager configuration data.
351
+
352
+ :param Dict compliance_results: Compliance assessment results
353
+ :return: Path to created evidence file
354
+ :rtype: str
355
+ """
356
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
357
+ evidence_file = os.path.join(tempfile.gettempdir(), f"ssm_evidence_{self.region}_{timestamp}.jsonl.gz")
358
+
359
+ try:
360
+ with gzip.open(evidence_file, "wt", encoding="utf-8") as f:
361
+ # Write metadata
362
+ metadata = {
363
+ "type": "metadata",
364
+ "timestamp": datetime.now().isoformat(),
365
+ "region": self.region,
366
+ "account_id": self.account_id,
367
+ "managed_instances_count": len(self.ssm_item.managed_instances),
368
+ "parameters_count": len(self.ssm_item.parameters),
369
+ "documents_count": len(self.ssm_item.documents),
370
+ "patch_baselines_count": len(self.ssm_item.patch_baselines),
371
+ "compliance_framework": "NIST800-53R5",
372
+ }
373
+ f.write(json.dumps(metadata) + "\n")
374
+
375
+ # Write compliance summary
376
+ summary = {"type": "compliance_summary", "results": compliance_results["overall"]}
377
+ f.write(json.dumps(summary) + "\n")
378
+
379
+ # Write managed instances
380
+ for instance in self.ssm_item.managed_instances:
381
+ instance_record = {
382
+ "type": "managed_instance",
383
+ "instance_id": instance.get("InstanceId"),
384
+ "ping_status": instance.get("PingStatus"),
385
+ "platform": instance.get("PlatformName"),
386
+ "agent_version": instance.get("AgentVersion"),
387
+ "patch_summary": instance.get("PatchSummary", {}),
388
+ }
389
+ f.write(json.dumps(instance_record, default=str) + "\n")
390
+
391
+ # Write patch baselines
392
+ for baseline in self.ssm_item.patch_baselines:
393
+ baseline_record = {
394
+ "type": "patch_baseline",
395
+ "baseline_id": baseline.get("BaselineId"),
396
+ "name": baseline.get("BaselineName"),
397
+ "os": baseline.get("OperatingSystem"),
398
+ "default": baseline.get("DefaultBaseline", False),
399
+ }
400
+ f.write(json.dumps(baseline_record, default=str) + "\n")
401
+
402
+ # Write maintenance windows
403
+ for window in self.ssm_item.maintenance_windows:
404
+ window_record = {
405
+ "type": "maintenance_window",
406
+ "window_id": window.get("WindowId"),
407
+ "name": window.get("Name"),
408
+ "enabled": window.get("Enabled", False),
409
+ "schedule": window.get("Schedule"),
410
+ }
411
+ f.write(json.dumps(window_record, default=str) + "\n")
412
+
413
+ # Write compliance summary
414
+ if self.ssm_item.compliance_summary:
415
+ compliance_record = {
416
+ "type": "compliance_data",
417
+ "total_compliant": self.ssm_item.compliance_summary.get("TotalCompliant", 0),
418
+ "total_non_compliant": self.ssm_item.compliance_summary.get("TotalNonCompliant", 0),
419
+ "compliance_types": self.ssm_item.compliance_summary.get("ComplianceTypes", []),
420
+ }
421
+ f.write(json.dumps(compliance_record, default=str) + "\n")
422
+
423
+ logger.info(f"Created evidence file: {evidence_file}")
424
+ return evidence_file
425
+
426
+ except Exception as e:
427
+ logger.error(f"Failed to create evidence file: {e}", exc_info=True)
428
+ raise
429
+
430
+ def _create_ssp_attachment_with_evidence(self, evidence_file_path: str) -> None:
431
+ """
432
+ Create SSP attachment with Systems Manager evidence.
433
+
434
+ :param str evidence_file_path: Path to evidence file
435
+ """
436
+ try:
437
+ date_str = datetime.now().strftime("%Y%m%d")
438
+ file_name_pattern = f"ssm_evidence_{self.region}_{date_str}"
439
+
440
+ # Check if evidence for today already exists using base class method
441
+ if self.check_for_existing_evidence(file_name_pattern):
442
+ logger.info(
443
+ f"Evidence file for Systems Manager in region {self.region} already exists for today. "
444
+ "Skipping upload to avoid duplicates."
445
+ )
446
+ return
447
+
448
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
449
+ file_name = f"ssm_evidence_{self.region}_{timestamp}.jsonl.gz"
450
+
451
+ # Read the compressed file
452
+ with open(evidence_file_path, "rb") as f:
453
+ file_data = f.read()
454
+
455
+ # Upload file to RegScale
456
+ success = File.upload_file_to_regscale(
457
+ file_name=file_name,
458
+ parent_id=self.plan_id,
459
+ parent_module="securityplans",
460
+ api=self.api,
461
+ file_data=file_data,
462
+ tags="aws,ssm,systems-manager,patch,config,compliance,automated",
463
+ )
464
+
465
+ if success:
466
+ logger.info(f"Successfully uploaded Systems Manager evidence file: {file_name}")
467
+ # Note: SSP attachments don't return IDs from upload_file_to_regscale
468
+ # Control linking would need to be implemented if required for attachments
469
+ if self.evidence_control_ids:
470
+ pass # Placeholder for future SSP attachment-to-control linking
471
+ else:
472
+ logger.error("Failed to upload Systems Manager evidence file")
473
+
474
+ except Exception as e:
475
+ logger.error(f"Failed to create SSP attachment: {e}", exc_info=True)
476
+
477
+ def _link_evidence_to_controls(self, evidence_id: int, is_attachment: bool = False) -> None:
478
+ """
479
+ Link evidence to specified control IDs.
480
+
481
+ :param int evidence_id: Evidence or attachment ID
482
+ :param bool is_attachment: True if linking attachment, False for evidence record
483
+ """
484
+ try:
485
+ for control_id in self.evidence_control_ids:
486
+ if is_attachment:
487
+ self.api.link_ssp_attachment_to_control(self.plan_id, evidence_id, control_id)
488
+ else:
489
+ self.api.link_evidence_to_control(evidence_id, control_id)
490
+ logger.info(f"Linked evidence {evidence_id} to control {control_id}")
491
+ except Exception as e:
492
+ logger.error(f"Failed to link evidence to controls: {e}", exc_info=True)