regscale-cli 6.27.2.0__py3-none-any.whl → 6.27.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 (40) 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/login.py +21 -4
  7. regscale/core/utils/date.py +77 -1
  8. regscale/integrations/commercial/aws/scanner.py +4 -1
  9. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  10. regscale/integrations/control_matcher.py +78 -23
  11. regscale/integrations/public/csam/csam.py +572 -763
  12. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  13. regscale/integrations/public/csam/csam_common.py +154 -0
  14. regscale/integrations/public/csam/csam_controls.py +432 -0
  15. regscale/integrations/public/csam/csam_poam.py +124 -0
  16. regscale/integrations/public/fedramp/click.py +17 -4
  17. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  18. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  19. regscale/integrations/scanner_integration.py +16 -1
  20. regscale/models/integration_models/cisa_kev_data.json +49 -19
  21. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  22. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
  23. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  24. regscale/models/platform.py +3 -0
  25. regscale/models/regscale_models/__init__.py +5 -0
  26. regscale/models/regscale_models/component.py +1 -1
  27. regscale/models/regscale_models/control_implementation.py +55 -24
  28. regscale/models/regscale_models/organization.py +3 -0
  29. regscale/models/regscale_models/regscale_model.py +17 -5
  30. regscale/models/regscale_models/security_plan.py +1 -0
  31. regscale/regscale.py +11 -1
  32. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
  33. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +40 -36
  34. tests/regscale/core/test_login.py +171 -4
  35. tests/regscale/integrations/test_control_matcher.py +24 -0
  36. tests/regscale/models/test_control_implementation.py +118 -3
  37. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
  38. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
  39. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
  40. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/top_level.txt +0 -0
@@ -82,6 +82,30 @@ class Vulnerabilities(SynqlyModel):
82
82
  severity_filter = f"severity[in]{','.join(mapped_severities)}"
83
83
  return severity_filter
84
84
 
85
+ @staticmethod
86
+ def _translate_asset_filter(replace: str, replace_with: str, asset_filters: Optional[list[str]]) -> list[str]:
87
+ """
88
+ Translate asset filters to the correct format for the integration
89
+
90
+ :param str replace: The string to replace
91
+ :param str replace_with: The string to replace with
92
+ :param list[str] asset_filters: The asset filters to translate
93
+ :return: The translated asset filters
94
+ :rtype: list[str]
95
+ """
96
+ translated_asset_filters = []
97
+ for asset_filter in asset_filters:
98
+ # Remove outer double quotes if present
99
+ cleaned_filter = asset_filter
100
+ if cleaned_filter.startswith('"') and cleaned_filter.endswith('"') and len(cleaned_filter) > 1:
101
+ cleaned_filter = cleaned_filter[1:-1]
102
+
103
+ if replace_with in cleaned_filter:
104
+ translated_asset_filters.append(cleaned_filter)
105
+ elif replace in cleaned_filter:
106
+ translated_asset_filters.append(cleaned_filter.replace(replace, replace_with))
107
+ return translated_asset_filters
108
+
85
109
  def _handle_scan_date_options(self, regscale_ssp_id: int, **kwargs) -> list[str]:
86
110
  """
87
111
  Handle scan date options for the integration sync process
@@ -94,6 +118,13 @@ class Vulnerabilities(SynqlyModel):
94
118
 
95
119
  vuln_filter = [self._build_severity_filter(kwargs.get("minimum_severity_filter"))]
96
120
 
121
+ if asset_filters := kwargs.get("filter", []):
122
+ vuln_filter.extend(
123
+ self._translate_asset_filter(
124
+ replace="device.", replace_with="resources.data.", asset_filters=asset_filters
125
+ )
126
+ )
127
+
97
128
  if kwargs.get("all_scans"):
98
129
  vuln_filter.append("finding.last_seen_time[gte]915148800") # Friday, January 1, 1999 12:00:00 AM UTC
99
130
  elif scan_date := kwargs.get("scan_date"):
@@ -118,8 +149,10 @@ class Vulnerabilities(SynqlyModel):
118
149
  self.logger.debug(f"Vulnerability filter: {vuln_filter}")
119
150
 
120
151
  # Pop the filter from kwargs so it doesn't get passed to query_findings
121
- asset_filter = kwargs.pop("filter", [])
122
- if asset_filter:
152
+ if asset_filter := kwargs.pop("filter", []):
153
+ asset_filter = self._translate_asset_filter(
154
+ replace="resources.data.", replace_with="device.", asset_filters=asset_filter
155
+ )
123
156
  self.logger.debug(f"Asset filter: {asset_filter}")
124
157
 
125
158
  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 *
@@ -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
@@ -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.27.3.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