regscale-cli 6.27.2.0__py3-none-any.whl → 6.28.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -0
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/login.py +4 -1
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/core/login.py +21 -4
- regscale/core/utils/date.py +77 -1
- 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 +853 -205
- 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/commercial/synqly/query_builder.py +4 -1
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/control_matcher.py +78 -23
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/public/csam/csam.py +572 -763
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +17 -4
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
- regscale/integrations/public/fedramp/poam/scanner.py +74 -7
- regscale/integrations/scanner_integration.py +415 -85
- regscale/models/integration_models/cisa_kev_data.json +80 -20
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
- regscale/models/platform.py +3 -0
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/component.py +1 -1
- regscale/models/regscale_models/control_implementation.py +55 -24
- 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/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +17 -5
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/regscale.py +11 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
- tests/regscale/core/test_login.py +171 -4
- 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
- tests/regscale/integrations/test_control_matcher.py +24 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""AWS IAM Evidence Integration for RegScale CLI."""
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import boto3
|
|
15
|
+
from botocore.exceptions import ClientError
|
|
16
|
+
|
|
17
|
+
from regscale.core.app.api import Api
|
|
18
|
+
from regscale.core.app.utils.app_utils import get_current_datetime
|
|
19
|
+
from regscale.integrations.commercial.aws.iam_control_mappings import IAMControlMapper
|
|
20
|
+
from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
|
|
21
|
+
from regscale.models.regscale_models.evidence import Evidence
|
|
22
|
+
from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
|
|
23
|
+
from regscale.models.regscale_models.file import File
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("regscale")
|
|
26
|
+
|
|
27
|
+
IAM_CACHE_FILE = os.path.join("artifacts", "aws", "iam_data.json")
|
|
28
|
+
CACHE_TTL_SECONDS = 4 * 60 * 60
|
|
29
|
+
|
|
30
|
+
HTML_STRONG_OPEN = "<strong>"
|
|
31
|
+
HTML_STRONG_CLOSE = "</strong>"
|
|
32
|
+
HTML_P_OPEN = "<p>"
|
|
33
|
+
HTML_P_CLOSE = "</p>"
|
|
34
|
+
HTML_UL_OPEN = "<ul>"
|
|
35
|
+
HTML_UL_CLOSE = "</ul>"
|
|
36
|
+
HTML_LI_OPEN = "<li>"
|
|
37
|
+
HTML_LI_CLOSE = "</li>"
|
|
38
|
+
HTML_H2_OPEN = "<h2>"
|
|
39
|
+
HTML_H2_CLOSE = "</h2>"
|
|
40
|
+
HTML_H3_OPEN = "<h3>"
|
|
41
|
+
HTML_H3_CLOSE = "</h3>"
|
|
42
|
+
HTML_BR = "<br>"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IAMComplianceItem(ComplianceItem):
|
|
46
|
+
"""Compliance item representing AWS IAM assessment."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, iam_data: Dict[str, Any], control_mapper: IAMControlMapper):
|
|
49
|
+
self.iam_data = iam_data
|
|
50
|
+
self.control_mapper = control_mapper
|
|
51
|
+
self._users = iam_data.get("users", [])
|
|
52
|
+
self._groups = iam_data.get("groups", [])
|
|
53
|
+
self._roles = iam_data.get("roles", [])
|
|
54
|
+
self._policies = iam_data.get("policies", [])
|
|
55
|
+
self._compliance_results = control_mapper.assess_iam_compliance(iam_data)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def resource_id(self) -> str:
|
|
59
|
+
return "iam-account"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def resource_name(self) -> str:
|
|
63
|
+
return f"AWS IAM Account ({len(self._users)} users, {len(self._roles)} roles)"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def control_id(self) -> str:
|
|
67
|
+
for control_id, result in self._compliance_results.items():
|
|
68
|
+
if result == "FAIL":
|
|
69
|
+
return control_id
|
|
70
|
+
return list(self._compliance_results.keys())[0] if self._compliance_results else "AC-2"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def compliance_result(self) -> str:
|
|
74
|
+
if not self._compliance_results:
|
|
75
|
+
return "PASS"
|
|
76
|
+
return "FAIL" if "FAIL" in self._compliance_results.values() else "PASS"
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def severity(self) -> Optional[str]:
|
|
80
|
+
if self.compliance_result == "PASS":
|
|
81
|
+
return None
|
|
82
|
+
if self._compliance_results.get("AC-2") == "FAIL" or self._compliance_results.get("IA-2") == "FAIL":
|
|
83
|
+
return "HIGH"
|
|
84
|
+
return "MEDIUM"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def description(self) -> str:
|
|
88
|
+
desc_parts = [
|
|
89
|
+
f"{HTML_H3_OPEN}AWS IAM Access Control Assessment{HTML_H3_CLOSE}",
|
|
90
|
+
HTML_P_OPEN,
|
|
91
|
+
f"{HTML_STRONG_OPEN}Users:{HTML_STRONG_CLOSE} {len(self._users)}{HTML_BR}",
|
|
92
|
+
f"{HTML_STRONG_OPEN}Groups:{HTML_STRONG_CLOSE} {len(self._groups)}{HTML_BR}",
|
|
93
|
+
f"{HTML_STRONG_OPEN}Roles:{HTML_STRONG_CLOSE} {len(self._roles)}{HTML_BR}",
|
|
94
|
+
f"{HTML_STRONG_OPEN}Managed Policies:{HTML_STRONG_CLOSE} {len(self._policies)}",
|
|
95
|
+
HTML_P_CLOSE,
|
|
96
|
+
f"{HTML_H3_OPEN}Control Compliance Results{HTML_H3_CLOSE}",
|
|
97
|
+
HTML_UL_OPEN,
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
for control_id, result in self._compliance_results.items():
|
|
101
|
+
result_color = "#d32f2f" if result == "FAIL" else "#2e7d32"
|
|
102
|
+
control_desc = self.control_mapper.get_control_description(control_id)
|
|
103
|
+
desc_parts.append(
|
|
104
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
|
|
105
|
+
f"<span style='color: {result_color};'>{result}</span> - {control_desc}{HTML_LI_CLOSE}"
|
|
106
|
+
)
|
|
107
|
+
desc_parts.append(HTML_UL_CLOSE)
|
|
108
|
+
|
|
109
|
+
if self.compliance_result == "FAIL":
|
|
110
|
+
desc_parts.append(f"{HTML_H3_OPEN}Remediation Guidance{HTML_H3_CLOSE}")
|
|
111
|
+
desc_parts.append(HTML_UL_OPEN)
|
|
112
|
+
if self._compliance_results.get("AC-2") == "FAIL":
|
|
113
|
+
desc_parts.append(f"{HTML_LI_OPEN}Enable MFA for all IAM users{HTML_LI_CLOSE}")
|
|
114
|
+
desc_parts.append(f"{HTML_LI_OPEN}Secure root account with MFA and remove access keys{HTML_LI_CLOSE}")
|
|
115
|
+
if self._compliance_results.get("AC-6") == "FAIL":
|
|
116
|
+
desc_parts.append(
|
|
117
|
+
f"{HTML_LI_OPEN}Remove AdministratorAccess from users, use groups/roles{HTML_LI_CLOSE}"
|
|
118
|
+
)
|
|
119
|
+
if self._compliance_results.get("IA-2") == "FAIL":
|
|
120
|
+
desc_parts.append(f"{HTML_LI_OPEN}Strengthen password policy requirements{HTML_LI_CLOSE}")
|
|
121
|
+
if self._compliance_results.get("IA-5") == "FAIL":
|
|
122
|
+
desc_parts.append(f"{HTML_LI_OPEN}Rotate access keys older than 90 days{HTML_LI_CLOSE}")
|
|
123
|
+
if self._compliance_results.get("AC-3") == "FAIL":
|
|
124
|
+
desc_parts.append(f"{HTML_LI_OPEN}Review and restrict role trust policies{HTML_LI_CLOSE}")
|
|
125
|
+
desc_parts.append(HTML_UL_CLOSE)
|
|
126
|
+
|
|
127
|
+
return "\n".join(desc_parts)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def framework(self) -> str:
|
|
131
|
+
return self.control_mapper.framework
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AWSIAMEvidenceIntegration(ComplianceIntegration):
|
|
135
|
+
"""Process AWS IAM data and create evidence/compliance records in RegScale."""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
plan_id: int,
|
|
140
|
+
region: str = "us-east-1",
|
|
141
|
+
framework: str = "NIST800-53R5",
|
|
142
|
+
create_issues: bool = True,
|
|
143
|
+
update_control_status: bool = True,
|
|
144
|
+
create_poams: bool = False,
|
|
145
|
+
parent_module: str = "securityplans",
|
|
146
|
+
collect_evidence: bool = False,
|
|
147
|
+
evidence_as_attachments: bool = True,
|
|
148
|
+
evidence_control_ids: Optional[List[str]] = None,
|
|
149
|
+
evidence_frequency: int = 30,
|
|
150
|
+
force_refresh: bool = False,
|
|
151
|
+
**kwargs,
|
|
152
|
+
):
|
|
153
|
+
super().__init__(
|
|
154
|
+
plan_id=plan_id,
|
|
155
|
+
framework=framework,
|
|
156
|
+
create_issues=create_issues,
|
|
157
|
+
update_control_status=update_control_status,
|
|
158
|
+
create_poams=create_poams,
|
|
159
|
+
parent_module=parent_module,
|
|
160
|
+
**kwargs,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.region = region
|
|
164
|
+
self.title = "AWS IAM"
|
|
165
|
+
self.collect_evidence = collect_evidence
|
|
166
|
+
self.evidence_as_attachments = evidence_as_attachments
|
|
167
|
+
self.evidence_control_ids = evidence_control_ids
|
|
168
|
+
self.evidence_frequency = evidence_frequency
|
|
169
|
+
self.force_refresh = force_refresh
|
|
170
|
+
self.control_mapper = IAMControlMapper(framework=framework)
|
|
171
|
+
self.api = Api()
|
|
172
|
+
|
|
173
|
+
profile = kwargs.get("profile")
|
|
174
|
+
aws_access_key_id = kwargs.get("aws_access_key_id")
|
|
175
|
+
aws_secret_access_key = kwargs.get("aws_secret_access_key")
|
|
176
|
+
aws_session_token = kwargs.get("aws_session_token")
|
|
177
|
+
|
|
178
|
+
if aws_access_key_id and aws_secret_access_key:
|
|
179
|
+
logger.info("Initializing AWS IAM client with explicit credentials")
|
|
180
|
+
self.session = boto3.Session(
|
|
181
|
+
region_name=region,
|
|
182
|
+
aws_access_key_id=aws_access_key_id,
|
|
183
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
184
|
+
aws_session_token=aws_session_token,
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
logger.info(f"Initializing AWS IAM client with profile: {profile if profile else 'default'}")
|
|
188
|
+
self.session = boto3.Session(profile_name=profile, region_name=region)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
self.client = self.session.client("iam")
|
|
192
|
+
logger.info("Successfully created AWS IAM client")
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Failed to create AWS IAM client: {e}")
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
self.raw_iam_data: Dict[str, Any] = {}
|
|
198
|
+
|
|
199
|
+
def _is_cache_valid(self) -> bool:
|
|
200
|
+
if not os.path.exists(IAM_CACHE_FILE):
|
|
201
|
+
return False
|
|
202
|
+
file_age = time.time() - os.path.getmtime(IAM_CACHE_FILE)
|
|
203
|
+
is_valid = file_age < CACHE_TTL_SECONDS
|
|
204
|
+
if is_valid:
|
|
205
|
+
logger.info(f"Using cached IAM data (age: {file_age / 3600:.1f} hours)")
|
|
206
|
+
return is_valid
|
|
207
|
+
|
|
208
|
+
def _load_cached_data(self) -> Dict[str, Any]:
|
|
209
|
+
try:
|
|
210
|
+
with open(IAM_CACHE_FILE, encoding="utf-8") as file:
|
|
211
|
+
data = json.load(file)
|
|
212
|
+
|
|
213
|
+
# Validate cache format - must be a dict
|
|
214
|
+
if not isinstance(data, dict):
|
|
215
|
+
logger.warning("Invalid cache format detected (not a dict). Invalidating cache.")
|
|
216
|
+
return {}
|
|
217
|
+
|
|
218
|
+
return data
|
|
219
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
220
|
+
logger.warning(f"Error reading cache: {e}")
|
|
221
|
+
return {}
|
|
222
|
+
|
|
223
|
+
def _save_to_cache(self, iam_data: Dict[str, Any]) -> None:
|
|
224
|
+
try:
|
|
225
|
+
os.makedirs(os.path.dirname(IAM_CACHE_FILE), exist_ok=True)
|
|
226
|
+
with open(IAM_CACHE_FILE, "w", encoding="utf-8") as file:
|
|
227
|
+
json.dump(iam_data, file, indent=2, default=str)
|
|
228
|
+
logger.info(f"Cached IAM data to {IAM_CACHE_FILE}")
|
|
229
|
+
except IOError as e:
|
|
230
|
+
logger.warning(f"Error writing cache: {e}")
|
|
231
|
+
|
|
232
|
+
def _fetch_fresh_iam_data(self) -> Dict[str, Any]:
|
|
233
|
+
logger.info("Fetching IAM data from AWS...")
|
|
234
|
+
iam_data = {}
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
iam_data["account_summary"] = self.client.get_account_summary().get("SummaryMap", {})
|
|
238
|
+
iam_data["password_policy"] = self._get_password_policy()
|
|
239
|
+
iam_data["users"] = self._list_users()
|
|
240
|
+
logger.info(f"Found {len(iam_data['users'])} IAM users")
|
|
241
|
+
iam_data["groups"] = self._list_groups()
|
|
242
|
+
logger.info(f"Found {len(iam_data['groups'])} IAM groups")
|
|
243
|
+
iam_data["roles"] = self._list_roles()
|
|
244
|
+
logger.info(f"Found {len(iam_data['roles'])} IAM roles")
|
|
245
|
+
iam_data["policies"] = self._list_policies()
|
|
246
|
+
logger.info(f"Found {len(iam_data['policies'])} customer managed policies")
|
|
247
|
+
except ClientError as e:
|
|
248
|
+
logger.error(f"Error fetching IAM data: {e}")
|
|
249
|
+
return {}
|
|
250
|
+
|
|
251
|
+
return iam_data
|
|
252
|
+
|
|
253
|
+
def _get_password_policy(self) -> Dict[str, Any]:
|
|
254
|
+
try:
|
|
255
|
+
return self.client.get_account_password_policy().get("PasswordPolicy", {})
|
|
256
|
+
except ClientError as e:
|
|
257
|
+
if e.response["Error"]["Code"] == "NoSuchEntity":
|
|
258
|
+
logger.warning("No account password policy configured")
|
|
259
|
+
return {}
|
|
260
|
+
raise
|
|
261
|
+
|
|
262
|
+
def _list_users(self) -> List[Dict[str, Any]]:
|
|
263
|
+
users = []
|
|
264
|
+
try:
|
|
265
|
+
paginator = self.client.get_paginator("list_users")
|
|
266
|
+
for page in paginator.paginate():
|
|
267
|
+
for user in page.get("Users", []):
|
|
268
|
+
user_name = user["UserName"]
|
|
269
|
+
user["MfaEnabled"] = self._user_has_mfa(user_name)
|
|
270
|
+
user["AccessKeys"] = self._list_user_access_keys(user_name)
|
|
271
|
+
user["AttachedPolicies"] = self._list_user_attached_policies(user_name)
|
|
272
|
+
user["InlinePolicies"] = self._list_user_inline_policies(user_name)
|
|
273
|
+
user["PasswordLastUsed"] = self._get_password_last_used(user)
|
|
274
|
+
users.append(user)
|
|
275
|
+
except ClientError as e:
|
|
276
|
+
logger.error(f"Error listing users: {e}")
|
|
277
|
+
return users
|
|
278
|
+
|
|
279
|
+
def _user_has_mfa(self, user_name: str) -> bool:
|
|
280
|
+
try:
|
|
281
|
+
mfa_devices = self.client.list_mfa_devices(UserName=user_name).get("MFADevices", [])
|
|
282
|
+
return len(mfa_devices) > 0
|
|
283
|
+
except ClientError:
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def _list_user_access_keys(self, user_name: str) -> List[Dict[str, Any]]:
|
|
287
|
+
try:
|
|
288
|
+
keys = self.client.list_access_keys(UserName=user_name).get("AccessKeyMetadata", [])
|
|
289
|
+
for key in keys:
|
|
290
|
+
created_date = key.get("CreateDate")
|
|
291
|
+
if created_date:
|
|
292
|
+
age = (datetime.now(created_date.tzinfo) - created_date).days
|
|
293
|
+
key["AgeDays"] = age
|
|
294
|
+
return keys
|
|
295
|
+
except ClientError:
|
|
296
|
+
return []
|
|
297
|
+
|
|
298
|
+
def _list_user_attached_policies(self, user_name: str) -> List[Dict[str, Any]]:
|
|
299
|
+
try:
|
|
300
|
+
return self.client.list_attached_user_policies(UserName=user_name).get("AttachedPolicies", [])
|
|
301
|
+
except ClientError:
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
def _list_user_inline_policies(self, user_name: str) -> List[Dict[str, Any]]:
|
|
305
|
+
try:
|
|
306
|
+
policy_names = self.client.list_user_policies(UserName=user_name).get("PolicyNames", [])
|
|
307
|
+
policies = []
|
|
308
|
+
for policy_name in policy_names:
|
|
309
|
+
policy_doc = self.client.get_user_policy(UserName=user_name, PolicyName=policy_name)
|
|
310
|
+
policies.append({"PolicyName": policy_name, "PolicyDocument": policy_doc.get("PolicyDocument", {})})
|
|
311
|
+
return policies
|
|
312
|
+
except ClientError:
|
|
313
|
+
return []
|
|
314
|
+
|
|
315
|
+
def _get_password_last_used(self, user: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
316
|
+
password_last_used = user.get("PasswordLastUsed")
|
|
317
|
+
if password_last_used:
|
|
318
|
+
days_since = (datetime.now(password_last_used.tzinfo) - password_last_used).days
|
|
319
|
+
return {"LastUsedDate": password_last_used, "DaysSinceUsed": days_since}
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
def _list_groups(self) -> List[Dict[str, Any]]:
|
|
323
|
+
groups = []
|
|
324
|
+
try:
|
|
325
|
+
paginator = self.client.get_paginator("list_groups")
|
|
326
|
+
for page in paginator.paginate():
|
|
327
|
+
groups.extend(page.get("Groups", []))
|
|
328
|
+
except ClientError as e:
|
|
329
|
+
logger.error(f"Error listing groups: {e}")
|
|
330
|
+
return groups
|
|
331
|
+
|
|
332
|
+
def _list_roles(self) -> List[Dict[str, Any]]:
|
|
333
|
+
roles = []
|
|
334
|
+
try:
|
|
335
|
+
paginator = self.client.get_paginator("list_roles")
|
|
336
|
+
for page in paginator.paginate():
|
|
337
|
+
for role in page.get("Roles", []):
|
|
338
|
+
role_name = role["RoleName"]
|
|
339
|
+
role["AttachedPolicies"] = self._list_role_attached_policies(role_name)
|
|
340
|
+
roles.append(role)
|
|
341
|
+
except ClientError as e:
|
|
342
|
+
logger.error(f"Error listing roles: {e}")
|
|
343
|
+
return roles
|
|
344
|
+
|
|
345
|
+
def _list_role_attached_policies(self, role_name: str) -> List[Dict[str, Any]]:
|
|
346
|
+
try:
|
|
347
|
+
return self.client.list_attached_role_policies(RoleName=role_name).get("AttachedPolicies", [])
|
|
348
|
+
except ClientError:
|
|
349
|
+
return []
|
|
350
|
+
|
|
351
|
+
def _list_policies(self) -> List[Dict[str, Any]]:
|
|
352
|
+
policies = []
|
|
353
|
+
try:
|
|
354
|
+
paginator = self.client.get_paginator("list_policies")
|
|
355
|
+
for page in paginator.paginate(Scope="Local"):
|
|
356
|
+
policies.extend(page.get("Policies", []))
|
|
357
|
+
except ClientError as e:
|
|
358
|
+
logger.error(f"Error listing policies: {e}")
|
|
359
|
+
return policies
|
|
360
|
+
|
|
361
|
+
def fetch_compliance_data(self) -> List[Dict[str, Any]]:
|
|
362
|
+
if not self.force_refresh and self._is_cache_valid():
|
|
363
|
+
cached_data = self._load_cached_data()
|
|
364
|
+
if cached_data:
|
|
365
|
+
self.raw_iam_data = cached_data
|
|
366
|
+
return [cached_data]
|
|
367
|
+
|
|
368
|
+
if self.force_refresh:
|
|
369
|
+
logger.info("Force refresh requested, fetching fresh IAM data...")
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
iam_data = self._fetch_fresh_iam_data()
|
|
373
|
+
self.raw_iam_data = iam_data
|
|
374
|
+
self._save_to_cache(iam_data)
|
|
375
|
+
return [iam_data] if iam_data else []
|
|
376
|
+
except ClientError as e:
|
|
377
|
+
logger.error(f"Error fetching IAM data: {e}")
|
|
378
|
+
return []
|
|
379
|
+
|
|
380
|
+
def create_compliance_item(self, raw_data: Dict[str, Any]) -> ComplianceItem:
|
|
381
|
+
return IAMComplianceItem(raw_data, self.control_mapper)
|
|
382
|
+
|
|
383
|
+
def sync_compliance(self) -> None:
|
|
384
|
+
super().sync_compliance()
|
|
385
|
+
if self.collect_evidence:
|
|
386
|
+
logger.info("Evidence collection enabled, starting evidence collection...")
|
|
387
|
+
self._collect_iam_evidence()
|
|
388
|
+
|
|
389
|
+
def _collect_iam_evidence(self) -> None:
|
|
390
|
+
if not self.raw_iam_data:
|
|
391
|
+
logger.warning("No IAM data available for evidence collection")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
scan_date = get_current_datetime(dt_format="%Y-%m-%d")
|
|
395
|
+
|
|
396
|
+
if self.evidence_as_attachments:
|
|
397
|
+
logger.info("Creating SSP file attachment with IAM evidence...")
|
|
398
|
+
self._create_ssp_attachment(scan_date)
|
|
399
|
+
else:
|
|
400
|
+
logger.info("Creating Evidence record with IAM evidence...")
|
|
401
|
+
self._create_evidence_record(scan_date)
|
|
402
|
+
|
|
403
|
+
def _create_ssp_attachment(self, scan_date: str) -> None:
|
|
404
|
+
try:
|
|
405
|
+
# Check for existing evidence to avoid duplicates
|
|
406
|
+
date_str = datetime.now().strftime("%Y%m%d")
|
|
407
|
+
file_name_pattern = f"iam_evidence_{date_str}"
|
|
408
|
+
|
|
409
|
+
if self.check_for_existing_evidence(file_name_pattern):
|
|
410
|
+
logger.info("Evidence file for IAM already exists for today. Skipping upload to avoid duplicates.")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# Add timestamp to make filename unique if run multiple times per day
|
|
414
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
415
|
+
file_name = f"iam_evidence_{timestamp}.jsonl.gz"
|
|
416
|
+
|
|
417
|
+
compliance_item = self.create_compliance_item(self.raw_iam_data)
|
|
418
|
+
evidence_entry = {
|
|
419
|
+
**self.raw_iam_data,
|
|
420
|
+
"compliance_assessment": {
|
|
421
|
+
"overall_result": compliance_item.compliance_result,
|
|
422
|
+
"control_results": compliance_item._compliance_results,
|
|
423
|
+
"assessed_controls": list(compliance_item._compliance_results.keys()),
|
|
424
|
+
"assessment_date": scan_date,
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
jsonl_content = json.dumps(evidence_entry, default=str)
|
|
428
|
+
|
|
429
|
+
compressed_buffer = BytesIO()
|
|
430
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
431
|
+
gz_file.write(jsonl_content)
|
|
432
|
+
|
|
433
|
+
compressed_data = compressed_buffer.getvalue()
|
|
434
|
+
api = Api()
|
|
435
|
+
success = File.upload_file_to_regscale(
|
|
436
|
+
file_name=file_name,
|
|
437
|
+
parent_id=self.plan_id,
|
|
438
|
+
parent_module=self.parent_module,
|
|
439
|
+
api=api,
|
|
440
|
+
file_data=compressed_data,
|
|
441
|
+
tags="aws,iam,access-control,automated",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
if success:
|
|
445
|
+
logger.info(f"Successfully uploaded IAM evidence file: {file_name}")
|
|
446
|
+
else:
|
|
447
|
+
logger.error("Failed to upload IAM evidence file")
|
|
448
|
+
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.error(f"Error creating SSP attachment: {e}", exc_info=True)
|
|
451
|
+
|
|
452
|
+
def _create_evidence_record(self, scan_date: str) -> None:
|
|
453
|
+
try:
|
|
454
|
+
title = f"AWS IAM Evidence - {scan_date}"
|
|
455
|
+
description = self._build_evidence_description(scan_date)
|
|
456
|
+
due_date = (datetime.now() + timedelta(days=self.evidence_frequency)).isoformat()
|
|
457
|
+
|
|
458
|
+
evidence = Evidence(
|
|
459
|
+
title=title,
|
|
460
|
+
description=description,
|
|
461
|
+
status="Collected",
|
|
462
|
+
updateFrequency=self.evidence_frequency,
|
|
463
|
+
dueDate=due_date,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
created_evidence = evidence.create()
|
|
467
|
+
if not created_evidence or not created_evidence.id:
|
|
468
|
+
logger.error("Failed to create evidence record")
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
logger.info(f"Created evidence record {created_evidence.id}: {title}")
|
|
472
|
+
self._upload_evidence_file(created_evidence.id, scan_date)
|
|
473
|
+
self._link_evidence_to_ssp(created_evidence.id)
|
|
474
|
+
|
|
475
|
+
# Link to controls if specified
|
|
476
|
+
if self.evidence_control_ids:
|
|
477
|
+
self._link_evidence_to_controls(created_evidence.id, is_attachment=False)
|
|
478
|
+
|
|
479
|
+
except Exception as e:
|
|
480
|
+
logger.error(f"Error creating evidence record: {e}", exc_info=True)
|
|
481
|
+
|
|
482
|
+
def _build_evidence_description(self, scan_date: str) -> str:
|
|
483
|
+
users = self.raw_iam_data.get("users", [])
|
|
484
|
+
groups = self.raw_iam_data.get("groups", [])
|
|
485
|
+
roles = self.raw_iam_data.get("roles", [])
|
|
486
|
+
compliance_item = self.create_compliance_item(self.raw_iam_data)
|
|
487
|
+
|
|
488
|
+
desc_parts = [
|
|
489
|
+
"<h1>AWS IAM Access Control Evidence</h1>",
|
|
490
|
+
f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Assessment Date:{HTML_STRONG_CLOSE} {scan_date}{HTML_P_CLOSE}",
|
|
491
|
+
f"{HTML_H2_OPEN}IAM Summary{HTML_H2_CLOSE}",
|
|
492
|
+
HTML_UL_OPEN,
|
|
493
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Users:{HTML_STRONG_CLOSE} {len(users)}{HTML_LI_CLOSE}",
|
|
494
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Groups:{HTML_STRONG_CLOSE} {len(groups)}{HTML_LI_CLOSE}",
|
|
495
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Roles:{HTML_STRONG_CLOSE} {len(roles)}{HTML_LI_CLOSE}",
|
|
496
|
+
HTML_UL_CLOSE,
|
|
497
|
+
f"{HTML_H2_OPEN}Control Compliance Results{HTML_H2_CLOSE}",
|
|
498
|
+
HTML_UL_OPEN,
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
for control_id, result in compliance_item._compliance_results.items():
|
|
502
|
+
control_desc = self.control_mapper.get_control_description(control_id)
|
|
503
|
+
result_color = "#d32f2f" if result == "FAIL" else "#2e7d32"
|
|
504
|
+
desc_parts.append(
|
|
505
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
|
|
506
|
+
f"<span style='color: {result_color};'>{result}</span> - {control_desc}{HTML_LI_CLOSE}"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
desc_parts.append(HTML_UL_CLOSE)
|
|
510
|
+
return "\n".join(desc_parts)
|
|
511
|
+
|
|
512
|
+
def _upload_evidence_file(self, evidence_id: int, scan_date: str) -> None:
|
|
513
|
+
try:
|
|
514
|
+
compliance_item = self.create_compliance_item(self.raw_iam_data)
|
|
515
|
+
evidence_entry = {
|
|
516
|
+
**self.raw_iam_data,
|
|
517
|
+
"compliance_assessment": {
|
|
518
|
+
"overall_result": compliance_item.compliance_result,
|
|
519
|
+
"control_results": compliance_item._compliance_results,
|
|
520
|
+
"assessed_controls": list(compliance_item._compliance_results.keys()),
|
|
521
|
+
"assessment_date": scan_date,
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
jsonl_content = json.dumps(evidence_entry, default=str)
|
|
525
|
+
|
|
526
|
+
compressed_buffer = BytesIO()
|
|
527
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
528
|
+
gz_file.write(jsonl_content)
|
|
529
|
+
|
|
530
|
+
compressed_data = compressed_buffer.getvalue()
|
|
531
|
+
file_name = f"iam_evidence_{scan_date}.jsonl.gz"
|
|
532
|
+
|
|
533
|
+
api = Api()
|
|
534
|
+
success = File.upload_file_to_regscale(
|
|
535
|
+
file_name=file_name,
|
|
536
|
+
parent_id=evidence_id,
|
|
537
|
+
parent_module="evidence",
|
|
538
|
+
api=api,
|
|
539
|
+
file_data=compressed_data,
|
|
540
|
+
tags="aws,iam,access-control",
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
if success:
|
|
544
|
+
logger.info(f"Uploaded IAM evidence file to Evidence {evidence_id}")
|
|
545
|
+
else:
|
|
546
|
+
logger.warning(f"Failed to upload evidence file to Evidence {evidence_id}")
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
logger.error(f"Error uploading evidence file: {e}", exc_info=True)
|
|
550
|
+
|
|
551
|
+
def _link_evidence_to_ssp(self, evidence_id: int) -> None:
|
|
552
|
+
try:
|
|
553
|
+
mapping = EvidenceMapping(evidenceID=evidence_id, mappedID=self.plan_id, mappingType=self.parent_module)
|
|
554
|
+
mapping.create()
|
|
555
|
+
logger.info(f"Linked evidence {evidence_id} to SSP {self.plan_id}")
|
|
556
|
+
except Exception as ex:
|
|
557
|
+
logger.warning(f"Failed to link evidence to SSP: {ex}")
|
|
558
|
+
|
|
559
|
+
def _link_evidence_to_controls(self, evidence_id: int, is_attachment: bool = False) -> None:
|
|
560
|
+
"""
|
|
561
|
+
Link evidence to specified control IDs.
|
|
562
|
+
|
|
563
|
+
:param int evidence_id: Evidence or attachment ID
|
|
564
|
+
:param bool is_attachment: True if linking attachment, False for evidence record
|
|
565
|
+
"""
|
|
566
|
+
try:
|
|
567
|
+
for control_id in self.evidence_control_ids:
|
|
568
|
+
if is_attachment:
|
|
569
|
+
self.api.link_ssp_attachment_to_control(self.plan_id, evidence_id, control_id)
|
|
570
|
+
else:
|
|
571
|
+
self.api.link_evidence_to_control(evidence_id, control_id)
|
|
572
|
+
logger.info(f"Linked evidence {evidence_id} to control {control_id}")
|
|
573
|
+
except Exception as e:
|
|
574
|
+
logger.error(f"Failed to link evidence to controls: {e}", exc_info=True)
|