regscale-cli 6.19.0.1__py3-none-any.whl → 6.19.2.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/__init__.py +1 -1
- regscale/core/app/utils/app_utils.py +1 -1
- regscale/integrations/commercial/amazon/common.py +5 -4
- regscale/integrations/commercial/aws/scanner.py +3 -2
- regscale/integrations/commercial/synqly/assets.py +10 -0
- regscale/integrations/commercial/synqly/ticketing.py +25 -0
- regscale/integrations/commercial/tenablev2/commands.py +34 -4
- regscale/integrations/commercial/tenablev2/sync_compliance.py +550 -0
- regscale/integrations/commercial/wizv2/click.py +3 -3
- regscale/integrations/scanner_integration.py +3 -2
- regscale/models/app_models/import_validater.py +2 -0
- regscale/models/integration_models/cisa_kev_data.json +188 -10
- regscale/models/integration_models/flat_file_importer/__init__.py +26 -9
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/assessment_plan.py +1 -1
- regscale/models/regscale_models/assessment_result.py +39 -0
- regscale/models/regscale_models/line_of_inquiry.py +2 -2
- regscale/models/regscale_models/regscale_model.py +16 -15
- regscale/models/regscale_models/software_inventory.py +1 -1
- regscale/models/regscale_models/supply_chain.py +4 -4
- regscale/models/regscale_models/user.py +11 -0
- regscale/utils/graphql_client.py +2 -1
- {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/METADATA +45 -45
- {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/RECORD +28 -26
- {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync the compliance data from Tenable.io to RegScale
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from regscale.core.app.api import Api
|
|
15
|
+
from regscale.core.app.application import Application
|
|
16
|
+
from regscale.core.app.utils.app_utils import (
|
|
17
|
+
format_dict_to_html,
|
|
18
|
+
get_current_datetime,
|
|
19
|
+
)
|
|
20
|
+
from regscale.models.integration_models.tenable_models.models import AssetCheck
|
|
21
|
+
from regscale.models.regscale_models import ControlImplementation
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("regscale")
|
|
24
|
+
console = Console()
|
|
25
|
+
artifacts_dir = "./artifacts"
|
|
26
|
+
REGSCALE_INC = "RegScale, Inc."
|
|
27
|
+
REGSCALE_CLI = "RegScale CLI"
|
|
28
|
+
FULLY_IMPLEMENTED = "Fully Implemented"
|
|
29
|
+
NOT_IMPLEMENTED = "Not Implemented"
|
|
30
|
+
IN_REMEDIATION = "In Remediation"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def sync_compliance_data(ssp_id: int, catalog_id: int, framework: str, offline: Optional[Path] = None) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Sync the compliance data from Tenable.io to create control implementations for controls in frameworks
|
|
36
|
+
:param int ssp_id: The ID number from RegScale of the System Security Plan
|
|
37
|
+
:param int catalog_id: The ID number from RegScale Catalog that the System Security Plan's controls belong to
|
|
38
|
+
:param str framework: The framework to use. from Tenable.io frameworks MUST be the same RegScale Catalog of controls
|
|
39
|
+
:param Optional[Path] offline: The file path to load control data instead of fetching from Tenable.io, defaults to None
|
|
40
|
+
:rtype: None
|
|
41
|
+
"""
|
|
42
|
+
logger.info("Note: This command only available for Tenable.io")
|
|
43
|
+
logger.info("Note: This command Requires admin access.")
|
|
44
|
+
app = Application()
|
|
45
|
+
config = app.config
|
|
46
|
+
# we specifically don't gen client here, so we only get the client for Tenable.io as its only supported there
|
|
47
|
+
|
|
48
|
+
compliance_data = _get_compliance_data(config=config, offline=offline) # type: ignore
|
|
49
|
+
|
|
50
|
+
dict_of_frameworks_and_asset_checks: Dict = dict()
|
|
51
|
+
framework_controls: Dict[str, List[str]] = {}
|
|
52
|
+
asset_checks: Dict[str, List[AssetCheck]] = {}
|
|
53
|
+
passing_controls: Dict = dict()
|
|
54
|
+
# partial_passing_controls: Dict = dict()
|
|
55
|
+
failing_controls: Dict = dict()
|
|
56
|
+
for findings in compliance_data:
|
|
57
|
+
asset_check = AssetCheck(**findings)
|
|
58
|
+
for ref in asset_check.reference:
|
|
59
|
+
if ref.framework not in framework_controls:
|
|
60
|
+
framework_controls[ref.framework] = []
|
|
61
|
+
if ref.control not in framework_controls[ref.framework]: # Avoid duplicate controls
|
|
62
|
+
framework_controls[ref.framework].append(ref.control)
|
|
63
|
+
formatted_control_id = convert_control_id(ref.control)
|
|
64
|
+
# sort controls by status
|
|
65
|
+
add_control_to_status_dict(
|
|
66
|
+
control_id=formatted_control_id,
|
|
67
|
+
status=asset_check.status,
|
|
68
|
+
dict_obj=failing_controls,
|
|
69
|
+
desired_status="FAILED",
|
|
70
|
+
)
|
|
71
|
+
add_control_to_status_dict(
|
|
72
|
+
control_id=formatted_control_id,
|
|
73
|
+
status=asset_check.status,
|
|
74
|
+
dict_obj=passing_controls,
|
|
75
|
+
desired_status="PASSED",
|
|
76
|
+
)
|
|
77
|
+
remove_passing_controls_if_in_failed_status(passing=passing_controls, failing=failing_controls)
|
|
78
|
+
if formatted_control_id not in asset_checks:
|
|
79
|
+
asset_checks[formatted_control_id] = [asset_check]
|
|
80
|
+
else:
|
|
81
|
+
asset_checks[formatted_control_id].append(asset_check)
|
|
82
|
+
dict_of_frameworks_and_asset_checks = {
|
|
83
|
+
key: {"controls": framework_controls, "asset_checks": asset_checks} for key in framework_controls.keys()
|
|
84
|
+
}
|
|
85
|
+
logger.info(f"Found {len(dict_of_frameworks_and_asset_checks)} findings to process")
|
|
86
|
+
framework_data = dict_of_frameworks_and_asset_checks.get(framework, None)
|
|
87
|
+
process_compliance_data(
|
|
88
|
+
framework_data=framework_data,
|
|
89
|
+
catalog_id=catalog_id,
|
|
90
|
+
ssp_id=ssp_id,
|
|
91
|
+
framework=framework,
|
|
92
|
+
passing_controls=passing_controls,
|
|
93
|
+
failing_controls=failing_controls,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _get_compliance_data(config: dict, offline: Optional[Path] = None) -> Dict:
|
|
98
|
+
"""
|
|
99
|
+
Get compliance data from Tenable.io
|
|
100
|
+
:param dict config: Configuration dictionary
|
|
101
|
+
:param Optional[Path] offline: File path to load control data instead of fetching from Tenable.io
|
|
102
|
+
:return: Compliance data
|
|
103
|
+
:rtype: Dict
|
|
104
|
+
"""
|
|
105
|
+
from regscale import __version__
|
|
106
|
+
from tenable.io import TenableIO
|
|
107
|
+
|
|
108
|
+
if offline:
|
|
109
|
+
with open(offline.absolute(), "r") as f:
|
|
110
|
+
compliance_data = json.load(f)
|
|
111
|
+
else:
|
|
112
|
+
client = TenableIO(
|
|
113
|
+
url=config["tenableUrl"],
|
|
114
|
+
access_key=config["tenableAccessKey"],
|
|
115
|
+
secret_key=config["tenableSecretKey"],
|
|
116
|
+
vendor=REGSCALE_INC,
|
|
117
|
+
product=REGSCALE_CLI,
|
|
118
|
+
build=__version__,
|
|
119
|
+
)
|
|
120
|
+
compliance_data = client.exports.compliance()
|
|
121
|
+
return compliance_data
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def add_control_to_status_dict(control_id: str, status: str, dict_obj: Dict, desired_status: str) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Add a control to a status dictionary
|
|
127
|
+
:param str control_id: The control id to add to the dictionary
|
|
128
|
+
:param str status: The status of the control
|
|
129
|
+
:param Dict dict_obj: The dictionary to add the control to
|
|
130
|
+
:param str desired_status: The desired status of the control
|
|
131
|
+
:rtype: None
|
|
132
|
+
"""
|
|
133
|
+
friendly_control_id = control_id.lower()
|
|
134
|
+
if status == desired_status and friendly_control_id not in dict_obj:
|
|
135
|
+
dict_obj[friendly_control_id] = desired_status
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def remove_passing_controls_if_in_failed_status(passing: Dict, failing: Dict) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Remove passing controls if they are in failed status
|
|
141
|
+
:param Dict passing: Dictionary of passing controls
|
|
142
|
+
:param Dict failing: Dictionary of failing controls
|
|
143
|
+
:rtype: None
|
|
144
|
+
"""
|
|
145
|
+
to_remove = []
|
|
146
|
+
for k in passing.keys():
|
|
147
|
+
if k in failing.keys():
|
|
148
|
+
to_remove.append(k)
|
|
149
|
+
|
|
150
|
+
for k in to_remove:
|
|
151
|
+
del passing[k]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def process_compliance_data(
|
|
155
|
+
framework_data: Dict,
|
|
156
|
+
catalog_id: int,
|
|
157
|
+
ssp_id: int,
|
|
158
|
+
framework: str,
|
|
159
|
+
passing_controls: Dict,
|
|
160
|
+
failing_controls: Dict,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Processes the compliance data from Tenable.io to create control implementations for controls in frameworks
|
|
164
|
+
:param Dict framework_data: List of tenable.io controls per framework
|
|
165
|
+
:param int catalog_id: The catalog id
|
|
166
|
+
:param int ssp_id: The ssp id
|
|
167
|
+
:param str framework: The framework name
|
|
168
|
+
:param Dict passing_controls: Dictionary of passing controls
|
|
169
|
+
:param Dict failing_controls: Dictionary of failing controls
|
|
170
|
+
:rtype: None
|
|
171
|
+
"""
|
|
172
|
+
if not framework_data:
|
|
173
|
+
return
|
|
174
|
+
framework_controls = framework_data.get("controls", {})
|
|
175
|
+
asset_checks = framework_data.get("asset_checks", {})
|
|
176
|
+
existing_implementation_dict = get_existing_control_implementations(ssp_id)
|
|
177
|
+
catalog_controls = get_controls(catalog_id)
|
|
178
|
+
matched_controls = []
|
|
179
|
+
for tenable_framework, tenable_controls in framework_controls.items():
|
|
180
|
+
logger.info(f"Found {len(tenable_controls)} controls that passed for framework: {tenable_framework}")
|
|
181
|
+
# logger.info(f"tenable_controls: {tenable_controls[0]}") if len(tenable_controls) >0 else None
|
|
182
|
+
if tenable_framework == framework:
|
|
183
|
+
matched_controls = get_matched_controls(tenable_controls, catalog_controls)
|
|
184
|
+
|
|
185
|
+
logger.info(f"Found {len(matched_controls)} controls that matched")
|
|
186
|
+
|
|
187
|
+
control_implementations = create_control_implementations(
|
|
188
|
+
controls=matched_controls,
|
|
189
|
+
parent_id=ssp_id,
|
|
190
|
+
parent_module="securityplans",
|
|
191
|
+
existing_implementation_dict=existing_implementation_dict,
|
|
192
|
+
passing_controls=passing_controls,
|
|
193
|
+
failing_controls=failing_controls,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
logger.info(f"SSP now has {len(control_implementations)} control implementations")
|
|
197
|
+
catalog_controls_dict = {c["id"]: c for c in catalog_controls}
|
|
198
|
+
create_assessments(control_implementations, catalog_controls_dict, asset_checks)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def create_assessments(
|
|
202
|
+
control_implementations: List[Dict],
|
|
203
|
+
catalog_controls_dict: Dict,
|
|
204
|
+
asset_checks: Dict,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Create assessments from control implementations
|
|
208
|
+
:param List[Dict] control_implementations: List of control implementations
|
|
209
|
+
:param Dict catalog_controls_dict: Dictionary of catalog controls
|
|
210
|
+
:param Dict asset_checks: Dictionary of asset checks
|
|
211
|
+
:rtype: None
|
|
212
|
+
:return: None
|
|
213
|
+
"""
|
|
214
|
+
app = Application()
|
|
215
|
+
user_id = app.config.get("userId", "")
|
|
216
|
+
assessments_to_create = []
|
|
217
|
+
for cim in control_implementations:
|
|
218
|
+
control = catalog_controls_dict.get(cim["controlID"], {})
|
|
219
|
+
check = asset_checks.get(control["controlId"].lower())
|
|
220
|
+
assessment = create_assessment_from_cim(cim, user_id, control, check)
|
|
221
|
+
assessments_to_create.append(assessment)
|
|
222
|
+
update_control_implementations(control_implementations, assessments_to_create)
|
|
223
|
+
post_assessments_to_api(assessments_to_create)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_control_assessments(control: Dict, assessments_to_create: List[Dict]) -> List[Dict]:
|
|
227
|
+
"""
|
|
228
|
+
Get control assessments
|
|
229
|
+
:param Dict control: Control
|
|
230
|
+
:param List[Dict] assessments_to_create: List of assessments to create
|
|
231
|
+
:return: List of control assessments
|
|
232
|
+
:rtype: List[Dict]
|
|
233
|
+
"""
|
|
234
|
+
return [
|
|
235
|
+
assess
|
|
236
|
+
for assess in assessments_to_create
|
|
237
|
+
if assess["controlID"] == control["id"] and assess["status"] == "Complete"
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def post_assessments_to_api(assessments_to_create: List[Dict]) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Post assessments to the API
|
|
244
|
+
:param List[Dict] assessments_to_create: List of assessments to create
|
|
245
|
+
:rtype: None
|
|
246
|
+
"""
|
|
247
|
+
app = Application()
|
|
248
|
+
api = Api()
|
|
249
|
+
assessment_url = urljoin(app.config.get("domain", ""), "/api/assessments/batchCreate")
|
|
250
|
+
assessment_response = api.post(url=assessment_url, json=assessments_to_create)
|
|
251
|
+
if assessment_response.ok:
|
|
252
|
+
logger.info(f"Created {len(assessment_response.json())} Assessments!")
|
|
253
|
+
else:
|
|
254
|
+
logger.debug(assessment_response.status_code)
|
|
255
|
+
logger.error(f"Failed to insert Assessment.\n{assessment_response.text}")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def update_control_implementations(control_implementations: List[Dict], assessments_to_create: List[Dict]) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Update control implementations with assessments
|
|
261
|
+
:param List[Dict] control_implementations: List of control implementations
|
|
262
|
+
:param List[Dict] assessments_to_create: List of assessments to create
|
|
263
|
+
:rtype: None
|
|
264
|
+
"""
|
|
265
|
+
for control in control_implementations:
|
|
266
|
+
control_assessments = get_control_assessments(control, assessments_to_create)
|
|
267
|
+
if sorted_assessments := sort_assessments(control_assessments):
|
|
268
|
+
update_control_object(control, sorted_assessments)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def update_control_object(control: Dict, sorted_assessments: List[Dict]) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Update control object
|
|
274
|
+
:param Dict control: Control
|
|
275
|
+
:param List[Dict] sorted_assessments: Sorted assessments
|
|
276
|
+
:rtype: None
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
dt_format = "%Y-%m-%d %H:%M:%S"
|
|
280
|
+
app = Application()
|
|
281
|
+
control["dateLastAssessed"] = sorted_assessments[0]["actualFinish"]
|
|
282
|
+
control["lastAssessmentResult"] = sorted_assessments[0]["assessmentResult"]
|
|
283
|
+
if control.get("lastAssessmentResult"):
|
|
284
|
+
control_obj = ControlImplementation(**control)
|
|
285
|
+
if control_obj.lastAssessmentResult == "Fail" and control_obj.status != IN_REMEDIATION:
|
|
286
|
+
control_obj.status = IN_REMEDIATION
|
|
287
|
+
control_obj.plannedImplementationDate = (datetime.now() + timedelta(30)).strftime(dt_format)
|
|
288
|
+
control_obj.stepsToImplement = "n/a"
|
|
289
|
+
elif control_obj.status == IN_REMEDIATION:
|
|
290
|
+
control_obj.plannedImplementationDate = (
|
|
291
|
+
(datetime.now() + timedelta(30)).strftime(dt_format)
|
|
292
|
+
if not control_obj.plannedImplementationDate
|
|
293
|
+
else control_obj.plannedImplementationDate
|
|
294
|
+
)
|
|
295
|
+
control_obj.stepsToImplement = "n/a" if not control_obj.stepsToImplement else control_obj.stepsToImplement
|
|
296
|
+
elif control_obj.lastAssessmentResult == "Pass" and control_obj.status != FULLY_IMPLEMENTED:
|
|
297
|
+
control_obj.status = FULLY_IMPLEMENTED
|
|
298
|
+
ControlImplementation.update(app=app, implementation=control_obj)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def sort_assessments(control_assessments: List[Dict]) -> List[Dict]:
|
|
302
|
+
"""
|
|
303
|
+
Sort assessments by actual finish date
|
|
304
|
+
:param List[Dict] control_assessments: List of control assessments
|
|
305
|
+
:return: Sorted assessments
|
|
306
|
+
:rtype: List[Dict]
|
|
307
|
+
"""
|
|
308
|
+
dt_format = "%Y-%m-%d %H:%M:%S"
|
|
309
|
+
return sorted(
|
|
310
|
+
control_assessments,
|
|
311
|
+
key=lambda x: datetime.strptime(x["actualFinish"], dt_format),
|
|
312
|
+
reverse=True,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_assessment_status_from_implementation_status(status: str) -> str:
|
|
317
|
+
"""
|
|
318
|
+
Get the assessment status from the implementation status
|
|
319
|
+
:param str status: Implementation status
|
|
320
|
+
:return: Assessment status
|
|
321
|
+
:rtype: str
|
|
322
|
+
"""
|
|
323
|
+
if status == FULLY_IMPLEMENTED:
|
|
324
|
+
return "Pass"
|
|
325
|
+
if status == IN_REMEDIATION:
|
|
326
|
+
return "Fail"
|
|
327
|
+
else:
|
|
328
|
+
return "N/A"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def create_assessment_from_cim(cim: Dict, user_id: str, control: Dict, check: List[AssetCheck]) -> Dict:
|
|
332
|
+
"""
|
|
333
|
+
Create an assessment from a control implementation
|
|
334
|
+
:param Dict cim: Control Implementation
|
|
335
|
+
:param str user_id: User ID
|
|
336
|
+
:param Dict control: Control
|
|
337
|
+
:param List[AssetCheck] check: Asset Check
|
|
338
|
+
:return: Assessment
|
|
339
|
+
:rtype: Dict
|
|
340
|
+
"""
|
|
341
|
+
assessment_result = get_assessment_status_from_implementation_status(cim.get("status"))
|
|
342
|
+
summary_dict = check[0].dict() if check else dict()
|
|
343
|
+
summary_dict.pop("reference", None)
|
|
344
|
+
title = summary_dict.get("check_name") if summary_dict else control.get("title")
|
|
345
|
+
html_summary = format_dict_to_html(summary_dict)
|
|
346
|
+
document_reviewed = check[0].audit_file if check else None
|
|
347
|
+
check_name = check[0].check_name if check else None
|
|
348
|
+
methodology = check[0].check_info if check else None
|
|
349
|
+
summary_of_results = check[0].description if check else None
|
|
350
|
+
uuid = check[0].asset_uuid if check and check[0].asset_uuid is not None else None
|
|
351
|
+
title_part = f"{title} - {uuid}" if uuid else f"{title}"
|
|
352
|
+
uuid_title = f"{title_part} Automated Assessment test"
|
|
353
|
+
return {
|
|
354
|
+
"leadAssessorId": user_id,
|
|
355
|
+
"title": uuid_title,
|
|
356
|
+
"assessmentType": "Control Testing",
|
|
357
|
+
"plannedStart": get_current_datetime(),
|
|
358
|
+
"plannedFinish": get_current_datetime(),
|
|
359
|
+
"status": "Complete",
|
|
360
|
+
"assessmentResult": assessment_result if assessment_result else "N/A",
|
|
361
|
+
"controlID": cim["id"],
|
|
362
|
+
"actualFinish": get_current_datetime(),
|
|
363
|
+
"assessmentReport": html_summary if html_summary else "Passed",
|
|
364
|
+
"parentId": cim["id"],
|
|
365
|
+
"parentModule": "controls",
|
|
366
|
+
"assessmentPlan": check_name if check_name else None,
|
|
367
|
+
"documentsReviewed": document_reviewed if document_reviewed else None,
|
|
368
|
+
"methodology": methodology if methodology else None,
|
|
369
|
+
"summaryOfResults": summary_of_results if summary_of_results else None,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def get_matched_controls(tenable_controls: List[Dict], catalog_controls: List[Dict]) -> List[Dict]:
|
|
374
|
+
"""
|
|
375
|
+
Get controls that match between Tenable and the catalog
|
|
376
|
+
:param List[Dict] tenable_controls: List of controls from Tenable
|
|
377
|
+
:param List[Dict] catalog_controls: List of controls from the catalog
|
|
378
|
+
:return: List of matched controls
|
|
379
|
+
:rtype: List[Dict]
|
|
380
|
+
"""
|
|
381
|
+
matched_controls = []
|
|
382
|
+
for control in tenable_controls:
|
|
383
|
+
formatted_control = convert_control_id(control)
|
|
384
|
+
logger.info(formatted_control)
|
|
385
|
+
for catalog_control in catalog_controls:
|
|
386
|
+
if catalog_control["controlId"].lower() == formatted_control.lower():
|
|
387
|
+
logger.info(f"Catalog Control {formatted_control} matched")
|
|
388
|
+
matched_controls.append(catalog_control)
|
|
389
|
+
break
|
|
390
|
+
return matched_controls
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def create_control_implementations(
|
|
394
|
+
controls: list,
|
|
395
|
+
parent_id: int,
|
|
396
|
+
parent_module: str,
|
|
397
|
+
existing_implementation_dict: Dict,
|
|
398
|
+
passing_controls: Dict,
|
|
399
|
+
failing_controls: Dict,
|
|
400
|
+
) -> List[Dict]:
|
|
401
|
+
"""
|
|
402
|
+
Creates a list of control implementations
|
|
403
|
+
:param list controls: list of controls
|
|
404
|
+
:param int parent_id: parent control id
|
|
405
|
+
:param str parent_module: parent module
|
|
406
|
+
:param Dict existing_implementation_dict: Dictionary of existing control implementations
|
|
407
|
+
:param Dict passing_controls: Dictionary of passing controls
|
|
408
|
+
:param Dict failing_controls: Dictionary of failing controls
|
|
409
|
+
:return: list of control implementations
|
|
410
|
+
:rtype: List[Dict]
|
|
411
|
+
"""
|
|
412
|
+
app = Application()
|
|
413
|
+
api = Api()
|
|
414
|
+
user_id = app.config.get("userId")
|
|
415
|
+
domain = app.config.get("domain")
|
|
416
|
+
control_implementations = []
|
|
417
|
+
to_create = []
|
|
418
|
+
to_update = []
|
|
419
|
+
for control in controls:
|
|
420
|
+
lower_case_control_id = control["controlId"].lower()
|
|
421
|
+
status = check_implementation(
|
|
422
|
+
passing_controls=passing_controls,
|
|
423
|
+
failing_controls=failing_controls,
|
|
424
|
+
control_id=lower_case_control_id,
|
|
425
|
+
)
|
|
426
|
+
if control["controlId"] not in existing_implementation_dict.keys():
|
|
427
|
+
cim = ControlImplementation(
|
|
428
|
+
controlOwnerId=user_id,
|
|
429
|
+
dateLastAssessed=get_current_datetime(),
|
|
430
|
+
status=status,
|
|
431
|
+
controlID=control["id"],
|
|
432
|
+
parentId=parent_id,
|
|
433
|
+
parentModule=parent_module,
|
|
434
|
+
createdById=user_id,
|
|
435
|
+
dateCreated=get_current_datetime(),
|
|
436
|
+
lastUpdatedById=user_id,
|
|
437
|
+
dateLastUpdated=get_current_datetime(),
|
|
438
|
+
).dict()
|
|
439
|
+
cim["controlSource"] = "Baseline"
|
|
440
|
+
to_create.append(cim)
|
|
441
|
+
|
|
442
|
+
else:
|
|
443
|
+
# update existing control implementation data
|
|
444
|
+
existing_imp = existing_implementation_dict.get(control["controlId"])
|
|
445
|
+
existing_imp["status"] = status
|
|
446
|
+
existing_imp["dateLastAssessed"] = get_current_datetime()
|
|
447
|
+
existing_imp["lastUpdatedById"] = user_id
|
|
448
|
+
existing_imp["dateLastUpdated"] = get_current_datetime()
|
|
449
|
+
del existing_imp["createdBy"]
|
|
450
|
+
del existing_imp["systemRole"]
|
|
451
|
+
del existing_imp["controlOwner"]
|
|
452
|
+
del existing_imp["lastUpdatedBy"]
|
|
453
|
+
to_update.append(existing_imp)
|
|
454
|
+
|
|
455
|
+
if len(to_create) > 0:
|
|
456
|
+
ci_url = urljoin(domain, "/api/controlImplementation/batchCreate")
|
|
457
|
+
resp = api.post(url=ci_url, json=to_create)
|
|
458
|
+
if resp.ok:
|
|
459
|
+
control_implementations.extend(resp.json())
|
|
460
|
+
logger.info(f"Created {len(to_create)} Control Implementation(s), Successfully!")
|
|
461
|
+
else:
|
|
462
|
+
resp.raise_for_status()
|
|
463
|
+
if len(to_update) > 0:
|
|
464
|
+
ci_url = urljoin(domain, "/api/controlImplementation/batchUpdate")
|
|
465
|
+
resp = api.post(url=ci_url, json=to_update)
|
|
466
|
+
if resp.ok:
|
|
467
|
+
control_implementations.extend(resp.json())
|
|
468
|
+
logger.info(f"Updated {len(to_update)} Control Implementation(s), Successfully!")
|
|
469
|
+
else:
|
|
470
|
+
resp.raise_for_status()
|
|
471
|
+
return control_implementations
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def check_implementation(passing_controls: Dict, failing_controls: Dict, control_id: str) -> str:
|
|
475
|
+
"""
|
|
476
|
+
Checks the status of a control implementation
|
|
477
|
+
:param Dict passing_controls: Dictionary of passing controls
|
|
478
|
+
:param Dict failing_controls: Dictionary of failing controls
|
|
479
|
+
:param str control_id: control id
|
|
480
|
+
:return: status of control implementation
|
|
481
|
+
:rtype: str
|
|
482
|
+
"""
|
|
483
|
+
if control_id in passing_controls.keys():
|
|
484
|
+
return FULLY_IMPLEMENTED
|
|
485
|
+
elif control_id in failing_controls.keys():
|
|
486
|
+
return IN_REMEDIATION
|
|
487
|
+
else:
|
|
488
|
+
return NOT_IMPLEMENTED
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def convert_control_id(control_id: str) -> str:
|
|
492
|
+
"""
|
|
493
|
+
Convert the control id to a format that can be used in Tenable.io
|
|
494
|
+
:param str control_id: The control id to convert
|
|
495
|
+
:return: The converted control id
|
|
496
|
+
:rtype: str
|
|
497
|
+
"""
|
|
498
|
+
# Convert to lowercase
|
|
499
|
+
control_id = control_id.lower()
|
|
500
|
+
|
|
501
|
+
# Check if there's a parenthesis and replace its content
|
|
502
|
+
if "(" in control_id and ")" in control_id:
|
|
503
|
+
inner_value = control_id.split("(")[1].split(")")[0]
|
|
504
|
+
control_id = control_id.replace(f"({inner_value})", f".{inner_value}")
|
|
505
|
+
|
|
506
|
+
return control_id
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def get_existing_control_implementations(parent_id: int) -> Dict:
|
|
510
|
+
"""
|
|
511
|
+
fetch existing control implementations
|
|
512
|
+
:param int parent_id: parent control id
|
|
513
|
+
:return: Dictionary of existing control implementations
|
|
514
|
+
:rtype: Dict
|
|
515
|
+
"""
|
|
516
|
+
app = Application()
|
|
517
|
+
api = Api()
|
|
518
|
+
domain = app.config.get("domain")
|
|
519
|
+
existing_implementation_dict = {}
|
|
520
|
+
get_url = urljoin(domain, f"/api/controlImplementation/getAllByPlan/{parent_id}")
|
|
521
|
+
response = api.get(get_url)
|
|
522
|
+
if response.ok:
|
|
523
|
+
existing_control_implementations_json = response.json()
|
|
524
|
+
for cim in existing_control_implementations_json:
|
|
525
|
+
existing_implementation_dict[cim["controlName"]] = cim
|
|
526
|
+
logger.info(f"Found {len(existing_implementation_dict)} existing control implementations")
|
|
527
|
+
elif response.status_code == 404:
|
|
528
|
+
logger.info(f"No existing control implementations found for {parent_id}")
|
|
529
|
+
else:
|
|
530
|
+
logger.warning(f"Unable to get existing control implementations. {response.text}")
|
|
531
|
+
|
|
532
|
+
return existing_implementation_dict
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def get_controls(catalog_id: int) -> List[Dict]:
|
|
536
|
+
"""
|
|
537
|
+
Gets all the controls
|
|
538
|
+
:param int catalog_id: catalog id
|
|
539
|
+
:return: list of controls
|
|
540
|
+
:rtype: List[Dict]
|
|
541
|
+
"""
|
|
542
|
+
app = Application()
|
|
543
|
+
api = Api()
|
|
544
|
+
url = urljoin(app.config.get("domain"), f"/api/SecurityControls/getList/{catalog_id}")
|
|
545
|
+
response = api.get(url)
|
|
546
|
+
if response.ok:
|
|
547
|
+
return response.json()
|
|
548
|
+
else:
|
|
549
|
+
response.raise_for_status()
|
|
550
|
+
return []
|
|
@@ -128,7 +128,7 @@ def inventory(
|
|
|
128
128
|
)
|
|
129
129
|
def issues(
|
|
130
130
|
wiz_project_id: str,
|
|
131
|
-
|
|
131
|
+
regscale_ssp_id: int,
|
|
132
132
|
client_id: str,
|
|
133
133
|
client_secret: str,
|
|
134
134
|
filter_by_override: Optional[str] = None,
|
|
@@ -152,9 +152,9 @@ def issues(
|
|
|
152
152
|
|
|
153
153
|
filter_by["project"] = wiz_project_id
|
|
154
154
|
|
|
155
|
-
scanner = WizIssue(plan_id=
|
|
155
|
+
scanner = WizIssue(plan_id=regscale_ssp_id)
|
|
156
156
|
scanner.sync_findings(
|
|
157
|
-
plan_id=
|
|
157
|
+
plan_id=regscale_ssp_id,
|
|
158
158
|
filter_by_override=filter_by_override, # type: ignore
|
|
159
159
|
client_id=client_id, # type: ignore
|
|
160
160
|
client_secret=client_secret, # type: ignore
|
|
@@ -14,7 +14,7 @@ import time
|
|
|
14
14
|
from abc import ABC, abstractmethod
|
|
15
15
|
from collections import defaultdict
|
|
16
16
|
from concurrent.futures import ThreadPoolExecutor
|
|
17
|
-
from typing import Any, Dict, Generic, Iterator, List, Optional, Set, TypeVar, Union
|
|
17
|
+
from typing import Any, Callable, Dict, Generic, Iterator, List, Optional, Set, TypeVar, Union
|
|
18
18
|
|
|
19
19
|
from rich.progress import Progress, TaskID
|
|
20
20
|
|
|
@@ -616,6 +616,7 @@ class ScannerIntegration(ABC):
|
|
|
616
616
|
|
|
617
617
|
# Close Outdated Findings
|
|
618
618
|
close_outdated_findings = True
|
|
619
|
+
closed_count = 0
|
|
619
620
|
|
|
620
621
|
def __init__(self, plan_id: int, tenant_id: int = 1, is_component: bool = False, **kwargs):
|
|
621
622
|
"""
|
|
@@ -2258,7 +2259,7 @@ class ScannerIntegration(ABC):
|
|
|
2258
2259
|
self.handle_passing_checklist(finding=finding, plan_id=self.plan_id)
|
|
2259
2260
|
|
|
2260
2261
|
# Process vulnerability if applicable
|
|
2261
|
-
if finding.status != regscale_models.IssueStatus.Closed:
|
|
2262
|
+
if finding.status != regscale_models.IssueStatus.Closed or ScannerVariables.ingestClosedIssues:
|
|
2262
2263
|
if asset := self.get_asset_by_identifier(finding.asset_identifier):
|
|
2263
2264
|
if vulnerability_id := self.handle_vulnerability(finding, asset, scan_history):
|
|
2264
2265
|
current_vulnerabilities[asset.id].add(vulnerability_id)
|
|
@@ -183,6 +183,8 @@ class ImportValidater:
|
|
|
183
183
|
df = pandas.read_csv(file_path, skiprows=self.skip_rows - 1, on_bad_lines="warn")
|
|
184
184
|
else:
|
|
185
185
|
df = pandas.read_csv(file_path, on_bad_lines="warn")
|
|
186
|
+
if self.ignore_unnamed:
|
|
187
|
+
df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
|
|
186
188
|
except pandas.errors.ParserError:
|
|
187
189
|
raise ValidationException(f"Unable to parse the {CSV} file: {file_path}")
|
|
188
190
|
self.validate_headers(df.columns)
|