regscale-cli 6.20.1.1__py3-none-any.whl → 6.20.3.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 (55) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/utils/variables.py +5 -3
  3. regscale/integrations/commercial/__init__.py +15 -0
  4. regscale/integrations/commercial/axonius/__init__.py +0 -0
  5. regscale/integrations/commercial/axonius/axonius_integration.py +70 -0
  6. regscale/integrations/commercial/burp.py +14 -0
  7. regscale/integrations/commercial/grype/commands.py +8 -1
  8. regscale/integrations/commercial/grype/scanner.py +2 -1
  9. regscale/integrations/commercial/jira.py +288 -137
  10. regscale/integrations/commercial/opentext/commands.py +14 -5
  11. regscale/integrations/commercial/opentext/scanner.py +3 -2
  12. regscale/integrations/commercial/qualys/__init__.py +3 -3
  13. regscale/integrations/commercial/stigv2/click_commands.py +6 -37
  14. regscale/integrations/commercial/synqly/assets.py +10 -0
  15. regscale/integrations/commercial/tenablev2/commands.py +12 -4
  16. regscale/integrations/commercial/tenablev2/sc_scanner.py +21 -1
  17. regscale/integrations/commercial/tenablev2/sync_compliance.py +3 -0
  18. regscale/integrations/commercial/trivy/commands.py +11 -4
  19. regscale/integrations/commercial/trivy/scanner.py +2 -1
  20. regscale/integrations/commercial/wizv2/constants.py +4 -0
  21. regscale/integrations/commercial/wizv2/scanner.py +67 -14
  22. regscale/integrations/commercial/wizv2/utils.py +24 -10
  23. regscale/integrations/commercial/wizv2/variables.py +7 -0
  24. regscale/integrations/jsonl_scanner_integration.py +8 -1
  25. regscale/integrations/public/cisa.py +58 -63
  26. regscale/integrations/public/fedramp/fedramp_cis_crm.py +153 -104
  27. regscale/integrations/scanner_integration.py +30 -8
  28. regscale/integrations/variables.py +1 -0
  29. regscale/models/app_models/click.py +49 -1
  30. regscale/models/app_models/import_validater.py +3 -1
  31. regscale/models/integration_models/axonius_models/__init__.py +0 -0
  32. regscale/models/integration_models/axonius_models/connectors/__init__.py +3 -0
  33. regscale/models/integration_models/axonius_models/connectors/assets.py +111 -0
  34. regscale/models/integration_models/burp.py +11 -8
  35. regscale/models/integration_models/cisa_kev_data.json +204 -23
  36. regscale/models/integration_models/flat_file_importer/__init__.py +36 -176
  37. regscale/models/integration_models/jira_task_sync.py +27 -0
  38. regscale/models/integration_models/qualys.py +6 -7
  39. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  40. regscale/models/regscale_models/__init__.py +2 -1
  41. regscale/models/regscale_models/control_implementation.py +39 -2
  42. regscale/models/regscale_models/issue.py +1 -0
  43. regscale/models/regscale_models/regscale_model.py +49 -1
  44. regscale/models/regscale_models/risk_issue_mapping.py +61 -0
  45. regscale/models/regscale_models/task.py +1 -0
  46. regscale/regscale.py +1 -4
  47. regscale/utils/graphql_client.py +4 -4
  48. regscale/utils/string.py +13 -0
  49. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/METADATA +1 -1
  50. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/RECORD +54 -48
  51. regscale/integrations/commercial/synqly_jira.py +0 -840
  52. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/LICENSE +0 -0
  53. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/WHEEL +0 -0
  54. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/entry_points.txt +0 -0
  55. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/top_level.txt +0 -0
@@ -425,6 +425,8 @@ class IntegrationFinding:
425
425
  operational_requirements: Optional[str] = None
426
426
  deviation_rationale: Optional[str] = None
427
427
  is_cwe: bool = False
428
+ affected_controls: Optional[str] = None
429
+ identification: Optional[str] = "Vulnerability Assessment"
428
430
 
429
431
  poam_comments: Optional[str] = None
430
432
  vulnerability_id: Optional[int] = None
@@ -633,6 +635,8 @@ class ScannerIntegration(ABC):
633
635
  self.plan_id: int = plan_id
634
636
  self.tenant_id: int = tenant_id
635
637
  self.is_component: bool = is_component
638
+ if self.is_component:
639
+ self.component = regscale_models.Component.get_object(self.plan_id)
636
640
  self.components: ThreadSafeList[Any] = ThreadSafeList()
637
641
  self.asset_map_by_identifier: ThreadSafeDict[str, regscale_models.Asset] = ThreadSafeDict()
638
642
  self.software_to_create: ThreadSafeList[regscale_models.SoftwareInventory] = ThreadSafeList()
@@ -819,11 +823,12 @@ class ScannerIntegration(ABC):
819
823
  @abstractmethod
820
824
  def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
821
825
  """
822
- Fetches findings from the integration
826
+ Fetches findings from the integration.
823
827
 
824
- :return: A list of findings
825
- :rtype: List[IntegrationFinding]
828
+ :return: An iterator of findings
829
+ :yield: Iterator[IntegrationFinding]
826
830
  """
831
+ pass
827
832
 
828
833
  @abstractmethod
829
834
  def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
@@ -831,7 +836,7 @@ class ScannerIntegration(ABC):
831
836
  Fetches assets from the integration
832
837
 
833
838
  :return: An iterator of assets
834
- :rtype: Iterator[IntegrationAsset]
839
+ :yield: Iterator[IntegrationAsset]
835
840
  """
836
841
 
837
842
  def get_finding_status(self, status: Optional[str]) -> regscale_models.IssueStatus:
@@ -1024,10 +1029,10 @@ class ScannerIntegration(ABC):
1024
1029
  logger.warning("Asset has no identifier, skipping")
1025
1030
  return
1026
1031
 
1027
- component = None
1032
+ component = getattr(self, "component") if self.is_component else None
1028
1033
  if component_name:
1029
1034
  logger.debug("Searching for component: %s...", component_name)
1030
- component = self.components_by_title.get(component_name)
1035
+ component = component or self.components_by_title.get(component_name)
1031
1036
  if not component:
1032
1037
  logger.debug("No existing component found with name %s, proceeding to create it...", component_name)
1033
1038
  component = regscale_models.Component(
@@ -1610,7 +1615,7 @@ class ScannerIntegration(ABC):
1610
1615
  issue.severityLevel = finding.severity
1611
1616
  issue.issueOwnerId = self.assessor_id
1612
1617
  issue.securityPlanId = self.plan_id if not self.is_component else None
1613
- issue.identification = "Vulnerability Assessment"
1618
+ issue.identification = finding.identification
1614
1619
  issue.dateFirstDetected = finding.first_seen
1615
1620
  issue.dueDate = finding.due_date
1616
1621
  issue.description = description
@@ -1628,6 +1633,7 @@ class ScannerIntegration(ABC):
1628
1633
  # Get control implementation ID for CCI if it exists
1629
1634
  # Only add CCI control ID if it exists
1630
1635
  cci_control_ids = [control_id] if control_id is not None else []
1636
+ issue.affectedControls = finding.affected_controls
1631
1637
 
1632
1638
  issue.controlImplementationIds = list(set(finding._control_implementation_ids + cci_control_ids)) # noqa
1633
1639
  issue.isPoam = is_poam
@@ -1790,7 +1796,10 @@ class ScannerIntegration(ABC):
1790
1796
  :return: True if the issue should be a POAM, False otherwise
1791
1797
  :rtype: bool
1792
1798
  """
1793
- if ScannerVariables.vulnerabilityCreation.lower() == "poamcreation":
1799
+ if (
1800
+ ScannerVariables.vulnerabilityCreation.lower() == "poamcreation"
1801
+ or ScannerVariables.complianceCreation.lower() == "poam"
1802
+ ):
1794
1803
  return True
1795
1804
  if finding.due_date < get_current_datetime():
1796
1805
  return True
@@ -2702,6 +2711,7 @@ class ScannerIntegration(ABC):
2702
2711
  logger.info("Syncing %s findings...", kwargs.get("title", cls.title))
2703
2712
  instance = cls(plan_id=plan_id, **kwargs)
2704
2713
  instance.set_keys(**kwargs)
2714
+ instance.ensure_data_types()
2705
2715
  # If a progress object was passed, use it instead of creating a new one
2706
2716
  instance.finding_progress = kwargs.pop("progress") if "progress" in kwargs else create_progress_object()
2707
2717
  instance.enable_finding_date_update = kwargs.get("enable_finding_date_update", False)
@@ -2771,6 +2781,7 @@ class ScannerIntegration(ABC):
2771
2781
  logger.info("Syncing %s assets...", kwargs.get("title", cls.title))
2772
2782
  instance = cls(plan_id=plan_id, **kwargs)
2773
2783
  instance.set_keys(**kwargs)
2784
+ instance.ensure_data_types()
2774
2785
  instance.asset_progress = kwargs.pop("progress") if "progress" in kwargs else create_progress_object()
2775
2786
  if asset_count := kwargs.get("asset_count"):
2776
2787
  instance.num_assets_to_process = asset_count
@@ -2813,6 +2824,17 @@ class ScannerIntegration(ABC):
2813
2824
  else:
2814
2825
  logger.debug("Unable to set the %s attribute", key)
2815
2826
 
2827
+ def ensure_data_types(self) -> None:
2828
+ """
2829
+ A method to enforce kwarg data types.
2830
+
2831
+ :return: None
2832
+ :rtype: None
2833
+ """
2834
+ # Ensure scan_date is a string
2835
+ if not isinstance(self.scan_date, str):
2836
+ self.scan_date = date_str(self.scan_date)
2837
+
2816
2838
  def log_error(self, msg: str, *args) -> None:
2817
2839
  """
2818
2840
  Logs an error message
@@ -25,3 +25,4 @@ class ScannerVariables(metaclass=RsVariablesMeta):
25
25
  issueDueDates: RsVariableType(dict, "dueDates", default="{'high': 60, 'moderate': 120, 'low': 364}", required=False) # type: ignore # noqa: F722,F821
26
26
  maxRetries: RsVariableType(int, "3", default=3, required=False) # type: ignore
27
27
  timeout: RsVariableType(int, "60", default=60, required=False) # type: ignore
28
+ complianceCreation: RsVariableType(str, "Assessment|Issue|POAM", default="Assessment", required=False) # type: ignore # noqa: F722,F821
@@ -2,7 +2,7 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """Module to allow dynamic click arguments and store commonly used click commands"""
4
4
 
5
- from typing import Tuple, Any, Optional
5
+ from typing import Any, Callable, Optional, Tuple
6
6
 
7
7
  import click
8
8
  from pathlib import Path
@@ -128,6 +128,54 @@ def regscale_ssp_id(
128
128
  )
129
129
 
130
130
 
131
+ def ssp_or_component_id(
132
+ ssp_kwargs: Optional[dict] = None,
133
+ component_kwargs: Optional[dict] = None,
134
+ ) -> Tuple[click.option, click.option]:
135
+ """
136
+ Function to return click.option for RegScale Component ID and SSP ID, user must provide either SSP ID or Component ID
137
+
138
+ :param Optional[dict] ssp_kwargs: kwargs to pass to click.option for RegScale SSP ID
139
+ :param Optional[dict] component_kwargs: kwargs to pass to click.option for RegScale Component ID
140
+ :return: click.option for RegScale Component ID and SSP ID
141
+ :rtype: click.option
142
+ """
143
+ if ssp_kwargs is None:
144
+ ssp_kwargs = {}
145
+ if component_kwargs is None:
146
+ component_kwargs = {}
147
+
148
+ def decorator(this_func) -> Callable[[Callable], click.option]:
149
+ """
150
+ Decorator to return click.option for RegScale Component ID and SSP ID
151
+ """
152
+ this_func = click.option(
153
+ "-id",
154
+ "-p",
155
+ "--regscale_ssp_id",
156
+ "--plan_id",
157
+ type=click.INT,
158
+ help=ssp_kwargs.pop("help", "The ID number from RegScale of the System Security Plan."),
159
+ prompt=ssp_kwargs.pop("prompt", None),
160
+ cls=NotRequiredIf,
161
+ not_required_if=["component_id"],
162
+ **ssp_kwargs,
163
+ )(this_func)
164
+ this_func = click.option(
165
+ "-c",
166
+ "--component_id",
167
+ type=click.INT,
168
+ help=component_kwargs.pop("help", "The ID number from RegScale of the Component."),
169
+ prompt=component_kwargs.pop("prompt", None),
170
+ cls=NotRequiredIf,
171
+ not_required_if=["regscale_ssp_id"],
172
+ **component_kwargs,
173
+ )(this_func)
174
+ return this_func
175
+
176
+ return decorator
177
+
178
+
131
179
  def regscale_id(
132
180
  help: str = "Enter the desired ID # from RegScale.",
133
181
  required: bool = True,
@@ -58,6 +58,7 @@ class ImportValidater:
58
58
  skip_rows: Optional[int] = None,
59
59
  prompt: bool = True,
60
60
  ignore_unnamed: bool = False,
61
+ warn_extra_headers: bool = True,
61
62
  ):
62
63
  self.ignore_unnamed = ignore_unnamed
63
64
  self.prompt = prompt
@@ -74,6 +75,7 @@ class ImportValidater:
74
75
  self.keys = keys
75
76
  self.worksheet_name = worksheet_name
76
77
  self.skip_rows = skip_rows
78
+ self.warn_extra_headers = warn_extra_headers
77
79
  if self.file_type not in self._supported_types:
78
80
  raise ValidationException(
79
81
  f"Unsupported file type: {self.file_type}, supported types are: {', '.join(self._supported_types)}",
@@ -148,7 +150,7 @@ class ImportValidater:
148
150
  raise ValidationException(
149
151
  f"{', '.join([f'`{header}`' for header in missing_headers])} header(s) not found in {self.file_path}"
150
152
  )
151
- if extra_headers:
153
+ if extra_headers and self.warn_extra_headers:
152
154
  logger.warning("Extra headers found in the file: %s", ", ".join(extra_headers))
153
155
 
154
156
  if self.disable_mapping:
@@ -0,0 +1,3 @@
1
+ """
2
+ File that contains all connectors for the Axonius API.
3
+ """
@@ -0,0 +1,111 @@
1
+ """Assets Connector Model"""
2
+
3
+ from typing import Iterator, Optional
4
+ import pandas as pd
5
+ import datetime
6
+ from datetime import date
7
+ import warnings
8
+ import json
9
+ import re
10
+
11
+ from pydantic import ConfigDict
12
+
13
+ from regscale.integrations.scanner_integration import (
14
+ IntegrationAsset,
15
+ IntegrationFinding,
16
+ ScannerIntegration,
17
+ ScannerIntegrationType,
18
+ )
19
+ from regscale.models.regscale_models import IssueSeverity, AssetStatus, ControlImplementation, SecurityControl
20
+ from regscale.core.app.api import Api
21
+ from regscale.core.app.application import Application
22
+
23
+
24
+ class AxoniusIntegration(ScannerIntegration):
25
+ from regscale.integrations.variables import ScannerVariables
26
+
27
+ title = "Axonius"
28
+ # Required fields from ScannerIntegration
29
+ asset_identifier_field = "otherTrackingNumber"
30
+ finding_severity_map = {
31
+ "I": IssueSeverity.Critical,
32
+ "II": IssueSeverity.High,
33
+ "III": IssueSeverity.Moderate,
34
+ "IV": IssueSeverity.Low,
35
+ }
36
+ type = (
37
+ ScannerIntegrationType.CHECKLIST
38
+ if ScannerVariables.complianceCreation.lower() == "assessment"
39
+ else ScannerIntegrationType.CONTROL_TEST
40
+ )
41
+ app = Application()
42
+
43
+ def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
44
+ """
45
+ Fetches assets from Axonius
46
+
47
+ :yields: Iterator[IntegrationAsset]
48
+ """
49
+
50
+ # TEST: Parse Sample Axonius Object
51
+ axonius_object = pd.read_json("regscale/integrations/commercial/axonius/sample_axonius_object.json")
52
+
53
+ for ind, asset in axonius_object.iterrows():
54
+ integration_asset = IntegrationAsset(
55
+ name=asset["hostname"],
56
+ identifier=asset.COMPLIANCE_TABLE[0]["FISMA"],
57
+ serial_number=asset["serial"],
58
+ ip_address=asset["ip"],
59
+ status=AssetStatus.Active,
60
+ asset_category="Software",
61
+ asset_type="Other",
62
+ )
63
+ yield integration_asset
64
+
65
+ def fetch_findings(self, plan_id: int, *args, **kwargs) -> Iterator[IntegrationFinding]:
66
+ """
67
+ Unused method, but required by the parent class
68
+
69
+ :yields: Iterator[IntegrationFinding]
70
+
71
+ """
72
+ # TEST: Parse Sample Axonius Object
73
+ axonius_object = pd.read_json("regscale/integrations/commercial/axonius/sample_axonius_object.json")
74
+
75
+ for ind, asset in axonius_object.iterrows():
76
+ for finding in asset.COMPLIANCE_TABLE:
77
+ if finding["ComplianceResult"] != "PASSED":
78
+
79
+ # Look for Control Title, Otherwise use Control ID
80
+ existing_implementations = ControlImplementation.get_list_by_parent(
81
+ regscale_id=plan_id, regscale_module="securityplans"
82
+ )
83
+ finding_control = re.search("[A-Z]{2}-\d+\d?(\(\d+\d?\))?", str(finding["800-53r5"]))[ # noqa: W605
84
+ 0
85
+ ].lower()
86
+ try:
87
+ control_title = [
88
+ control
89
+ for control in existing_implementations
90
+ if control["controlId"].lower() == finding_control
91
+ ][0]["title"]
92
+ except Exception:
93
+ control_title = finding["800-53r5"]
94
+
95
+ integration_finding = IntegrationFinding(
96
+ title=f"Assessment Failure for Control ID: {control_title}",
97
+ asset_identifier=finding["FISMA"],
98
+ severity=self.finding_severity_map.get(finding["SEV"], IssueSeverity.NotAssigned),
99
+ identification="Security Control Assessment",
100
+ source_report="Axonius",
101
+ status="Open",
102
+ description=f"Issue for {finding['PLUGIN']}",
103
+ plugin_name=finding["PLUGIN"],
104
+ category="Other",
105
+ control_labels=[finding["800-53r5"]],
106
+ security_check=f"<strong>PLUGIN: </strong>{finding['PLUGIN']}<br><br><strong>FISMA: </strong>{finding['FISMA']}<br><br><strong>Compliance Result: </strong>{finding['ComplianceResult']}<br><br><strong>CCI: </strong>{finding['CCI']}<br><br><strong>800-53r5: </strong>{finding['800-53r5']}<br><br><strong>CSF: </strong>{finding['CSF']}<br><br><strong>VULID: </strong>{finding['VULID']}<br><br><strong>STIG: </strong>{finding['STIG']}",
107
+ baseline=finding["STIG"],
108
+ results=finding["ComplianceResult"],
109
+ affected_controls=finding["800-53r5"],
110
+ )
111
+ yield integration_finding
@@ -9,10 +9,10 @@ from pathlib import Path
9
9
  from typing import Any, Generator, List, Optional, TextIO
10
10
  from urllib.parse import urlparse
11
11
  from xml.etree.ElementTree import Element, ParseError, fromstring, parse
12
+ from logging import getLogger
12
13
 
13
14
  from regscale.core.app.api import Api
14
15
  from regscale.core.app.application import Application
15
- from regscale.core.app.logz import create_logger
16
16
  from regscale.core.app.utils.app_utils import check_file_path, get_current_datetime
17
17
  from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding
18
18
  from regscale.models.integration_models.burp_models import BurpRequest, BurpResponse, Issue, RequestResponse
@@ -26,7 +26,7 @@ class Burp:
26
26
  """Burp Scan information"""
27
27
 
28
28
  def __init__(self, app: Application, file_path: str, encoding="utf-8", **kwargs) -> "Burp":
29
- logger = create_logger("Burp")
29
+ logger = getLogger("regscale")
30
30
  logger.info("Now processing %s", file_path)
31
31
  self.integration_assets: Generator[IntegrationAsset, None, None] = (x for x in [])
32
32
  self.integration_findings: Generator[IntegrationFinding, None, None] = (x for x in [])
@@ -293,10 +293,10 @@ class Burp:
293
293
  :rtype: RequestResponse
294
294
  """
295
295
  request_data = item.find(".//request")
296
- if response_data := item.find(".//response"):
297
- base64_dat = bool(request_data.attrib["base64"]) if "base64" in item.attrib else False
296
+ if (response_data := item.find(".//response")) is not None:
297
+ base64_dat = request_data.attrib.get("base64", "false").lower() == "true"
298
298
  response_data_is_base64 = BurpResponse.is_base64(response_data.text)
299
- method = request_data.attrib["method"] if "method" in item.attrib else "GET"
299
+ method = request_data.attrib.get("method", "GET")
300
300
  request = (
301
301
  BurpRequest(dataString=request_data.text, base64=base64_dat, method=method)
302
302
  if BurpRequest.is_base64(request_data.text)
@@ -395,7 +395,8 @@ class Burp:
395
395
  root = fromstring(html)
396
396
  return [link.text.strip() for link in root.iter("a")]
397
397
 
398
- def get_domain_name(self, url: str) -> str:
398
+ @staticmethod
399
+ def get_domain_name(url: str) -> str:
399
400
  """
400
401
  Get the domain name from a URL
401
402
 
@@ -407,7 +408,8 @@ class Burp:
407
408
  domain_name = parsed_url.hostname
408
409
  return domain_name
409
410
 
410
- def strip_html_tags(self, text: str) -> str:
411
+ @staticmethod
412
+ def strip_html_tags(text: str) -> str:
411
413
  """
412
414
  Strip HTML tags from a string.
413
415
 
@@ -420,7 +422,8 @@ class Burp:
420
422
  clean = re.sub(r"<.*?>", "", text)
421
423
  return clean
422
424
 
423
- def extract_cve(self, input_string: str) -> Optional[str]:
425
+ @staticmethod
426
+ def extract_cve(input_string: str) -> Optional[str]:
424
427
  """
425
428
  Extract CVEs from a string.
426
429