regscale-cli 6.21.2.1__py3-none-any.whl → 6.22.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 (30) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +3 -0
  3. regscale/core/app/utils/app_utils.py +31 -0
  4. regscale/integrations/commercial/jira.py +27 -5
  5. regscale/integrations/commercial/qualys/__init__.py +160 -60
  6. regscale/integrations/commercial/qualys/scanner.py +300 -39
  7. regscale/integrations/commercial/wizv2/async_client.py +4 -0
  8. regscale/integrations/commercial/wizv2/scanner.py +50 -24
  9. regscale/integrations/public/__init__.py +13 -0
  10. regscale/integrations/public/fedramp/fedramp_cis_crm.py +175 -51
  11. regscale/integrations/scanner_integration.py +519 -151
  12. regscale/models/integration_models/cisa_kev_data.json +34 -3
  13. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  14. regscale/models/regscale_models/__init__.py +2 -0
  15. regscale/models/regscale_models/catalog.py +1 -1
  16. regscale/models/regscale_models/control_implementation.py +8 -8
  17. regscale/models/regscale_models/form_field_value.py +5 -3
  18. regscale/models/regscale_models/inheritance.py +44 -0
  19. regscale/models/regscale_models/milestone.py +20 -3
  20. regscale/regscale.py +2 -0
  21. {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/METADATA +1 -1
  22. {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/RECORD +27 -29
  23. tests/regscale/models/test_tenable_integrations.py +811 -105
  24. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
  25. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
  26. regscale/integrations/public/fedramp/parts_mapper.py +0 -107
  27. {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/LICENSE +0 -0
  28. {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/WHEEL +0 -0
  29. {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/entry_points.txt +0 -0
  30. {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.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.21.2.1" # fallback version
36
+ return "6.22.0.0" # fallback version
37
37
 
38
38
 
39
39
  __version__ = get_version_from_pyproject()
@@ -81,6 +81,9 @@ class Application(metaclass=Singleton):
81
81
  "crowdstrikeClientId": DEFAULT_CLIENT,
82
82
  "crowdstrikeClientSecret": DEFAULT_SECRET,
83
83
  "crowdstrikeBaseUrl": "<crowdstrikeApiUrl>",
84
+ "csamToken": DEFAULT_SECRET,
85
+ "csamURL": "<myCSAMURLgoeshere>",
86
+ "csamFilter": {},
84
87
  "dependabotId": "<myGithubUserIdGoesHere>",
85
88
  "dependabotOwner": "<myGithubRepoOwnerGoesHere>",
86
89
  "dependabotRepo": "<myGithubRepoNameGoesHere>",
@@ -1108,3 +1108,34 @@ def extract_vuln_id_from_strings(text: str) -> Union[list, str]:
1108
1108
  if res:
1109
1109
  return res # no need to save spaces
1110
1110
  return text
1111
+
1112
+
1113
+ def filter_list(input_list: list, input_filter: Optional[dict]) -> list:
1114
+ """
1115
+ Filter an input list based on the filter
1116
+ Implicit "and" between all keys
1117
+ Implicit "or" between values within a key
1118
+
1119
+ :param list filter_list: List of data to be filtered
1120
+ :param dict input_filter: Filter criteria
1121
+ :return: Filtered list
1122
+ :return_type: list
1123
+ """
1124
+ if not input_filter:
1125
+ return input_list
1126
+
1127
+ filtered_results = []
1128
+ for item in input_list:
1129
+ match = True
1130
+ for key, value in input_filter.items():
1131
+ if isinstance(value, list):
1132
+ if item.get(key) not in value:
1133
+ match = False
1134
+ break
1135
+ elif item.get(key) != value:
1136
+ match = False
1137
+ break
1138
+ if match:
1139
+ filtered_results.append(item)
1140
+
1141
+ return filtered_results
@@ -98,6 +98,12 @@ def jira():
98
98
  is_flag=True,
99
99
  help="Use token authentication for Jira API instead of basic auth, defaults to False.",
100
100
  )
101
+ @click.option(
102
+ "--jql",
103
+ type=click.STRING,
104
+ help="Custom JQL query for filtering Jira issues.",
105
+ required=False,
106
+ )
101
107
  def issues(
102
108
  regscale_id: int,
103
109
  regscale_module: str,
@@ -105,6 +111,7 @@ def issues(
105
111
  jira_issue_type: str,
106
112
  sync_attachments: bool = True,
107
113
  token_auth: bool = False,
114
+ jql: Optional[str] = None,
108
115
  ):
109
116
  """Sync issues from Jira into RegScale."""
110
117
  sync_regscale_and_jira(
@@ -114,6 +121,7 @@ def issues(
114
121
  jira_issue_type=jira_issue_type,
115
122
  sync_attachments=sync_attachments,
116
123
  token_auth=token_auth,
124
+ jql=jql,
117
125
  )
118
126
 
119
127
 
@@ -143,12 +151,19 @@ def issues(
143
151
  is_flag=True,
144
152
  help="Use token authentication for Jira API instead of basic auth, defaults to False.",
145
153
  )
154
+ @click.option(
155
+ "--jql",
156
+ type=click.STRING,
157
+ help="Custom JQL query for filtering Jira tasks.",
158
+ required=False,
159
+ )
146
160
  def tasks(
147
161
  regscale_id: int,
148
162
  regscale_module: str,
149
163
  jira_project: str,
150
164
  sync_attachments: bool = True,
151
165
  token_auth: bool = False,
166
+ jql: Optional[str] = None,
152
167
  ):
153
168
  """Sync tasks from Jira into RegScale."""
154
169
  sync_regscale_and_jira(
@@ -159,6 +174,7 @@ def tasks(
159
174
  sync_attachments=sync_attachments,
160
175
  sync_tasks_only=True,
161
176
  token_auth=token_auth,
177
+ jql=jql,
162
178
  )
163
179
 
164
180
 
@@ -202,6 +218,7 @@ def sync_regscale_and_jira(
202
218
  sync_attachments: bool = True,
203
219
  sync_tasks_only: bool = False,
204
220
  token_auth: bool = False,
221
+ jql: Optional[str] = None,
205
222
  ) -> None:
206
223
  """
207
224
  Sync issues, bidirectionally, from Jira into RegScale as issues
@@ -213,6 +230,7 @@ def sync_regscale_and_jira(
213
230
  :param bool sync_attachments: Whether to sync attachments in RegScale & Jira, defaults to True
214
231
  :param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
215
232
  :param bool token_auth: Use token authentication for Jira API, defaults to False
233
+ :param Optional[str] jql: Custom JQL query for filtering Jira issues/tasks, defaults to None
216
234
  :rtype: None
217
235
  """
218
236
  app = check_license()
@@ -225,11 +243,15 @@ def sync_regscale_and_jira(
225
243
  # create Jira client
226
244
  jira_client = create_jira_client(config, token_auth)
227
245
 
228
- jql_str = (
229
- f"project = {jira_project} AND issueType = {jira_issue_type}"
230
- if sync_tasks_only
231
- else f"project = {jira_project}"
232
- )
246
+ # Use custom JQL if provided, otherwise build default JQL
247
+ if jql:
248
+ jql_str = jql
249
+ else:
250
+ jql_str = (
251
+ f"project = {jira_project} AND issueType = {jira_issue_type}"
252
+ if sync_tasks_only
253
+ else f"project = {jira_project}"
254
+ )
233
255
  regscale_objects, regscale_attachments = get_regscale_data_and_attachments(
234
256
  parent_id=parent_id,
235
257
  parent_module=parent_module,
@@ -280,7 +280,7 @@ class FindingProgressTracker:
280
280
  try:
281
281
  finding = next(self.findings_iter)
282
282
  self.count += 1
283
- if finding and hasattr(finding, "external_id"):
283
+ if finding and hasattr(finding, "external_id") and finding.external_id is not None:
284
284
  self.finding_ids.append(finding.external_id)
285
285
  self.progress.update(self.finding_task, advance=1)
286
286
  return finding
@@ -378,6 +378,11 @@ def import_total_cloud(
378
378
  if exclude_tags and not include_tags:
379
379
  error_and_exit("You must provide --include_tags when using --exclude_tags to import Qualys Total Cloud data.")
380
380
 
381
+ # Ensure vulnerability creation is properly set
382
+ if not vulnerability_creation:
383
+ vulnerability_creation = "IssueCreation" # Default to IssueCreation for Qualys
384
+ logger.info("No vulnerability creation setting provided, defaulting to IssueCreation for Qualys Total Cloud")
385
+
381
386
  containers_lst = []
382
387
  try:
383
388
  # Configure scanner variables and fetch data
@@ -1631,26 +1636,12 @@ def sync_assets(
1631
1636
  """
1632
1637
  parent_module = "components" if is_component else "securityplans"
1633
1638
  update_assets = []
1634
- for qualys_asset in qualys_assets: # you can list as many input dicts as you want here
1635
- logger.debug("qualys_asset: %s", qualys_asset)
1636
- if not isinstance(qualys_asset, dict):
1637
- logger.error("Expected dict, got %s: %s", type(qualys_asset), qualys_asset)
1638
- continue
1639
- # Update parent id to SSP or Component on insert
1640
- if lookup_assets := lookup_asset(reg_assets, qualys_asset["ASSET_ID"]):
1641
- for asset in set(lookup_assets):
1642
- asset.parentId = ssp_id
1643
- asset.parentModule = parent_module
1644
- asset.otherTrackingNumber = qualys_asset["ID"]
1645
- asset.ipAddress = qualys_asset["IP"]
1646
- asset.qualysId = qualys_asset["ASSET_ID"]
1647
- try:
1648
- assert asset.id
1649
- # avoid duplication
1650
- if asset.qualysId not in [v["qualysId"] for v in update_assets]:
1651
- update_assets.append(asset)
1652
- except AssertionError as aex:
1653
- logger.error("Asset does not have an id, unable to update!\n%s", aex)
1639
+
1640
+ for qualys_asset in qualys_assets:
1641
+ processed_asset = _process_single_qualys_asset(qualys_asset, reg_assets, ssp_id, parent_module)
1642
+ if processed_asset:
1643
+ update_assets.append(processed_asset)
1644
+
1654
1645
  update_and_insert_assets(
1655
1646
  qualys_assets=qualys_assets,
1656
1647
  reg_assets=reg_assets,
@@ -1661,6 +1652,57 @@ def sync_assets(
1661
1652
  )
1662
1653
 
1663
1654
 
1655
+ def _process_single_qualys_asset(
1656
+ qualys_asset: dict, reg_assets: list[Asset], ssp_id: int, parent_module: str
1657
+ ) -> Optional[Asset]:
1658
+ """
1659
+ Process a single Qualys asset and return the updated RegScale asset if found.
1660
+
1661
+ :param dict qualys_asset: Single Qualys asset dictionary
1662
+ :param list[Asset] reg_assets: List of RegScale assets
1663
+ :param int ssp_id: RegScale System Security Plan or Component ID
1664
+ :param str parent_module: Parent module name
1665
+ :return: Updated RegScale asset or None if not found
1666
+ :rtype: Optional[Asset]
1667
+ """
1668
+ logger.debug("qualys_asset: %s", qualys_asset)
1669
+
1670
+ if not isinstance(qualys_asset, dict):
1671
+ logger.error("Expected dict, got %s: %s", type(qualys_asset), qualys_asset)
1672
+ return None
1673
+
1674
+ lookup_assets = lookup_asset(reg_assets, qualys_asset["ASSET_ID"])
1675
+ if not lookup_assets:
1676
+ return None
1677
+
1678
+ return _update_regscale_asset(lookup_assets[0], qualys_asset, ssp_id, parent_module)
1679
+
1680
+
1681
+ def _update_regscale_asset(asset: Asset, qualys_asset: dict, ssp_id: int, parent_module: str) -> Optional[Asset]:
1682
+ """
1683
+ Update a RegScale asset with Qualys asset data.
1684
+
1685
+ :param Asset asset: RegScale asset to update
1686
+ :param dict qualys_asset: Qualys asset data
1687
+ :param int ssp_id: RegScale System Security Plan or Component ID
1688
+ :param str parent_module: Parent module name
1689
+ :return: Updated asset or None if update failed
1690
+ :rtype: Optional[Asset]
1691
+ """
1692
+ try:
1693
+ asset.parentId = ssp_id
1694
+ asset.parentModule = parent_module
1695
+ asset.otherTrackingNumber = qualys_asset["ID"]
1696
+ asset.ipAddress = qualys_asset["IP"]
1697
+ asset.qualysId = qualys_asset["ASSET_ID"]
1698
+
1699
+ assert asset.id
1700
+ return asset
1701
+ except AssertionError as aex:
1702
+ logger.error("Asset does not have an id, unable to update!\n%s", aex)
1703
+ return None
1704
+
1705
+
1664
1706
  def update_and_insert_assets(
1665
1707
  qualys_assets: list[dict],
1666
1708
  reg_assets: list[Asset],
@@ -1681,48 +1723,106 @@ def update_and_insert_assets(
1681
1723
  :rtype: None
1682
1724
  """
1683
1725
  parent_module = "components" if is_component else "securityplans"
1684
- insert_assets = []
1685
- if assets_to_be_inserted := [
1726
+
1727
+ # Handle asset insertion
1728
+ insert_assets = _prepare_assets_for_insertion(qualys_assets, reg_assets, ssp_id, parent_module, config)
1729
+ if insert_assets:
1730
+ _create_assets_in_batch(insert_assets)
1731
+
1732
+ # Handle asset updates
1733
+ if update_assets:
1734
+ _update_assets_in_batch(update_assets)
1735
+
1736
+
1737
+ def _prepare_assets_for_insertion(
1738
+ qualys_assets: list[dict], reg_assets: list[Asset], ssp_id: int, parent_module: str, config: dict
1739
+ ) -> list[Asset]:
1740
+ """
1741
+ Prepare new assets for insertion into RegScale.
1742
+
1743
+ :param list[dict] qualys_assets: List of Qualys assets
1744
+ :param list[Asset] reg_assets: List of RegScale assets
1745
+ :param int ssp_id: RegScale System Security Plan or Component ID
1746
+ :param str parent_module: Parent module name
1747
+ :param dict config: Configuration dictionary
1748
+ :return: List of assets to insert
1749
+ :rtype: list[Asset]
1750
+ """
1751
+ assets_to_be_inserted = [
1686
1752
  qualys_asset
1687
1753
  for qualys_asset in qualys_assets
1688
1754
  if qualys_asset["ASSET_ID"] not in [asset["ASSET_ID"] for asset in inner_join(reg_assets, qualys_assets)]
1689
- ]:
1690
- for qualys_asset in assets_to_be_inserted:
1691
- # Do Insert
1692
- r_asset = Asset(
1693
- name=f'Qualys Asset #{qualys_asset["ASSET_ID"]} IP: {qualys_asset["IP"]}',
1694
- otherTrackingNumber=qualys_asset["ID"],
1695
- parentId=ssp_id,
1696
- parentModule=parent_module,
1697
- ipAddress=qualys_asset["IP"],
1698
- assetOwnerId=config["userId"],
1699
- assetType="Other",
1700
- assetCategory=regscale_models.AssetCategory.Hardware,
1701
- status="Off-Network",
1702
- qualysId=qualys_asset["ASSET_ID"],
1703
- )
1704
- # avoid duplication
1705
- if r_asset.qualysId not in {v["qualysId"] for v in insert_assets}:
1706
- insert_assets.append(r_asset)
1707
- try:
1708
- created_assets = Asset.batch_create(insert_assets, job_progress)
1709
- logger.info(
1710
- "RegScale Asset(s) successfully created: %i/%i",
1711
- len(created_assets),
1712
- len(insert_assets),
1713
- )
1714
- except requests.exceptions.RequestException as rex:
1715
- logger.error("Unable to create Qualys Assets in RegScale\n%s", rex)
1716
- if update_assets:
1717
- try:
1718
- updated_assets = Asset.batch_update(update_assets, job_progress)
1719
- logger.info(
1720
- "RegScale Asset(s) successfully updated: %i/%i",
1721
- len(updated_assets),
1722
- len(update_assets),
1723
- )
1724
- except requests.RequestException as rex:
1725
- logger.error("Unable to Update Qualys Assets to RegScale\n%s", rex)
1755
+ ]
1756
+
1757
+ insert_assets = []
1758
+ for qualys_asset in assets_to_be_inserted:
1759
+ r_asset = _create_regscale_asset_from_qualys(qualys_asset, ssp_id, parent_module, config)
1760
+ # avoid duplication
1761
+ if r_asset.qualysId not in {v["qualysId"] for v in insert_assets}:
1762
+ insert_assets.append(r_asset)
1763
+
1764
+ return insert_assets
1765
+
1766
+
1767
+ def _create_regscale_asset_from_qualys(qualys_asset: dict, ssp_id: int, parent_module: str, config: dict) -> Asset:
1768
+ """
1769
+ Create a RegScale asset from Qualys asset data.
1770
+
1771
+ :param dict qualys_asset: Qualys asset data
1772
+ :param int ssp_id: RegScale System Security Plan or Component ID
1773
+ :param str parent_module: Parent module name
1774
+ :param dict config: Configuration dictionary
1775
+ :return: New RegScale asset
1776
+ :rtype: Asset
1777
+ """
1778
+ return Asset(
1779
+ name=f'Qualys Asset #{qualys_asset["ASSET_ID"]} IP: {qualys_asset["IP"]}',
1780
+ otherTrackingNumber=qualys_asset["ID"],
1781
+ parentId=ssp_id,
1782
+ parentModule=parent_module,
1783
+ ipAddress=qualys_asset["IP"],
1784
+ assetOwnerId=config["userId"],
1785
+ assetType="Other",
1786
+ assetCategory=regscale_models.AssetCategory.Hardware,
1787
+ status="Off-Network",
1788
+ qualysId=qualys_asset["ASSET_ID"],
1789
+ )
1790
+
1791
+
1792
+ def _create_assets_in_batch(insert_assets: list[Asset]) -> None:
1793
+ """
1794
+ Create assets in batch and handle any errors.
1795
+
1796
+ :param list[Asset] insert_assets: List of assets to create
1797
+ :rtype: None
1798
+ """
1799
+ try:
1800
+ created_assets = Asset.batch_create(insert_assets, job_progress)
1801
+ logger.info(
1802
+ "RegScale Asset(s) successfully created: %i/%i",
1803
+ len(created_assets),
1804
+ len(insert_assets),
1805
+ )
1806
+ except requests.exceptions.RequestException as rex:
1807
+ logger.error("Unable to create Qualys Assets in RegScale\n%s", rex)
1808
+
1809
+
1810
+ def _update_assets_in_batch(update_assets: list[Asset]) -> None:
1811
+ """
1812
+ Update assets in batch and handle any errors.
1813
+
1814
+ :param list[Asset] update_assets: List of assets to update
1815
+ :rtype: None
1816
+ """
1817
+ try:
1818
+ updated_assets = Asset.batch_update(update_assets, job_progress)
1819
+ logger.info(
1820
+ "RegScale Asset(s) successfully updated: %i/%i",
1821
+ len(updated_assets),
1822
+ len(update_assets),
1823
+ )
1824
+ except requests.RequestException as rex:
1825
+ logger.error("Unable to Update Qualys Assets to RegScale\n%s", rex)
1726
1826
 
1727
1827
 
1728
1828
  def sync_issues(ssp_id: int, qualys_assets_and_issues: list[dict], is_component: bool = False) -> None: