regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.1.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/commercial/synqly/ticketing.py +27 -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 +65 -5
- 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.1.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
- 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.1.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""AWS Config Conformance Pack to Control Mappings."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("regscale")
|
|
10
|
+
|
|
11
|
+
# Control ID constants
|
|
12
|
+
CONTROL_IA_2_1 = "IA-2(1)"
|
|
13
|
+
|
|
14
|
+
# NIST 800-53 R5 Conformance Pack Control Mappings
|
|
15
|
+
# Maps AWS Config rule names to NIST 800-53 R5 control IDs
|
|
16
|
+
NIST_80053_R5_MAPPINGS = {
|
|
17
|
+
# Access Control (AC) Family
|
|
18
|
+
"iam-password-policy": ["AC-2", "IA-5"],
|
|
19
|
+
"iam-user-mfa-enabled": ["AC-2", CONTROL_IA_2_1],
|
|
20
|
+
"iam-root-access-key-check": ["AC-2", "AC-6"],
|
|
21
|
+
"iam-user-unused-credentials-check": ["AC-2"],
|
|
22
|
+
"iam-user-group-membership-check": ["AC-2"],
|
|
23
|
+
"iam-policy-no-statements-with-admin-access": ["AC-6"],
|
|
24
|
+
"iam-policy-no-statements-with-full-access": ["AC-6"],
|
|
25
|
+
"s3-bucket-public-read-prohibited": ["AC-3", "AC-4"],
|
|
26
|
+
"s3-bucket-public-write-prohibited": ["AC-3", "AC-4"],
|
|
27
|
+
"ec2-security-group-attached-to-eni": ["AC-4"],
|
|
28
|
+
"restricted-ssh": ["AC-4", "AC-17"],
|
|
29
|
+
"restricted-common-ports": ["AC-4"],
|
|
30
|
+
# Audit and Accountability (AU) Family
|
|
31
|
+
"cloudtrail-enabled": ["AU-2", "AU-3", "AU-6", "AU-12"],
|
|
32
|
+
"cloud-trail-cloud-watch-logs-enabled": ["AU-6"],
|
|
33
|
+
"cloudtrail-log-file-validation-enabled": ["AU-9"],
|
|
34
|
+
"cloudtrail-encryption-enabled": ["AU-9"],
|
|
35
|
+
"cloudtrail-s3-dataevents-enabled": ["AU-2"],
|
|
36
|
+
"multi-region-cloudtrail-enabled": ["AU-2"],
|
|
37
|
+
"s3-bucket-logging-enabled": ["AU-2"],
|
|
38
|
+
"rds-logging-enabled": ["AU-2"],
|
|
39
|
+
"elb-logging-enabled": ["AU-2"],
|
|
40
|
+
"cloudwatch-alarm-action-check": ["AU-6"],
|
|
41
|
+
# Configuration Management (CM) Family
|
|
42
|
+
"ec2-instance-managed-by-systems-manager": ["CM-2", "CM-6"],
|
|
43
|
+
"ec2-managedinstance-patch-compliance-status-check": ["CM-6", "SI-2"], # Maps to both CM and SI families
|
|
44
|
+
"approved-amis-by-tag": ["CM-2"],
|
|
45
|
+
# Identification and Authentication (IA) Family
|
|
46
|
+
"mfa-enabled-for-iam-console-access": [CONTROL_IA_2_1],
|
|
47
|
+
"root-account-mfa-enabled": [CONTROL_IA_2_1],
|
|
48
|
+
# System and Communications Protection (SC) Family
|
|
49
|
+
"s3-bucket-ssl-requests-only": ["SC-8", "SC-13"],
|
|
50
|
+
"alb-http-to-https-redirection-check": ["SC-8"],
|
|
51
|
+
"elb-tls-https-listeners-only": ["SC-8"],
|
|
52
|
+
"rds-snapshot-encrypted": ["SC-13"],
|
|
53
|
+
"encrypted-volumes": ["SC-13"],
|
|
54
|
+
"s3-bucket-server-side-encryption-enabled": ["SC-13"],
|
|
55
|
+
"ec2-ebs-encryption-by-default": ["SC-13"],
|
|
56
|
+
"rds-storage-encrypted": ["SC-13"],
|
|
57
|
+
"dynamodb-table-encrypted-kms": ["SC-13"],
|
|
58
|
+
# System and Information Integrity (SI) Family
|
|
59
|
+
"guardduty-enabled-centralized": ["SI-4"],
|
|
60
|
+
"securityhub-enabled": ["SI-4"],
|
|
61
|
+
"access-keys-rotated": ["SI-4"],
|
|
62
|
+
"vpc-flow-logs-enabled": ["SI-4"],
|
|
63
|
+
# Risk Assessment (RA) Family
|
|
64
|
+
"security-account-information-provided": ["RA-5"],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_control_ids_from_rule_name(rule_name: str) -> List[str]:
|
|
69
|
+
"""
|
|
70
|
+
Extract control IDs from AWS Config rule name using pattern matching.
|
|
71
|
+
|
|
72
|
+
Supports patterns like:
|
|
73
|
+
- "ac-2-iam-user-mfa-enabled"
|
|
74
|
+
- "nist-800-53-r5-ac-2"
|
|
75
|
+
- "iam-password-policy-ac-2-ia-5"
|
|
76
|
+
|
|
77
|
+
:param str rule_name: Config rule name
|
|
78
|
+
:return: List of extracted control IDs
|
|
79
|
+
:rtype: List[str]
|
|
80
|
+
"""
|
|
81
|
+
control_ids = []
|
|
82
|
+
|
|
83
|
+
# Pattern for NIST control IDs: AC-2, SI-3(1), etc.
|
|
84
|
+
pattern = r"\b([A-Z]{2}-\d+(?:\(\d+\))?)\b"
|
|
85
|
+
|
|
86
|
+
matches = re.findall(pattern, rule_name.upper())
|
|
87
|
+
control_ids.extend(matches)
|
|
88
|
+
|
|
89
|
+
return list(set(control_ids)) # Remove duplicates
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def extract_control_ids_from_tags(tags: Dict[str, str]) -> List[str]:
|
|
93
|
+
"""
|
|
94
|
+
Extract control IDs from AWS Config rule tags.
|
|
95
|
+
|
|
96
|
+
Expected tag format:
|
|
97
|
+
- ControlID=AC-2
|
|
98
|
+
- ControlID=AC-2,AU-3,SI-2
|
|
99
|
+
- ControlIDs=AC-2,AU-3
|
|
100
|
+
|
|
101
|
+
:param Dict[str, str] tags: Dictionary of tag key-value pairs
|
|
102
|
+
:return: List of extracted control IDs
|
|
103
|
+
:rtype: List[str]
|
|
104
|
+
"""
|
|
105
|
+
control_ids = []
|
|
106
|
+
|
|
107
|
+
# Check for ControlID or ControlIDs tags
|
|
108
|
+
for tag_key in ["ControlID", "ControlIDs", "Control-ID", "Control-IDs"]:
|
|
109
|
+
if tag_key in tags:
|
|
110
|
+
tag_value = tags[tag_key]
|
|
111
|
+
# Split by comma and clean up
|
|
112
|
+
ids = [cid.strip().upper() for cid in tag_value.split(",") if cid.strip()]
|
|
113
|
+
control_ids.extend(ids)
|
|
114
|
+
|
|
115
|
+
return list(set(control_ids)) # Remove duplicates
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_control_mappings_for_framework(framework: str) -> Dict[str, List[str]]:
|
|
119
|
+
"""
|
|
120
|
+
Get control mappings for a specific framework.
|
|
121
|
+
|
|
122
|
+
:param str framework: Framework name (e.g., "NIST800-53R5")
|
|
123
|
+
:return: Dictionary mapping rule names to control IDs
|
|
124
|
+
:rtype: Dict[str, List[str]]
|
|
125
|
+
"""
|
|
126
|
+
framework_upper = framework.upper().replace("-", "").replace("_", "")
|
|
127
|
+
|
|
128
|
+
if "NIST80053" in framework_upper or "NIST800" in framework_upper:
|
|
129
|
+
return NIST_80053_R5_MAPPINGS
|
|
130
|
+
|
|
131
|
+
# Add more framework mappings as needed
|
|
132
|
+
# elif "PCI" in framework_upper:
|
|
133
|
+
# return PCI_DSS_MAPPINGS
|
|
134
|
+
# elif "CIS" in framework_upper:
|
|
135
|
+
# return CIS_MAPPINGS
|
|
136
|
+
|
|
137
|
+
logger.warning(f"No built-in control mappings available for framework: {framework}")
|
|
138
|
+
return {}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def map_rule_to_controls(
|
|
142
|
+
rule_name: str,
|
|
143
|
+
rule_description: Optional[str] = None,
|
|
144
|
+
rule_tags: Optional[Dict[str, str]] = None,
|
|
145
|
+
framework: str = "NIST800-53R5",
|
|
146
|
+
) -> List[str]:
|
|
147
|
+
"""
|
|
148
|
+
Map an AWS Config rule to control IDs using multiple strategies.
|
|
149
|
+
|
|
150
|
+
Priority order:
|
|
151
|
+
1. Framework-specific mappings (conformance pack)
|
|
152
|
+
2. Rule tags (ControlID tag)
|
|
153
|
+
3. Pattern matching in rule name
|
|
154
|
+
4. Pattern matching in rule description
|
|
155
|
+
|
|
156
|
+
:param str rule_name: Config rule name
|
|
157
|
+
:param Optional[str] rule_description: Config rule description
|
|
158
|
+
:param Optional[Dict[str, str]] rule_tags: Config rule tags
|
|
159
|
+
:param str framework: Target framework
|
|
160
|
+
:return: List of mapped control IDs
|
|
161
|
+
:rtype: List[str]
|
|
162
|
+
"""
|
|
163
|
+
control_ids = []
|
|
164
|
+
|
|
165
|
+
# Strategy 1: Check framework-specific mappings
|
|
166
|
+
framework_mappings = get_control_mappings_for_framework(framework)
|
|
167
|
+
if rule_name in framework_mappings:
|
|
168
|
+
control_ids.extend(framework_mappings[rule_name])
|
|
169
|
+
logger.debug(f"Rule '{rule_name}' mapped to controls via framework mappings: {control_ids}")
|
|
170
|
+
|
|
171
|
+
# Strategy 2: Check rule tags
|
|
172
|
+
if rule_tags:
|
|
173
|
+
tag_control_ids = extract_control_ids_from_tags(rule_tags)
|
|
174
|
+
if tag_control_ids:
|
|
175
|
+
control_ids.extend(tag_control_ids)
|
|
176
|
+
logger.debug(f"Rule '{rule_name}' mapped to controls via tags: {tag_control_ids}")
|
|
177
|
+
|
|
178
|
+
# Strategy 3: Pattern matching in rule name
|
|
179
|
+
if not control_ids:
|
|
180
|
+
name_control_ids = extract_control_ids_from_rule_name(rule_name)
|
|
181
|
+
if name_control_ids:
|
|
182
|
+
control_ids.extend(name_control_ids)
|
|
183
|
+
logger.debug(f"Rule '{rule_name}' mapped to controls via name pattern: {name_control_ids}")
|
|
184
|
+
|
|
185
|
+
# Strategy 4: Pattern matching in rule description
|
|
186
|
+
if not control_ids and rule_description:
|
|
187
|
+
desc_control_ids = extract_control_ids_from_rule_name(rule_description)
|
|
188
|
+
if desc_control_ids:
|
|
189
|
+
control_ids.extend(desc_control_ids)
|
|
190
|
+
logger.debug(f"Rule '{rule_name}' mapped to controls via description pattern: {desc_control_ids}")
|
|
191
|
+
|
|
192
|
+
# Remove duplicates and sort
|
|
193
|
+
control_ids = sorted(set(control_ids))
|
|
194
|
+
|
|
195
|
+
if not control_ids:
|
|
196
|
+
logger.debug(f"Rule '{rule_name}' could not be mapped to any controls")
|
|
197
|
+
|
|
198
|
+
return control_ids
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""AWS Evidence Generator for SSP compliance documentation"""
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from io import BytesIO
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from regscale.core.app.api import Api
|
|
13
|
+
from regscale.models.regscale_models.evidence import Evidence
|
|
14
|
+
from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
|
|
15
|
+
from regscale.models.regscale_models.file import File
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("regscale")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AWSEvidenceGenerator:
|
|
21
|
+
"""Generate compliance evidence from AWS security findings"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, api: Api, ssp_id: Optional[int] = None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize evidence generator
|
|
26
|
+
|
|
27
|
+
:param Api api: RegScale API instance
|
|
28
|
+
:param Optional[int] ssp_id: Security Plan ID to link evidence
|
|
29
|
+
"""
|
|
30
|
+
self.api = api
|
|
31
|
+
self.ssp_id = ssp_id
|
|
32
|
+
|
|
33
|
+
def create_evidence_from_scan(
|
|
34
|
+
self,
|
|
35
|
+
service_name: str,
|
|
36
|
+
findings: List[dict],
|
|
37
|
+
ocsf_data: Optional[List[dict]] = None,
|
|
38
|
+
update_frequency: int = 30,
|
|
39
|
+
control_ids: Optional[List[int]] = None,
|
|
40
|
+
) -> Optional[Evidence]:
|
|
41
|
+
"""
|
|
42
|
+
Create evidence record from AWS service scan
|
|
43
|
+
|
|
44
|
+
:param str service_name: AWS service name (GuardDuty, SecurityHub, etc.)
|
|
45
|
+
:param List[dict] findings: List of AWS findings (native format)
|
|
46
|
+
:param Optional[List[dict]] ocsf_data: OCSF-formatted findings
|
|
47
|
+
:param int update_frequency: Evidence update frequency in days (default: 30)
|
|
48
|
+
:param Optional[List[int]] control_ids: Control IDs to link evidence
|
|
49
|
+
:return: Created Evidence object or None
|
|
50
|
+
:rtype: Optional[Evidence]
|
|
51
|
+
"""
|
|
52
|
+
if not findings:
|
|
53
|
+
logger.warning("No findings provided, skipping evidence creation")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Generate evidence title with timestamp
|
|
57
|
+
scan_date = datetime.now().strftime("%Y-%m-%d")
|
|
58
|
+
title = f"{service_name} Findings Scan - {scan_date}"
|
|
59
|
+
|
|
60
|
+
# Create description with finding summary
|
|
61
|
+
total_findings = len(findings)
|
|
62
|
+
severity_counts = self._count_severities(findings, service_name)
|
|
63
|
+
description = self._build_evidence_description(service_name, total_findings, severity_counts, ocsf_data)
|
|
64
|
+
|
|
65
|
+
# Calculate due date based on update frequency
|
|
66
|
+
due_date = (datetime.now() + timedelta(days=update_frequency)).isoformat()
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Create evidence record
|
|
70
|
+
evidence = Evidence(
|
|
71
|
+
title=title,
|
|
72
|
+
description=description,
|
|
73
|
+
status="Collected",
|
|
74
|
+
updateFrequency=update_frequency,
|
|
75
|
+
dueDate=due_date,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Create evidence in RegScale
|
|
79
|
+
created_evidence = evidence.create()
|
|
80
|
+
if not created_evidence or not created_evidence.id:
|
|
81
|
+
logger.error("Failed to create evidence record")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
logger.info("Created evidence record %s: %s", created_evidence.id, title)
|
|
85
|
+
|
|
86
|
+
# Upload findings as file attachments
|
|
87
|
+
self._attach_findings_files(
|
|
88
|
+
created_evidence.id,
|
|
89
|
+
findings,
|
|
90
|
+
ocsf_data,
|
|
91
|
+
service_name,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Link evidence to SSP if provided
|
|
95
|
+
if self.ssp_id:
|
|
96
|
+
self._link_to_ssp(created_evidence.id)
|
|
97
|
+
|
|
98
|
+
# Link evidence to specific controls if provided
|
|
99
|
+
if control_ids:
|
|
100
|
+
self._link_to_controls(created_evidence.id, control_ids)
|
|
101
|
+
|
|
102
|
+
return created_evidence
|
|
103
|
+
|
|
104
|
+
except Exception as ex:
|
|
105
|
+
logger.error("Failed to create evidence: %s", ex)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _count_severities(self, findings: List[dict], service_name: str) -> dict:
|
|
109
|
+
"""
|
|
110
|
+
Count findings by severity level
|
|
111
|
+
|
|
112
|
+
:param List[dict] findings: AWS findings
|
|
113
|
+
:param str service_name: AWS service name for severity field mapping
|
|
114
|
+
:return: Dictionary with severity counts
|
|
115
|
+
:rtype: dict
|
|
116
|
+
"""
|
|
117
|
+
severity_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
|
|
118
|
+
|
|
119
|
+
for finding in findings:
|
|
120
|
+
# Map service-specific severity fields
|
|
121
|
+
if service_name == "GuardDuty":
|
|
122
|
+
severity = finding.get("Severity", 0)
|
|
123
|
+
# GuardDuty uses numeric severity (1.0-8.9)
|
|
124
|
+
if severity >= 7.0:
|
|
125
|
+
severity_counts["HIGH"] += 1
|
|
126
|
+
elif severity >= 4.0:
|
|
127
|
+
severity_counts["MEDIUM"] += 1
|
|
128
|
+
else:
|
|
129
|
+
severity_counts["LOW"] += 1
|
|
130
|
+
|
|
131
|
+
elif service_name == "SecurityHub":
|
|
132
|
+
# Security Hub uses normalized severity
|
|
133
|
+
severity_label = finding.get("Severity", {}).get("Label", "INFO")
|
|
134
|
+
severity_counts[severity_label] = severity_counts.get(severity_label, 0) + 1
|
|
135
|
+
|
|
136
|
+
elif service_name == "CloudTrail":
|
|
137
|
+
# CloudTrail events don't have severity, count as INFO
|
|
138
|
+
severity_counts["INFO"] += 1
|
|
139
|
+
|
|
140
|
+
return severity_counts
|
|
141
|
+
|
|
142
|
+
def _build_evidence_description(
|
|
143
|
+
self,
|
|
144
|
+
service_name: str,
|
|
145
|
+
total_findings: int,
|
|
146
|
+
severity_counts: dict,
|
|
147
|
+
ocsf_data: Optional[List[dict]],
|
|
148
|
+
) -> str:
|
|
149
|
+
"""
|
|
150
|
+
Build evidence description with finding summary
|
|
151
|
+
|
|
152
|
+
:param str service_name: AWS service name
|
|
153
|
+
:param int total_findings: Total number of findings
|
|
154
|
+
:param dict severity_counts: Severity breakdown
|
|
155
|
+
:param Optional[List[dict]] ocsf_data: OCSF-formatted findings
|
|
156
|
+
:return: Evidence description text
|
|
157
|
+
:rtype: str
|
|
158
|
+
"""
|
|
159
|
+
description_parts = [
|
|
160
|
+
f"Automated evidence collection from AWS {service_name}.",
|
|
161
|
+
f"Total findings: {total_findings}",
|
|
162
|
+
"",
|
|
163
|
+
"Severity Breakdown:",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
for severity, count in severity_counts.items():
|
|
167
|
+
if count > 0:
|
|
168
|
+
description_parts.append(f" - {severity}: {count}")
|
|
169
|
+
|
|
170
|
+
description_parts.extend(["", "Files attached:"])
|
|
171
|
+
description_parts.append(f" - {service_name.lower()}_findings_native.jsonl.gz (AWS native format, compressed)")
|
|
172
|
+
|
|
173
|
+
if ocsf_data:
|
|
174
|
+
description_parts.append(
|
|
175
|
+
f" - {service_name.lower()}_findings_ocsf.jsonl.gz (OCSF normalized format, compressed)"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return "\n".join(description_parts)
|
|
179
|
+
|
|
180
|
+
def _attach_findings_files(
|
|
181
|
+
self,
|
|
182
|
+
evidence_id: int,
|
|
183
|
+
findings: List[dict],
|
|
184
|
+
ocsf_data: Optional[List[dict]],
|
|
185
|
+
service_name: str,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Upload findings as file attachments to evidence
|
|
189
|
+
|
|
190
|
+
:param int evidence_id: Evidence record ID
|
|
191
|
+
:param List[dict] findings: Native AWS findings
|
|
192
|
+
:param Optional[List[dict]] ocsf_data: OCSF-formatted findings
|
|
193
|
+
:param str service_name: AWS service name
|
|
194
|
+
"""
|
|
195
|
+
# Upload native findings as compressed JSONL
|
|
196
|
+
native_jsonl = "\n".join([json.dumps(f) for f in findings])
|
|
197
|
+
|
|
198
|
+
# Compress the JSONL data
|
|
199
|
+
compressed_buffer = BytesIO()
|
|
200
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
201
|
+
gz_file.write(native_jsonl)
|
|
202
|
+
|
|
203
|
+
compressed_data = compressed_buffer.getvalue()
|
|
204
|
+
|
|
205
|
+
success = File.upload_file_to_regscale(
|
|
206
|
+
file_name=f"{service_name.lower()}_findings_native.jsonl.gz",
|
|
207
|
+
parent_id=evidence_id,
|
|
208
|
+
parent_module="evidence",
|
|
209
|
+
api=self.api,
|
|
210
|
+
file_data=compressed_data,
|
|
211
|
+
tags=f"aws,{service_name.lower()},native,compressed",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if success:
|
|
215
|
+
logger.info("Uploaded compressed native findings file for evidence %s", evidence_id)
|
|
216
|
+
else:
|
|
217
|
+
logger.warning("Failed to upload compressed native findings file for evidence %s", evidence_id)
|
|
218
|
+
|
|
219
|
+
# Upload OCSF findings if available
|
|
220
|
+
if ocsf_data:
|
|
221
|
+
ocsf_jsonl = "\n".join([json.dumps(f) for f in ocsf_data])
|
|
222
|
+
|
|
223
|
+
# Compress the OCSF JSONL data
|
|
224
|
+
compressed_buffer = BytesIO()
|
|
225
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
226
|
+
gz_file.write(ocsf_jsonl)
|
|
227
|
+
|
|
228
|
+
compressed_data = compressed_buffer.getvalue()
|
|
229
|
+
|
|
230
|
+
success = File.upload_file_to_regscale(
|
|
231
|
+
file_name=f"{service_name.lower()}_findings_ocsf.jsonl.gz",
|
|
232
|
+
parent_id=evidence_id,
|
|
233
|
+
parent_module="evidence",
|
|
234
|
+
api=self.api,
|
|
235
|
+
file_data=compressed_data,
|
|
236
|
+
tags=f"aws,{service_name.lower()},ocsf,compressed",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if success:
|
|
240
|
+
logger.info("Uploaded compressed OCSF findings file for evidence %s", evidence_id)
|
|
241
|
+
else:
|
|
242
|
+
logger.warning("Failed to upload compressed OCSF findings file for evidence %s", evidence_id)
|
|
243
|
+
|
|
244
|
+
def _link_to_ssp(self, evidence_id: int) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Link evidence to Security Plan
|
|
247
|
+
|
|
248
|
+
:param int evidence_id: Evidence record ID
|
|
249
|
+
"""
|
|
250
|
+
if not self.ssp_id:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
mapping = EvidenceMapping(
|
|
254
|
+
evidenceID=evidence_id,
|
|
255
|
+
mappedID=self.ssp_id,
|
|
256
|
+
mappingType="securityplans",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
mapping.create()
|
|
261
|
+
logger.info("Linked evidence %s to SSP %s", evidence_id, self.ssp_id)
|
|
262
|
+
except Exception as ex:
|
|
263
|
+
logger.warning("Failed to link evidence to SSP: %s", ex)
|
|
264
|
+
|
|
265
|
+
def _link_to_controls(self, evidence_id: int, control_ids: List[int]) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Link evidence to specific security controls
|
|
268
|
+
|
|
269
|
+
:param int evidence_id: Evidence record ID
|
|
270
|
+
:param List[int] control_ids: List of control IDs
|
|
271
|
+
"""
|
|
272
|
+
for control_id in control_ids:
|
|
273
|
+
mapping = EvidenceMapping(
|
|
274
|
+
evidenceID=evidence_id,
|
|
275
|
+
mappedID=control_id,
|
|
276
|
+
mappingType="securityControlAssessments",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
mapping.create()
|
|
281
|
+
logger.info("Linked evidence %s to control %s", evidence_id, control_id)
|
|
282
|
+
except Exception as ex:
|
|
283
|
+
logger.warning("Failed to link evidence to control %s: %s", control_id, ex)
|