regscale-cli 6.23.0.1__py3-none-any.whl → 6.24.0.1__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 +2 -0
- regscale/integrations/commercial/__init__.py +1 -0
- regscale/integrations/commercial/jira.py +95 -22
- regscale/integrations/commercial/sarif/sarif_converter.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +132 -2
- regscale/integrations/commercial/wizv2/compliance_report.py +1574 -0
- regscale/integrations/commercial/wizv2/constants.py +72 -2
- regscale/integrations/commercial/wizv2/data_fetcher.py +61 -0
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +775 -27
- regscale/integrations/commercial/wizv2/policy_compliance.py +599 -181
- regscale/integrations/commercial/wizv2/reports.py +243 -0
- regscale/integrations/commercial/wizv2/scanner.py +668 -245
- regscale/integrations/compliance_integration.py +534 -56
- regscale/integrations/due_date_handler.py +210 -0
- regscale/integrations/public/cci_importer.py +444 -0
- regscale/integrations/scanner_integration.py +718 -153
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/cisa_kev_data.json +18 -3
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/control_implementation.py +13 -3
- regscale/models/regscale_models/form_field_value.py +1 -1
- regscale/models/regscale_models/milestone.py +1 -0
- regscale/models/regscale_models/regscale_model.py +225 -60
- regscale/models/regscale_models/security_plan.py +3 -2
- regscale/regscale.py +7 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/METADATA +17 -17
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/RECORD +45 -28
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/integrations/public/__init__.py +0 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +458 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +851 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_module_integration.py +582 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Due Date Handler for Scanner Integrations"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from regscale.core.app.application import Application
|
|
10
|
+
from regscale.core.utils.date import date_str, get_day_increment
|
|
11
|
+
from regscale.integrations.public.cisa import pull_cisa_kev
|
|
12
|
+
from regscale.models import regscale_models
|
|
13
|
+
from regscale.utils.threading import ThreadSafeDict
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("regscale")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DueDateHandler:
|
|
19
|
+
"""
|
|
20
|
+
Handles due date calculations for scanner integrations based on:
|
|
21
|
+
1. Init.yaml timeline configurations per integration
|
|
22
|
+
2. KEV (Known Exploited Vulnerabilities) dates from CISA
|
|
23
|
+
3. Default severity-based timelines
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, integration_name: str, config: Optional[Dict[str, Any]] = None):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the DueDateHandler for a specific integration
|
|
29
|
+
|
|
30
|
+
:param str integration_name: Name of the integration (e.g., 'wiz', 'qualys', 'tenable')
|
|
31
|
+
:param Optional[Dict[str, Any]] config: Optional config override, uses Application config if None
|
|
32
|
+
"""
|
|
33
|
+
self.integration_name = integration_name.lower()
|
|
34
|
+
self.config = config or Application().config
|
|
35
|
+
self._kev_data: Optional[ThreadSafeDict] = None
|
|
36
|
+
|
|
37
|
+
# Default due date timelines (days)
|
|
38
|
+
self.default_timelines = {
|
|
39
|
+
regscale_models.IssueSeverity.Critical: 30,
|
|
40
|
+
regscale_models.IssueSeverity.High: 60,
|
|
41
|
+
regscale_models.IssueSeverity.Moderate: 120,
|
|
42
|
+
regscale_models.IssueSeverity.Low: 364,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Load integration-specific timelines from config
|
|
46
|
+
self.integration_timelines = self._load_integration_timelines()
|
|
47
|
+
|
|
48
|
+
def _load_integration_timelines(self) -> Dict[regscale_models.IssueSeverity, int]:
|
|
49
|
+
"""
|
|
50
|
+
Load timeline configurations for this integration from init.yaml
|
|
51
|
+
|
|
52
|
+
:return: Dictionary mapping severity to days
|
|
53
|
+
:rtype: Dict[regscale_models.IssueSeverity, int]
|
|
54
|
+
"""
|
|
55
|
+
timelines = self.default_timelines.copy()
|
|
56
|
+
|
|
57
|
+
issues_config = self.config.get("issues", {})
|
|
58
|
+
integration_config = issues_config.get(self.integration_name, {})
|
|
59
|
+
|
|
60
|
+
if integration_config:
|
|
61
|
+
logger.debug(f"Found timeline config for {self.integration_name}: {integration_config}")
|
|
62
|
+
|
|
63
|
+
# Map config keys to severity levels
|
|
64
|
+
severity_mapping = {
|
|
65
|
+
"critical": regscale_models.IssueSeverity.Critical,
|
|
66
|
+
"high": regscale_models.IssueSeverity.High,
|
|
67
|
+
"moderate": regscale_models.IssueSeverity.Moderate,
|
|
68
|
+
"medium": regscale_models.IssueSeverity.Moderate, # Some integrations use 'medium'
|
|
69
|
+
"low": regscale_models.IssueSeverity.Low,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for config_key, severity in severity_mapping.items():
|
|
73
|
+
if config_key in integration_config:
|
|
74
|
+
timelines[severity] = integration_config[config_key]
|
|
75
|
+
|
|
76
|
+
return timelines
|
|
77
|
+
|
|
78
|
+
def _get_kev_data(self) -> ThreadSafeDict:
|
|
79
|
+
"""
|
|
80
|
+
Get KEV data from CISA, using cache if available
|
|
81
|
+
|
|
82
|
+
:return: Thread-safe dictionary containing KEV data
|
|
83
|
+
:rtype: ThreadSafeDict
|
|
84
|
+
"""
|
|
85
|
+
if self._kev_data is None:
|
|
86
|
+
try:
|
|
87
|
+
kev_data = pull_cisa_kev()
|
|
88
|
+
self._kev_data = ThreadSafeDict()
|
|
89
|
+
self._kev_data.update(kev_data)
|
|
90
|
+
logger.debug("Loaded KEV data from CISA")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"Failed to load KEV data: {e}")
|
|
93
|
+
self._kev_data = ThreadSafeDict()
|
|
94
|
+
|
|
95
|
+
return self._kev_data
|
|
96
|
+
|
|
97
|
+
def _should_use_kev(self) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Check if this integration should use KEV dates
|
|
100
|
+
|
|
101
|
+
:return: True if KEV should be used for this integration
|
|
102
|
+
:rtype: bool
|
|
103
|
+
"""
|
|
104
|
+
issues_config = self.config.get("issues", {})
|
|
105
|
+
integration_config = issues_config.get(self.integration_name, {})
|
|
106
|
+
return integration_config.get("useKev", True) # Default to True if not specified
|
|
107
|
+
|
|
108
|
+
def _get_kev_due_date(self, cve: str) -> Optional[str]:
|
|
109
|
+
"""
|
|
110
|
+
Get the KEV due date for a specific CVE
|
|
111
|
+
|
|
112
|
+
:param str cve: The CVE identifier
|
|
113
|
+
:return: KEV due date string if found, None otherwise
|
|
114
|
+
:rtype: Optional[str]
|
|
115
|
+
"""
|
|
116
|
+
if not self._should_use_kev() or not cve:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
kev_data = self._get_kev_data()
|
|
120
|
+
|
|
121
|
+
# Find the KEV entry for this CVE
|
|
122
|
+
kev_entry = next(
|
|
123
|
+
(entry for entry in kev_data.get("vulnerabilities", []) if entry.get("cveID", "").upper() == cve.upper()),
|
|
124
|
+
None,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if kev_entry:
|
|
128
|
+
kev_due_date = kev_entry.get("dueDate")
|
|
129
|
+
if kev_due_date:
|
|
130
|
+
logger.debug(f"Found KEV due date for {cve}: {kev_due_date}")
|
|
131
|
+
return kev_due_date
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def calculate_due_date(
|
|
136
|
+
self,
|
|
137
|
+
severity: regscale_models.IssueSeverity,
|
|
138
|
+
created_date: str,
|
|
139
|
+
cve: Optional[str] = None,
|
|
140
|
+
title: Optional[str] = None,
|
|
141
|
+
) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Calculate the due date for an issue based on severity, KEV status, and integration config
|
|
144
|
+
|
|
145
|
+
:param regscale_models.IssueSeverity severity: The severity of the issue
|
|
146
|
+
:param str created_date: The creation date of the issue
|
|
147
|
+
:param Optional[str] cve: The CVE identifier (if applicable)
|
|
148
|
+
:param Optional[str] title: The title of the issue (for additional context)
|
|
149
|
+
:return: The calculated due date string
|
|
150
|
+
:rtype: str
|
|
151
|
+
"""
|
|
152
|
+
# First, check if this CVE has a KEV due date
|
|
153
|
+
if cve:
|
|
154
|
+
kev_due_date = self._get_kev_due_date(cve)
|
|
155
|
+
if kev_due_date:
|
|
156
|
+
# Parse the KEV due date and created date
|
|
157
|
+
try:
|
|
158
|
+
from dateutil.parser import parse as date_parse
|
|
159
|
+
|
|
160
|
+
kev_date = date_parse(kev_due_date).date()
|
|
161
|
+
created_dt = date_parse(created_date).date()
|
|
162
|
+
|
|
163
|
+
# If KEV due date is after creation date, use KEV date
|
|
164
|
+
# If KEV due date is before creation date, add the difference to creation date
|
|
165
|
+
if kev_date >= created_dt:
|
|
166
|
+
logger.debug(f"Using KEV due date {kev_due_date} for CVE {cve}")
|
|
167
|
+
return kev_due_date
|
|
168
|
+
else:
|
|
169
|
+
# KEV date has passed, calculate new due date from creation
|
|
170
|
+
days_diff = (created_dt - kev_date).days
|
|
171
|
+
# Give at least 30 days from creation for critical KEV items
|
|
172
|
+
adjusted_days = max(30, days_diff)
|
|
173
|
+
due_date = date_str(get_day_increment(start=created_date, days=adjusted_days))
|
|
174
|
+
logger.debug(f"KEV date passed, using adjusted due date {due_date} for CVE {cve}")
|
|
175
|
+
return due_date
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.warning(f"Failed to parse KEV due date {kev_due_date}: {e}")
|
|
179
|
+
|
|
180
|
+
# Fall back to severity-based timeline from integration config
|
|
181
|
+
days = self.integration_timelines.get(severity, self.default_timelines[severity])
|
|
182
|
+
due_date = date_str(get_day_increment(start=created_date, days=days))
|
|
183
|
+
|
|
184
|
+
logger.debug(f"Using {self.integration_name} timeline: {severity.name} = {days} days, due date = {due_date}")
|
|
185
|
+
|
|
186
|
+
return due_date
|
|
187
|
+
|
|
188
|
+
def get_integration_config(self) -> Dict[str, Any]:
|
|
189
|
+
"""
|
|
190
|
+
Get the full integration configuration from init.yaml
|
|
191
|
+
|
|
192
|
+
:return: Integration configuration dictionary
|
|
193
|
+
:rtype: Dict[str, Any]
|
|
194
|
+
"""
|
|
195
|
+
issues_config = self.config.get("issues", {})
|
|
196
|
+
return issues_config.get(self.integration_name, {})
|
|
197
|
+
|
|
198
|
+
def get_timeline_info(self) -> Dict[str, Any]:
|
|
199
|
+
"""
|
|
200
|
+
Get information about current timeline configuration
|
|
201
|
+
|
|
202
|
+
:return: Dictionary with timeline information
|
|
203
|
+
:rtype: Dict[str, Any]
|
|
204
|
+
"""
|
|
205
|
+
return {
|
|
206
|
+
"integration_name": self.integration_name,
|
|
207
|
+
"use_kev": self._should_use_kev(),
|
|
208
|
+
"timelines": {severity.name: days for severity, days in self.integration_timelines.items()},
|
|
209
|
+
"config_source": "init.yaml",
|
|
210
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""RegScale CLI command to normalize CCI data from XML files."""
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from regscale.core.app.application import Application
|
|
11
|
+
from regscale.core.app.utils.app_utils import error_and_exit
|
|
12
|
+
from regscale.models.regscale_models import Catalog, SecurityControl, CCI
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("regscale")
|
|
15
|
+
|
|
16
|
+
# RegScale date format constant
|
|
17
|
+
REGSCALE_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CCIImporter:
|
|
21
|
+
"""Imports CCI data from XML files and maps to security controls."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, xml_data: ET.Element, version: str = "5", verbose: bool = False):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the CCI importer.
|
|
26
|
+
|
|
27
|
+
:param ET.Element xml_data: The root element of the XML data
|
|
28
|
+
:param str version: NIST version to use for filtering
|
|
29
|
+
:param bool verbose: Whether to output verbose information
|
|
30
|
+
"""
|
|
31
|
+
self.xml_data = xml_data
|
|
32
|
+
self.normalized_cci: Dict[str, List[Dict]] = {}
|
|
33
|
+
self.verbose = verbose
|
|
34
|
+
self.reference_version = version
|
|
35
|
+
self._user_context: Optional[Tuple[Optional[str], int]] = None
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _parse_control_id(ref_index: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Extract the main control_id from a reference index (e.g., 'AC-1 a 1 (b)' -> 'AC-1').
|
|
41
|
+
|
|
42
|
+
:param str ref_index: Reference index string to parse
|
|
43
|
+
:return: Main control ID
|
|
44
|
+
:rtype: str
|
|
45
|
+
"""
|
|
46
|
+
parts = ref_index.strip().split()
|
|
47
|
+
return parts[0] if parts else ""
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _extract_cci_data(cci_item: ET.Element) -> Tuple[Optional[str], str]:
|
|
51
|
+
"""
|
|
52
|
+
Extract CCI ID and definition from CCI item.
|
|
53
|
+
|
|
54
|
+
:param ET.Element cci_item: XML element containing CCI data
|
|
55
|
+
:return: Tuple of (cci_id, definition)
|
|
56
|
+
:rtype: Tuple[Optional[str], str]
|
|
57
|
+
"""
|
|
58
|
+
cci_id = cci_item.get("id")
|
|
59
|
+
definition_elem = cci_item.find(".//{http://iase.disa.mil/cci}definition")
|
|
60
|
+
definition = definition_elem.text if definition_elem is not None and definition_elem.text else ""
|
|
61
|
+
return cci_id, definition
|
|
62
|
+
|
|
63
|
+
def _process_references(self, references: List[ET.Element], cci_id: str, definition: str) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Process reference elements and add to normalized CCI data.
|
|
66
|
+
|
|
67
|
+
:param List[ET.Element] references: List of reference XML elements
|
|
68
|
+
:param str cci_id: CCI identifier
|
|
69
|
+
:param str definition: CCI definition text
|
|
70
|
+
:rtype: None
|
|
71
|
+
"""
|
|
72
|
+
for ref in references:
|
|
73
|
+
if not self._is_valid_reference(ref):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
ref_index = ref.get("index")
|
|
77
|
+
if ref_index:
|
|
78
|
+
main_control = self._parse_control_id(ref_index)
|
|
79
|
+
self._add_cci_to_control(main_control, cci_id, definition)
|
|
80
|
+
|
|
81
|
+
def _is_valid_reference(self, ref: ET.Element) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Check if reference matches the target version.
|
|
84
|
+
|
|
85
|
+
:param ET.Element ref: Reference XML element
|
|
86
|
+
:return: True if reference version matches target version
|
|
87
|
+
:rtype: bool
|
|
88
|
+
"""
|
|
89
|
+
ref_version = ref.get("version")
|
|
90
|
+
return ref_version is not None and ref_version == self.reference_version
|
|
91
|
+
|
|
92
|
+
def _add_cci_to_control(self, main_control: str, cci_id: str, definition: str) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Add CCI data to the normalized structure for a control.
|
|
95
|
+
|
|
96
|
+
:param str main_control: Control identifier
|
|
97
|
+
:param str cci_id: CCI identifier
|
|
98
|
+
:param str definition: CCI definition
|
|
99
|
+
:rtype: None
|
|
100
|
+
"""
|
|
101
|
+
if main_control not in self.normalized_cci:
|
|
102
|
+
self.normalized_cci[main_control] = []
|
|
103
|
+
self.normalized_cci[main_control].append({"cci_id": cci_id, "definition": definition})
|
|
104
|
+
|
|
105
|
+
def parse_cci(self) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Parse CCI items from XML and normalize them, mapping to parent control only.
|
|
108
|
+
|
|
109
|
+
:rtype: None
|
|
110
|
+
"""
|
|
111
|
+
if self.verbose:
|
|
112
|
+
click.echo("Parsing CCI items from XML...")
|
|
113
|
+
|
|
114
|
+
for cci_item in self.xml_data.findall(".//{http://iase.disa.mil/cci}cci_item"):
|
|
115
|
+
cci_id, definition = self._extract_cci_data(cci_item)
|
|
116
|
+
if not cci_id:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
references = cci_item.findall(".//{http://iase.disa.mil/cci}reference")
|
|
120
|
+
self._process_references(references, cci_id, definition)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _get_catalog(catalog_id: int) -> Catalog:
|
|
124
|
+
"""
|
|
125
|
+
Get the catalog with specified ID.
|
|
126
|
+
|
|
127
|
+
:param int catalog_id: ID of the catalog to retrieve
|
|
128
|
+
:return: Catalog instance
|
|
129
|
+
:rtype: Catalog
|
|
130
|
+
:raises SystemExit: If catalog not found
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
catalog = Catalog.get(id=catalog_id)
|
|
134
|
+
if catalog is None:
|
|
135
|
+
error_and_exit(f"Catalog with id {catalog_id} not found. Please ensure the catalog exists.")
|
|
136
|
+
return catalog
|
|
137
|
+
except Exception:
|
|
138
|
+
error_and_exit(f"Catalog with id {catalog_id} not found. Please ensure the catalog exists.")
|
|
139
|
+
|
|
140
|
+
def _get_user_context(self) -> Tuple[Optional[str], int]:
|
|
141
|
+
"""
|
|
142
|
+
Get user ID and tenant ID from application config.
|
|
143
|
+
|
|
144
|
+
:return: Tuple of (user_id, tenant_id)
|
|
145
|
+
:rtype: Tuple[Optional[str], int]
|
|
146
|
+
"""
|
|
147
|
+
if self._user_context is None:
|
|
148
|
+
app = Application()
|
|
149
|
+
user_id = app.config.get("userId")
|
|
150
|
+
tenant_id = app.config.get("tenantId", 1)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
user_id = str(user_id) if user_id else None
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
user_id = None
|
|
156
|
+
if self.verbose:
|
|
157
|
+
logger.warning("userId in config is not set or invalid; created_by will be None.")
|
|
158
|
+
|
|
159
|
+
# Convert tenant_id to int if it's a string
|
|
160
|
+
try:
|
|
161
|
+
tenant_id = int(tenant_id)
|
|
162
|
+
except (TypeError, ValueError):
|
|
163
|
+
tenant_id = 1
|
|
164
|
+
if self.verbose:
|
|
165
|
+
logger.warning("tenantId in config is not valid; using default value 1.")
|
|
166
|
+
|
|
167
|
+
self._user_context = (user_id, tenant_id)
|
|
168
|
+
|
|
169
|
+
return self._user_context
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _find_existing_cci(control_id: int, cci_id: str) -> Optional[CCI]:
|
|
173
|
+
"""
|
|
174
|
+
Find existing CCI by ID within a control.
|
|
175
|
+
|
|
176
|
+
:param int control_id: Security control ID
|
|
177
|
+
:param str cci_id: CCI identifier to search for
|
|
178
|
+
:return: Existing CCI instance or None
|
|
179
|
+
:rtype: Optional[CCI]
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
existing_ccis: List[CCI] = CCI.get_all_by_parent(parent_id=control_id)
|
|
183
|
+
for existing in existing_ccis:
|
|
184
|
+
if existing.uuid == cci_id:
|
|
185
|
+
return existing
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _create_cci_data(
|
|
192
|
+
cci_id: str, definition: str, user_id: Optional[str], tenant_id: int, current_time: str
|
|
193
|
+
) -> Dict:
|
|
194
|
+
"""
|
|
195
|
+
Create common CCI data structure.
|
|
196
|
+
|
|
197
|
+
:param str cci_id: CCI identifier
|
|
198
|
+
:param str definition: CCI definition
|
|
199
|
+
:param Optional[str] user_id: User ID
|
|
200
|
+
:param int tenant_id: Tenant ID
|
|
201
|
+
:param str current_time: Current timestamp string
|
|
202
|
+
:return: Dictionary with common CCI attributes
|
|
203
|
+
:rtype: Dict
|
|
204
|
+
"""
|
|
205
|
+
return {
|
|
206
|
+
"name": cci_id,
|
|
207
|
+
"description": definition,
|
|
208
|
+
"controlType": "policy",
|
|
209
|
+
"publishDate": current_time,
|
|
210
|
+
"dateLastUpdated": current_time,
|
|
211
|
+
"lastUpdatedById": user_id,
|
|
212
|
+
"isPublic": True,
|
|
213
|
+
"tenantsId": tenant_id,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _update_existing_cci(existing_cci: CCI, cci_data: Dict) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Update an existing CCI with new data.
|
|
220
|
+
|
|
221
|
+
:param CCI existing_cci: CCI instance to update
|
|
222
|
+
:param Dict cci_data: Dictionary with CCI attributes
|
|
223
|
+
:rtype: None
|
|
224
|
+
"""
|
|
225
|
+
for key, value in cci_data.items():
|
|
226
|
+
setattr(existing_cci, key, value)
|
|
227
|
+
existing_cci.create_or_update()
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _create_new_cci(cci_id: str, cci_data: Dict, control_id: int, user_id: Optional[str], current_time: str) -> CCI:
|
|
231
|
+
"""
|
|
232
|
+
Create a new CCI instance.
|
|
233
|
+
|
|
234
|
+
:param str cci_id: CCI identifier
|
|
235
|
+
:param Dict cci_data: Dictionary with common CCI attributes
|
|
236
|
+
:param int control_id: Security control ID
|
|
237
|
+
:param Optional[str] user_id: User ID
|
|
238
|
+
:param str current_time: Current timestamp string
|
|
239
|
+
:return: Created CCI instance
|
|
240
|
+
:rtype: CCI
|
|
241
|
+
"""
|
|
242
|
+
new_cci = CCI(
|
|
243
|
+
uuid=cci_id,
|
|
244
|
+
securityControlId=control_id,
|
|
245
|
+
createdById=user_id,
|
|
246
|
+
dateCreated=current_time,
|
|
247
|
+
**cci_data,
|
|
248
|
+
)
|
|
249
|
+
new_cci.create()
|
|
250
|
+
return new_cci
|
|
251
|
+
|
|
252
|
+
def _process_cci_for_control(
|
|
253
|
+
self, control_id: int, cci_list: List[Dict], user_id: Optional[str], tenant_id: int
|
|
254
|
+
) -> Tuple[int, int]:
|
|
255
|
+
"""
|
|
256
|
+
Process all CCI items for a specific control.
|
|
257
|
+
|
|
258
|
+
:param int control_id: Security control ID
|
|
259
|
+
:param List[Dict] cci_list: List of CCI data dictionaries
|
|
260
|
+
:param Optional[str] user_id: User ID
|
|
261
|
+
:param int tenant_id: Tenant ID
|
|
262
|
+
:return: Tuple of (created_count, updated_count)
|
|
263
|
+
:rtype: Tuple[int, int]
|
|
264
|
+
"""
|
|
265
|
+
created_count = 0
|
|
266
|
+
updated_count = 0
|
|
267
|
+
current_time = datetime.datetime.now().strftime(REGSCALE_DATE_FORMAT)
|
|
268
|
+
|
|
269
|
+
for cci in cci_list:
|
|
270
|
+
cci_id = cci["cci_id"]
|
|
271
|
+
definition = cci["definition"]
|
|
272
|
+
|
|
273
|
+
existing_cci = self._find_existing_cci(control_id, cci_id)
|
|
274
|
+
cci_data = self._create_cci_data(cci_id, definition, user_id, tenant_id, current_time)
|
|
275
|
+
|
|
276
|
+
if existing_cci:
|
|
277
|
+
self._update_existing_cci(existing_cci, cci_data)
|
|
278
|
+
updated_count += 1
|
|
279
|
+
else:
|
|
280
|
+
self._create_new_cci(cci_id, cci_data, control_id, user_id, current_time)
|
|
281
|
+
created_count += 1
|
|
282
|
+
|
|
283
|
+
return created_count, updated_count
|
|
284
|
+
|
|
285
|
+
def map_to_security_controls(self, catalog_id: int = 1) -> Dict[str, int]:
|
|
286
|
+
"""
|
|
287
|
+
Map normalized CCI data to security controls in the database.
|
|
288
|
+
|
|
289
|
+
:param int catalog_id: ID of the catalog containing security controls (default: 1)
|
|
290
|
+
:return: Dictionary with operation statistics
|
|
291
|
+
:rtype: Dict[str, int]
|
|
292
|
+
"""
|
|
293
|
+
if self.verbose:
|
|
294
|
+
click.echo("Mapping CCI data to security controls...")
|
|
295
|
+
|
|
296
|
+
catalog = self._get_catalog(catalog_id)
|
|
297
|
+
security_controls: List[SecurityControl] = SecurityControl.get_all_by_parent(parent_id=catalog.id)
|
|
298
|
+
control_map = {sc.controlId: sc.id for sc in security_controls}
|
|
299
|
+
|
|
300
|
+
user_id, tenant_id = self._get_user_context()
|
|
301
|
+
|
|
302
|
+
created_count = 0
|
|
303
|
+
updated_count = 0
|
|
304
|
+
skipped_count = 0
|
|
305
|
+
|
|
306
|
+
for main_control, cci_list in self.normalized_cci.items():
|
|
307
|
+
if main_control in control_map:
|
|
308
|
+
control_id = control_map[main_control]
|
|
309
|
+
control_created, control_updated = self._process_cci_for_control(
|
|
310
|
+
control_id, cci_list, user_id, tenant_id
|
|
311
|
+
)
|
|
312
|
+
created_count += control_created
|
|
313
|
+
updated_count += control_updated
|
|
314
|
+
else:
|
|
315
|
+
skipped_count += len(cci_list)
|
|
316
|
+
if self.verbose:
|
|
317
|
+
click.echo(f"Warning: Control not found for key: {main_control}", err=True)
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"created": created_count,
|
|
321
|
+
"updated": updated_count,
|
|
322
|
+
"skipped": skipped_count,
|
|
323
|
+
"total_processed": len(self.normalized_cci),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
def get_normalized_cci(self) -> Dict[str, List[Dict]]:
|
|
327
|
+
"""
|
|
328
|
+
Get the normalized CCI data.
|
|
329
|
+
|
|
330
|
+
:return: Dictionary of normalized CCI data
|
|
331
|
+
:rtype: Dict[str, List[Dict]]
|
|
332
|
+
"""
|
|
333
|
+
return self.normalized_cci
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _load_xml_file(xml_file: str) -> ET.Element:
|
|
337
|
+
"""
|
|
338
|
+
Load and parse XML file.
|
|
339
|
+
|
|
340
|
+
:param str xml_file: Path to XML file
|
|
341
|
+
:return: Root element of parsed XML
|
|
342
|
+
:rtype: ET.Element
|
|
343
|
+
:raises click.ClickException: If XML parsing fails
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
click.echo(f"Loading XML file: {xml_file}")
|
|
347
|
+
tree = ET.parse(xml_file)
|
|
348
|
+
return tree.getroot()
|
|
349
|
+
except ET.ParseError as e:
|
|
350
|
+
click.echo(click.style(f"Failed to parse XML file: {e}", fg="red"), err=True)
|
|
351
|
+
error_and_exit(f"Failed to parse XML file: {e}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _display_verbose_output(normalized_data: Dict[str, List[Dict]]) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Display detailed normalized CCI data.
|
|
357
|
+
|
|
358
|
+
:param Dict[str, List[Dict]] normalized_data: Dictionary of normalized CCI data
|
|
359
|
+
:rtype: None
|
|
360
|
+
"""
|
|
361
|
+
click.echo("\nNormalized CCI Data:")
|
|
362
|
+
for key, value in normalized_data.items():
|
|
363
|
+
click.echo(f" {key}: {len(value)} CCI items")
|
|
364
|
+
for cci in value:
|
|
365
|
+
definition_preview = cci["definition"][:100] + "..." if len(cci["definition"]) > 100 else cci["definition"]
|
|
366
|
+
click.echo(f" - {cci['cci_id']}: {definition_preview}")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _display_results(stats: Dict[str, int]) -> None:
|
|
370
|
+
"""
|
|
371
|
+
Display database operation results.
|
|
372
|
+
|
|
373
|
+
:param Dict[str, int] stats: Dictionary with operation statistics
|
|
374
|
+
:rtype: None
|
|
375
|
+
"""
|
|
376
|
+
click.echo(
|
|
377
|
+
click.style(
|
|
378
|
+
f"\nDatabase operations completed:"
|
|
379
|
+
f"\n - Created: {stats['created']}"
|
|
380
|
+
f"\n - Updated: {stats['updated']}"
|
|
381
|
+
f"\n - Skipped: {stats['skipped']}"
|
|
382
|
+
f"\n - Total processed: {stats['total_processed']}",
|
|
383
|
+
fg="green",
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _process_cci_import(importer: CCIImporter, dry_run: bool, verbose: bool, catalog_id: int) -> None:
|
|
389
|
+
"""
|
|
390
|
+
Process CCI import with optional database operations.
|
|
391
|
+
|
|
392
|
+
:param CCIImporter importer: CCIImporter instance
|
|
393
|
+
:param bool dry_run: Whether to skip database operations
|
|
394
|
+
:param bool verbose: Whether to display verbose output
|
|
395
|
+
:param int catalog_id: ID of the catalog containing security controls
|
|
396
|
+
:rtype: None
|
|
397
|
+
"""
|
|
398
|
+
importer.parse_cci()
|
|
399
|
+
normalized_data = importer.get_normalized_cci()
|
|
400
|
+
|
|
401
|
+
click.echo(click.style(f"Successfully parsed {len(normalized_data)} normalized CCI entries", fg="green"))
|
|
402
|
+
|
|
403
|
+
if verbose:
|
|
404
|
+
_display_verbose_output(normalized_data)
|
|
405
|
+
|
|
406
|
+
if not dry_run:
|
|
407
|
+
stats = importer.map_to_security_controls(catalog_id)
|
|
408
|
+
_display_results(stats)
|
|
409
|
+
else:
|
|
410
|
+
click.echo(click.style("\nDRY RUN MODE: No database changes were made", fg="yellow"))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@click.command(name="cci_importer")
|
|
414
|
+
@click.option(
|
|
415
|
+
"--xml_file", "-f", type=click.Path(exists=True), default=None, required=False, help="Path to the CCI XML file."
|
|
416
|
+
)
|
|
417
|
+
@click.option("--dry-run", "-d", is_flag=True, help="Parse and display normalized data without saving to database")
|
|
418
|
+
@click.option("--verbose", "-v", is_flag=True, help="Display detailed output including all normalized CCI data")
|
|
419
|
+
@click.option(
|
|
420
|
+
"--nist-version", "-n", type=click.Choice(["4", "5"]), default="5", help="NIST 800-53 Revision version (default: 5)"
|
|
421
|
+
)
|
|
422
|
+
@click.option(
|
|
423
|
+
"--catalog-id", "-c", type=click.INT, default=1, help="ID of the catalog containing security controls (default: 1)"
|
|
424
|
+
)
|
|
425
|
+
def cci_importer(xml_file: str, dry_run: bool, verbose: bool, nist_version: str, catalog_id: int) -> None:
|
|
426
|
+
"""Import CCI data from XML files and map to security controls.
|
|
427
|
+
|
|
428
|
+
If no XML file is specified, defaults to 'artifacts/U_CCI_List.xml' in the project directory.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
if not xml_file:
|
|
433
|
+
import importlib.resources as pkg_resources
|
|
434
|
+
|
|
435
|
+
xml_file = pkg_resources.path("regscale.models.integration_models", "CCI_List.xml")
|
|
436
|
+
root = _load_xml_file(xml_file)
|
|
437
|
+
importer = CCIImporter(root, version=nist_version, verbose=verbose)
|
|
438
|
+
_process_cci_import(importer, dry_run, verbose, catalog_id)
|
|
439
|
+
|
|
440
|
+
except click.ClickException:
|
|
441
|
+
raise
|
|
442
|
+
except Exception as e:
|
|
443
|
+
click.echo(click.style(f"Unexpected error: {e}", fg="red"), err=True)
|
|
444
|
+
raise click.ClickException(f"Unexpected error: {e}")
|