regscale-cli 6.27.2.0__py3-none-any.whl → 6.28.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 (140) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +11 -2
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/dev/cli.py +26 -0
  10. regscale/dev/version.py +72 -0
  11. regscale/integrations/commercial/__init__.py +15 -1
  12. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  13. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  14. regscale/integrations/commercial/amazon/common.py +48 -58
  15. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  16. regscale/integrations/commercial/aws/cli.py +3093 -55
  17. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  18. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  19. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  20. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  21. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  22. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  23. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  24. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  25. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  26. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  27. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  28. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  29. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  30. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  31. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  32. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  33. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  34. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  35. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  36. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  37. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  38. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  39. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  40. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  41. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  42. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  43. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  44. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  45. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  46. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  47. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  48. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  49. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  50. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  51. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  52. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  53. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  54. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  55. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  56. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  57. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  58. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  59. regscale/integrations/commercial/aws/scanner.py +853 -205
  60. regscale/integrations/commercial/aws/security_hub.py +319 -0
  61. regscale/integrations/commercial/aws/session_manager.py +282 -0
  62. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  63. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  64. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  65. regscale/integrations/compliance_integration.py +308 -38
  66. regscale/integrations/control_matcher.py +78 -23
  67. regscale/integrations/due_date_handler.py +3 -0
  68. regscale/integrations/public/csam/csam.py +572 -763
  69. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  70. regscale/integrations/public/csam/csam_common.py +154 -0
  71. regscale/integrations/public/csam/csam_controls.py +432 -0
  72. regscale/integrations/public/csam/csam_poam.py +124 -0
  73. regscale/integrations/public/fedramp/click.py +17 -4
  74. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  75. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  76. regscale/integrations/scanner_integration.py +415 -85
  77. regscale/models/integration_models/cisa_kev_data.json +80 -20
  78. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  79. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
  80. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  81. regscale/models/platform.py +3 -0
  82. regscale/models/regscale_models/__init__.py +5 -0
  83. regscale/models/regscale_models/assessment.py +2 -1
  84. regscale/models/regscale_models/component.py +1 -1
  85. regscale/models/regscale_models/control_implementation.py +55 -24
  86. regscale/models/regscale_models/control_objective.py +74 -5
  87. regscale/models/regscale_models/file.py +2 -0
  88. regscale/models/regscale_models/issue.py +2 -5
  89. regscale/models/regscale_models/organization.py +3 -0
  90. regscale/models/regscale_models/regscale_model.py +17 -5
  91. regscale/models/regscale_models/security_plan.py +1 -0
  92. regscale/regscale.py +11 -1
  93. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  94. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
  95. tests/regscale/core/test_login.py +171 -4
  96. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  97. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  98. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  99. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  100. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  101. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  102. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  103. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  104. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  105. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  106. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  107. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  108. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  109. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  110. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  111. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  112. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  113. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  114. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  115. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  116. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  117. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  118. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  119. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  120. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  121. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  122. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  123. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  124. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  125. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  126. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  127. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  128. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  129. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  130. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  131. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  132. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  133. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  134. tests/regscale/integrations/commercial/test_aws.py +55 -56
  135. tests/regscale/integrations/test_control_matcher.py +24 -0
  136. tests/regscale/models/test_control_implementation.py +118 -3
  137. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  138. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  139. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  140. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -78,10 +78,44 @@ class Vulnerabilities(SynqlyModel):
78
78
  "low": ["low", "medium", "high", "critical"],
79
79
  "info": ["info", "low", "medium", "high", "critical"],
80
80
  }
81
- mapped_severities = severity_map.get(severity.lower(), severity)
81
+ if severity[0].lower() == "a":
82
+ mapped_severities = list(severity_map.keys())
83
+ else:
84
+ mapped_severities = severity_map.get(severity.lower(), severity)
82
85
  severity_filter = f"severity[in]{','.join(mapped_severities)}"
83
86
  return severity_filter
84
87
 
88
+ def _translate_asset_filter(self, replace: str, replace_with: str, asset_filters: Optional[list[str]]) -> list[str]:
89
+ """
90
+ Translate asset filters to the correct format for the integration
91
+
92
+ :param str replace: The string to replace
93
+ :param str replace_with: The string to replace with
94
+ :param list[str] asset_filters: The asset filters to translate
95
+ :return: The translated asset filters
96
+ :rtype: list[str]
97
+ """
98
+ translated_asset_filters = []
99
+ for asset_filter in asset_filters:
100
+ # Remove outer double quotes if present
101
+ cleaned_filter = asset_filter
102
+ if cleaned_filter.startswith('"') and cleaned_filter.endswith('"') and len(cleaned_filter) > 1:
103
+ cleaned_filter = cleaned_filter[1:-1]
104
+ if cleaned_filter.startswith("'") and cleaned_filter.endswith("'") and len(cleaned_filter) > 1:
105
+ cleaned_filter = cleaned_filter[1:-1]
106
+
107
+ if replace_with in cleaned_filter:
108
+ possible_filter = cleaned_filter
109
+ elif replace in cleaned_filter:
110
+ possible_filter = cleaned_filter.replace(replace, replace_with)
111
+ self.logger.debug(f"Translated filter: from {cleaned_filter} to {possible_filter}")
112
+ else:
113
+ continue
114
+ valid_filter, _ = self.filter_parser.validate_filter(self.integration_id, possible_filter)
115
+ if valid_filter:
116
+ translated_asset_filters.append(possible_filter)
117
+ return translated_asset_filters
118
+
85
119
  def _handle_scan_date_options(self, regscale_ssp_id: int, **kwargs) -> list[str]:
86
120
  """
87
121
  Handle scan date options for the integration sync process
@@ -94,6 +128,11 @@ class Vulnerabilities(SynqlyModel):
94
128
 
95
129
  vuln_filter = [self._build_severity_filter(kwargs.get("minimum_severity_filter"))]
96
130
 
131
+ if asset_filters := kwargs.get("filter", []):
132
+ vuln_filter.extend(
133
+ self._translate_asset_filter(replace="device.", replace_with="resources.", asset_filters=asset_filters)
134
+ )
135
+
97
136
  if kwargs.get("all_scans"):
98
137
  vuln_filter.append("finding.last_seen_time[gte]915148800") # Friday, January 1, 1999 12:00:00 AM UTC
99
138
  elif scan_date := kwargs.get("scan_date"):
@@ -118,8 +157,10 @@ class Vulnerabilities(SynqlyModel):
118
157
  self.logger.debug(f"Vulnerability filter: {vuln_filter}")
119
158
 
120
159
  # Pop the filter from kwargs so it doesn't get passed to query_findings
121
- asset_filter = kwargs.pop("filter", [])
122
- if asset_filter:
160
+ if asset_filter := kwargs.pop("filter", []):
161
+ asset_filter = self._translate_asset_filter(
162
+ replace="resources.", replace_with="device.", asset_filters=asset_filter
163
+ )
123
164
  self.logger.debug(f"Asset filter: {asset_filter}")
124
165
 
125
166
  self.logger.info(f"Fetching asset data from {self.integration_name}...")
@@ -106,9 +106,8 @@ class Mapper:
106
106
  regscale_issue.manualDetectionId = ticket.id
107
107
  return regscale_issue
108
108
 
109
- @staticmethod
110
109
  def _ocsf_asset_to_regscale(
111
- connector: Union["Edr", "Vulnerabilities"], asset: Union[InventoryInfo, OCSFAsset, SoftwareInfo], **kwargs
110
+ self, connector: Union["Edr", "Vulnerabilities"], asset: Union[InventoryInfo, OCSFAsset, SoftwareInfo], **kwargs
112
111
  ) -> IntegrationAsset:
113
112
  """
114
113
  Convert OCSF Asset to RegScale Asset
@@ -134,6 +133,9 @@ class Mapper:
134
133
  else:
135
134
  name = device_data.name or device_data.hostname or f"{connector.provider} Asset: {device_data.uid}"
136
135
  category = AssetCategory.Software
136
+ ip_v4s, ip_v6s = self._determine_ip_addresses(
137
+ device_data.ip_addresses if device_data.ip_addresses else [device_data.ip]
138
+ )
137
139
  return IntegrationAsset(
138
140
  name=name,
139
141
  identifier=device_data.uid,
@@ -143,7 +145,8 @@ class Mapper:
143
145
  parent_module=SecurityPlan.get_module_string(),
144
146
  mac_address=device_data.mac,
145
147
  fqdn=device_data.hostname,
146
- ip_address=", ".join(device_data.ip_addresses) if device_data.ip_addresses else device_data.ip,
148
+ ip_address=", ".join(ip_v4s),
149
+ ipv6_address=", ".join(ip_v6s),
147
150
  location=device_data.location or device_data.zone,
148
151
  vlan_id=device_data.vlan_uid,
149
152
  other_tracking_number=device_data.uid,
@@ -155,6 +158,31 @@ class Mapper:
155
158
  software_inventory=software_inventory,
156
159
  )
157
160
 
161
+ @staticmethod
162
+ def _determine_ip_addresses(ips: list[str]) -> tuple[list[str], list[str]]:
163
+ """
164
+ Parse the list of ips and return a tuple of two lists: list of IP v4 and IP v6 addresses
165
+
166
+ :param list[str] ips: List of IP addresses
167
+ :return: Tuple containing two lists, list of IP v4 addresses and list of IP v6 addresses
168
+ :rtype: tuple[list[str], list[str]]
169
+ """
170
+ import ipaddress
171
+
172
+ ip_v4s = []
173
+ ip_v6s = []
174
+ for ip in ips:
175
+ try:
176
+ ipaddress.IPv4Address(ip)
177
+ ip_v4s.append(ip)
178
+ except ipaddress.AddressValueError:
179
+ try:
180
+ ipaddress.IPv6Address(ip)
181
+ ip_v6s.append(ip)
182
+ except ipaddress.AddressValueError:
183
+ continue
184
+ return ip_v4s, ip_v6s
185
+
158
186
  @staticmethod
159
187
  def _determine_date(
160
188
  attribute: str, finding: Optional["Finding"] = None, vuln: Optional["Vulnerability"] = None
@@ -205,6 +233,7 @@ class Mapper:
205
233
  "plugin_id": vuln.cve.uid if vuln else finding.finding.product_uid,
206
234
  "severity": vuln.severity if vuln and getattr(vuln, "severity") else finding.severity_id,
207
235
  "remediation": getattr(vuln, "remediation") if vuln else finding.finding.remediation.desc,
236
+ "title": getattr(vuln, "title") or finding.finding.title,
208
237
  }
209
238
  if vuln:
210
239
  finding_data["cve"] = vuln.cve.uid
@@ -267,28 +296,28 @@ class Mapper:
267
296
  """
268
297
  base = vuln if vuln else finding.finding
269
298
  finding_data = self._parse_finding_data(finding, vuln)
270
- if resource_data := getattr(resource, "data"):
271
- dns = resource_data.get("hostname")
272
- ip_address = resource_data.get("ipv4")
273
- else:
274
- dns = resource.uid if vuln else None
275
- ip_address = None
299
+ resource_data = getattr(resource, "data", {})
300
+ dns = resource_data.get("hostname") or resource.uid if vuln else None
301
+ ip_v4s, ip_v6s = self._determine_ip_addresses(
302
+ resource_data["ipAddresses"] if resource_data.get("ipAddresses") else [resource_data.get("ip")]
303
+ )
304
+ ips = ip_v4s + ip_v6s
276
305
 
277
306
  finding_obj = IntegrationFinding(
278
307
  control_labels=[],
279
308
  category=f"{connector.integration_name} Vulnerability",
280
- title=base.title,
309
+ title=finding_data["title"],
281
310
  plugin_name=connector.integration_name,
282
311
  severity=Issue.assign_severity(finding.severity), # type: ignore
283
312
  description=base.desc,
284
313
  status=finding.status or "Open",
285
314
  first_seen=self._datetime_to_str(finding_data["first_seen"]),
286
315
  last_seen=self._datetime_to_str(finding_data["last_seen"]),
287
- ip_address=ip_address,
316
+ ip_address=", ".join(ips),
288
317
  plugin_id=finding_data["plugin_id"],
289
318
  dns=dns,
290
319
  severity_int=finding_data["severity"],
291
- issue_title=base.title,
320
+ issue_title=finding_data["title"],
292
321
  cve=finding_data["cve"],
293
322
  evidence=finding.evidence,
294
323
  impact=finding.impact,
@@ -44,6 +44,7 @@ class RegScaleAuth(BaseModel):
44
44
  password: Optional[Union[str, SecretStr]] = None,
45
45
  domain: Optional[str] = None,
46
46
  mfa_token: Optional[str] = None,
47
+ app_id: Optional[int] = 1,
47
48
  ) -> "RegScaleAuth":
48
49
  """
49
50
  Authenticate with RegScale and return a token and a user_id
@@ -52,6 +53,7 @@ class RegScaleAuth(BaseModel):
52
53
  :param Optional[Union[str, SecretStr]] password: Password to log in with, defaults to None
53
54
  :param Optional[str] domain: Domain to log into, defaults to None
54
55
  :param Optional[str] mfa_token: mfa_token to verify, defaults to None
56
+ :param Optional[int] app_id: The app ID to login with
55
57
  :return: RegScaleAuth object
56
58
  :rtype: RegScaleAuth
57
59
  """
@@ -88,6 +90,7 @@ class RegScaleAuth(BaseModel):
88
90
  password=password.get_secret_value(),
89
91
  domain=domain,
90
92
  mfa_token=mfa_token,
93
+ app_id=app_id,
91
94
  )
92
95
  return cls(
93
96
  user_id=uid,
@@ -51,6 +51,8 @@ from .link import *
51
51
  from .master_assessment import *
52
52
  from .milestone import *
53
53
  from .meta_data import *
54
+ from .module import *
55
+ from .modules import *
54
56
  from .objective import *
55
57
  from .organization import *
56
58
  from .parameter import *
@@ -81,10 +83,13 @@ from .stig import *
81
83
  from .supply_chain import *
82
84
  from .system_role import *
83
85
  from .system_role_external_assignment import *
86
+ from .tag import *
87
+ from .tag_mapping import *
84
88
  from .task import *
85
89
  from .threat import *
86
90
  from .team import *
87
91
  from .user import *
92
+ from .user_group import *
88
93
  from .vulnerability import *
89
94
  from .vulnerability_mapping import *
90
95
  from .workflow import *
@@ -5,6 +5,7 @@ from concurrent.futures import ThreadPoolExecutor
5
5
  from enum import Enum
6
6
  from typing import Any, List, Optional, Union
7
7
 
8
+ from pydantic import Field
8
9
  from requests import JSONDecodeError
9
10
 
10
11
  from regscale.core.app.api import Api
@@ -60,7 +61,7 @@ class Assessment(RegScaleModel):
60
61
  _module_slug = "assessments"
61
62
 
62
63
  id: int = 0
63
- leadAssessorId: str = ""
64
+ leadAssessorId: str = Field(default_factory=RegScaleModel.get_user_id)
64
65
  title: Optional[str] = None
65
66
  assessmentType: Optional[Union[AssessmentType, str]] = None
66
67
  plannedStart: Optional[str] = None
@@ -76,7 +76,7 @@ class Component(RegScaleModel):
76
76
  externalId: Optional[str] = None
77
77
  isPublic: bool = True
78
78
  riskCategorization: Optional[str] = None
79
- complianceSettingsId: Optional[int] = None
79
+ complianceSettingsId: Optional[int] = 1
80
80
 
81
81
  @staticmethod
82
82
  def _get_additional_endpoints() -> ConfigDict:
@@ -489,15 +489,15 @@ class ControlImplementation(RegScaleModel):
489
489
  :return: A dictionary mapping control IDs to implementation IDs
490
490
  :rtype: Dict[str, int]
491
491
  """
492
- logger.info("Getting control label map by parent...")
492
+ logger.debug("Getting control label map by parent...")
493
493
  response = cls._get_api_handler().get(
494
494
  endpoint=cls.get_endpoint("get_all_by_parent").format(intParentID=parent_id, strModule=parent_module)
495
495
  )
496
496
 
497
497
  if response and response.ok:
498
- logger.info("Fetched control label map by parent successfully.")
498
+ logger.debug("Fetched control label map by parent successfully.")
499
499
  return {parentheses_to_dot(ci["controlName"]): ci["id"] for ci in response.json()}
500
- logger.info("Unable to get control label map by parent.")
500
+ logger.debug("Unable to get control label map by parent.")
501
501
  return {}
502
502
 
503
503
  @classmethod
@@ -509,14 +509,14 @@ class ControlImplementation(RegScaleModel):
509
509
  :return: A dictionary mapping control IDs to implementation IDs
510
510
  :rtype: Dict[int, int]
511
511
  """
512
- logger.info("Getting control id map by parent...")
512
+ logger.debug("Getting control id map by parent...")
513
513
  response = cls._get_api_handler().get(
514
514
  endpoint=cls.get_endpoint("get_all_by_parent").format(intParentID=parent_id, strModule=parent_module)
515
515
  )
516
516
  if response and response.ok:
517
- logger.info("Fetched control id map by parent successfully.")
517
+ logger.debug("Fetched control id map by parent successfully.")
518
518
  return {ci["controlID"]: ci["id"] for ci in response.json()}
519
- logger.info("Unable to get control id map by parent.")
519
+ logger.debug("Unable to get control id map by parent.")
520
520
  return {}
521
521
 
522
522
  @classmethod
@@ -1218,6 +1218,8 @@ class ControlImplementation(RegScaleModel):
1218
1218
  :return: list GraphQL response from RegScale
1219
1219
  :rtype: list
1220
1220
  """
1221
+ from regscale.core.app.internal.control_editor import _extract_control_owner_display
1222
+
1221
1223
  body = """
1222
1224
  query{
1223
1225
  controlImplementations (skip: 0, take: 50, where: {parentId: {eq: parent_id} parentModule: {eq: "parent_module"}}) {
@@ -1263,22 +1265,25 @@ class ControlImplementation(RegScaleModel):
1263
1265
  moded_item = {}
1264
1266
  moded_item["id"] = item["id"]
1265
1267
  moded_item["controlID"] = item["controlID"]
1266
- moded_item["controlOwnerId"] = (
1267
- str(item["controlOwner"]["lastName"]).strip()
1268
- + ", "
1269
- + str(item["controlOwner"]["firstName"]).strip()
1270
- + " ("
1271
- + str(item["controlOwner"]["userName"]).strip()
1272
- + ")"
1273
- )
1274
- moded_item["controlName"] = item["control"]["controlId"]
1275
- moded_item["controlTitle"] = item["control"]["title"]
1276
- moded_item["description"] = item["control"]["description"]
1277
- moded_item["status"] = item["status"]
1278
- moded_item["policy"] = item["policy"]
1279
- moded_item["implementation"] = item["implementation"]
1280
- moded_item["responsibility"] = item["responsibility"]
1281
- moded_item["inheritable"] = item["inheritable"]
1268
+
1269
+ # Extract control owner display using centralized method
1270
+ moded_item["controlOwnerId"] = _extract_control_owner_display(item)
1271
+
1272
+ # Handle case where control or its fields might be None
1273
+ if item.get("control") and item["control"] is not None:
1274
+ moded_item["controlName"] = item["control"].get("controlId", "")
1275
+ moded_item["controlTitle"] = item["control"].get("title", "")
1276
+ moded_item["description"] = item["control"].get("description", "")
1277
+ else:
1278
+ moded_item["controlName"] = ""
1279
+ moded_item["controlTitle"] = ""
1280
+ moded_item["description"] = ""
1281
+
1282
+ moded_item["status"] = item.get("status", "")
1283
+ moded_item["policy"] = item.get("policy", "")
1284
+ moded_item["implementation"] = item.get("implementation", "")
1285
+ moded_item["responsibility"] = item.get("responsibility", "")
1286
+ moded_item["inheritable"] = item.get("inheritable", False)
1282
1287
  moded_data.append(moded_item)
1283
1288
  return moded_data
1284
1289
  return []
@@ -1351,8 +1356,34 @@ class ControlImplementation(RegScaleModel):
1351
1356
  :return: list of control implementations, or None if not found
1352
1357
  :rtype: Optional[list[dict]]
1353
1358
  """
1354
- endpoint = cls.get_endpoint("get_list_by_parent").format(int_id=regscale_id, str_module=regscale_module)
1355
- response = cls._get_api_handler().get(endpoint=endpoint)
1359
+ query = {
1360
+ "parentID": 0,
1361
+ "module": "",
1362
+ "friendlyName": "",
1363
+ "workbench": "",
1364
+ "base": "",
1365
+ "sort": "sortId",
1366
+ "direction": "Ascending",
1367
+ "simpleSearch": "",
1368
+ "page": 1,
1369
+ "pageSize": 1000,
1370
+ "query": {
1371
+ "id": 0,
1372
+ "viewName": "",
1373
+ "module": "",
1374
+ "scope": "",
1375
+ "createdById": "",
1376
+ "dateCreated": None,
1377
+ "parameters": [],
1378
+ },
1379
+ "groupBy": "",
1380
+ "intDays": 0,
1381
+ "subTab": True,
1382
+ }
1383
+ query["parentId"] = regscale_id
1384
+ query["module"] = regscale_module
1385
+ endpoint = cls.get_endpoint("filter_control_implementations")
1386
+ response = cls._get_api_handler().post(endpoint=endpoint, data=query)
1356
1387
  if response and response.ok:
1357
1388
  return response.json()
1358
1389
  return None
@@ -250,13 +250,82 @@ class ControlObjective(RegScaleModel):
250
250
  return control_objectives
251
251
 
252
252
 
253
+ def _process_objective_ccis(objective: ControlObjective, ccis_to_control_ids: dict[str, set[int]]) -> None:
254
+ """
255
+ Process CCI IDs from a control objective.
256
+
257
+ :param ControlObjective objective: The control objective to process
258
+ :param dict ccis_to_control_ids: Dictionary to update with CCI mappings
259
+ :return: None
260
+ """
261
+ if not objective.otherId:
262
+ return
263
+
264
+ cci_ids = objective.otherId.split(",")
265
+ for cci_id in cci_ids:
266
+ cci_id = cci_id.strip()
267
+ if cci_id and cci_id.startswith("CCI-"):
268
+ ccis_to_control_ids[cci_id].add(objective.securityControlId)
269
+
270
+
271
+ def _fetch_cci_objectives_batch(parent_id: int, skip: int, take: int) -> list[ControlObjective]:
272
+ """
273
+ Fetch a batch of CCI objectives.
274
+
275
+ :param int parent_id: The parent ID
276
+ :param int skip: Number of items to skip
277
+ :param int take: Number of items to take
278
+ :return: List of control objectives
279
+ :rtype: list[ControlObjective]
280
+ """
281
+ return ControlObjective.fetch_control_objectives_by_other_id(
282
+ parent_id=parent_id, other_id_contains="CCI-", skip=skip, take=take
283
+ )
284
+
285
+
253
286
  def map_ccis_to_control_ids(parent_id: int) -> dict:
287
+ """
288
+ Map CCI IDs to control IDs with pagination support.
289
+
290
+ :param int parent_id: The parent ID to fetch objectives for
291
+ :return: Dictionary mapping CCI IDs to sets of control IDs
292
+ :rtype: dict
293
+ """
294
+ import logging
295
+
296
+ logger = logging.getLogger("regscale")
254
297
  ccis_to_control_ids: dict[str, set[int]] = defaultdict(set)
255
- objectives = ControlObjective.fetch_control_objectives_by_other_id(parent_id=parent_id, other_id_contains="CCI-")
256
298
 
257
- for objective in objectives:
258
- cci_ids = objective.otherId.split(",")
259
- for cci_id in cci_ids:
260
- ccis_to_control_ids[cci_id.strip()].add(objective.securityControlId)
299
+ try:
300
+ skip = 0
301
+ take = 50 # Use 50 as RegScale API limit
302
+ total_fetched = 0
303
+ max_iterations = 100 # Increase safety limit since batch size is smaller
304
+
305
+ for _ in range(max_iterations):
306
+ objectives = _fetch_cci_objectives_batch(parent_id, skip, take)
307
+ if not objectives:
308
+ break
309
+
310
+ # Process each objective
311
+ for objective in objectives:
312
+ _process_objective_ccis(objective, ccis_to_control_ids)
313
+
314
+ total_fetched += len(objectives)
315
+
316
+ # Check if we've reached the end
317
+ if len(objectives) < take:
318
+ break
319
+
320
+ skip += take
321
+ else:
322
+ logger.warning(f"Reached max iterations ({max_iterations}). Total fetched: {total_fetched}")
323
+
324
+ except Exception as e:
325
+ logger.debug(f"Error fetching CCI to control map: {e}")
326
+ return {}
327
+
328
+ if ccis_to_control_ids:
329
+ logger.debug(f"Mapped {len(ccis_to_control_ids)} unique CCIs to controls")
261
330
 
262
331
  return ccis_to_control_ids
@@ -385,6 +385,8 @@ class File(BaseModel):
385
385
  file_type_header = "application/gzip"
386
386
  elif file_type == ".msg":
387
387
  file_type_header = "application/vnd.ms-outlook"
388
+ elif file_type == ".jsonl":
389
+ file_type_header = "application/jsonl+json"
388
390
  else:
389
391
  logger = logging.getLogger("regscale")
390
392
  logger.warning(f"Unacceptable file type for upload: {file_type}")
@@ -1025,7 +1025,6 @@ class Issue(RegScaleModel):
1025
1025
  """
1026
1026
  take = 50
1027
1027
  skip = 0
1028
- total_fetched = 0
1029
1028
  supports_multiple_controls = cls.is_multiple_controls_supported()
1030
1029
  fields = cls._get_query_fields(supports_multiple_controls)
1031
1030
 
@@ -1039,14 +1038,12 @@ class Issue(RegScaleModel):
1039
1038
  cls._log_progress(skip, take, len(items), total_count, logger)
1040
1039
  cls._process_issue_items(items, supports_multiple_controls, control_issues)
1041
1040
 
1042
- total_fetched += len(items)
1043
-
1044
1041
  if not response.get(cls.get_module_string(), {}).get("pageInfo", {}).get("hasNextPage", False):
1045
1042
  break
1046
1043
 
1047
1044
  skip += take
1048
-
1049
- return total_fetched
1045
+ # return total count here as counting the items yields the wrong value.
1046
+ return total_count
1050
1047
 
1051
1048
  @classmethod
1052
1049
  def _get_query_fields(cls, supports_multiple_controls: bool) -> str:
@@ -15,7 +15,10 @@ class Organization(RegScaleModel):
15
15
  id: int = 0
16
16
  name: Optional[str] = ""
17
17
  description: Optional[str] = ""
18
+ orgCode: Optional[str] = ""
19
+ orgUrl: Optional[str] = ""
18
20
  status: Optional[str] = "Active"
21
+ externalId: Optional[str] = ""
19
22
 
20
23
  @staticmethod
21
24
  def _get_additional_endpoints() -> dict:
@@ -1258,17 +1258,18 @@ class RegScaleModel(BaseModel, ABC):
1258
1258
  return ConfigDict()
1259
1259
 
1260
1260
  @classmethod
1261
- def get_endpoint(cls, endpoint_type: str) -> str:
1261
+ def get_endpoint(cls, endpoint_type: str, suppress_error: bool = False) -> str:
1262
1262
  """
1263
1263
  Get the endpoint for a specific type.
1264
1264
 
1265
1265
  :param str endpoint_type: The type of endpoint
1266
+ :param bool suppress_error: Whether to suppress the error if the endpoint is not found, defaults to False
1266
1267
  :raises ValueError: If the endpoint type is not found
1267
1268
  :return: The endpoint
1268
1269
  :rtype: str
1269
1270
  """
1270
1271
  endpoint = cls._get_endpoints().get(endpoint_type, "na") # noqa
1271
- if not endpoint or endpoint == "na":
1272
+ if not endpoint or endpoint == "na" and not suppress_error:
1272
1273
  logger.error(f"{cls.__name__} does not have endpoint {endpoint_type}")
1273
1274
  raise ValueError(f"Endpoint {endpoint_type} not found")
1274
1275
  endpoint = str(endpoint).replace("{model_slug}", cls.get_module_slug())
@@ -1566,11 +1567,22 @@ class RegScaleModel(BaseModel, ABC):
1566
1567
  f"[#f68d1f]Updating {total_items} RegScale {cls.__name__}s...",
1567
1568
  total=total_items,
1568
1569
  )
1570
+ endpoint = cls.get_endpoint("batch_update", suppress_error=True)
1571
+ if not endpoint or endpoint == "na":
1572
+ logger.debug(f"No batch_update endpoint found for {cls.__name__}, using save method instead")
1573
+ for item in items:
1574
+ updated_item = item.save()
1575
+ cls.cache_object(updated_item)
1576
+ results.append(updated_item)
1577
+ if progress and update_job is not None:
1578
+ progress.advance(update_job, advance=1)
1579
+ cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
1580
+ return results
1569
1581
  for i in range(0, total_items, batch_size):
1570
1582
  batch = items[i : i + batch_size]
1571
1583
  batch_results = cls._handle_list_response(
1572
1584
  cls._get_api_handler().put(
1573
- endpoint=cls.get_endpoint("batch_update"),
1585
+ endpoint=endpoint,
1574
1586
  data=[item.model_dump() for item in batch if item],
1575
1587
  )
1576
1588
  )
@@ -1583,10 +1595,10 @@ class RegScaleModel(BaseModel, ABC):
1583
1595
  cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
1584
1596
 
1585
1597
  if progress_context:
1586
- process_batch(progress_context)
1598
+ process_batch(progress=progress_context, remove_progress_bar=remove_progress)
1587
1599
  else:
1588
1600
  with create_progress_object() as create_progress:
1589
- process_batch(create_progress)
1601
+ process_batch(progress=create_progress, remove_progress_bar=remove_progress)
1590
1602
 
1591
1603
  return results
1592
1604
 
@@ -43,6 +43,7 @@ class SecurityPlan(RegScaleModel):
43
43
  environment: Optional[str] = ""
44
44
  lawsAndRegulations: Optional[str] = ""
45
45
  authorizationBoundary: Optional[str] = ""
46
+ authorizationTerminationDate: Optional[str] = ""
46
47
  networkArchitecture: Optional[str] = ""
47
48
  dataFlow: Optional[str] = ""
48
49
  overallCategorization: Optional[str] = ""
regscale/regscale.py CHANGED
@@ -477,12 +477,21 @@ def validate_token():
477
477
  cls=NotRequiredIf,
478
478
  not_required_if=["token"],
479
479
  )
480
+ @click.option(
481
+ "--app_id",
482
+ type=click.INT,
483
+ help="RegScale App ID to login with.",
484
+ default=1,
485
+ prompt=False,
486
+ required=False,
487
+ )
480
488
  def login(
481
489
  username: Optional[str],
482
490
  password: Optional[str],
483
491
  token: Optional[str] = None,
484
492
  domain: Optional[str] = None,
485
493
  mfa_token: Optional[str] = None,
494
+ app_id: Optional[int] = 1,
486
495
  ):
487
496
  """Logs the user into their RegScale instance."""
488
497
  from regscale.core.app.application import Application
@@ -502,9 +511,10 @@ def login(
502
511
  mfa_token=mfa_token,
503
512
  app=app,
504
513
  host=domain,
514
+ app_id=app_id,
505
515
  )
506
516
  else:
507
- lg.login(str_user=username, str_password=password, app=app, host=domain)
517
+ lg.login(str_user=username, str_password=password, app=app, host=domain, app_id=app_id)
508
518
 
509
519
 
510
520
  # Check the health of the RegScale Application
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.27.2.0
3
+ Version: 6.28.0.0
4
4
  Summary: Command Line Interface (CLI) for bulk processing/loading data into RegScale
5
5
  Home-page: https://github.com/RegScale/regscale-cli
6
6
  Author: Travis Howerton