regscale-cli 6.20.10.0__py3-none-any.whl → 6.21.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 (37) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +12 -5
  3. regscale/core/app/internal/set_permissions.py +58 -27
  4. regscale/integrations/commercial/nessus/scanner.py +2 -0
  5. regscale/integrations/commercial/sonarcloud.py +35 -36
  6. regscale/integrations/commercial/synqly/ticketing.py +51 -0
  7. regscale/integrations/integration_override.py +15 -6
  8. regscale/integrations/scanner_integration.py +163 -35
  9. regscale/models/integration_models/amazon_models/inspector_scan.py +32 -57
  10. regscale/models/integration_models/aqua.py +92 -78
  11. regscale/models/integration_models/cisa_kev_data.json +47 -4
  12. regscale/models/integration_models/defenderimport.py +64 -59
  13. regscale/models/integration_models/ecr_models/ecr.py +100 -147
  14. regscale/models/integration_models/flat_file_importer/__init__.py +52 -38
  15. regscale/models/integration_models/ibm.py +29 -47
  16. regscale/models/integration_models/nexpose.py +156 -68
  17. regscale/models/integration_models/prisma.py +46 -66
  18. regscale/models/integration_models/qualys.py +99 -93
  19. regscale/models/integration_models/snyk.py +229 -158
  20. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  21. regscale/models/integration_models/veracode.py +15 -20
  22. regscale/models/integration_models/xray.py +276 -82
  23. regscale/models/regscale_models/control_implementation.py +14 -12
  24. regscale/models/regscale_models/milestone.py +1 -1
  25. regscale/models/regscale_models/rbac.py +22 -0
  26. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.0.0.dist-info}/METADATA +1 -1
  27. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.0.0.dist-info}/RECORD +37 -36
  28. tests/fixtures/test_fixture.py +58 -2
  29. tests/regscale/core/test_app.py +5 -3
  30. tests/regscale/integrations/test_integration_mapping.py +522 -40
  31. tests/regscale/integrations/test_issue_due_date.py +1 -1
  32. tests/regscale/integrations/test_update_finding_dates.py +336 -0
  33. tests/regscale/models/test_asset.py +406 -50
  34. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.0.0.dist-info}/LICENSE +0 -0
  35. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.0.0.dist-info}/WHEEL +0 -0
  36. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.0.0.dist-info}/entry_points.txt +0 -0
  37. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.0.0.dist-info}/top_level.txt +0 -0
regscale/_version.py CHANGED
@@ -33,7 +33,7 @@ def get_version_from_pyproject() -> str:
33
33
  return match.group(1)
34
34
  except Exception:
35
35
  pass
36
- return "6.20.10.0" # fallback version
36
+ return "6.21.0.0" # fallback version
37
37
 
38
38
 
39
39
  __version__ = get_version_from_pyproject()
@@ -245,6 +245,7 @@ class Application(metaclass=Singleton):
245
245
  "snowPassword": "<snowPassword>",
246
246
  "snowUrl": "<mySnowUrl>",
247
247
  "snowUserName": "<snowUserName>",
248
+ "sonarUrl": "https://sonarcloud.io",
248
249
  "sonarToken": "<mySonarToken>",
249
250
  "tenableAccessKey": "<tenableAccessKeyGoesHere>",
250
251
  "tenableSecretKey": "<tenableSecretKeyGoesHere>",
@@ -295,7 +296,10 @@ class Application(metaclass=Singleton):
295
296
  self.running_in_airflow = os.getenv("REGSCALE_AIRFLOW") == "true"
296
297
  if isinstance(config, str):
297
298
  config = self._read_config_from_str(config)
298
- self.config = self._gen_config(config)
299
+ if self.running_in_airflow:
300
+ self.config = self._fetch_config_from_regscale(config)
301
+ else:
302
+ self.config = self._gen_config(config)
299
303
  self.os = platform.system()
300
304
  self.input_host = ""
301
305
  # Ensure maxThreads is an integer for ThreadManager
@@ -536,10 +540,6 @@ class Application(metaclass=Singleton):
536
540
  self.logger.debug("Successfully retrieved config from Click context.")
537
541
  return click_config
538
542
 
539
- if self.running_in_airflow:
540
- if airflow_config := self._get_airflow_config(config):
541
- self.logger.debug("Successfully retrieved config from Airflow.")
542
- return airflow_config
543
543
  try:
544
544
  if config and self.local_config:
545
545
  self.logger.debug(f"Config provided as :\n{type(config)}")
@@ -572,6 +572,13 @@ class Application(metaclass=Singleton):
572
572
  return config
573
573
 
574
574
  def _get_airflow_config(self, config: Optional[Union[dict, str]] = None) -> Optional[dict]:
575
+ """
576
+ Get config from Airflow DAG config, or from the environment variables if not provided.
577
+
578
+ :param Optional[Union[dict, str]] config: Configuration dictionary, defaults to None
579
+ :return: Configuration dictionary
580
+ :rtype: Optional[dict]
581
+ """
575
582
  if config:
576
583
  self.logger.debug(f"Received config from Airflow as: {type(config)}")
577
584
  # check to see if config is a string because airflow can pass a string instead of a dict
@@ -1,11 +1,9 @@
1
1
  import click
2
2
  from pathlib import Path
3
3
  import os
4
- from openpyxl import Workbook, load_workbook
5
- from openpyxl.styles import Protection, Font, NamedStyle
6
- from openpyxl.worksheet.worksheet import Worksheet
4
+ from rich.progress import track
5
+ from openpyxl import Workbook
7
6
  from openpyxl.worksheet.datavalidation import DataValidation
8
- from openpyxl import Workbook, load_workbook
9
7
  from openpyxl.utils.dataframe import dataframe_to_rows
10
8
 
11
9
  from regscale.core.app.logz import create_logger
@@ -118,38 +116,71 @@ def import_permissions_workbook(file: Path):
118
116
  """
119
117
  # Read in the spreadsheet
120
118
  records = get_records(file=file)
121
- for record in records:
119
+ for index in track(
120
+ range(len(records)),
121
+ description="Processing rbacs...",
122
+ ):
123
+ record = records[index]
124
+ # Check the records:
125
+
122
126
  if not Group.get_object(record["group_id"]):
123
127
  logger.error(f"Group {record['group_id']} doesn't exist in this instance. Skipping row")
124
128
  continue
125
129
 
126
- regscale_module_id = Modules.get_module_to_id(record["regscale_module"])
127
- regscale_id = record["regscale_id"]
128
- if record[READ_UPDATE] == "R":
129
- permissions = 1
130
- elif record[READ_UPDATE] == "RU":
131
- permissions = 2
132
- else:
133
- permissions = 0
134
-
135
- # Set the record to private
136
130
  my_class = Modules.module_to_class(record["regscale_module"])
137
- obj = my_class.get_object(regscale_id)
131
+ obj = my_class.get_object(record["regscale_id"])
138
132
  if not obj:
139
- logger.error(f"RegScale {record['regscale_module']} record {regscale_id} doesn't exist. Skipping row")
133
+ logger.error(
134
+ f"RegScale {record['regscale_module']} record {record['regscale_id']} doesn't exist. Skipping row"
135
+ )
140
136
  continue
141
137
 
142
- # Add the permission
143
- RBAC.add(
144
- module_id=regscale_module_id,
145
- parent_id=regscale_id,
146
- group_id=record["group_id"],
147
- permission_type=permissions,
138
+ # Set the permissions
139
+ set_permissions(record=record)
140
+
141
+
142
+ def set_permissions(record: dict):
143
+ """
144
+ Sets the permissions for each record
145
+
146
+ :param dict record: permissions dictionary
147
+ """
148
+
149
+ if record[READ_UPDATE] == "R":
150
+ permissions = 1
151
+ elif record[READ_UPDATE] == "RU":
152
+ permissions = 2
153
+ else:
154
+ permissions = 0
155
+
156
+ # Add the permission
157
+ if not RBAC.add(
158
+ module_id=Modules.get_module_to_id(record["regscale_module"]),
159
+ parent_id=record["regscale_id"],
160
+ group_id=record["group_id"],
161
+ permission_type=permissions,
162
+ ):
163
+ logger.warning(
164
+ f"Failed to set permissions for {record['regscale_module']} {record['regscale_id']} for group {record['group_id']}"
165
+ )
166
+ return
167
+
168
+ if not RBAC.public(
169
+ module_id=Modules.get_module_to_id(record["regscale_module"]),
170
+ parent_id=record["regscale_id"],
171
+ is_public=0 if record[PUBLIC_PRIVATE] == "private" else 1,
172
+ ):
173
+ logger.warning(
174
+ f"Failed to set public/private for {record['regscale_module']} {record['regscale_id']} for group {record['group_id']}"
148
175
  )
149
- RBAC.public(
150
- module_id=regscale_module_id,
151
- parent_id=regscale_id,
152
- is_public=0 if record[PUBLIC_PRIVATE] == "private" else 1,
176
+ return
177
+
178
+ if not RBAC.reset(
179
+ module_id=Modules.get_module_to_id(record["regscale_module"]),
180
+ parent_id=record["regscale_id"],
181
+ ):
182
+ logger.warning(
183
+ f"Failed to proliferate permissions for {record['regscale_module']} {record['regscale_id']} for group {record['group_id']}"
153
184
  )
154
185
 
155
186
 
@@ -83,6 +83,8 @@ class NessusIntegration(ScannerIntegration):
83
83
  for file in iterate_files(file_collection):
84
84
  content = read_file(file)
85
85
  root = ET.fromstring(content)
86
+ if scan_dt := nfr.scan.scan_time_start(root):
87
+ self.scan_date = scan_dt.strftime("%Y-%m-%d")
86
88
  for nessus_asset in nfr.scan.report_hosts(root):
87
89
  asset_name = nfr.host.report_host_name(nessus_asset)
88
90
  for nessus_vulnerability in root.iterfind(f"./Report/ReportHost[@name='{asset_name}']/ReportItem"):
@@ -2,100 +2,93 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """RegScale SonarCloud Integration"""
4
4
 
5
- # standard python imports
5
+ import logging
6
6
  import math
7
- import sys
8
7
  from typing import Optional
8
+ from urllib.parse import urljoin
9
9
 
10
10
  import click
11
11
  import requests # type: ignore
12
12
 
13
13
  from regscale.core.app.api import Api
14
14
  from regscale.core.app.application import Application
15
- from regscale.core.app.logz import create_logger
16
15
  from regscale.core.app.utils.app_utils import (
17
- get_current_datetime,
18
16
  days_between,
17
+ error_and_exit,
18
+ get_current_datetime,
19
19
  )
20
20
  from regscale.models import regscale_id, regscale_module
21
21
  from regscale.models.regscale_models.assessment import Assessment
22
22
  from regscale.models.regscale_models.issue import Issue
23
23
 
24
- # create logger function to log to the console
25
- logger = create_logger()
24
+ logger = logging.getLogger("regscale")
26
25
 
27
26
 
28
- def get_sonarcloud_results(config: dict) -> list[list[dict]]:
27
+ def get_sonarcloud_results(config: dict, branch: Optional[str] = None) -> list[list[dict]]:
29
28
  """
30
29
  Retrieve Sonarcloud Results from the Sonarcloud.io API
30
+
31
31
  :param dict config: RegScale CLI configuration
32
+ :param Optional[str] branch: Branch name to filter results, defaults to None
32
33
  :return: json response data from API GET request
33
34
  :rtype: list[list[dict]]
34
35
  """
36
+ # create an empty list to hold multiple pages of data
37
+ complete = []
35
38
  # api endpoint
36
- url = "https://sonarcloud.io/api/issues/search"
39
+ url = urljoin(config["sonarUrl"], "/api/issues/search")
37
40
  # SONAR_TOKEN from Sonarcloud
38
41
  token = config["sonarToken"]
39
42
  # arguments to pass to the API call
40
- query = {
41
- "organization": "regscale",
42
- "projects": "RegScale_regscale",
43
- "branch": "main",
44
- "projectKey": "RegScale_regscale",
43
+ params = {
45
44
  "statuses": "OPEN, CONFIRMED, REOPENED",
46
- "createdInLast": "1m",
47
45
  "ps": 500,
48
46
  }
47
+ if branch:
48
+ params["branch"] = branch
49
49
  # GET request pulls in data to check results size
50
- r = requests.get(url, auth=(str(token), ""), params=query)
50
+ r = requests.get(url, auth=(str(token), ""), params=params)
51
51
  # if the status code does not equal 200
52
52
  if r and not r.ok:
53
53
  # exit the script gracefully
54
- sys.exit("Sonarcloud API call failed please check the configuration")
54
+ error_and_exit(f"Sonarcloud API call failed please check the configuration\n{r.status_code}: {r.text}")
55
55
  # pull in response data to a dictionary
56
56
  data = r.json()
57
57
  # find the total results number
58
58
  total = data["paging"]["total"]
59
+ complete.extend(data.get("issues", []))
60
+ logger.info(f"Found {total} issue(s) from SonarCloud/Qube.")
59
61
  # find the number of results in each result page
60
62
  size = data["paging"]["pageSize"]
61
63
  # calculate the number of pages to iterate through sequentially
62
64
  pages = math.ceil(total / size)
63
- # create an empty list to hold multiple pages of data
64
- complete = []
65
65
  # loop through each page number
66
66
  for i in range(1, pages + 1, 1):
67
67
  # parameters to pass to the API call
68
- filters = {
69
- "organization": "regscale",
70
- "projects": "RegScale_regscale",
71
- "branch": "main",
72
- "projectKey": "RegScale_regscale",
73
- "statuses": "OPEN, CONFIRMED, REOPENED",
74
- "createdInLast": "1m",
75
- "ps": 500,
76
- "p": f"{i}",
77
- }
68
+ params["p"] = str(i)
78
69
  # for each page make a GET request to pull in the data
79
- r = requests.get(url, auth=(str(token), ""), params=filters)
70
+ r = requests.get(url, auth=(str(token), ""), params=params)
80
71
  # pull in response data to a dictionary
81
72
  data = r.json()
82
73
  # extract only the issues from the data
83
74
  issues = data["issues"]
84
75
  # add each page to the total results page
85
- complete.append(issues)
76
+ complete.extend(issues)
86
77
  # return the list of json response objects for use
78
+ logger.info(f"Retrieved {len(complete)}/{total} issue(s) from SonarCloud/Qube.")
87
79
  return complete
88
80
 
89
81
 
90
- def build_data(api: Api) -> list[dict]:
82
+ def build_data(api: Api, branch: Optional[str] = None) -> list[dict]:
91
83
  """
92
84
  Build vulnerability alert data list
93
85
  :param Api api: API object
86
+ :param Optional[str] branch: Branch name to filter results, defaults to None
94
87
  :return: vulnerability data list
95
88
  :rtype: list[dict]
96
89
  """
97
90
  # execute GET request
98
- data = get_sonarcloud_results(config=api.config)
91
+ data = get_sonarcloud_results(config=api.config, branch=branch)
99
92
  # create empty list to hold json response dicts
100
93
  vulnerability_data_list = []
101
94
  # loop through the lists in API response data
@@ -196,11 +189,14 @@ def create_alert_assessment(
196
189
  return None
197
190
 
198
191
 
199
- def create_alert_issues(parent_id: Optional[int] = None, parent_module: Optional[str] = None) -> None:
192
+ def create_alert_issues(
193
+ parent_id: Optional[int] = None, parent_module: Optional[str] = None, branch: Optional[str] = None
194
+ ) -> None:
200
195
  """
201
196
  Create child issues from the alert assessment
202
197
  :param Optional[int] parent_id: Parent ID record to associate the assessment to, defaults to None
203
198
  :param Optional[str] parent_module: Parent module to associate the assessment to, defaults to None
199
+ :param Optional[str] branch: Branch name to filter results, defaults to None
204
200
  :rtype: None
205
201
  """
206
202
  # set environment and application configuration
@@ -210,7 +206,7 @@ def create_alert_issues(parent_id: Optional[int] = None, parent_module: Optional
210
206
  assessment_id = create_alert_assessment(api=api, parent_id=parent_id, parent_module=parent_module)
211
207
 
212
208
  # create vulnerability data list
213
- vuln_data_list = build_data(api)
209
+ vuln_data_list = build_data(api, branch)
214
210
  # loop through each vulnerability alert in the list
215
211
  for vulnerability in vuln_data_list:
216
212
  # create issue model
@@ -253,8 +249,11 @@ def sonarcloud() -> None:
253
249
  @sonarcloud.command(name="sync_alerts")
254
250
  @regscale_id(required=False, default=None)
255
251
  @regscale_module(required=False, default=None)
256
- def create_alerts(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None) -> None:
252
+ @click.option("--branch", help="Branch name to filter results, defaults to None")
253
+ def create_alerts(
254
+ regscale_id: Optional[int] = None, regscale_module: Optional[str] = None, branch: Optional[str] = None
255
+ ) -> None:
257
256
  """
258
257
  Create a child assessment and child issues in RegScale from SonarCloud alerts.
259
258
  """
260
- create_alert_issues(regscale_id, regscale_module)
259
+ create_alert_issues(regscale_id, regscale_module, branch)
@@ -100,6 +100,57 @@ def sync_jira(
100
100
  )
101
101
 
102
102
 
103
+ @ticketing.command(name="sync_jira_service_management")
104
+ @regscale_id()
105
+ @regscale_module()
106
+ @click.option(
107
+ "--project",
108
+ type=click.STRING,
109
+ help="jira_service_management project",
110
+ required=True,
111
+ prompt="jira_service_management project",
112
+ )
113
+ @click.option(
114
+ "--default_issue_type",
115
+ type=click.STRING,
116
+ help="Default issue type when creating tickets.",
117
+ required=False,
118
+ )
119
+ @click.option(
120
+ "--default_project",
121
+ type=click.STRING,
122
+ help="Default project when listing, creating, or editing tickets.",
123
+ required=False,
124
+ )
125
+ @click.option(
126
+ "--sync_attachments",
127
+ type=click.BOOL,
128
+ help="Whether to sync attachments between Jira Service Management and RegScale",
129
+ required=False,
130
+ default=True,
131
+ )
132
+ def sync_jira_service_management(
133
+ regscale_id: int,
134
+ regscale_module: str,
135
+ project: str,
136
+ default_issue_type: str,
137
+ default_project: str,
138
+ sync_attachments: bool,
139
+ ) -> None:
140
+ """Sync Ticketing data between Jira Service Management and RegScale."""
141
+ from regscale.models.integration_models.synqly_models.connectors import Ticketing
142
+
143
+ ticketing_jira_service_management = Ticketing("jira_service_management")
144
+ ticketing_jira_service_management.run_sync(
145
+ regscale_id=regscale_id,
146
+ regscale_module=regscale_module,
147
+ project=project,
148
+ default_issue_type=default_issue_type,
149
+ default_project=default_project,
150
+ sync_attachments=sync_attachments,
151
+ )
152
+
153
+
103
154
  @ticketing.command(name="sync_pagerduty")
104
155
  @regscale_id()
105
156
  @regscale_module()
@@ -2,8 +2,6 @@
2
2
  A simple singleton class that loads custom integration mappings, if available
3
3
  """
4
4
 
5
- from regscale.core.app.application import Application
6
-
7
5
  # pylint: disable=C0415
8
6
 
9
7
 
@@ -74,9 +72,12 @@ class IntegrationOverride:
74
72
  :return: The mapped field name
75
73
  :rtype: Optional[str]
76
74
  """
77
- if integration and self.mapping_exists(integration, field_name):
75
+ if integration and field_name and self.mapping_exists(integration, field_name):
78
76
  integration_map = self.mapping.get(integration.lower(), {})
79
- return integration_map.get(field_name.lower())
77
+ # Find the actual key that matches case-insensitively
78
+ for key in integration_map.keys():
79
+ if key.lower() == field_name.lower():
80
+ return integration_map.get(key)
80
81
  return None
81
82
 
82
83
  def mapping_exists(self, integration: str, field_name: str) -> bool:
@@ -88,8 +89,16 @@ class IntegrationOverride:
88
89
  :return: Whether the mapping exists
89
90
  :rtype: bool
90
91
  """
92
+ if not integration or not field_name:
93
+ return False
91
94
  the_map = self.mapping.get(integration.lower())
92
- return the_map and field_name.lower() in the_map and the_map.get(field_name.lower()) != "default"
95
+ if not the_map:
96
+ return False
97
+ # Find the actual key that matches case-insensitively
98
+ for key in the_map.keys():
99
+ if key.lower() == field_name.lower():
100
+ return the_map.get(key) != "default"
101
+ return False
93
102
 
94
103
  def field_map_validation(self, obj: Any, model_type: str) -> Optional[str]:
95
104
  """
@@ -131,7 +140,7 @@ class IntegrationOverride:
131
140
  },
132
141
  }
133
142
  # The type an associated fields we are able to override. Limited for now.
134
- supported_fields = {"asset": {"ipAddress", "name", "fqdn", "dns"}}
143
+ supported_fields = {"asset": {"ipAddress", "name", "fqdn", "dns"}, "issue": {"dateFirstDetected"}}
135
144
  if regscale_field not in supported_fields.get(model_type.lower(), set()):
136
145
  return match
137
146