regscale-cli 6.17.0.0__py3-none-any.whl → 6.19.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.

Files changed (48) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/api.py +5 -0
  3. regscale/core/login.py +3 -0
  4. regscale/integrations/api_paginator.py +932 -0
  5. regscale/integrations/api_paginator_example.py +348 -0
  6. regscale/integrations/commercial/__init__.py +11 -10
  7. regscale/integrations/commercial/burp.py +4 -0
  8. regscale/integrations/commercial/{qualys.py → qualys/__init__.py} +756 -105
  9. regscale/integrations/commercial/qualys/scanner.py +1051 -0
  10. regscale/integrations/commercial/qualys/variables.py +21 -0
  11. regscale/integrations/commercial/sicura/api.py +1 -0
  12. regscale/integrations/commercial/stigv2/click_commands.py +36 -8
  13. regscale/integrations/commercial/stigv2/stig_integration.py +63 -9
  14. regscale/integrations/commercial/tenablev2/__init__.py +9 -0
  15. regscale/integrations/commercial/tenablev2/authenticate.py +23 -2
  16. regscale/integrations/commercial/tenablev2/commands.py +779 -0
  17. regscale/integrations/commercial/tenablev2/jsonl_scanner.py +1999 -0
  18. regscale/integrations/commercial/tenablev2/sc_scanner.py +600 -0
  19. regscale/integrations/commercial/tenablev2/scanner.py +7 -5
  20. regscale/integrations/commercial/tenablev2/utils.py +21 -4
  21. regscale/integrations/commercial/tenablev2/variables.py +4 -0
  22. regscale/integrations/jsonl_scanner_integration.py +523 -142
  23. regscale/integrations/scanner_integration.py +102 -26
  24. regscale/integrations/transformer/__init__.py +17 -0
  25. regscale/integrations/transformer/data_transformer.py +445 -0
  26. regscale/integrations/transformer/mappings/__init__.py +8 -0
  27. regscale/integrations/variables.py +2 -0
  28. regscale/models/__init__.py +5 -2
  29. regscale/models/integration_models/cisa_kev_data.json +63 -7
  30. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  31. regscale/models/regscale_models/asset.py +5 -2
  32. regscale/models/regscale_models/file.py +5 -2
  33. regscale/regscale.py +3 -1
  34. {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/METADATA +1 -1
  35. {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/RECORD +47 -31
  36. tests/regscale/core/test_version.py +22 -0
  37. tests/regscale/integrations/__init__.py +0 -0
  38. tests/regscale/integrations/test_api_paginator.py +597 -0
  39. tests/regscale/integrations/test_integration_mapping.py +60 -0
  40. tests/regscale/integrations/test_issue_creation.py +317 -0
  41. tests/regscale/integrations/test_issue_due_date.py +46 -0
  42. tests/regscale/integrations/transformer/__init__.py +0 -0
  43. tests/regscale/integrations/transformer/test_data_transformer.py +850 -0
  44. regscale/integrations/commercial/tenablev2/click.py +0 -1637
  45. {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/LICENSE +0 -0
  46. {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/WHEEL +0 -0
  47. {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/entry_points.txt +0 -0
  48. {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/top_level.txt +0 -0
@@ -1,1637 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """Tenable integration for RegScale CLI"""
4
-
5
- import queue
6
- from concurrent.futures import wait
7
- from typing import TYPE_CHECKING
8
-
9
- from regscale.integrations.integration_override import IntegrationOverride
10
-
11
- # Delay import of Tenable libraries
12
- if TYPE_CHECKING:
13
- from tenable.io import TenableIO # type: ignore
14
- from tenable.sc import TenableSC # type: ignore
15
- import pandas as pd # Type Checking
16
-
17
- import collections
18
- import json
19
- import os
20
- import re
21
- import tempfile
22
- import time
23
- import uuid
24
- from concurrent.futures import ThreadPoolExecutor
25
- from datetime import datetime, timedelta
26
- from itertools import groupby
27
- from pathlib import Path
28
- from threading import current_thread, get_ident, get_native_id
29
- from typing import Dict, List, Optional, Set, Tuple, Union
30
- from urllib.parse import urljoin
31
-
32
- import click
33
- import requests
34
- from requests.exceptions import RequestException
35
- from rich.console import Console
36
- from rich.pretty import pprint
37
- from rich.progress import track
38
- from tenable.sc.analysis import AnalysisResultsIterator
39
-
40
- from regscale import __version__
41
- from regscale.core.app.api import Api
42
- from regscale.core.app.application import Application
43
- from regscale.core.app.logz import create_logger
44
- from regscale.core.app.utils.app_utils import (
45
- check_file_path,
46
- check_license,
47
- create_progress_object,
48
- epoch_to_datetime,
49
- error_and_exit,
50
- format_dict_to_html,
51
- get_current_datetime,
52
- regscale_string_to_epoch,
53
- save_data_to,
54
- )
55
- from regscale.core.app.utils.pickle_file_handler import PickleFileHandler
56
- from regscale.integrations.commercial.nessus.nessus_utils import get_cpe_file
57
- from regscale.models.app_models.click import file_types, hidden_file_path, regscale_ssp_id, save_output_to
58
- from regscale.models.integration_models.tenable_models.integration import SCIntegration
59
- from regscale.models.integration_models.tenable_models.models import AssetCheck, TenableAsset, TenableIOAsset
60
- from regscale.models.regscale_models import ControlImplementation
61
- from regscale.models.regscale_models.asset import Asset
62
- from regscale.models.regscale_models.issue import Issue
63
- from regscale.models.regscale_models.scan_history import ScanHistory
64
- from regscale.utils.threading import ThreadSafeCounter
65
- from regscale.validation.address import validate_mac_address
66
-
67
- console = Console()
68
-
69
- logger = create_logger("rich")
70
- REGSCALE_INC = "RegScale, Inc."
71
- REGSCALE_CLI = "RegScale CLI"
72
-
73
- FULLY_IMPLEMENTED = "Fully Implemented"
74
- NOT_IMPLEMENTED = "Not Implemented"
75
- IN_REMEDIATION = "In Remediation"
76
-
77
- DONE_MSG = "Done!"
78
-
79
-
80
- #####################################################################################################
81
- #
82
- # Tenable.sc Documentation: https://docs.tenable.com/tenablesc/api/index.htm
83
- # pyTenable GitHub repo: https://github.com/tenable/pyTenable
84
- # Python tenable.sc documentation: https://pytenable.readthedocs.io/en/stable/api/sc/index.html
85
- #
86
- #####################################################################################################
87
-
88
-
89
- # Create group to handle OSCAL processing
90
- @click.group()
91
- def tenable():
92
- """Performs actions on the Tenable APIs."""
93
-
94
-
95
- @tenable.group(help="[BETA] Performs actions on the Tenable.io API.")
96
- def io():
97
- """Performs actions on the Tenable.io API."""
98
-
99
-
100
- @tenable.group(help="[BETA] Performs actions on the Tenable.sc API.")
101
- def sc():
102
- """Performs actions on the Tenable.sc API."""
103
-
104
-
105
- @tenable.group(help="[BETA] Import Nessus scans and assets to RegScale.")
106
- def nessus():
107
- """Performs actions on the Tenable.sc API."""
108
-
109
-
110
- @nessus.command(name="import_nessus")
111
- @click.option(
112
- "--folder_path",
113
- prompt="Enter the folder path of the Nessus files to process",
114
- help="RegScale will load the Nessus Scans",
115
- type=click.Path(exists=True),
116
- )
117
- @click.option(
118
- "--scan_date",
119
- type=click.DateTime(formats=["%Y-%m-%d"]),
120
- help="The the scan date of the file.",
121
- required=False,
122
- )
123
- @regscale_ssp_id()
124
- def import_nessus(folder_path: click.Path, regscale_ssp_id: click.INT, scan_date: click.DateTime):
125
- """Import Nessus scans, vulnerabilities and assets to RegScale."""
126
- from regscale.integrations.commercial.nessus.scanner import NessusIntegration
127
-
128
- NessusIntegration.sync_assets(plan_id=regscale_ssp_id, path=folder_path)
129
- NessusIntegration.sync_findings(
130
- plan_id=regscale_ssp_id, path=folder_path, enable_finding_date_update=True, scan_date=scan_date
131
- )
132
-
133
-
134
- @nessus.command(name="update_cpe_dictionary")
135
- def update_cpe_dictionary():
136
- """
137
- Manually update the CPE 2.2 dictionary from NIST.
138
- """
139
- get_cpe_file(download=True)
140
-
141
-
142
- @sc.command(name="export_scans")
143
- @save_output_to()
144
- @file_types([".json", ".csv", ".xlsx"])
145
- def export_scans(save_output_to: Path, file_type: str):
146
- """Export scans from Tenable Host to a .json, .csv or .xlsx file."""
147
- # get the scan results
148
- results = get_usable_scan_list()
149
-
150
- # check if file path exists
151
- check_file_path(save_output_to)
152
-
153
- # set the file name
154
- file_name = f"tenable_scans_{get_current_datetime('%m%d%Y')}"
155
-
156
- # save the data as the selected file by the user
157
- save_data_to(
158
- file=Path(f"{save_output_to}/{file_name}{file_type}"),
159
- data=results,
160
- )
161
-
162
-
163
- def validate_tags(ctx: click.Context, param: click.Option, value: str) -> List[Tuple[str, str]]:
164
- """
165
- Validate the tuple elements.
166
-
167
- :param click.Context ctx: Click context
168
- :param click.Option param: Click option
169
- :param str value: A string value to parse and validate
170
- :return: Tuple of validated values
171
- :rtype: List[Tuple[str,str]]
172
- :raise ValueError: If the value is not in the correct format
173
- """
174
- if not value:
175
- return []
176
-
177
- tuple_list = []
178
- for item in value.split(","):
179
- parts = [part for part in item.strip().split(":") if part]
180
- if len(parts) != 2:
181
- raise ValueError(f"""Invalid format: "{item}". Expected 'key:value'""")
182
- tuple_list.append((parts[0], parts[1]))
183
-
184
- return tuple_list
185
-
186
-
187
- def get_usable_scan_list() -> list:
188
- """
189
- Usable Scans from Tenable Host
190
-
191
- :return: List of scans from Tenable
192
- :rtype: list
193
- """
194
- results = []
195
- try:
196
- client = gen_client()
197
- results = client.scans.list()["usable"]
198
- except Exception as ex:
199
- logger.error(ex)
200
- return results
201
-
202
-
203
- def get_detailed_scans(scan_list: list = None) -> list:
204
- """
205
- Generate list of detailed scans (Warning: this action could take 20 minutes or more to complete)
206
-
207
- :param list scan_list: List of scans from Tenable, defaults to None
208
- :raise SystemExit: If there is an error with the request
209
- :return: Detailed list of Tenable scans
210
- :rtype: list
211
- """
212
- client = gen_client()
213
- detailed_scans = []
214
- for scan in track(scan_list, description="Fetching detailed scans..."):
215
- try:
216
- det = client.scans.details(id=scan["id"])
217
- detailed_scans.append(det)
218
- except RequestException as ex: # This is the correct syntax
219
- raise SystemExit(ex) from ex
220
-
221
- return detailed_scans
222
-
223
-
224
- @sc.command(name="save_queries")
225
- @save_output_to()
226
- @file_types([".json", ".csv", ".xlsx"])
227
- def save_queries(save_output_to: Path, file_type: str):
228
- """Get a list of query definitions and save them as a .json, .csv or .xlsx file."""
229
- # get the queries from Tenable
230
- query_list = get_queries()
231
-
232
- # check if file path exists
233
- check_file_path(save_output_to)
234
-
235
- # set the file name
236
- file_name = f"tenable_queries_{get_current_datetime('%m%d%Y')}"
237
-
238
- # save the data as a .json file
239
- save_data_to(
240
- file=Path(f"{save_output_to}{os.sep}{file_name}{file_type}"),
241
- data=query_list,
242
- )
243
-
244
-
245
- def get_queries() -> list:
246
- """
247
- List of query definitions
248
-
249
- :return: List of queries from Tenable
250
- :rtype: list
251
- """
252
- app = Application()
253
- tsc = gen_tsc(app.config)
254
- return tsc.queries.list()
255
-
256
-
257
- @sc.command(name="query_vuln")
258
- @click.option(
259
- "--query_id",
260
- type=click.INT,
261
- help="Tenable query ID to retrieve via API",
262
- prompt="Enter Tenable query ID",
263
- required=True,
264
- )
265
- @regscale_ssp_id()
266
- @click.option(
267
- "--scan_date",
268
- "-sd",
269
- type=click.DateTime(formats=["%Y-%m-%d"]),
270
- help="The scan date of the file.",
271
- required=False,
272
- )
273
- # Add Prompt for RegScale SSP name
274
- def query_vuln(query_id: int, regscale_ssp_id: int, scan_date: datetime = None):
275
- """Query Tenable vulnerabilities and sync assets to RegScale."""
276
- q_vuln(query_id=query_id, ssp_id=regscale_ssp_id, scan_date=scan_date)
277
-
278
-
279
- @io.command(name="sync_assets")
280
- @regscale_ssp_id()
281
- @click.option(
282
- "--tags",
283
- type=click.STRING,
284
- help='Optional tags to filter assets, wrap in double quotes, e.g. --tags "Tag1:tag1a,Tag2:tag2b"',
285
- default=None,
286
- required=False,
287
- callback=validate_tags,
288
- )
289
- # Add Prompt for RegScale SSP name
290
- def query_assets(regscale_ssp_id: int, tags: Optional[List[Tuple[str, str]]] = None):
291
- """Query Tenable Assets and sync to RegScale."""
292
- # Validate ssp
293
- from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
294
-
295
- TenableIntegration.sync_assets(plan_id=regscale_ssp_id, tags=tags)
296
-
297
-
298
- @io.command(name="sync_vulns")
299
- @regscale_ssp_id()
300
- @click.option(
301
- "--tags",
302
- type=click.STRING,
303
- help='Optional tags to filter vulns, wrap in double quotes, e.g. --tags "Tag1:tag1a,Tag2:tag2b"',
304
- default=None,
305
- required=False,
306
- callback=validate_tags,
307
- )
308
- @click.option(
309
- "--scan_date",
310
- "-sd",
311
- type=click.DateTime(formats=["%Y-%m-%d"]),
312
- help="The scan date of the file.",
313
- required=False,
314
- )
315
- def query_vulns(regscale_ssp_id: int, tags: Optional[List[Tuple[str, str]]] = None, scan_date: datetime = None):
316
- """
317
- Query Tenable vulnerabilities and sync assets, vulnerabilities and issues to RegScale.
318
- """
319
- from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
320
-
321
- TenableIntegration.sync_findings(plan_id=regscale_ssp_id, tags=tags, scan_date=scan_date)
322
-
323
-
324
- def validate_regscale_security_plan(parent_id: int) -> bool:
325
- """
326
- Validate RegScale Security Plan exists
327
-
328
- :param int parent_id: The ID number from RegScale of the System Security Plan
329
- :return: If API call was successful
330
- :rtype: bool
331
- """
332
- app = check_license()
333
- config = app.config
334
- headers = {
335
- "Authorization": config["token"],
336
- }
337
- url = urljoin(config["domain"], f"/api/securityplans/{parent_id}")
338
- response = requests.get(url, headers=headers)
339
- return response.ok
340
-
341
-
342
- @io.command(name="list_jobs")
343
- @click.option(
344
- "--job_type",
345
- default="vulns",
346
- type=click.Choice(["vulns", "assets"]),
347
- show_default=True,
348
- help="Tenable job type.",
349
- required=False,
350
- )
351
- @click.option(
352
- "--last",
353
- type=click.INT,
354
- default=100,
355
- show_default=True,
356
- help="Filter the last n jobs.",
357
- required=False,
358
- )
359
- @click.option(
360
- "--job_status",
361
- type=click.Choice(["processing", "finished", "cancelled"]),
362
- help="Filter by status.",
363
- required=False,
364
- )
365
- def list_jobs(job_type: str, last: int, job_status: str):
366
- """Retrieve a list of jobs from Tenable.io."""
367
- app = Application()
368
- config = app.config
369
- client = gen_tio(config)
370
- if job_status:
371
- jobs = [job for job in client.exports.jobs(job_type) if job["status"] == str(job_status).upper()]
372
- else:
373
- jobs = client.exports.jobs(job_type)
374
- jobs = sorted(jobs, key=lambda k: (k["created"]), reverse=False)
375
- # filter the last N jobs
376
- for job in jobs[len(jobs) - last :]:
377
- console.print(
378
- f"UUID: {job['uuid']}, STATUS: {job['status']}, CREATED: {epoch_to_datetime(job['created'], epoch_type='milliseconds')}"
379
- )
380
-
381
-
382
- @io.command(name="cancel_job")
383
- @click.option(
384
- "--uuid",
385
- type=click.STRING,
386
- help="Tenable job UUID.",
387
- prompt="Enter the UUID of the job to cancel.",
388
- required=True,
389
- )
390
- @click.option(
391
- "--job_type",
392
- default="vulns",
393
- type=click.Choice(["vulns", "assets"]),
394
- show_default=True,
395
- help="Tenable job type.",
396
- required=False,
397
- )
398
- def cancel_job(uuid: str, job_type: str):
399
- """Cancel a Tenable IO job."""
400
- app = Application()
401
- config = app.config
402
- client = gen_tio(config)
403
- client.exports.cancel(job_type, export_uuid=uuid)
404
-
405
-
406
- def process_vulnerabilities(counts: collections.Counter, reg_assets: list, ssp_id: int, tenable_vulns: list) -> list:
407
- """
408
- Process Tenable vulnerabilities
409
-
410
- :param collections.Counter counts: Dictionary of counts of each vulnerability
411
- :param list reg_assets: List of RegScale assets
412
- :param int ssp_id: RegScale System Security Plan ID
413
- :param list tenable_vulns: List of Tenable vulnerabilities
414
- :return: List of assets to update
415
- :rtype: list
416
- """
417
- update_assets = []
418
- for vuln in set(tenable_vulns):
419
- update_assets = process_vuln(counts, reg_assets, ssp_id, vuln)
420
- return update_assets
421
-
422
-
423
- def q_vuln(query_id: int, ssp_id: int, scan_date: datetime = None) -> list:
424
- """
425
- Query Tenable vulnerabilities
426
-
427
- :param int query_id: Tenable query ID
428
- :param int ssp_id: RegScale System Security Plan ID
429
- :param datetime scan_date: Scan date, defaults to None
430
- :return: List of queries from Tenable
431
- :rtype: list
432
- """
433
- check_license()
434
- # At SSP level, provide a list of vulnerabilities and the counts of each
435
- fetch_vulns(query_id=query_id, regscale_ssp_id=ssp_id, scan_date=scan_date)
436
-
437
-
438
- def process_vuln(counts: collections.Counter, reg_assets: list, ssp_id: int, vuln: TenableAsset) -> list:
439
- """
440
- Process Tenable vulnerability data
441
-
442
- :param collections.Counter counts: Dictionary of counts of each vulnerability
443
- :param list reg_assets: List of RegScale assets
444
- :param int ssp_id: RegScale System Security Plan ID
445
- :param TenableAsset vuln: Tenable vulnerability object
446
- :return: List of assets to update
447
- :rtype: list
448
- """
449
- update_assets = []
450
- vuln.count = dict(counts)[vuln.pluginName]
451
- lookup_assets = lookup_asset(reg_assets, vuln.macAddress, vuln.dnsName)
452
- # Update parent id to SSP on insert
453
- if len(lookup_assets) > 0:
454
- for asset in set(lookup_assets):
455
- # Do update
456
- # asset = reg_asset[0]
457
- asset.parentId = ssp_id
458
- asset.parentModule = "securityplans"
459
- asset.macAddress = vuln.macAddress.upper()
460
- asset.osVersion = vuln.operatingSystem
461
- asset.purchaseDate = "01-01-1970"
462
- asset.endOfLifeDate = "01-01-1970"
463
- if asset.ipAddress is None:
464
- asset.ipAddress = vuln.ip
465
- asset.operatingSystem = determine_os(asset.operatingSystem)
466
- try:
467
- assert asset.id
468
- # avoid duplication
469
- if asset not in update_assets:
470
- update_assets.append(asset)
471
- except AssertionError as aex:
472
- logger.error("Asset does not have an id, unable to update!\n%s", aex)
473
- return update_assets
474
-
475
-
476
- def determine_os(os_string: str) -> str:
477
- """
478
- Determine RegScale friendly OS name
479
-
480
- :param str os_string: String of the asset's OS
481
- :return: RegScale acceptable OS
482
- :rtype: str
483
- """
484
- linux_words = ["linux", "ubuntu", "hat", "centos", "rocky", "alma", "alpine"]
485
- if re.compile("|".join(linux_words), re.IGNORECASE).search(os_string):
486
- return "Linux"
487
- elif (os_string.lower()).startswith("windows"):
488
- return "Windows Server" if "server" in os_string else "Windows Desktop"
489
- else:
490
- return "Other"
491
-
492
-
493
- def lookup_asset(asset_list: list, mac_address: str, dns_name: str = None) -> list:
494
- """
495
- Lookup asset in Tenable and return the data from Tenable
496
-
497
- :param list asset_list: List of assets to lookup in Tenable
498
- :param str mac_address: Mac address of asset
499
- :param str dns_name: DNS Name of the asset, defaults to None
500
- :return: List of assets that fit the provided filters
501
- :rtype: list
502
- """
503
- results = []
504
- if validate_mac_address(mac_address):
505
- if dns_name:
506
- results = [
507
- Asset(**asset)
508
- for asset in asset_list
509
- if "macAddress" in asset
510
- and asset["macAddress"] == mac_address
511
- and asset["name"] == dns_name
512
- and "macAddress" in asset
513
- and "name" in asset
514
- ]
515
- else:
516
- results = [asset for asset in asset_list if asset["macAddress"] == mac_address]
517
- # Return unique list
518
- return list(set(results))
519
-
520
-
521
- def create_issue_from_vuln(app: Application, row: "pd.Series", default_due_delta: int) -> "Issue":
522
- """
523
- Creates an Issue object from a Tenable vulnerability
524
-
525
- :param Application app: Application object
526
- :param pd.Series row: Row of data from Tenable
527
- :param int default_due_delta: Default due delta
528
- :return: Issue object
529
- :rtype: Issue
530
- """
531
-
532
- default_status = app.config["issues"]["tenable"]["status"]
533
- fmt = "%Y-%m-%d %H:%M:%S"
534
- plugin_id = row["pluginID"]
535
- port = row["port"]
536
- protocol = row["protocol"]
537
- due_date = datetime.strptime(row["last_scan"], fmt) + timedelta(days=default_due_delta)
538
- if due_date < datetime.now():
539
- due_date = datetime.now() + timedelta(days=default_due_delta)
540
- if "synopsis" in row:
541
- title = row["synopsis"]
542
- return Issue(
543
- title=title or row["pluginName"],
544
- description=row["description"] or row["pluginName"] + f"<br>Port: {port}<br>Protocol: {protocol}",
545
- issueOwnerId=app.config["userId"],
546
- status=default_status,
547
- severityLevel=Issue.assign_severity(row["severity"]),
548
- dueDate=due_date.strftime(fmt),
549
- identification="Vulnerability Assessment",
550
- parentId=row["regscale_ssp_id"],
551
- parentModule="securityplans",
552
- pluginId=plugin_id,
553
- vendorActions=row["solution"],
554
- assetIdentifier=f'DNS: {row["dnsName"]} - IP: {row["ip"]}',
555
- )
556
-
557
-
558
- def create_issue_from_row(app: Application, row: "pd.Series", default_due_delta: int) -> "Issue":
559
- """
560
- Creates an Issue object from a Tenable vulnerability
561
-
562
- :param Application app: Application object
563
- :param pd.Series row: Row of data from Tenable
564
- :param int default_due_delta: Default due delta
565
- :return: Issue object
566
- :rtype: Issue
567
- """
568
- if row["severity"] != "Info":
569
- issue = create_issue_from_vuln(app, row, default_due_delta)
570
- if isinstance(issue, Issue):
571
- return issue
572
- return None
573
-
574
-
575
- def prepare_issues_for_sync(
576
- app: Application, df: "pd.DataFrame", regscale_ssp_id: int
577
- ) -> Tuple[List["Issue"], List["Issue"]]:
578
- """
579
- Prepares Tenable vulnerabilities for synchronization as RegScale issues
580
-
581
- :param Application app: Application object
582
- :param pd.DataFrame df: Dataframe of Tenable data
583
- :param int regscale_ssp_id: RegScale System Security Plan ID
584
- :return: List of issues to insert, list of issues to update
585
- :rtype: Tuple[List[Issue], List[Issue]]
586
- """
587
-
588
- default_due_delta = app.config["issues"]["tenable"]["moderate"]
589
- existing_issues = Issue.get_all_by_parent(parent_id=regscale_ssp_id, parent_module="securityplans")
590
- sc_issues = []
591
- new_issues = set()
592
- update_issues = set()
593
- for index, row in df.iterrows():
594
- issue = create_issue_from_row(app, row, default_due_delta)
595
- if isinstance(issue, Issue):
596
- sc_issues.append(issue)
597
- # Generate list of completely new issues, and merge with existing issues if they have the same title
598
- # group issues by title
599
- grouped_issues = {k: list(g) for k, g in groupby(sc_issues, key=lambda x: x.title)}
600
- for title in grouped_issues:
601
- reg_key = 0
602
- regs = [iss for iss in existing_issues if iss.title == title and iss.id]
603
- if regs:
604
- reg_key = regs[0].id
605
- issues = set(grouped_issues[title])
606
- for issue in issues:
607
- asset_ident = combine_strings({iss.assetIdentifier for iss in issues})
608
- if reg_key and issue.title not in {iss.title for iss in update_issues}:
609
- issue.id = reg_key
610
- issue.assetIdentifier = asset_ident
611
- update_issues.add(issue)
612
- elif not reg_key:
613
- issue.assetIdentifier = asset_ident
614
- new_issues.add(issue)
615
-
616
- return list(new_issues), list(update_issues)
617
-
618
-
619
- def combine_strings(set_of_strings: Set[str]) -> str:
620
- """
621
- Combines a set of strings into a single string
622
-
623
- :param Set[str] set_of_strings: Set of strings
624
- :rtype: str
625
- :return: Combined string
626
- """
627
- return "<br>".join(set_of_strings)
628
-
629
-
630
- def sync_issues_to_regscale(new_issues: List["Issue"], update_issues: List["Issue"]) -> None:
631
- """
632
- Synchronizes issues to RegScale
633
-
634
- :param List[Issue] new_issues: New issues
635
- :param List[Issue] update_issues: Updated issues
636
- :rtype: None
637
- """
638
- logger = create_logger()
639
-
640
- if new_issues:
641
- logger.info(f"Creating {len(new_issues)} new issue(s) in RegScale...")
642
- Issue.batch_create(new_issues)
643
- logger.info("Finished creating issue(s) in RegScale.")
644
- else:
645
- logger.info("No new issues to create.")
646
-
647
- if update_issues:
648
- logger.info(f"Updating {len(update_issues)} existing issue(s) in RegScale...")
649
- Issue.batch_update(update_issues)
650
- logger.info("Finished updating issue(s) in RegScale.")
651
- else:
652
- logger.info("No issues to update.")
653
-
654
-
655
- def create_regscale_issue_from_vuln(regscale_ssp_id: int, df: Optional["pd.DataFrame"] = None) -> None:
656
- """
657
- Sync Tenable Vulnerabilities to RegScale issues
658
-
659
- :param int regscale_ssp_id: RegScale System Security Plan ID
660
- :param Optional["pd.DataFrame"] df: Pandas dataframe of Tenable data
661
- :rtype: None
662
- """
663
- import pandas as pd # Optimize import performance
664
-
665
- if df is None:
666
- df = pd.DataFrame()
667
- app = Application()
668
- new_issues, update_issues = prepare_issues_for_sync(app, df, regscale_ssp_id)
669
- sync_issues_to_regscale(new_issues, update_issues)
670
-
671
-
672
- def fetch_assets(ssp_id: int) -> list[TenableIOAsset]:
673
- """
674
- Fetch assets from Tenable IO and sync to RegScale
675
-
676
- :param int ssp_id: RegScale System Security Plan ID
677
- :return: List of Tenable assets
678
- :rtype: list[TenableIOAsset]
679
- """
680
- tenable_last_updated: int = 0
681
- app = Application()
682
- config = app.config
683
- client = gen_tio(config=config)
684
- assets: List[TenableIOAsset] = []
685
- logger.info("Fetching existing assets from RegScale...")
686
-
687
- existing_assets: List[Asset] = Asset.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans")
688
-
689
- logger.info("Found %i existing asset(s) in RegScale.", len(existing_assets))
690
-
691
- filtered_assets = [asset for asset in existing_assets if asset.tenableId and asset.dateLastUpdated]
692
- # Get last epoch updated from RegScale, limit to Tenable assets
693
- if filtered_assets:
694
- tenable_last_updated = max([regscale_string_to_epoch(asset.dateLastUpdated) for asset in filtered_assets])
695
- export = client.exports.assets(updated_at=tenable_last_updated)
696
- logger.info("Saving chunked asset files from Tenable IO for processing...")
697
- temp_loc = Path(tempfile.gettempdir()) / "tenable_io" / str(uuid.uuid4()) # random folder name
698
- # show process status
699
- box_len = 0
700
- status = client.exports.status(export_type=export.type, export_uuid=export.uuid)
701
- with create_progress_object(indeterminate=True) as job_progress:
702
- job_progress.add_task("Fetching Chunked Tenable IO data...", start=False, total=None)
703
- while status["status"] == "PROCESSING":
704
- box_len = len(status["chunks_available"])
705
- time.sleep(0.5)
706
- status = client.exports.status(export_type=export.type, export_uuid=export.uuid)
707
- # Process chunks of data
708
- with create_progress_object(indeterminate=True) as saving_progress:
709
- saving_task = saving_progress.add_task(
710
- "Saving Tenable IO data to disk...",
711
- total=box_len,
712
- )
713
- export.run_threaded(
714
- func=write_io_chunk,
715
- kwargs={"data_dir": temp_loc},
716
- num_threads=3,
717
- )
718
- saving_progress.update(saving_task, advance=1)
719
- process_to_regscale(data_dir=temp_loc, ssp_id=ssp_id, existing_assets=existing_assets)
720
- return assets
721
-
722
-
723
- def fetch_vulns(query_id: int = 0, regscale_ssp_id: int = 0, scan_date: datetime = None):
724
- """
725
- Fetch vulnerabilities from Tenable by query ID
726
-
727
- :param int query_id: Tenable query ID, defaults to 0
728
- :param int regscale_ssp_id: RegScale System Security Plan ID, defaults to 0
729
- :param datetime scan_date: Scan date, defaults to None
730
- """
731
-
732
- client = gen_client()
733
- if query_id and client._env_base == "TSC":
734
- vulns = client.analysis.vulns(query_id=query_id)
735
- sc = SCIntegration(plan_id=regscale_ssp_id, scan_date=scan_date)
736
- # Create pickle file to cache data
737
- # make sure folder exists
738
- with tempfile.TemporaryDirectory() as temp_dir:
739
- logger.info("Saving Tenable SC data to disk...%s", temp_dir)
740
- num_assets_processed, num_findings_to_process = consume_iterator_to_file(
741
- iterator=vulns, dir_path=Path(temp_dir), scanner=sc
742
- )
743
- iterables = tenable_dir_to_tuple_generator(Path(temp_dir))
744
- try:
745
- sc.sync_assets(
746
- plan_id=regscale_ssp_id,
747
- integration_assets=(asset for sublist in iterables[0] for asset in sublist),
748
- asset_count=num_assets_processed,
749
- )
750
- sc.sync_findings(
751
- plan_id=regscale_ssp_id,
752
- integration_findings=(finding for sublist in iterables[1] for finding in sublist),
753
- finding_count=num_findings_to_process,
754
- )
755
- except IndexError as ex:
756
- logger.error("Error processing Tenable SC data: %s", ex)
757
-
758
-
759
- def tenable_dir_to_tuple_generator(dir_path: Path):
760
- """
761
- Generate a tuple of chained generators for Tenable directories.
762
- """
763
- from itertools import chain
764
-
765
- assets_gen = chain.from_iterable(
766
- (dat["assets"] for dat in PickleFileHandler(file).read()) for file in dir_path.iterdir()
767
- )
768
- findings_gen = chain.from_iterable(
769
- (dat["findings"] for dat in PickleFileHandler(file).read()) for file in dir_path.iterdir()
770
- )
771
-
772
- return assets_gen, findings_gen
773
-
774
-
775
- def consume_iterator_to_file(iterator: AnalysisResultsIterator, dir_path: Path, scanner: SCIntegration) -> tuple:
776
- """
777
- Consume an iterator and write the results to a file
778
-
779
- :param AnalysisResultsIterator iterator: Tenable SC iterator
780
- :param Path dir_path: The directory to save the pickled files
781
- :param SCIntegration scanner: Tenable SC Integration object
782
- :rtype: tuple
783
- :return: The total count of assets and findings processed
784
- """
785
- app = Application()
786
- logger.info("Consuming Tenable SC iterator...")
787
- override = IntegrationOverride(app)
788
- asset_count = 0
789
- findings_count = 0
790
- total_count = ThreadSafeCounter()
791
- page_number = ThreadSafeCounter()
792
- rec_count = ThreadSafeCounter()
793
- process_list = queue.Queue()
794
- futures_lst = []
795
- with ThreadPoolExecutor(max_workers=5) as executor:
796
- for dat in iterator:
797
- total_count.increment()
798
- process_list.put(dat)
799
- rec_count.increment()
800
- if rec_count.value == len(iterator.page):
801
- page_number.increment()
802
- futures_lst.append(
803
- executor.submit(
804
- process_sc_chunk,
805
- app=app,
806
- vulns=pop_queue(queue=process_list, queue_len=len(iterator.page)),
807
- page=page_number.value,
808
- dir_path=dir_path,
809
- sc=scanner,
810
- override=override,
811
- )
812
- )
813
- rec_count.set(0)
814
- # Collect results from all threads
815
- asset_count = 0
816
- findings_count = 0
817
- # Wait for completion
818
- wait(futures_lst)
819
-
820
- for future in futures_lst:
821
- findings, assets = future.result()
822
- asset_count += assets
823
- findings_count += findings
824
-
825
- if total_count.value == 0:
826
- logger.warning("No Tenable SC data found.")
827
- return asset_count, findings_count
828
-
829
-
830
- def pop_queue(queue: queue.Queue, queue_len: int) -> list:
831
- """
832
- Pop items from a queue
833
-
834
- :param queue.Queue queue: Queue object
835
- :param int queue_len: Length of the queue
836
- :return: List of items from the queue
837
- :rtype: list
838
- """
839
- retrieved_items = []
840
-
841
- # Use a for loop to get 1000 items
842
- for _ in range(queue_len):
843
- # Check if the queue is not empty
844
- if not queue.empty():
845
- # Get an item from the queue and append it to the list
846
- retrieved_items.append(queue.get())
847
- else:
848
- # Break the loop if the queue is empty
849
- break
850
- return retrieved_items
851
-
852
-
853
- def process_sc_chunk(**kwargs) -> tuple:
854
- """
855
- Process Tenable SC chunk
856
-
857
- :param kwargs: Keyword arguments
858
- :rtype: tuple
859
- :return: Tuple of findings and assets
860
- """
861
- # iterator.page, iterator.page_count, file_path, query_id, ssp_id
862
- integration_mapping = kwargs.get("override")
863
-
864
- vulns = kwargs.get("vulns")
865
- dir_path = kwargs.get("dir_path")
866
- generated_file_name = f"tenable_scan_page_{kwargs.get('page')}.pkl"
867
- pickled_file_handler = PickleFileHandler(str(dir_path / generated_file_name))
868
- tenable_sc: SCIntegration = kwargs.get("sc")
869
- thread = current_thread()
870
- if not len(vulns):
871
- return (0, 0)
872
- # I can't add a to-do thanks to sonarlint, but we need to add CVE lookup from plugin id
873
- # append file to path
874
- # Process to RegScale
875
- tenable_vulns = [TenableAsset(**vuln) for vuln in vulns]
876
- # Empty "DNS" should just be IP
877
- for vuln in tenable_vulns:
878
- if not vuln.dnsName:
879
- vuln.dnsName = vuln.ip
880
- findings = []
881
- assets = []
882
- for vuln in tenable_vulns:
883
- findings += tenable_sc.parse_findings(vuln=vuln, integration_mapping=integration_mapping)
884
- if vuln.dnsName not in {asset.name for asset in assets}: # avoid duplicates
885
- assets.append(tenable_sc.to_integration_asset(vuln, **kwargs))
886
- pickled_file_handler.write({"assets": assets, "findings": findings})
887
-
888
- logger.info(
889
- "Submitting %i findings and %i assets to the CLI Job Queue from Tenable SC Page %i...",
890
- len(findings),
891
- len(assets),
892
- kwargs.get("page"),
893
- )
894
- logger.debug(f"Completed thread: name={thread.name}, idnet={get_ident()}, id={get_native_id()}")
895
- return (len(findings), len(assets))
896
-
897
-
898
- def get_last_pull_epoch(regscale_ssp_id: int) -> int:
899
- """
900
- Gather last pull epoch from RegScale Security Plan
901
-
902
- :param int regscale_ssp_id: RegScale System Security Plan ID
903
- :return: Last pull epoch
904
- :rtype: int
905
-
906
- """
907
- fmt: str = "%Y-%m-%d"
908
- two_months_ago: datetime = datetime.now() - timedelta(weeks=8)
909
- two_weeks_ago: datetime = datetime.now() - timedelta(weeks=2)
910
- last_pull: int = round(two_weeks_ago.timestamp()) # default the last pull date to two weeks
911
- # Limit the query with a filter_date to avoid taxing the database in the case of a large number of scans
912
- if res := ScanHistory.get_by_parent_recursive(
913
- parent_id=regscale_ssp_id, parent_module="securityplans", filter_date=two_months_ago.strftime(fmt)
914
- ):
915
- # order by ScanDate desc
916
- fmt = "%Y-%m-%dT%H:%M:%S"
917
- res = sorted(res, key=lambda x: datetime.strptime(x.scanDate, fmt), reverse=True)
918
- # Convert to timestampe
919
- last_pull = round((datetime.strptime(res[0].scanDate, fmt)).timestamp())
920
- return last_pull
921
-
922
-
923
- @sc.command(name="list_tags")
924
- def sc_tags():
925
- """List tags from Tenable"""
926
- list_tags()
927
-
928
-
929
- def list_tags() -> None:
930
- """
931
- Query a list of tags on the server and print to console
932
-
933
- :rtype: None
934
- """
935
- tag_list = get_tags()
936
- pprint(tag_list)
937
-
938
-
939
- def get_tags() -> list:
940
- """
941
- List of Tenable query definitions
942
-
943
- :return: List of unique tags for Tenable queries
944
- :rtype: list
945
- """
946
- client = gen_client()
947
- logger.debug(client._env_base)
948
- if client._env_base == "TSC":
949
- return client.queries.tags()
950
- return list(client.tags.list())
951
-
952
-
953
- def gen_client() -> Union["TenableIO", "TenableSC"]:
954
- """
955
- Return the appropriate Tenable client based on the URL
956
-
957
- :return: Client type
958
- :rtype: Union["TenableIO", "TenableSC"]
959
- """
960
- app = Application()
961
- config = app.config
962
- if "cloud.tenable.com" in config["tenableUrl"]:
963
- return gen_tio(config)
964
- return gen_tsc(config)
965
-
966
-
967
- def gen_tsc(config: dict) -> "TenableSC":
968
- """
969
- Generate Tenable Object
970
-
971
- :param dict config: Configuration dictionary
972
- :return: Tenable client
973
- :rtype: "TenableSC"
974
- """
975
- from restfly.errors import APIError
976
- from tenable.sc import TenableSC
977
-
978
- if not config:
979
- app = Application()
980
- config = app.config
981
- res = TenableSC(
982
- url=config["tenableUrl"],
983
- access_key=config["tenableAccessKey"],
984
- secret_key=config["tenableSecretKey"],
985
- vendor=REGSCALE_INC,
986
- product=REGSCALE_CLI,
987
- build=__version__,
988
- )
989
- try:
990
- res.status.status()
991
- except APIError:
992
- error_and_exit("Unable to authenticate with Tenable SC. Please check your credentials.", False)
993
- return res
994
-
995
-
996
- def gen_tio(config: dict) -> "TenableIO":
997
- """
998
- Generate Tenable Object
999
-
1000
- :param dict config: Configuration dictionary
1001
- :return: Tenable client
1002
- :rtype: "TenableIO"
1003
- """
1004
-
1005
- from restfly.errors import UnauthorizedError
1006
- from tenable.io import TenableIO
1007
-
1008
- res = TenableIO(
1009
- url=config["tenableUrl"],
1010
- access_key=config["tenableAccessKey"],
1011
- secret_key=config["tenableSecretKey"],
1012
- vendor=REGSCALE_INC,
1013
- product=REGSCALE_CLI,
1014
- build=__version__,
1015
- )
1016
-
1017
- try:
1018
- # Check a quick API to make sure we have access
1019
- res.scans.list(last_modified=datetime.now())
1020
- except UnauthorizedError:
1021
- error_and_exit(
1022
- "Unable to authenticate with Tenable Vulnerability Management (IO). Please check your credentials.", False
1023
- )
1024
-
1025
- return res
1026
-
1027
-
1028
- def get_controls(catalog_id: int) -> List[Dict]:
1029
- """
1030
- Gets all the controls
1031
-
1032
- :param int catalog_id: catalog id
1033
- :return: list of controls
1034
- :rtype: List[Dict]
1035
- """
1036
- app = Application()
1037
- api = Api()
1038
- url = urljoin(app.config.get("domain"), f"/api/SecurityControls/getList/{catalog_id}")
1039
- response = api.get(url)
1040
- if response.ok:
1041
- return response.json()
1042
- else:
1043
- response.raise_for_status()
1044
- return []
1045
-
1046
-
1047
- def create_control_implementations(
1048
- controls: list,
1049
- parent_id: int,
1050
- parent_module: str,
1051
- existing_implementation_dict: Dict,
1052
- passing_controls: Dict,
1053
- failing_controls: Dict,
1054
- ) -> List[Dict]:
1055
- """
1056
- Creates a list of control implementations
1057
-
1058
- :param list controls: list of controls
1059
- :param int parent_id: parent control id
1060
- :param str parent_module: parent module
1061
- :param Dict existing_implementation_dict: Dictionary of existing control implementations
1062
- :param Dict passing_controls: Dictionary of passing controls
1063
- :param Dict failing_controls: Dictionary of failing controls
1064
- :return: list of control implementations
1065
- :rtype: List[Dict]
1066
- """
1067
- app = Application()
1068
- api = Api()
1069
- user_id = app.config.get("userId")
1070
- domain = app.config.get("domain")
1071
- control_implementations = []
1072
- to_create = []
1073
- to_update = []
1074
- for control in controls:
1075
- lower_case_control_id = control["controlId"].lower()
1076
- status = check_implementation(
1077
- passing_controls=passing_controls,
1078
- failing_controls=failing_controls,
1079
- control_id=lower_case_control_id,
1080
- )
1081
- if control["controlId"] not in existing_implementation_dict.keys():
1082
- cim = ControlImplementation(
1083
- controlOwnerId=user_id,
1084
- dateLastAssessed=get_current_datetime(),
1085
- status=status,
1086
- controlID=control["id"],
1087
- parentId=parent_id,
1088
- parentModule=parent_module,
1089
- createdById=user_id,
1090
- dateCreated=get_current_datetime(),
1091
- lastUpdatedById=user_id,
1092
- dateLastUpdated=get_current_datetime(),
1093
- ).dict()
1094
- cim["controlSource"] = "Baseline"
1095
- to_create.append(cim)
1096
-
1097
- else:
1098
- # update existing control implementation data
1099
- existing_imp = existing_implementation_dict.get(control["controlId"])
1100
- existing_imp["status"] = status
1101
- existing_imp["dateLastAssessed"] = get_current_datetime()
1102
- existing_imp["lastUpdatedById"] = user_id
1103
- existing_imp["dateLastUpdated"] = get_current_datetime()
1104
- del existing_imp["createdBy"]
1105
- del existing_imp["systemRole"]
1106
- del existing_imp["controlOwner"]
1107
- del existing_imp["lastUpdatedBy"]
1108
- to_update.append(existing_imp)
1109
-
1110
- if len(to_create) > 0:
1111
- ci_url = urljoin(domain, "/api/controlImplementation/batchCreate")
1112
- resp = api.post(url=ci_url, json=to_create)
1113
- if resp.ok:
1114
- control_implementations.extend(resp.json())
1115
- logger.info(f"Created {len(to_create)} Control Implementation(s), Successfully!")
1116
- else:
1117
- resp.raise_for_status()
1118
- if len(to_update) > 0:
1119
- ci_url = urljoin(domain, "/api/controlImplementation/batchUpdate")
1120
- resp = api.post(url=ci_url, json=to_update)
1121
- if resp.ok:
1122
- control_implementations.extend(resp.json())
1123
- logger.info(f"Updated {len(to_update)} Control Implementation(s), Successfully!")
1124
- else:
1125
- resp.raise_for_status()
1126
- return control_implementations
1127
-
1128
-
1129
- def check_implementation(passing_controls: Dict, failing_controls: Dict, control_id: str) -> str:
1130
- """
1131
- Checks the status of a control implementation
1132
-
1133
- :param Dict passing_controls: Dictionary of passing controls
1134
- :param Dict failing_controls: Dictionary of failing controls
1135
- :param str control_id: control id
1136
- :return: status of control implementation
1137
- :rtype: str
1138
- """
1139
- if control_id in passing_controls.keys():
1140
- return FULLY_IMPLEMENTED
1141
- elif control_id in failing_controls.keys():
1142
- return IN_REMEDIATION
1143
- else:
1144
- return NOT_IMPLEMENTED
1145
-
1146
-
1147
- def get_existing_control_implementations(parent_id: int) -> Dict:
1148
- """
1149
- fetch existing control implementations
1150
-
1151
- :param int parent_id: parent control id
1152
- :return: Dictionary of existing control implementations
1153
- :rtype: Dict
1154
- """
1155
- app = Application()
1156
- api = Api()
1157
- domain = app.config.get("domain")
1158
- existing_implementation_dict = {}
1159
- get_url = urljoin(domain, f"/api/controlImplementation/getAllByPlan/{parent_id}")
1160
- response = api.get(get_url)
1161
- if response.ok:
1162
- existing_control_implementations_json = response.json()
1163
- for cim in existing_control_implementations_json:
1164
- existing_implementation_dict[cim["controlName"]] = cim
1165
- logger.info(f"Found {len(existing_implementation_dict)} existing control implementations")
1166
- elif response.status_code == 404:
1167
- logger.info(f"No existing control implementations found for {parent_id}")
1168
- else:
1169
- logger.warn(f"Unable to get existing control implementations. {response.text}")
1170
-
1171
- return existing_implementation_dict
1172
-
1173
-
1174
- def get_matched_controls(tenable_controls: List[Dict], catalog_controls: List[Dict]) -> List[Dict]:
1175
- """
1176
- Get controls that match between Tenable and the catalog
1177
-
1178
- :param List[Dict] tenable_controls: List of controls from Tenable
1179
- :param List[Dict] catalog_controls: List of controls from the catalog
1180
- :return: List of matched controls
1181
- :rtype: List[Dict]
1182
- """
1183
- matched_controls = []
1184
- for control in tenable_controls:
1185
- formatted_control = convert_control_id(control)
1186
- logger.info(formatted_control)
1187
- for catalog_control in catalog_controls:
1188
- if catalog_control["controlId"].lower() == formatted_control.lower():
1189
- logger.info(f"Catalog Control {formatted_control} matched")
1190
- matched_controls.append(catalog_control)
1191
- break
1192
- return matched_controls
1193
-
1194
-
1195
- def get_assessment_status_from_implementation_status(status: str) -> str:
1196
- """
1197
- Get the assessment status from the implementation status
1198
-
1199
- :param str status: Implementation status
1200
- :return: Assessment status
1201
- :rtype: str
1202
- """
1203
- if status == FULLY_IMPLEMENTED:
1204
- return "Pass"
1205
- if status == IN_REMEDIATION:
1206
- return "Fail"
1207
- else:
1208
- return "N/A"
1209
-
1210
-
1211
- def create_assessment_from_cim(cim: Dict, user_id: str, control: Dict, check: List[AssetCheck]) -> Dict:
1212
- """
1213
- Create an assessment from a control implementation
1214
-
1215
- :param Dict cim: Control Implementation
1216
- :param str user_id: User ID
1217
- :param Dict control: Control
1218
- :param List[AssetCheck] check: Asset Check
1219
- :return: Assessment
1220
- :rtype: Dict
1221
- """
1222
- assessment_result = get_assessment_status_from_implementation_status(cim.get("status"))
1223
- summary_dict = check[0].dict() if check else dict()
1224
- summary_dict.pop("reference", None)
1225
- title = summary_dict.get("check_name") if summary_dict else control.get("title")
1226
- html_summary = format_dict_to_html(summary_dict)
1227
- document_reviewed = check[0].audit_file if check else None
1228
- check_name = check[0].check_name if check else None
1229
- methodology = check[0].check_info if check else None
1230
- summary_of_results = check[0].description if check else None
1231
- uuid = check[0].asset_uuid if check and check[0].asset_uuid is not None else None
1232
- title_part = f"{title} - {uuid}" if uuid else f"{title}"
1233
- uuid_title = f"{title_part} Automated Assessment test"
1234
- return {
1235
- "leadAssessorId": user_id,
1236
- "title": uuid_title,
1237
- "assessmentType": "Control Testing",
1238
- "plannedStart": get_current_datetime(),
1239
- "plannedFinish": get_current_datetime(),
1240
- "status": "Complete",
1241
- "assessmentResult": assessment_result if assessment_result else "N/A",
1242
- "controlID": cim["id"],
1243
- "actualFinish": get_current_datetime(),
1244
- "assessmentReport": html_summary if html_summary else "Passed",
1245
- "parentId": cim["id"],
1246
- "parentModule": "controls",
1247
- "assessmentPlan": check_name if check_name else None,
1248
- "documentsReviewed": document_reviewed if document_reviewed else None,
1249
- "methodology": methodology if methodology else None,
1250
- "summaryOfResults": summary_of_results if summary_of_results else None,
1251
- }
1252
-
1253
-
1254
- def get_control_assessments(control: Dict, assessments_to_create: List[Dict]) -> List[Dict]:
1255
- """
1256
- Get control assessments
1257
-
1258
- :param Dict control: Control
1259
- :param List[Dict] assessments_to_create: List of assessments to create
1260
- :return: List of control assessments
1261
- :rtype: List[Dict]
1262
- """
1263
- return [
1264
- assess
1265
- for assess in assessments_to_create
1266
- if assess["controlID"] == control["id"] and assess["status"] == "Complete"
1267
- ]
1268
-
1269
-
1270
- def sort_assessments(control_assessments: List[Dict]) -> List[Dict]:
1271
- """
1272
- Sort assessments by actual finish date
1273
-
1274
- :param List[Dict] control_assessments: List of control assessments
1275
- :return: Sorted assessments
1276
- :rtype: List[Dict]
1277
- """
1278
- dt_format = "%Y-%m-%d %H:%M:%S"
1279
- return sorted(
1280
- control_assessments,
1281
- key=lambda x: datetime.strptime(x["actualFinish"], dt_format),
1282
- reverse=True,
1283
- )
1284
-
1285
-
1286
- def update_control_object(control: Dict, sorted_assessments: List[Dict]) -> None:
1287
- """
1288
- Update control object
1289
-
1290
- :param Dict control: Control
1291
- :param List[Dict] sorted_assessments: Sorted assessments
1292
- :rtype: None
1293
- """
1294
- dt_format = "%Y-%m-%d %H:%M:%S"
1295
- app = Application()
1296
- control["dateLastAssessed"] = sorted_assessments[0]["actualFinish"]
1297
- control["lastAssessmentResult"] = sorted_assessments[0]["assessmentResult"]
1298
- if control.get("lastAssessmentResult"):
1299
- control_obj = ControlImplementation(**control)
1300
- if control_obj.lastAssessmentResult == "Fail" and control_obj.status != IN_REMEDIATION:
1301
- control_obj.status = IN_REMEDIATION
1302
- control_obj.plannedImplementationDate = (datetime.now() + timedelta(30)).strftime(dt_format)
1303
- control_obj.stepsToImplement = "n/a"
1304
- elif control_obj.status == IN_REMEDIATION:
1305
- control_obj.plannedImplementationDate = (
1306
- (datetime.now() + timedelta(30)).strftime(dt_format)
1307
- if not control_obj.plannedImplementationDate
1308
- else control_obj.plannedImplementationDate
1309
- )
1310
- control_obj.stepsToImplement = "n/a" if not control_obj.stepsToImplement else control_obj.stepsToImplement
1311
- elif control_obj.lastAssessmentResult == "Pass" and control_obj.status != FULLY_IMPLEMENTED:
1312
- control_obj.status = FULLY_IMPLEMENTED
1313
- ControlImplementation.update(app=app, implementation=control_obj)
1314
-
1315
-
1316
- def update_control_implementations(control_implementations: List[Dict], assessments_to_create: List[Dict]) -> None:
1317
- """
1318
- Update control implementations with assessments
1319
-
1320
- :param List[Dict] control_implementations: List of control implementations
1321
- :param List[Dict] assessments_to_create: List of assessments to create
1322
- :rtype: None
1323
- """
1324
- for control in control_implementations:
1325
- control_assessments = get_control_assessments(control, assessments_to_create)
1326
- if sorted_assessments := sort_assessments(control_assessments):
1327
- update_control_object(control, sorted_assessments)
1328
-
1329
-
1330
- def post_assessments_to_api(assessments_to_create: List[Dict]) -> None:
1331
- """
1332
- Post assessments to the API
1333
-
1334
- :param List[Dict] assessments_to_create: List of assessments to create
1335
- :rtype: None
1336
- """
1337
- app = Application()
1338
- api = Api()
1339
- assessment_url = urljoin(app.config.get("domain", ""), "/api/assessments/batchCreate")
1340
- assessment_response = api.post(url=assessment_url, json=assessments_to_create)
1341
- if assessment_response.ok:
1342
- logger.info(f"Created {len(assessment_response.json())} Assessments!")
1343
- else:
1344
- logger.debug(assessment_response.status_code)
1345
- logger.error(f"Failed to insert Assessment.\n{assessment_response.text}")
1346
-
1347
-
1348
- def create_assessments(
1349
- control_implementations: List[Dict],
1350
- catalog_controls_dict: Dict,
1351
- asset_checks: Dict,
1352
- ) -> None:
1353
- """
1354
- Create assessments from control implementations
1355
-
1356
- :param List[Dict] control_implementations: List of control implementations
1357
- :param Dict catalog_controls_dict: Dictionary of catalog controls
1358
- :param Dict asset_checks: Dictionary of asset checks
1359
- :rtype: None
1360
- :return: None
1361
- """
1362
- app = Application()
1363
- user_id = app.config.get("userId", "")
1364
- assessments_to_create = []
1365
- for cim in control_implementations:
1366
- control = catalog_controls_dict.get(cim["controlID"], {})
1367
- check = asset_checks.get(control["controlId"].lower())
1368
- assessment = create_assessment_from_cim(cim, user_id, control, check)
1369
- assessments_to_create.append(assessment)
1370
- update_control_implementations(control_implementations, assessments_to_create)
1371
- post_assessments_to_api(assessments_to_create)
1372
-
1373
-
1374
- def process_compliance_data(
1375
- framework_data: Dict,
1376
- catalog_id: int,
1377
- ssp_id: int,
1378
- framework: str,
1379
- passing_controls: Dict,
1380
- failing_controls: Dict,
1381
- ) -> None:
1382
- """
1383
- Processes the compliance data from Tenable.io to create control implementations for controls in frameworks
1384
-
1385
- :param Dict framework_data: List of tenable.io controls per framework
1386
- :param int catalog_id: The catalog id
1387
- :param int ssp_id: The ssp id
1388
- :param str framework: The framework name
1389
- :param Dict passing_controls: Dictionary of passing controls
1390
- :param Dict failing_controls: Dictionary of failing controls
1391
- :rtype: None
1392
- """
1393
- if not framework_data:
1394
- return
1395
- framework_controls = framework_data.get("controls", {})
1396
- asset_checks = framework_data.get("asset_checks", {})
1397
- existing_implementation_dict = get_existing_control_implementations(ssp_id)
1398
- catalog_controls = get_controls(catalog_id)
1399
- matched_controls = []
1400
- for tenable_framework, tenable_controls in framework_controls.items():
1401
- logger.info(f"Found {len(tenable_controls)} controls that passed for framework: {tenable_framework}")
1402
- # logger.info(f"tenable_controls: {tenable_controls[0]}") if len(tenable_controls) >0 else None
1403
- if tenable_framework == framework:
1404
- matched_controls = get_matched_controls(tenable_controls, catalog_controls)
1405
-
1406
- logger.info(f"Found {len(matched_controls)} controls that matched")
1407
-
1408
- control_implementations = create_control_implementations(
1409
- controls=matched_controls,
1410
- parent_id=ssp_id,
1411
- parent_module="securityplans",
1412
- existing_implementation_dict=existing_implementation_dict,
1413
- passing_controls=passing_controls,
1414
- failing_controls=failing_controls,
1415
- )
1416
-
1417
- logger.info(f"SSP now has {len(control_implementations)} control implementations")
1418
- catalog_controls_dict = {c["id"]: c for c in catalog_controls}
1419
- create_assessments(control_implementations, catalog_controls_dict, asset_checks)
1420
-
1421
-
1422
- def convert_control_id(control_id: str) -> str:
1423
- """
1424
- Convert the control id to a format that can be used in Tenable.io
1425
-
1426
- :param str control_id: The control id to convert
1427
- :return: The converted control id
1428
- :rtype: str
1429
- """
1430
- # Convert to lowercase
1431
- control_id = control_id.lower()
1432
-
1433
- # Check if there's a parenthesis and replace its content
1434
- if "(" in control_id and ")" in control_id:
1435
- inner_value = control_id.split("(")[1].split(")")[0]
1436
- control_id = control_id.replace(f"({inner_value})", f".{inner_value}")
1437
-
1438
- return control_id
1439
-
1440
-
1441
- @io.command(name="sync_compliance_controls")
1442
- @regscale_ssp_id()
1443
- @click.option(
1444
- "--catalog_id",
1445
- type=click.INT,
1446
- help="The ID number from RegScale Catalog that the System Security Plan's controls belong to",
1447
- prompt="Enter RegScale Catalog ID",
1448
- required=True,
1449
- )
1450
- @click.option(
1451
- "--framework",
1452
- required=True,
1453
- type=click.Choice(["800-53", "800-53r5", "CSF", "800-171"], case_sensitive=True),
1454
- help="The framework to use. from Tenable.io frameworks MUST be the same RegScale Catalog of controls",
1455
- )
1456
- @hidden_file_path(help="The file path to load control data instead of fetching from Tenable.io")
1457
- def sync_compliance_data(regscale_ssp_id: int, catalog_id: int, framework: str, offline: Optional[Path] = None):
1458
- """
1459
- Sync the compliance data from Tenable.io to create control implementations for controls in frameworks.
1460
- """
1461
- _sync_compliance_data(ssp_id=regscale_ssp_id, catalog_id=catalog_id, framework=framework, offline=offline)
1462
-
1463
-
1464
- def _sync_compliance_data(ssp_id: int, catalog_id: int, framework: str, offline: Optional[Path] = None) -> None:
1465
- """
1466
- Sync the compliance data from Tenable.io to create control implementations for controls in frameworks
1467
- :param int ssp_id: The ID number from RegScale of the System Security Plan
1468
- :param int catalog_id: The ID number from RegScale Catalog that the System Security Plan's controls belong to
1469
- :param str framework: The framework to use. from Tenable.io frameworks MUST be the same RegScale Catalog of controls
1470
- :param Optional[Path] offline: The file path to load control data instead of fetching from Tenable.io, defaults to None
1471
- :rtype: None
1472
- """
1473
- logger.info("Note: This command only available for Tenable.io")
1474
- logger.info("Note: This command Requires admin access.")
1475
- app = Application()
1476
- config = app.config
1477
- # we specifically don't gen client here, so we only get the client for Tenable.io as its only supported there
1478
-
1479
- compliance_data = _get_compliance_data(config=config, offline=offline) # type: ignore
1480
-
1481
- dict_of_frameworks_and_asset_checks: Dict = dict()
1482
- framework_controls: Dict[str, List[str]] = {}
1483
- asset_checks: Dict[str, List[AssetCheck]] = {}
1484
- passing_controls: Dict = dict()
1485
- # partial_passing_controls: Dict = dict()
1486
- failing_controls: Dict = dict()
1487
- for findings in compliance_data:
1488
- asset_check = AssetCheck(**findings)
1489
- for ref in asset_check.reference:
1490
- if ref.framework not in framework_controls:
1491
- framework_controls[ref.framework] = []
1492
- if ref.control not in framework_controls[ref.framework]: # Avoid duplicate controls
1493
- framework_controls[ref.framework].append(ref.control)
1494
- formatted_control_id = convert_control_id(ref.control)
1495
- # sort controls by status
1496
- add_control_to_status_dict(
1497
- control_id=formatted_control_id,
1498
- status=asset_check.status,
1499
- dict_obj=failing_controls,
1500
- desired_status="FAILED",
1501
- )
1502
- add_control_to_status_dict(
1503
- control_id=formatted_control_id,
1504
- status=asset_check.status,
1505
- dict_obj=passing_controls,
1506
- desired_status="PASSED",
1507
- )
1508
- remove_passing_controls_if_in_failed_status(passing=passing_controls, failing=failing_controls)
1509
- if formatted_control_id not in asset_checks:
1510
- asset_checks[formatted_control_id] = [asset_check]
1511
- else:
1512
- asset_checks[formatted_control_id].append(asset_check)
1513
- dict_of_frameworks_and_asset_checks = {
1514
- key: {"controls": framework_controls, "asset_checks": asset_checks} for key in framework_controls.keys()
1515
- }
1516
- logger.info(f"Found {len(dict_of_frameworks_and_asset_checks)} findings to process")
1517
- framework_data = dict_of_frameworks_and_asset_checks.get(framework, None)
1518
- process_compliance_data(
1519
- framework_data=framework_data,
1520
- catalog_id=catalog_id,
1521
- ssp_id=ssp_id,
1522
- framework=framework,
1523
- passing_controls=passing_controls,
1524
- failing_controls=failing_controls,
1525
- )
1526
-
1527
-
1528
- def _get_compliance_data(config: dict, offline: Optional[Path] = None) -> Dict:
1529
- """
1530
- Get compliance data from Tenable.io
1531
-
1532
- :param dict config: Configuration dictionary
1533
- :param Optional[Path] offline: File path to load control data instead of fetching from Tenable.io
1534
- :return: Compliance data
1535
- :rtype: Dict
1536
- """
1537
- from tenable.io import TenableIO
1538
-
1539
- if offline:
1540
- with open(offline.absolute(), "r") as f:
1541
- compliance_data = json.load(f)
1542
- else:
1543
- client = TenableIO(
1544
- url=config["tenableUrl"],
1545
- access_key=config["tenableAccessKey"],
1546
- secret_key=config["tenableSecretKey"],
1547
- vendor=REGSCALE_INC,
1548
- product=REGSCALE_CLI,
1549
- build=__version__,
1550
- )
1551
- compliance_data = client.exports.compliance()
1552
- return compliance_data
1553
-
1554
-
1555
- def add_control_to_status_dict(control_id: str, status: str, dict_obj: Dict, desired_status: str) -> None:
1556
- """
1557
- Add a control to a status dictionary
1558
-
1559
- :param str control_id: The control id to add to the dictionary
1560
- :param str status: The status of the control
1561
- :param Dict dict_obj: The dictionary to add the control to
1562
- :param str desired_status: The desired status of the control
1563
- :rtype: None
1564
- """
1565
- friendly_control_id = control_id.lower()
1566
- if status == desired_status and friendly_control_id not in dict_obj:
1567
- dict_obj[friendly_control_id] = desired_status
1568
-
1569
-
1570
- def remove_passing_controls_if_in_failed_status(passing: Dict, failing: Dict) -> None:
1571
- """
1572
- Remove passing controls if they are in failed status
1573
-
1574
- :param Dict passing: Dictionary of passing controls
1575
- :param Dict failing: Dictionary of failing controls
1576
- :rtype: None
1577
- """
1578
- to_remove = []
1579
- for k in passing.keys():
1580
- if k in failing.keys():
1581
- to_remove.append(k)
1582
-
1583
- for k in to_remove:
1584
- del passing[k]
1585
-
1586
-
1587
- def write_io_chunk(
1588
- data: List[dict],
1589
- data_dir: Path,
1590
- export_uuid: str,
1591
- export_type: str,
1592
- export_chunk_id: int,
1593
- ) -> None:
1594
- """
1595
- Write a chunk of data to a file, this function is formatted for use with PyTenable and Tenable IO
1596
-
1597
- :param List[dict] data: Data to write to a file
1598
- :param Path data_dir: Directory to write the file to
1599
- :param str export_uuid: UUID of the export (Tenable IO)
1600
- :param str export_type: Type of export (Tenable IO)
1601
- :param int export_chunk_id: ID of the chunk (Tenable IO)
1602
- :rtype: None
1603
- """
1604
- # create tenable io directory
1605
- data_dir.mkdir(parents=True, exist_ok=True)
1606
- fn = data_dir / f"{export_type}-{export_uuid}-{export_chunk_id}.json"
1607
- # append file to path
1608
- with open(file=fn, mode="w", encoding="utf-8") as file_object:
1609
- json.dump(data, file_object)
1610
-
1611
-
1612
- def process_to_regscale(data_dir: Path, ssp_id: int, existing_assets: List[Asset]) -> None:
1613
- """
1614
- Process the Tenable data to RegScale
1615
-
1616
- :param Path data_dir: Directory to process the data from
1617
- :param int ssp_id: The ID of the System Security Plan
1618
- :param List[Asset] existing_assets: List of existing assets
1619
- :rtype: None
1620
- :return: None
1621
- """
1622
- # get all files in the directory
1623
- files = list(data_dir.glob("*.json"))
1624
- if not files:
1625
- logger.warning("No Tenable files found in %s.", data_dir)
1626
- return
1627
- logger.info("Processing %i chunked file(s) from Tenable...", len(list(files)))
1628
- for file in files:
1629
- logger.info("Processing chunked data: %s", file)
1630
- file_assets = []
1631
- with open(file=file, mode="r", encoding="utf-8") as file_object:
1632
- tenable_io_data = json.load(file_object)
1633
- for asset in tenable_io_data:
1634
- file_assets.append(TenableIOAsset(**asset))
1635
- TenableIOAsset.sync_to_regscale(assets=file_assets, ssp_id=ssp_id, existing_assets=existing_assets)
1636
- # remove processed file
1637
- file.unlink()