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.
- regscale/_version.py +1 -1
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/dev/cli.py +26 -0
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +15 -1
- regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/amazon/common.py +204 -0
- regscale/integrations/commercial/amazon/common.py +48 -58
- regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
- regscale/integrations/commercial/aws/cli.py +3093 -55
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +851 -206
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/scanner_integration.py +399 -84
- regscale/models/integration_models/cisa_kev_data.json +34 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/issue.py +2 -5
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/test_aws.py +55 -56
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""AWS Config Compliance Integration for RegScale CLI."""
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from io import BytesIO
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
import boto3
|
|
16
|
+
from botocore.exceptions import ClientError
|
|
17
|
+
|
|
18
|
+
from regscale.core.app.utils.app_utils import get_current_datetime
|
|
19
|
+
from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("regscale")
|
|
22
|
+
|
|
23
|
+
# Constants for file paths and cache TTL
|
|
24
|
+
CONFIG_COMPLIANCE_CACHE_FILE = os.path.join("artifacts", "aws", "config_compliance_assessments.json")
|
|
25
|
+
CACHE_TTL_SECONDS = 4 * 60 * 60 # 4 hours in seconds
|
|
26
|
+
|
|
27
|
+
# HTML tag constants to avoid duplication
|
|
28
|
+
HTML_STRONG_OPEN = "<strong>"
|
|
29
|
+
HTML_STRONG_CLOSE = "</strong>"
|
|
30
|
+
HTML_P_OPEN = "<p>"
|
|
31
|
+
HTML_P_CLOSE = "</p>"
|
|
32
|
+
HTML_UL_OPEN = "<ul>"
|
|
33
|
+
HTML_UL_CLOSE = "</ul>"
|
|
34
|
+
HTML_LI_OPEN = "<li>"
|
|
35
|
+
HTML_LI_CLOSE = "</li>"
|
|
36
|
+
HTML_H2_OPEN = "<h2>"
|
|
37
|
+
HTML_H2_CLOSE = "</h2>"
|
|
38
|
+
HTML_H3_OPEN = "<h3>"
|
|
39
|
+
HTML_H3_CLOSE = "</h3>"
|
|
40
|
+
HTML_H4_OPEN = "<h4>"
|
|
41
|
+
HTML_H4_CLOSE = "</h4>"
|
|
42
|
+
HTML_BR = "<br>"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AWSConfigComplianceItem(ComplianceItem):
|
|
46
|
+
"""
|
|
47
|
+
Compliance item from AWS Config rule evaluation.
|
|
48
|
+
|
|
49
|
+
Represents a control assessment based on AWS Config rule evaluations.
|
|
50
|
+
Multiple Config rules can map to a single control, and the control passes
|
|
51
|
+
only if ALL associated rules are compliant.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
control_id: str,
|
|
57
|
+
control_name: str,
|
|
58
|
+
framework: str,
|
|
59
|
+
rule_evaluations: List[Dict[str, Any]],
|
|
60
|
+
resource_id: Optional[str] = None,
|
|
61
|
+
resource_name: Optional[str] = None,
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Initialize from AWS Config rule evaluations.
|
|
65
|
+
|
|
66
|
+
:param str control_id: Control identifier (e.g., AC-2, SI-3)
|
|
67
|
+
:param str control_name: Human-readable control name
|
|
68
|
+
:param str framework: Compliance framework
|
|
69
|
+
:param List[Dict[str, Any]] rule_evaluations: Config rule evaluation results
|
|
70
|
+
:param Optional[str] resource_id: Resource identifier (AWS account ID typically)
|
|
71
|
+
:param Optional[str] resource_name: Resource name
|
|
72
|
+
"""
|
|
73
|
+
self._control_id = control_id
|
|
74
|
+
self._control_name = control_name
|
|
75
|
+
self._framework = framework
|
|
76
|
+
self.rule_evaluations = rule_evaluations
|
|
77
|
+
self._resource_id = resource_id or ""
|
|
78
|
+
self._resource_name = resource_name or ""
|
|
79
|
+
|
|
80
|
+
# Cache for aggregated compliance result
|
|
81
|
+
self._aggregated_compliance_result = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def resource_id(self) -> str:
|
|
85
|
+
"""Unique identifier for the resource being assessed."""
|
|
86
|
+
return self._resource_id
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def resource_name(self) -> str:
|
|
90
|
+
"""Human-readable name of the resource."""
|
|
91
|
+
return self._resource_name
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def control_id(self) -> str:
|
|
95
|
+
"""Control identifier (e.g., AC-3, SI-2)."""
|
|
96
|
+
return self._control_id
|
|
97
|
+
|
|
98
|
+
def _aggregate_rule_compliance(self) -> Optional[str]:
|
|
99
|
+
"""
|
|
100
|
+
Aggregate rule evaluation results to determine overall control compliance.
|
|
101
|
+
|
|
102
|
+
AWS Config rule compliance types:
|
|
103
|
+
- "COMPLIANT": Resource is compliant with the rule
|
|
104
|
+
- "NON_COMPLIANT": Resource violates the rule
|
|
105
|
+
- "NOT_APPLICABLE": Rule doesn't apply to this resource
|
|
106
|
+
- "INSUFFICIENT_DATA": Not enough data to determine compliance
|
|
107
|
+
|
|
108
|
+
Aggregation Logic:
|
|
109
|
+
1. If ANY rule shows "NON_COMPLIANT" → Control FAILS
|
|
110
|
+
2. If ALL applicable rules show "COMPLIANT" → Control PASSES
|
|
111
|
+
3. If only NOT_APPLICABLE or INSUFFICIENT_DATA → INCONCLUSIVE
|
|
112
|
+
|
|
113
|
+
:return: "PASS", "FAIL", or None (if inconclusive/no data)
|
|
114
|
+
:rtype: Optional[str]
|
|
115
|
+
"""
|
|
116
|
+
if not self.rule_evaluations:
|
|
117
|
+
logger.debug(f"Control {self.control_id}: No rule evaluations available")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
compliant_count = 0
|
|
121
|
+
non_compliant_count = 0
|
|
122
|
+
not_applicable_count = 0
|
|
123
|
+
insufficient_data_count = 0
|
|
124
|
+
|
|
125
|
+
for evaluation in self.rule_evaluations:
|
|
126
|
+
compliance_type = evaluation.get("compliance_type", "").upper()
|
|
127
|
+
|
|
128
|
+
if compliance_type == "NON_COMPLIANT":
|
|
129
|
+
non_compliant_count += 1
|
|
130
|
+
elif compliance_type == "COMPLIANT":
|
|
131
|
+
compliant_count += 1
|
|
132
|
+
elif compliance_type == "NOT_APPLICABLE":
|
|
133
|
+
not_applicable_count += 1
|
|
134
|
+
else: # INSUFFICIENT_DATA or other
|
|
135
|
+
insufficient_data_count += 1
|
|
136
|
+
|
|
137
|
+
total_evaluations = len(self.rule_evaluations)
|
|
138
|
+
|
|
139
|
+
logger.debug(
|
|
140
|
+
f"Control {self.control_id} rule summary: "
|
|
141
|
+
f"{non_compliant_count} NON_COMPLIANT, {compliant_count} COMPLIANT, "
|
|
142
|
+
f"{not_applicable_count} NOT_APPLICABLE, {insufficient_data_count} INSUFFICIENT_DATA "
|
|
143
|
+
f"out of {total_evaluations} total"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# If ANY rule is non-compliant, the control fails
|
|
147
|
+
if non_compliant_count > 0:
|
|
148
|
+
logger.info(
|
|
149
|
+
f"Control {self.control_id} FAILS: {non_compliant_count} non-compliant rule(s) "
|
|
150
|
+
f"out of {total_evaluations}"
|
|
151
|
+
)
|
|
152
|
+
return "FAIL"
|
|
153
|
+
|
|
154
|
+
# If we have compliant rules and no failures, control passes
|
|
155
|
+
if compliant_count > 0:
|
|
156
|
+
if not_applicable_count > 0 or insufficient_data_count > 0:
|
|
157
|
+
logger.info(
|
|
158
|
+
f"Control {self.control_id} PASSES: {compliant_count} compliant, "
|
|
159
|
+
f"{not_applicable_count} not applicable, "
|
|
160
|
+
f"{insufficient_data_count} insufficient data (no failures)"
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
logger.info(f"Control {self.control_id} PASSES: All {compliant_count} rules compliant")
|
|
164
|
+
return "PASS"
|
|
165
|
+
|
|
166
|
+
# If no applicable compliance checks available, we cannot determine status
|
|
167
|
+
logger.warning(
|
|
168
|
+
f"Control {self.control_id}: No conclusive compliance checks in {total_evaluations} evaluation(s)"
|
|
169
|
+
)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def compliance_result(self) -> Optional[str]:
|
|
174
|
+
"""
|
|
175
|
+
Result of compliance check (PASS, FAIL, etc).
|
|
176
|
+
|
|
177
|
+
Aggregates Config rule evaluations to determine control-level compliance.
|
|
178
|
+
|
|
179
|
+
:return: "PASS", "FAIL", or None (if no conclusive data available)
|
|
180
|
+
:rtype: Optional[str]
|
|
181
|
+
"""
|
|
182
|
+
# Use cached result if available
|
|
183
|
+
if self._aggregated_compliance_result is not None or hasattr(self, "_result_was_cached"):
|
|
184
|
+
return self._aggregated_compliance_result
|
|
185
|
+
|
|
186
|
+
# Aggregate rule compliance checks
|
|
187
|
+
result = self._aggregate_rule_compliance()
|
|
188
|
+
|
|
189
|
+
if result is None:
|
|
190
|
+
logger.info(
|
|
191
|
+
f"Control {self.control_id}: No conclusive data for compliance determination. "
|
|
192
|
+
f"Control status will not be updated. Rule evaluations: {len(self.rule_evaluations)}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Cache the result (including None)
|
|
196
|
+
self._aggregated_compliance_result = result
|
|
197
|
+
self._result_was_cached = True
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def severity(self) -> Optional[str]:
|
|
202
|
+
"""Severity level of the compliance violation (if failed)."""
|
|
203
|
+
if self.compliance_result != "FAIL":
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
# Determine severity based on number of non-compliant rules
|
|
207
|
+
non_compliant_count = sum(
|
|
208
|
+
1 for eval in self.rule_evaluations if eval.get("compliance_type", "").upper() == "NON_COMPLIANT"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if non_compliant_count >= 5:
|
|
212
|
+
return "HIGH"
|
|
213
|
+
elif non_compliant_count >= 2:
|
|
214
|
+
return "MEDIUM"
|
|
215
|
+
return "LOW"
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def description(self) -> str:
|
|
219
|
+
"""Description of the compliance check using HTML formatting."""
|
|
220
|
+
desc_parts = [
|
|
221
|
+
f"{HTML_H3_OPEN}AWS Config compliance assessment for control {self.control_id}{HTML_H3_CLOSE}",
|
|
222
|
+
HTML_P_OPEN,
|
|
223
|
+
f"{HTML_STRONG_OPEN}Control:{HTML_STRONG_CLOSE} {self._control_name}{HTML_BR}",
|
|
224
|
+
f"{HTML_STRONG_OPEN}Framework:{HTML_STRONG_CLOSE} {self._framework}{HTML_BR}",
|
|
225
|
+
f"{HTML_STRONG_OPEN}Total Rules:{HTML_STRONG_CLOSE} {len(self.rule_evaluations)}",
|
|
226
|
+
HTML_P_CLOSE,
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
# Add compliance summary
|
|
230
|
+
compliant_rules = [e for e in self.rule_evaluations if e.get("compliance_type") == "COMPLIANT"]
|
|
231
|
+
non_compliant_rules = [e for e in self.rule_evaluations if e.get("compliance_type") == "NON_COMPLIANT"]
|
|
232
|
+
|
|
233
|
+
desc_parts.extend(
|
|
234
|
+
[
|
|
235
|
+
f"{HTML_H4_OPEN}Compliance Summary{HTML_H4_CLOSE}",
|
|
236
|
+
HTML_UL_OPEN,
|
|
237
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Compliant Rules:{HTML_STRONG_CLOSE} {len(compliant_rules)}"
|
|
238
|
+
f"{HTML_LI_CLOSE}",
|
|
239
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Non-Compliant Rules:{HTML_STRONG_CLOSE} {len(non_compliant_rules)}"
|
|
240
|
+
f"{HTML_LI_CLOSE}",
|
|
241
|
+
HTML_UL_CLOSE,
|
|
242
|
+
]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Add non-compliant rules details
|
|
246
|
+
if non_compliant_rules:
|
|
247
|
+
desc_parts.append(f"{HTML_H4_OPEN}Non-Compliant Rules{HTML_H4_CLOSE}")
|
|
248
|
+
desc_parts.append(HTML_UL_OPEN)
|
|
249
|
+
for rule_eval in non_compliant_rules[:10]: # Show up to 10 non-compliant rules
|
|
250
|
+
rule_name = rule_eval.get("rule_name", "Unknown")
|
|
251
|
+
resource_count = rule_eval.get("non_compliant_resource_count", 0)
|
|
252
|
+
desc_parts.append(
|
|
253
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{rule_name}{HTML_STRONG_CLOSE}: "
|
|
254
|
+
f"{resource_count} non-compliant resource(s){HTML_LI_CLOSE}"
|
|
255
|
+
)
|
|
256
|
+
if len(non_compliant_rules) > 10:
|
|
257
|
+
desc_parts.append(
|
|
258
|
+
f"{HTML_LI_OPEN}... and {len(non_compliant_rules) - 10} more non-compliant rule(s){HTML_LI_CLOSE}"
|
|
259
|
+
)
|
|
260
|
+
desc_parts.append(HTML_UL_CLOSE)
|
|
261
|
+
|
|
262
|
+
return "\n".join(desc_parts)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def framework(self) -> str:
|
|
266
|
+
"""Compliance framework (e.g., NIST800-53R5, CSF)."""
|
|
267
|
+
return self._framework
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class ConfigEvidenceConfig:
|
|
272
|
+
"""Configuration for evidence collection from AWS Config."""
|
|
273
|
+
|
|
274
|
+
collect_evidence: bool = False
|
|
275
|
+
evidence_as_attachments: bool = True
|
|
276
|
+
evidence_as_records: bool = False
|
|
277
|
+
evidence_control_ids: Optional[List[str]] = None
|
|
278
|
+
evidence_frequency: int = 30
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class ConfigFilterConfig:
|
|
283
|
+
"""Configuration for filtering AWS Config resources."""
|
|
284
|
+
|
|
285
|
+
account_id: Optional[str] = None
|
|
286
|
+
tags: Optional[Dict[str, str]] = None
|
|
287
|
+
conformance_pack_name: Optional[str] = None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class AWSConfigCompliance(ComplianceIntegration):
|
|
291
|
+
"""Process AWS Config compliance assessments and create compliance records in RegScale."""
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
plan_id: int,
|
|
296
|
+
region: str = "us-east-1",
|
|
297
|
+
framework: str = "NIST800-53R5",
|
|
298
|
+
create_issues: bool = True,
|
|
299
|
+
update_control_status: bool = True,
|
|
300
|
+
create_poams: bool = False,
|
|
301
|
+
parent_module: str = "securityplans",
|
|
302
|
+
evidence_config: Optional[ConfigEvidenceConfig] = None,
|
|
303
|
+
filter_config: Optional[ConfigFilterConfig] = None,
|
|
304
|
+
use_security_hub: bool = False,
|
|
305
|
+
force_refresh: bool = False,
|
|
306
|
+
**kwargs,
|
|
307
|
+
):
|
|
308
|
+
"""
|
|
309
|
+
Initialize AWS Config compliance integration.
|
|
310
|
+
|
|
311
|
+
:param int plan_id: RegScale plan ID
|
|
312
|
+
:param str region: AWS region
|
|
313
|
+
:param str framework: Compliance framework
|
|
314
|
+
:param bool create_issues: Whether to create issues for failed compliance
|
|
315
|
+
:param bool update_control_status: Whether to update control implementation status
|
|
316
|
+
:param bool create_poams: Whether to mark issues as POAMs
|
|
317
|
+
:param str parent_module: RegScale parent module
|
|
318
|
+
:param Optional[ConfigEvidenceConfig] evidence_config: Evidence collection configuration
|
|
319
|
+
:param Optional[ConfigFilterConfig] filter_config: Resource filtering configuration
|
|
320
|
+
:param bool use_security_hub: Include Security Hub control findings
|
|
321
|
+
:param bool force_refresh: Force refresh of compliance data by bypassing cache
|
|
322
|
+
:param kwargs: Additional parameters including AWS credentials
|
|
323
|
+
"""
|
|
324
|
+
super().__init__(
|
|
325
|
+
plan_id=plan_id,
|
|
326
|
+
framework=framework,
|
|
327
|
+
create_issues=create_issues,
|
|
328
|
+
update_control_status=update_control_status,
|
|
329
|
+
create_poams=create_poams,
|
|
330
|
+
parent_module=parent_module,
|
|
331
|
+
**kwargs,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
self.region = region
|
|
335
|
+
self.title = "AWS Config"
|
|
336
|
+
|
|
337
|
+
# Evidence collection parameters
|
|
338
|
+
self.evidence_config = evidence_config or ConfigEvidenceConfig()
|
|
339
|
+
self.collect_evidence = self.evidence_config.collect_evidence
|
|
340
|
+
self.evidence_as_attachments = self.evidence_config.evidence_as_attachments
|
|
341
|
+
self.evidence_as_records = self.evidence_config.evidence_as_records
|
|
342
|
+
self.evidence_control_ids = self.evidence_config.evidence_control_ids
|
|
343
|
+
self.evidence_frequency = self.evidence_config.evidence_frequency
|
|
344
|
+
|
|
345
|
+
# Filtering parameters
|
|
346
|
+
self.filter_config = filter_config or ConfigFilterConfig()
|
|
347
|
+
self.conformance_pack_name = self.filter_config.conformance_pack_name
|
|
348
|
+
self.account_id = self.filter_config.account_id
|
|
349
|
+
self.tags = self.filter_config.tags or {}
|
|
350
|
+
|
|
351
|
+
# Security Hub integration
|
|
352
|
+
self.use_security_hub = use_security_hub
|
|
353
|
+
|
|
354
|
+
# Cache control
|
|
355
|
+
self.force_refresh = force_refresh
|
|
356
|
+
|
|
357
|
+
# Extract AWS credentials from kwargs
|
|
358
|
+
profile = kwargs.get("profile")
|
|
359
|
+
aws_access_key_id = kwargs.get("aws_access_key_id")
|
|
360
|
+
aws_secret_access_key = kwargs.get("aws_secret_access_key")
|
|
361
|
+
aws_session_token = kwargs.get("aws_session_token")
|
|
362
|
+
|
|
363
|
+
# Initialize AWS session
|
|
364
|
+
if aws_access_key_id and aws_secret_access_key:
|
|
365
|
+
logger.info("Initializing AWS Config client with explicit credentials")
|
|
366
|
+
self.session = boto3.Session(
|
|
367
|
+
region_name=region,
|
|
368
|
+
aws_access_key_id=aws_access_key_id,
|
|
369
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
370
|
+
aws_session_token=aws_session_token,
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
logger.info(f"Initializing AWS Config client with profile: {profile if profile else 'default'}")
|
|
374
|
+
self.session = boto3.Session(profile_name=profile, region_name=region)
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
self.config_client = self.session.client("config")
|
|
378
|
+
logger.info("Successfully created AWS Config client")
|
|
379
|
+
|
|
380
|
+
if self.use_security_hub:
|
|
381
|
+
self.securityhub_client = self.session.client("securityhub")
|
|
382
|
+
logger.info("Successfully created AWS Security Hub client")
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.error(f"Failed to create AWS client: {e}")
|
|
385
|
+
raise
|
|
386
|
+
|
|
387
|
+
def fetch_compliance_data(self) -> List[Dict[str, Any]]:
|
|
388
|
+
"""
|
|
389
|
+
Fetch raw compliance data from AWS Config.
|
|
390
|
+
|
|
391
|
+
Returns control-level compliance data aggregated from Config rules.
|
|
392
|
+
|
|
393
|
+
:return: List of raw compliance data (control + rule evaluations)
|
|
394
|
+
:rtype: List[Dict[str, Any]]
|
|
395
|
+
"""
|
|
396
|
+
# Check cache first unless force refresh
|
|
397
|
+
if not self.force_refresh and self._is_cache_valid():
|
|
398
|
+
cached_data = self._load_cached_data()
|
|
399
|
+
if cached_data:
|
|
400
|
+
return cached_data
|
|
401
|
+
|
|
402
|
+
if self.force_refresh:
|
|
403
|
+
logger.info("Force refresh requested, fetching fresh data from AWS Config...")
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
compliance_data = self._fetch_fresh_compliance_data()
|
|
407
|
+
self._save_to_cache(compliance_data)
|
|
408
|
+
return compliance_data
|
|
409
|
+
except ClientError as e:
|
|
410
|
+
logger.error(f"Error fetching compliance data from AWS Config: {e}")
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
def _fetch_fresh_compliance_data(self) -> List[Dict[str, Any]]:
|
|
414
|
+
"""
|
|
415
|
+
Fetch fresh compliance data from AWS Config.
|
|
416
|
+
|
|
417
|
+
:return: List of raw compliance data
|
|
418
|
+
:rtype: List[Dict[str, Any]]
|
|
419
|
+
"""
|
|
420
|
+
logger.info("Fetching compliance data from AWS Config...")
|
|
421
|
+
|
|
422
|
+
# Initialize and fetch Config rules
|
|
423
|
+
config_collector = self._initialize_config_collector()
|
|
424
|
+
config_rules = self._fetch_config_rules(config_collector)
|
|
425
|
+
|
|
426
|
+
# Build control mappings from rules
|
|
427
|
+
control_mappings, _ = self._build_control_mappings(config_collector, config_rules)
|
|
428
|
+
|
|
429
|
+
# Get and build compliance data
|
|
430
|
+
compliance_data = self._build_compliance_data(config_collector, control_mappings)
|
|
431
|
+
|
|
432
|
+
logger.info(f"Fetched {len(compliance_data)} control compliance items from AWS Config")
|
|
433
|
+
return compliance_data
|
|
434
|
+
|
|
435
|
+
def _initialize_config_collector(self):
|
|
436
|
+
"""Initialize Config collector with filtering."""
|
|
437
|
+
from regscale.integrations.commercial.aws.inventory.resources.config import ConfigCollector
|
|
438
|
+
|
|
439
|
+
self._log_filtering_info()
|
|
440
|
+
|
|
441
|
+
return ConfigCollector(session=self.session, region=self.region, account_id=self.account_id, tags=self.tags)
|
|
442
|
+
|
|
443
|
+
def _log_filtering_info(self) -> None:
|
|
444
|
+
"""Log filtering information if filters are applied."""
|
|
445
|
+
if self.account_id:
|
|
446
|
+
logger.info(f"Filtering Config rules by account ID: {self.account_id}")
|
|
447
|
+
if self.tags:
|
|
448
|
+
logger.info(f"Filtering Config rules by tags: {self.tags}")
|
|
449
|
+
|
|
450
|
+
def _fetch_config_rules(self, config_collector) -> List[Dict[str, Any]]:
|
|
451
|
+
"""Fetch Config rules from AWS."""
|
|
452
|
+
config_data = config_collector.collect()
|
|
453
|
+
config_rules = config_data.get("ConfigRules", [])
|
|
454
|
+
logger.info(f"Found {len(config_rules)} Config rules after filtering")
|
|
455
|
+
return config_rules
|
|
456
|
+
|
|
457
|
+
def _build_control_mappings(self, config_collector, config_rules: List[Dict[str, Any]]) -> Tuple[Dict, Dict]:
|
|
458
|
+
"""Build mappings between rules and controls."""
|
|
459
|
+
from regscale.integrations.commercial.aws.conformance_pack_mappings import map_rule_to_controls
|
|
460
|
+
|
|
461
|
+
control_mappings = {} # control_id -> list of rule names
|
|
462
|
+
rule_metadata = {} # rule_name -> {rule data, tags}
|
|
463
|
+
|
|
464
|
+
for rule in config_rules:
|
|
465
|
+
self._process_single_rule(rule, config_collector, control_mappings, rule_metadata, map_rule_to_controls)
|
|
466
|
+
|
|
467
|
+
logger.info(f"Mapped {len(config_rules)} rules to {len(control_mappings)} controls")
|
|
468
|
+
return control_mappings, rule_metadata
|
|
469
|
+
|
|
470
|
+
def _process_single_rule(
|
|
471
|
+
self, rule: Dict, config_collector, control_mappings: Dict, rule_metadata: Dict, map_rule_to_controls
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Process a single Config rule and map to controls."""
|
|
474
|
+
rule_name = rule.get("ConfigRuleName", "")
|
|
475
|
+
rule_tags = self._get_rule_tags(rule, config_collector)
|
|
476
|
+
|
|
477
|
+
# Map rule to controls
|
|
478
|
+
control_ids = map_rule_to_controls(
|
|
479
|
+
rule_name=rule_name,
|
|
480
|
+
rule_description=rule.get("Description"),
|
|
481
|
+
rule_tags=rule_tags,
|
|
482
|
+
framework=self.framework,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Store mappings
|
|
486
|
+
self._store_control_mappings(control_ids, rule_name, control_mappings)
|
|
487
|
+
rule_metadata[rule_name] = rule
|
|
488
|
+
|
|
489
|
+
def _get_rule_tags(self, rule: Dict, config_collector) -> Dict:
|
|
490
|
+
"""Get or fetch tags for a rule."""
|
|
491
|
+
rule_tags = rule.get("Tags", {})
|
|
492
|
+
rule_arn = rule.get("ConfigRuleArn", "")
|
|
493
|
+
|
|
494
|
+
if not rule_tags and rule_arn:
|
|
495
|
+
try:
|
|
496
|
+
rule_tags = config_collector._get_rule_tags(self.config_client, rule_arn)
|
|
497
|
+
rule["Tags"] = rule_tags
|
|
498
|
+
except Exception as e:
|
|
499
|
+
logger.debug(f"Could not fetch tags for rule {rule.get('ConfigRuleName', '')}: {e}")
|
|
500
|
+
|
|
501
|
+
return rule_tags
|
|
502
|
+
|
|
503
|
+
def _store_control_mappings(self, control_ids: List[str], rule_name: str, control_mappings: Dict) -> None:
|
|
504
|
+
"""Store control to rule mappings."""
|
|
505
|
+
for control_id in control_ids:
|
|
506
|
+
if control_id not in control_mappings:
|
|
507
|
+
control_mappings[control_id] = []
|
|
508
|
+
control_mappings[control_id].append(rule_name)
|
|
509
|
+
|
|
510
|
+
def _build_compliance_data(self, config_collector, control_mappings: Dict) -> List[Dict[str, Any]]:
|
|
511
|
+
"""Build compliance data structure from control mappings."""
|
|
512
|
+
control_compliance = config_collector.get_aggregate_compliance_by_control(control_mappings)
|
|
513
|
+
compliance_data = []
|
|
514
|
+
account_id = self._get_aws_account_id()
|
|
515
|
+
|
|
516
|
+
for control_id, rule_evaluations in control_compliance.items():
|
|
517
|
+
if rule_evaluations:
|
|
518
|
+
compliance_data.append(self._create_compliance_item_dict(control_id, rule_evaluations, account_id))
|
|
519
|
+
|
|
520
|
+
return compliance_data
|
|
521
|
+
|
|
522
|
+
def _create_compliance_item_dict(self, control_id: str, rule_evaluations: List, account_id: str) -> Dict[str, Any]:
|
|
523
|
+
"""Create a compliance item dictionary."""
|
|
524
|
+
return {
|
|
525
|
+
"control_id": control_id,
|
|
526
|
+
"control_name": f"Control {control_id}", # Will be enriched by RegScale lookup
|
|
527
|
+
"rule_evaluations": rule_evaluations,
|
|
528
|
+
"resource_id": account_id,
|
|
529
|
+
"resource_name": f"AWS Account {account_id}",
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
def _get_aws_account_id(self) -> str:
|
|
533
|
+
"""
|
|
534
|
+
Get AWS account ID from STS.
|
|
535
|
+
|
|
536
|
+
:return: AWS account ID
|
|
537
|
+
:rtype: str
|
|
538
|
+
"""
|
|
539
|
+
try:
|
|
540
|
+
sts_client = self.session.client("sts")
|
|
541
|
+
response = sts_client.get_caller_identity()
|
|
542
|
+
return response.get("Account", "")
|
|
543
|
+
except Exception as e:
|
|
544
|
+
logger.warning(f"Could not get AWS account ID: {e}")
|
|
545
|
+
return ""
|
|
546
|
+
|
|
547
|
+
def create_compliance_item(self, raw_data: Dict[str, Any]) -> ComplianceItem:
|
|
548
|
+
"""
|
|
549
|
+
Create a ComplianceItem from raw compliance data.
|
|
550
|
+
|
|
551
|
+
:param Dict[str, Any] raw_data: Raw compliance data (control + rule evaluations)
|
|
552
|
+
:return: ComplianceItem instance
|
|
553
|
+
:rtype: ComplianceItem
|
|
554
|
+
"""
|
|
555
|
+
control_id = raw_data.get("control_id", "")
|
|
556
|
+
control_name = raw_data.get("control_name", "")
|
|
557
|
+
rule_evaluations = raw_data.get("rule_evaluations", [])
|
|
558
|
+
resource_id = raw_data.get("resource_id")
|
|
559
|
+
resource_name = raw_data.get("resource_name")
|
|
560
|
+
|
|
561
|
+
return AWSConfigComplianceItem(
|
|
562
|
+
control_id=control_id,
|
|
563
|
+
control_name=control_name,
|
|
564
|
+
framework=self.framework,
|
|
565
|
+
rule_evaluations=rule_evaluations,
|
|
566
|
+
resource_id=resource_id,
|
|
567
|
+
resource_name=resource_name,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
def _is_cache_valid(self) -> bool:
|
|
571
|
+
"""Check if the cache file exists and is within the TTL."""
|
|
572
|
+
if not os.path.exists(CONFIG_COMPLIANCE_CACHE_FILE):
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
file_age = time.time() - os.path.getmtime(CONFIG_COMPLIANCE_CACHE_FILE)
|
|
576
|
+
is_valid = file_age < CACHE_TTL_SECONDS
|
|
577
|
+
|
|
578
|
+
if is_valid:
|
|
579
|
+
hours_old = file_age / 3600
|
|
580
|
+
logger.info(f"Using cached Config compliance data (age: {hours_old:.1f} hours)")
|
|
581
|
+
|
|
582
|
+
return is_valid
|
|
583
|
+
|
|
584
|
+
def _load_cached_data(self) -> List[Dict[str, Any]]:
|
|
585
|
+
"""Load compliance data from cache file."""
|
|
586
|
+
try:
|
|
587
|
+
with open(CONFIG_COMPLIANCE_CACHE_FILE, encoding="utf-8") as file:
|
|
588
|
+
cached_data = json.load(file)
|
|
589
|
+
logger.info(f"Loaded {len(cached_data)} compliance items from cache")
|
|
590
|
+
return cached_data
|
|
591
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
592
|
+
logger.warning(f"Error reading cache file: {e}. Fetching fresh data.")
|
|
593
|
+
return []
|
|
594
|
+
|
|
595
|
+
def _save_to_cache(self, compliance_data: List[Dict[str, Any]]) -> None:
|
|
596
|
+
"""Save compliance data to cache file."""
|
|
597
|
+
try:
|
|
598
|
+
os.makedirs(os.path.dirname(CONFIG_COMPLIANCE_CACHE_FILE), exist_ok=True)
|
|
599
|
+
|
|
600
|
+
with open(CONFIG_COMPLIANCE_CACHE_FILE, "w", encoding="utf-8") as file:
|
|
601
|
+
json.dump(compliance_data, file, indent=2, default=str)
|
|
602
|
+
|
|
603
|
+
logger.info(f"Cached {len(compliance_data)} compliance items")
|
|
604
|
+
except IOError as e:
|
|
605
|
+
logger.warning(f"Error writing to cache file: {e}")
|
|
606
|
+
|
|
607
|
+
def sync_compliance(self) -> None:
|
|
608
|
+
"""
|
|
609
|
+
Sync compliance data from AWS Config to RegScale.
|
|
610
|
+
|
|
611
|
+
Extends the base sync_compliance method to add evidence collection.
|
|
612
|
+
|
|
613
|
+
:return: None
|
|
614
|
+
:rtype: None
|
|
615
|
+
"""
|
|
616
|
+
# Call the base class sync_compliance to handle control assessments
|
|
617
|
+
super().sync_compliance()
|
|
618
|
+
|
|
619
|
+
# If evidence collection is enabled, collect evidence after compliance sync
|
|
620
|
+
if self.collect_evidence:
|
|
621
|
+
logger.info("Evidence collection enabled, starting evidence collection...")
|
|
622
|
+
try:
|
|
623
|
+
# Collect evidence based on mode
|
|
624
|
+
if self.evidence_as_records:
|
|
625
|
+
logger.info("Creating individual Evidence records per control...")
|
|
626
|
+
self._collect_evidence_as_records()
|
|
627
|
+
else:
|
|
628
|
+
logger.info("Creating consolidated evidence file for SSP...")
|
|
629
|
+
self._collect_evidence_as_ssp_attachment()
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.error(f"Error during evidence collection: {e}", exc_info=True)
|
|
632
|
+
|
|
633
|
+
def _collect_evidence_as_ssp_attachment(self) -> None:
|
|
634
|
+
"""
|
|
635
|
+
Collect evidence and attach as file to SecurityPlan (default mode).
|
|
636
|
+
|
|
637
|
+
:return: None
|
|
638
|
+
:rtype: None
|
|
639
|
+
"""
|
|
640
|
+
from regscale.core.app.api import Api
|
|
641
|
+
from regscale.models.regscale_models.file import File
|
|
642
|
+
|
|
643
|
+
logger.info("Collecting evidence as SSP-level attachment...")
|
|
644
|
+
|
|
645
|
+
# Collect all evidence data
|
|
646
|
+
all_evidence = self._collect_all_evidence_data()
|
|
647
|
+
|
|
648
|
+
if not all_evidence:
|
|
649
|
+
logger.warning("No evidence data collected")
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
# Generate filename
|
|
653
|
+
scan_date = get_current_datetime(dt_format="%Y-%m-%d")
|
|
654
|
+
safe_framework = self.framework.replace(" ", "_").replace("/", "_")
|
|
655
|
+
file_name = f"config_compliance_{safe_framework}_{scan_date}.jsonl.gz"
|
|
656
|
+
|
|
657
|
+
# Compress evidence data
|
|
658
|
+
jsonl_content = "\n".join([json.dumps(item, default=str) for item in all_evidence])
|
|
659
|
+
|
|
660
|
+
compressed_buffer = BytesIO()
|
|
661
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
662
|
+
gz_file.write(jsonl_content)
|
|
663
|
+
|
|
664
|
+
compressed_data = compressed_buffer.getvalue()
|
|
665
|
+
compressed_size_mb = len(compressed_data) / (1024 * 1024)
|
|
666
|
+
uncompressed_size_mb = len(jsonl_content.encode("utf-8")) / (1024 * 1024)
|
|
667
|
+
compression_ratio = (1 - (len(compressed_data) / len(jsonl_content.encode("utf-8")))) * 100
|
|
668
|
+
|
|
669
|
+
logger.info(
|
|
670
|
+
"Compressed evidence: %.2f MB -> %.2f MB (%.1f%% reduction)",
|
|
671
|
+
uncompressed_size_mb,
|
|
672
|
+
compressed_size_mb,
|
|
673
|
+
compression_ratio,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# Upload to SecurityPlan
|
|
677
|
+
api = Api()
|
|
678
|
+
success = File.upload_file_to_regscale(
|
|
679
|
+
file_name=file_name,
|
|
680
|
+
parent_id=self.plan_id,
|
|
681
|
+
parent_module="securityplans",
|
|
682
|
+
api=api,
|
|
683
|
+
file_data=compressed_data,
|
|
684
|
+
tags=f"aws,config,compliance,{safe_framework.lower()}",
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
if success:
|
|
688
|
+
logger.info(f"Successfully uploaded evidence file '{file_name}' to SecurityPlan {self.plan_id}")
|
|
689
|
+
else:
|
|
690
|
+
logger.warning(f"Failed to upload evidence file to SecurityPlan {self.plan_id}")
|
|
691
|
+
|
|
692
|
+
def _collect_evidence_as_records(self) -> None:
|
|
693
|
+
"""
|
|
694
|
+
Collect evidence and create individual Evidence records per control.
|
|
695
|
+
|
|
696
|
+
:return: None
|
|
697
|
+
:rtype: None
|
|
698
|
+
"""
|
|
699
|
+
from regscale.core.app.api import Api
|
|
700
|
+
from regscale.models.regscale_models.evidence import Evidence
|
|
701
|
+
from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
|
|
702
|
+
from regscale.models.regscale_models.file import File
|
|
703
|
+
|
|
704
|
+
logger.info("Collecting evidence as individual records per control...")
|
|
705
|
+
|
|
706
|
+
# Collect evidence grouped by control
|
|
707
|
+
evidence_by_control = self._collect_evidence_by_control()
|
|
708
|
+
|
|
709
|
+
if not evidence_by_control:
|
|
710
|
+
logger.warning("No evidence data collected")
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
scan_date = get_current_datetime(dt_format="%Y-%m-%d")
|
|
714
|
+
safe_framework = self.framework.replace(" ", "_").replace("/", "_")
|
|
715
|
+
api = Api()
|
|
716
|
+
evidence_records_created = 0
|
|
717
|
+
|
|
718
|
+
for control_id, control_evidence in evidence_by_control.items():
|
|
719
|
+
# Filter by evidence_control_ids if specified
|
|
720
|
+
if self.evidence_control_ids and control_id not in self.evidence_control_ids:
|
|
721
|
+
continue
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
# Create Evidence record
|
|
725
|
+
title = f"AWS Config Evidence - {control_id} - {scan_date}"
|
|
726
|
+
description = self._build_evidence_description(control_id, control_evidence)
|
|
727
|
+
due_date = (datetime.now() + timedelta(days=self.evidence_frequency)).isoformat()
|
|
728
|
+
|
|
729
|
+
evidence = Evidence(
|
|
730
|
+
title=title,
|
|
731
|
+
description=description,
|
|
732
|
+
status="Collected",
|
|
733
|
+
updateFrequency=self.evidence_frequency,
|
|
734
|
+
dueDate=due_date,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
created_evidence = evidence.create()
|
|
738
|
+
if not created_evidence or not created_evidence.id:
|
|
739
|
+
logger.error(f"Failed to create evidence record for control {control_id}")
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
logger.info(f"Created evidence record {created_evidence.id}: {title}")
|
|
743
|
+
|
|
744
|
+
# Compress and upload evidence file
|
|
745
|
+
file_name = f"config_evidence_{control_id}_{scan_date}.jsonl.gz"
|
|
746
|
+
jsonl_content = "\n".join([json.dumps(item, default=str) for item in control_evidence])
|
|
747
|
+
|
|
748
|
+
compressed_buffer = BytesIO()
|
|
749
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
750
|
+
gz_file.write(jsonl_content)
|
|
751
|
+
|
|
752
|
+
compressed_data = compressed_buffer.getvalue()
|
|
753
|
+
|
|
754
|
+
success = File.upload_file_to_regscale(
|
|
755
|
+
file_name=file_name,
|
|
756
|
+
parent_id=created_evidence.id,
|
|
757
|
+
parent_module="evidence",
|
|
758
|
+
api=api,
|
|
759
|
+
file_data=compressed_data,
|
|
760
|
+
tags=f"aws,config,{control_id.lower()},{safe_framework.lower()}",
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
if success:
|
|
764
|
+
logger.info(f"Uploaded evidence file for control {control_id}")
|
|
765
|
+
|
|
766
|
+
# Map evidence to SSP
|
|
767
|
+
mapping = EvidenceMapping(
|
|
768
|
+
evidenceID=created_evidence.id, mappedID=self.plan_id, mappingType="securityplans"
|
|
769
|
+
)
|
|
770
|
+
mapping.create()
|
|
771
|
+
logger.info(f"Linked evidence {created_evidence.id} to SSP {self.plan_id}")
|
|
772
|
+
|
|
773
|
+
evidence_records_created += 1
|
|
774
|
+
|
|
775
|
+
except Exception as ex:
|
|
776
|
+
logger.error(f"Failed to create evidence record for control {control_id}: {ex}", exc_info=True)
|
|
777
|
+
|
|
778
|
+
logger.info(f"Created {evidence_records_created} evidence record(s)")
|
|
779
|
+
|
|
780
|
+
def _collect_all_evidence_data(self) -> List[Dict[str, Any]]:
|
|
781
|
+
"""
|
|
782
|
+
Collect all evidence data for SSP-level attachment.
|
|
783
|
+
|
|
784
|
+
:return: List of evidence items
|
|
785
|
+
:rtype: List[Dict[str, Any]]
|
|
786
|
+
"""
|
|
787
|
+
from regscale.integrations.commercial.aws.inventory.resources.config import ConfigCollector
|
|
788
|
+
|
|
789
|
+
all_evidence = []
|
|
790
|
+
config_collector = ConfigCollector(session=self.session, region=self.region)
|
|
791
|
+
|
|
792
|
+
# Get all Config rules
|
|
793
|
+
config_data = config_collector.collect()
|
|
794
|
+
config_rules = config_data.get("ConfigRules", [])
|
|
795
|
+
|
|
796
|
+
for rule in config_rules:
|
|
797
|
+
rule_name = rule.get("ConfigRuleName", "")
|
|
798
|
+
|
|
799
|
+
try:
|
|
800
|
+
# Get compliance details for this rule
|
|
801
|
+
compliance_details = config_collector.get_compliance_details(rule_name)
|
|
802
|
+
|
|
803
|
+
for detail in compliance_details:
|
|
804
|
+
evidence_item = {
|
|
805
|
+
"rule_name": rule_name,
|
|
806
|
+
"compliance_type": detail.get("ComplianceType", ""),
|
|
807
|
+
"resource_type": detail.get("EvaluationResultIdentifier", {})
|
|
808
|
+
.get("EvaluationResultQualifier", {})
|
|
809
|
+
.get("ResourceType", ""),
|
|
810
|
+
"resource_id": detail.get("EvaluationResultIdentifier", {})
|
|
811
|
+
.get("EvaluationResultQualifier", {})
|
|
812
|
+
.get("ResourceId", ""),
|
|
813
|
+
"config_rule_invoked_time": str(detail.get("ConfigRuleInvokedTime", "")),
|
|
814
|
+
"result_recorded_time": str(detail.get("ResultRecordedTime", "")),
|
|
815
|
+
"annotation": detail.get("Annotation", ""),
|
|
816
|
+
}
|
|
817
|
+
all_evidence.append(evidence_item)
|
|
818
|
+
|
|
819
|
+
except Exception as e:
|
|
820
|
+
logger.debug(f"Error collecting evidence for rule {rule_name}: {e}")
|
|
821
|
+
|
|
822
|
+
logger.info(f"Collected {len(all_evidence)} evidence items")
|
|
823
|
+
return all_evidence
|
|
824
|
+
|
|
825
|
+
def _collect_evidence_by_control(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
826
|
+
"""
|
|
827
|
+
Collect evidence data grouped by control ID.
|
|
828
|
+
|
|
829
|
+
:return: Dictionary mapping control_id to list of evidence items
|
|
830
|
+
:rtype: Dict[str, List[Dict[str, Any]]]
|
|
831
|
+
"""
|
|
832
|
+
from regscale.integrations.commercial.aws.inventory.resources.config import ConfigCollector
|
|
833
|
+
from regscale.integrations.commercial.aws.conformance_pack_mappings import map_rule_to_controls
|
|
834
|
+
|
|
835
|
+
evidence_by_control = {}
|
|
836
|
+
config_collector = ConfigCollector(session=self.session, region=self.region)
|
|
837
|
+
|
|
838
|
+
# Get all Config rules
|
|
839
|
+
config_data = config_collector.collect()
|
|
840
|
+
config_rules = config_data.get("ConfigRules", [])
|
|
841
|
+
|
|
842
|
+
for rule in config_rules:
|
|
843
|
+
rule_name = rule.get("ConfigRuleName", "")
|
|
844
|
+
rule_description = rule.get("Description")
|
|
845
|
+
rule_tags = rule.get("Tags", {})
|
|
846
|
+
|
|
847
|
+
# Map rule to controls
|
|
848
|
+
control_ids = map_rule_to_controls(
|
|
849
|
+
rule_name=rule_name,
|
|
850
|
+
rule_description=rule_description,
|
|
851
|
+
rule_tags=rule_tags,
|
|
852
|
+
framework=self.framework,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
if not control_ids:
|
|
856
|
+
continue
|
|
857
|
+
|
|
858
|
+
try:
|
|
859
|
+
# Get compliance details for this rule
|
|
860
|
+
compliance_details = config_collector.get_compliance_details(rule_name)
|
|
861
|
+
|
|
862
|
+
for detail in compliance_details:
|
|
863
|
+
evidence_item = {
|
|
864
|
+
"rule_name": rule_name,
|
|
865
|
+
"compliance_type": detail.get("ComplianceType", ""),
|
|
866
|
+
"resource_type": detail.get("EvaluationResultIdentifier", {})
|
|
867
|
+
.get("EvaluationResultQualifier", {})
|
|
868
|
+
.get("ResourceType", ""),
|
|
869
|
+
"resource_id": detail.get("EvaluationResultIdentifier", {})
|
|
870
|
+
.get("EvaluationResultQualifier", {})
|
|
871
|
+
.get("ResourceId", ""),
|
|
872
|
+
"config_rule_invoked_time": str(detail.get("ConfigRuleInvokedTime", "")),
|
|
873
|
+
"result_recorded_time": str(detail.get("ResultRecordedTime", "")),
|
|
874
|
+
"annotation": detail.get("Annotation", ""),
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
# Add to each control this rule maps to
|
|
878
|
+
for control_id in control_ids:
|
|
879
|
+
if control_id not in evidence_by_control:
|
|
880
|
+
evidence_by_control[control_id] = []
|
|
881
|
+
evidence_by_control[control_id].append(evidence_item)
|
|
882
|
+
|
|
883
|
+
except Exception as e:
|
|
884
|
+
logger.debug(f"Error collecting evidence for rule {rule_name}: {e}")
|
|
885
|
+
|
|
886
|
+
logger.info(f"Collected evidence for {len(evidence_by_control)} controls")
|
|
887
|
+
return evidence_by_control
|
|
888
|
+
|
|
889
|
+
def _build_evidence_description(self, control_id: str, control_evidence: List[Dict[str, Any]]) -> str:
|
|
890
|
+
"""
|
|
891
|
+
Build HTML description for evidence record.
|
|
892
|
+
|
|
893
|
+
:param str control_id: Control ID
|
|
894
|
+
:param List[Dict[str, Any]] control_evidence: Evidence items for this control
|
|
895
|
+
:return: HTML description
|
|
896
|
+
:rtype: str
|
|
897
|
+
"""
|
|
898
|
+
compliant_count = sum(1 for e in control_evidence if e.get("compliance_type") == "COMPLIANT")
|
|
899
|
+
non_compliant_count = sum(1 for e in control_evidence if e.get("compliance_type") == "NON_COMPLIANT")
|
|
900
|
+
|
|
901
|
+
desc_parts = [
|
|
902
|
+
f"{HTML_H3_OPEN}AWS Config Evidence for Control {control_id}{HTML_H3_CLOSE}",
|
|
903
|
+
f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Framework:{HTML_STRONG_CLOSE} {self.framework}{HTML_P_CLOSE}",
|
|
904
|
+
f"{HTML_H4_OPEN}Summary{HTML_H4_CLOSE}",
|
|
905
|
+
HTML_UL_OPEN,
|
|
906
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Total Evidence Items:{HTML_STRONG_CLOSE} {len(control_evidence)}"
|
|
907
|
+
f"{HTML_LI_CLOSE}",
|
|
908
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Compliant:{HTML_STRONG_CLOSE} {compliant_count}{HTML_LI_CLOSE}",
|
|
909
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Non-Compliant:{HTML_STRONG_CLOSE} {non_compliant_count}"
|
|
910
|
+
f"{HTML_LI_CLOSE}",
|
|
911
|
+
HTML_UL_CLOSE,
|
|
912
|
+
]
|
|
913
|
+
|
|
914
|
+
return "\n".join(desc_parts)
|