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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +3 -0
- regscale/core/app/utils/app_utils.py +31 -0
- regscale/integrations/commercial/jira.py +27 -5
- regscale/integrations/commercial/qualys/__init__.py +160 -60
- regscale/integrations/commercial/qualys/scanner.py +300 -39
- regscale/integrations/commercial/wizv2/async_client.py +4 -0
- regscale/integrations/commercial/wizv2/scanner.py +50 -24
- regscale/integrations/public/__init__.py +13 -0
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +175 -51
- regscale/integrations/scanner_integration.py +519 -151
- regscale/models/integration_models/cisa_kev_data.json +34 -3
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/__init__.py +2 -0
- regscale/models/regscale_models/catalog.py +1 -1
- regscale/models/regscale_models/control_implementation.py +8 -8
- regscale/models/regscale_models/form_field_value.py +5 -3
- regscale/models/regscale_models/inheritance.py +44 -0
- regscale/models/regscale_models/milestone.py +20 -3
- regscale/regscale.py +2 -0
- {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/RECORD +27 -29
- tests/regscale/models/test_tenable_integrations.py +811 -105
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
- regscale/integrations/public/fedramp/parts_mapper.py +0 -107
- {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.2.1.dist-info → regscale_cli-6.22.0.0.dist-info}/entry_points.txt +0 -0
- {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
regscale/core/app/application.py
CHANGED
|
@@ -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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
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
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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:
|