regscale-cli 6.21.0.0__py3-none-any.whl → 6.21.2.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.
Files changed (54) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +7 -0
  3. regscale/integrations/commercial/__init__.py +9 -10
  4. regscale/integrations/commercial/amazon/common.py +79 -2
  5. regscale/integrations/commercial/aws/cli.py +183 -9
  6. regscale/integrations/commercial/aws/scanner.py +544 -9
  7. regscale/integrations/commercial/cpe.py +18 -1
  8. regscale/integrations/commercial/import_all/import_all_cmd.py +2 -2
  9. regscale/integrations/commercial/microsoft_defender/__init__.py +0 -0
  10. regscale/integrations/commercial/{defender.py → microsoft_defender/defender.py} +38 -612
  11. regscale/integrations/commercial/microsoft_defender/defender_api.py +286 -0
  12. regscale/integrations/commercial/microsoft_defender/defender_constants.py +80 -0
  13. regscale/integrations/commercial/microsoft_defender/defender_scanner.py +168 -0
  14. regscale/integrations/commercial/qualys/__init__.py +24 -86
  15. regscale/integrations/commercial/qualys/containers.py +2 -0
  16. regscale/integrations/commercial/qualys/scanner.py +7 -2
  17. regscale/integrations/commercial/sonarcloud.py +110 -71
  18. regscale/integrations/commercial/tenablev2/jsonl_scanner.py +2 -1
  19. regscale/integrations/commercial/wizv2/async_client.py +10 -3
  20. regscale/integrations/commercial/wizv2/click.py +105 -26
  21. regscale/integrations/commercial/wizv2/constants.py +249 -1
  22. regscale/integrations/commercial/wizv2/data_fetcher.py +401 -0
  23. regscale/integrations/commercial/wizv2/finding_processor.py +295 -0
  24. regscale/integrations/commercial/wizv2/issue.py +2 -2
  25. regscale/integrations/commercial/wizv2/parsers.py +3 -2
  26. regscale/integrations/commercial/wizv2/policy_compliance.py +3057 -0
  27. regscale/integrations/commercial/wizv2/policy_compliance_helpers.py +564 -0
  28. regscale/integrations/commercial/wizv2/scanner.py +19 -25
  29. regscale/integrations/commercial/wizv2/utils.py +258 -85
  30. regscale/integrations/commercial/wizv2/variables.py +4 -3
  31. regscale/integrations/compliance_integration.py +1607 -0
  32. regscale/integrations/public/fedramp/fedramp_five.py +93 -8
  33. regscale/integrations/public/fedramp/markdown_parser.py +7 -1
  34. regscale/integrations/scanner_integration.py +57 -6
  35. regscale/models/__init__.py +1 -1
  36. regscale/models/app_models/__init__.py +1 -0
  37. regscale/models/integration_models/cisa_kev_data.json +103 -4
  38. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  39. regscale/{integrations/commercial/wizv2/models.py → models/integration_models/wizv2.py} +4 -12
  40. regscale/models/regscale_models/file.py +4 -0
  41. regscale/models/regscale_models/issue.py +151 -8
  42. regscale/models/regscale_models/regscale_model.py +4 -2
  43. regscale/models/regscale_models/security_plan.py +1 -1
  44. regscale/utils/graphql_client.py +3 -1
  45. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/METADATA +9 -9
  46. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/RECORD +52 -44
  47. tests/regscale/core/test_version_regscale.py +5 -3
  48. tests/regscale/integrations/test_wiz_policy_compliance_affected_controls.py +154 -0
  49. tests/regscale/test_authorization.py +0 -65
  50. tests/regscale/test_init.py +0 -96
  51. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/LICENSE +0 -0
  52. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/WHEEL +0 -0
  53. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/entry_points.txt +0 -0
  54. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,18 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """Class for a Wiz.io integration"""
3
+ """Wiz v2 Integration Models (RegScale pattern)."""
4
4
 
5
- # standard python imports
6
5
  from enum import Enum
7
6
  from typing import Optional
7
+ from datetime import datetime
8
8
 
9
9
  from pydantic import BaseModel, Field
10
- from datetime import datetime
11
10
 
12
11
  from regscale.models import regscale_models
13
12
 
14
13
 
15
14
  class AssetCategory(Enum):
16
- """Map Wiz assetTypes with RegScale assetCategories"""
15
+ """Map Wiz assetTypes with RegScale assetCategories."""
17
16
 
18
17
  SERVICE_USAGE_TECHNOLOGY = regscale_models.AssetCategory.Hardware
19
18
  GATEWAY = regscale_models.AssetCategory.Hardware
@@ -90,6 +89,7 @@ class ComplianceReport(BaseModel):
90
89
  control_id: Optional[str] = Field(None, alias="Control ID")
91
90
  compliance_check: Optional[str] = Field(None, alias="Compliance Check Name (Wiz Subcategory)")
92
91
  control_description: Optional[str] = Field(None, alias="Control Description")
92
+ issue_finding_id: Optional[str] = Field(None, alias="Issue/Finding ID")
93
93
  severity: Optional[str] = Field(None, alias="Severity")
94
94
  result: str = Field(..., alias="Result")
95
95
  framework: Optional[str] = Field(None, alias="Framework")
@@ -102,11 +102,3 @@ class ComplianceReport(BaseModel):
102
102
  resource_id: str = Field(..., alias="Resource ID")
103
103
  resource_region: Optional[str] = Field(None, alias="Resource Region")
104
104
  resource_cloud_platform: Optional[str] = Field(None, alias="Resource Cloud Platform")
105
-
106
-
107
- # # Attempt to create an instance of the model again
108
- # example_row = data.iloc[0].to_dict()
109
- # example_compliance_report = ComplianceReport(**example_row)
110
- #
111
- # # Display the instance
112
- # example_compliance_report.dict()
@@ -375,6 +375,10 @@ class File(BaseModel):
375
375
  except KeyError:
376
376
  if file_type == ".xlsx":
377
377
  file_type_header = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
378
+ elif file_type == ".docx":
379
+ file_type_header = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
380
+ elif file_type == ".pptx":
381
+ file_type_header = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
378
382
  elif file_type == ".nessus":
379
383
  file_type_header = "text/xml"
380
384
  elif file_type == ".gz":
@@ -447,8 +447,9 @@ class Issue(RegScaleModel):
447
447
  return "High"
448
448
  return ""
449
449
 
450
- @staticmethod
450
+ @classmethod
451
451
  def get_due_date(
452
+ cls,
452
453
  severity: Union[IssueSeverity, str],
453
454
  config: dict,
454
455
  key: str,
@@ -481,18 +482,37 @@ class Issue(RegScaleModel):
481
482
  severity = IssueSeverity.NotAssigned.value
482
483
 
483
484
  if severity == IssueSeverity.Critical.value:
484
- days = config["issues"].get(key, {}).get("critical", 0)
485
+ days = cls._get_days_for_values(["critical"], config, key)
486
+ start_date = start_date + datetime.timedelta(days=days)
485
487
  elif severity == IssueSeverity.High.value:
486
- days = config["issues"].get(key, {}).get("high", 0)
488
+ days = cls._get_days_for_values(["high"], config, key)
487
489
  elif severity == IssueSeverity.Moderate.value:
488
- days = config["issues"].get(key, {}).get("moderate", 0) or config["issues"].get(key, {}).get("medium", 0)
490
+ days = cls._get_days_for_values(["moderate", "medium"], config, key)
489
491
  elif severity == IssueSeverity.Low.value:
490
- days = config["issues"].get(key, {}).get("low", 0)
492
+ days = cls._get_days_for_values(["low", "minor"], config, key)
491
493
  else:
492
494
  days = 60
493
495
  due_date = start_date + datetime.timedelta(days=days)
494
496
  return due_date.strftime(dt_format)
495
497
 
498
+ @staticmethod
499
+ def _get_days_for_values(possible_values: List[str], config: dict, key: str, default: Optional[int] = 30) -> int:
500
+ """
501
+ Get the number of days for the given possible values from the configuration.
502
+
503
+ :param List[str] possible_values: List of possible values to check
504
+ :param dict config: Application config
505
+ :param str key: The key to use for init.yaml from the issues section to determine the due date
506
+ :param Optional[int] default: Default number of days to return if no values match, defaults to 30
507
+ :return: Number of days for the first matching value, or 0 if none match
508
+ :rtype: int
509
+ """
510
+ for value in possible_values:
511
+ days = config["issues"].get(key, {}).get(value, 0)
512
+ if days > 0:
513
+ return days
514
+ return default
515
+
496
516
  @staticmethod
497
517
  def assign_severity(value: Optional[Any] = None) -> str:
498
518
  """
@@ -518,11 +538,11 @@ class Issue(RegScaleModel):
518
538
  else:
519
539
  severity = severity_levels["low"]
520
540
  elif isinstance(value, str):
521
- if value.lower() in ["low", "lowest"]:
541
+ if value.lower() in ["low", "lowest", "minor"]:
522
542
  severity = severity_levels["low"]
523
- elif value.lower() in ["medium", "moderate"]:
543
+ elif value.lower() in ["medium", "moderate", "major"]:
524
544
  severity = severity_levels["moderate"]
525
- elif value.lower() in ["high", "critical", "highest"]:
545
+ elif value.lower() in ["high", "critical", "highest", "critical", "blocker"]:
526
546
  severity = severity_levels["high"]
527
547
  elif value in list(severity_levels.values()):
528
548
  severity = value
@@ -1270,6 +1290,129 @@ class Issue(RegScaleModel):
1270
1290
  raise ValueError(f"riskAdjustment must be one of {allowed_values}")
1271
1291
  return v
1272
1292
 
1293
+ # New method to determine and set isPoam based on NIST/FedRAMP criteria
1294
+ def set_is_poam(
1295
+ self,
1296
+ config: Optional[Dict[str, Any]] = None,
1297
+ standard: str = "fedramp",
1298
+ current_date: Optional[datetime.datetime] = None,
1299
+ ) -> None:
1300
+ """
1301
+ Sets the isPoam field based on NIST 800-53 or FedRAMP criteria, preserving historical POAM status.
1302
+
1303
+ Criteria:
1304
+ - Preserves isPoam=True for imported data, even if closed, for reporting purposes.
1305
+ - For new issues:
1306
+ - Skips if false positive, operational requirement, or deviation rationale exists.
1307
+ - FedRAMP: High/Critical issues are POAMs if open; scan-based issues are POAMs if overdue; non-scan issues are POAMs if open.
1308
+ - NIST: POAM for any open deficiency unless accepted as residual risk.
1309
+ - Uses config thresholds (e.g., {'critical': 30, 'high': 90, 'medium': 90, 'low': 365, 'status': 'Open'}).
1310
+
1311
+ Args:
1312
+ config: Optional dictionary with severity thresholds and status from init.yaml.
1313
+ Defaults to FedRAMP: {'critical': 30, 'high': 30, 'medium': 90, 'low': 180, 'status': 'Open'}.
1314
+ For NIST, uses {'critical': 30, 'high': 90, 'medium': 90, 'low': 180, 'status': 'Open'}.
1315
+ standard: 'fedramp' (default) or 'nist'.
1316
+ current_date: Optional datetime for calculation (defaults to current time).
1317
+
1318
+ Returns:
1319
+ None: Sets the isPoam attribute directly.
1320
+ """
1321
+ # Use current time if not provided
1322
+ current_date = current_date or datetime.datetime.now()
1323
+
1324
+ # Preserve historical POAM status for imported data
1325
+ if self.isPoam:
1326
+ return
1327
+
1328
+ # Define open statuses
1329
+ open_statuses = {
1330
+ IssueStatus.Open,
1331
+ IssueStatus.Delayed,
1332
+ IssueStatus.PendingVerification,
1333
+ IssueStatus.VendorDependency,
1334
+ IssueStatus.PendingApproval,
1335
+ }
1336
+
1337
+ # Skip if issue is accepted as residual risk
1338
+ if self.falsePositive or self.operationalRequirement or self.deviationRationale:
1339
+ self.isPoam = False
1340
+ return
1341
+
1342
+ # Load default thresholds based on standard if config is not provided
1343
+ config = config or (
1344
+ {"critical": 30, "high": 30, "medium": 90, "low": 180, "status": "Open"}
1345
+ if standard == "fedramp"
1346
+ else {"critical": 30, "high": 90, "medium": 90, "low": 180, "status": "Open"}
1347
+ )
1348
+
1349
+ # Map severity to remediation days
1350
+ severity_map = {
1351
+ IssueSeverity.Critical: config.get("critical", 30),
1352
+ IssueSeverity.High: config.get("high", 90),
1353
+ IssueSeverity.Moderate: config.get("medium", 90),
1354
+ IssueSeverity.Low: config.get("low", 365),
1355
+ IssueSeverity.NotAssigned: config.get("low", 365),
1356
+ }
1357
+
1358
+ # Normalize severity
1359
+ severity = (
1360
+ IssueSeverity(self.severityLevel)
1361
+ if self.severityLevel in {s.value for s in IssueSeverity}
1362
+ else IssueSeverity.NotAssigned
1363
+ )
1364
+ threshold_days = severity_map[severity]
1365
+
1366
+ # Get detection date
1367
+ detection_date_str = self.dateFirstDetected or self.dateCreated
1368
+ if not detection_date_str:
1369
+ self.isPoam = False
1370
+ return
1371
+
1372
+ try:
1373
+ detection_date = datetime.datetime.strptime(detection_date_str, "%Y-%m-%dT%H:%M:%S")
1374
+ except ValueError:
1375
+ try:
1376
+ detection_date = datetime.datetime.strptime(detection_date_str, "%Y-%m-%d")
1377
+ except ValueError:
1378
+ self.isPoam = False
1379
+ return
1380
+
1381
+ days_since_detection = (current_date - detection_date).days
1382
+
1383
+ # Define scan sources
1384
+ scan_sources = {"Vulnerability Assessment", "FDCC/USGCB", "Penetration Test"}
1385
+ is_scan = self.identification in scan_sources
1386
+
1387
+ # Apply standard-specific logic
1388
+ if standard == "fedramp":
1389
+ # FedRAMP: High/Critical are always POAMs if open
1390
+ if severity in {IssueSeverity.High, IssueSeverity.Critical}:
1391
+ self.isPoam = self.status in open_statuses
1392
+ # Scan-based: POAM if overdue
1393
+ elif is_scan:
1394
+ self.isPoam = days_since_detection > threshold_days
1395
+ # Non-scan: POAM if open
1396
+ else:
1397
+ self.isPoam = self.status in open_statuses
1398
+
1399
+ # Handle vendor dependencies
1400
+ if self.vendorDependency and self.vendorLastUpdate:
1401
+ try:
1402
+ vendor_date = datetime.datetime.strptime(self.vendorLastUpdate, "%Y-%m-%dT%H:%M:%S")
1403
+ days_since_vendor = (current_date - vendor_date).days
1404
+ self.isPoam = days_since_vendor > threshold_days
1405
+ except ValueError:
1406
+ pass # Fall back to detection date logic
1407
+
1408
+ else: # NIST 800-53
1409
+ # NIST: POAM for any open deficiency
1410
+ self.isPoam = self.status in open_statuses
1411
+
1412
+ # Apply status filter from config if specified
1413
+ if "status" in config:
1414
+ self.isPoam = self.isPoam and self.status == config["status"]
1415
+
1273
1416
 
1274
1417
  def build_issue_dict_from_query(a: Dict[str, Any]) -> Dict[str, Any]:
1275
1418
  """
@@ -1279,7 +1279,9 @@ class RegScaleModel(BaseModel, ABC):
1279
1279
  exc_info=True,
1280
1280
  )
1281
1281
  if response and not response.ok:
1282
- logger.error(f"Response Error: Code #{response.status_code}: {response.reason}\n{response.text}")
1282
+ logger.error(
1283
+ f"Response Error: Code #{response.status_code}: {response.reason}\n{response.text}", exc_info=True
1284
+ )
1283
1285
  if response is None:
1284
1286
  error_msg = "Response was None"
1285
1287
  logger.error(error_msg)
@@ -1522,7 +1524,7 @@ class RegScaleModel(BaseModel, ABC):
1522
1524
  :return: A list of objects
1523
1525
  :rtype: List[T]
1524
1526
  """
1525
- response = cls._get_api_handler().get(endpoint=cls.get_endpoint("list"))
1527
+ response = cls._get_api_handler().get(endpoint=cls.get_endpoint("list").format(module_slug=cls._module_slug))
1526
1528
  if response.ok:
1527
1529
  return cast(List[T], [cls.get_object(object_id=sp["id"]) for sp in response.json()])
1528
1530
  else:
@@ -176,7 +176,7 @@ class SecurityPlan(RegScaleModel):
176
176
  return {}
177
177
 
178
178
  @classmethod
179
- def get_list(cls) -> list:
179
+ def get_ssp_list(cls) -> list:
180
180
  """
181
181
  Get a list of objects.
182
182
 
@@ -5,6 +5,8 @@ A module for making paginated GraphQL queries.
5
5
  import logging
6
6
  from typing import List, Dict, Optional, Any
7
7
 
8
+ import graphql
9
+
8
10
  from regscale.core.app.utils.app_utils import create_progress_object, error_and_exit
9
11
 
10
12
  logger = logging.getLogger(__name__)
@@ -90,7 +92,7 @@ class PaginatedGraphQLClient:
90
92
  return result
91
93
  except Exception as e:
92
94
  logger.error(f"An error occurred while executing the query: {str(e)}", exc_info=True)
93
- logger.error(f"Query: {self.query}")
95
+ logger.error(f"Query: {graphql.print_ast(self.query)}")
94
96
  error_and_exit(f"Variable: {variables}")
95
97
 
96
98
  def fetch_results(self, variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.21.0.0
3
+ Version: 6.21.2.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
@@ -56,7 +56,7 @@ Requires-Dist: pydantic ~=2.11.0
56
56
  Requires-Dist: pypandoc
57
57
  Requires-Dist: pypandoc-binary
58
58
  Requires-Dist: pytest
59
- Requires-Dist: python-dateutil ~=2.8.2
59
+ Requires-Dist: python-dateutil ~=2.9.0
60
60
  Requires-Dist: python-docx
61
61
  Requires-Dist: python-jwt ==4.1.0
62
62
  Requires-Dist: pyxnat ==1.5.*
@@ -137,7 +137,7 @@ Requires-Dist: pydantic ~=2.11.0 ; extra == 'airflow'
137
137
  Requires-Dist: pypandoc ; extra == 'airflow'
138
138
  Requires-Dist: pypandoc-binary ; extra == 'airflow'
139
139
  Requires-Dist: pytest ; extra == 'airflow'
140
- Requires-Dist: python-dateutil ~=2.8.2 ; extra == 'airflow'
140
+ Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'airflow'
141
141
  Requires-Dist: python-docx ; extra == 'airflow'
142
142
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'airflow'
143
143
  Requires-Dist: pyxnat ==1.5.* ; extra == 'airflow'
@@ -222,7 +222,7 @@ Requires-Dist: pydantic ~=2.11.0 ; extra == 'airflow-azure'
222
222
  Requires-Dist: pypandoc ; extra == 'airflow-azure'
223
223
  Requires-Dist: pypandoc-binary ; extra == 'airflow-azure'
224
224
  Requires-Dist: pytest ; extra == 'airflow-azure'
225
- Requires-Dist: python-dateutil ~=2.8.2 ; extra == 'airflow-azure'
225
+ Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'airflow-azure'
226
226
  Requires-Dist: python-docx ; extra == 'airflow-azure'
227
227
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'airflow-azure'
228
228
  Requires-Dist: pyxnat ==1.5.* ; extra == 'airflow-azure'
@@ -307,7 +307,7 @@ Requires-Dist: pyodbc ; extra == 'airflow-sqlserver'
307
307
  Requires-Dist: pypandoc ; extra == 'airflow-sqlserver'
308
308
  Requires-Dist: pypandoc-binary ; extra == 'airflow-sqlserver'
309
309
  Requires-Dist: pytest ; extra == 'airflow-sqlserver'
310
- Requires-Dist: python-dateutil ~=2.8.2 ; extra == 'airflow-sqlserver'
310
+ Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'airflow-sqlserver'
311
311
  Requires-Dist: python-docx ; extra == 'airflow-sqlserver'
312
312
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'airflow-sqlserver'
313
313
  Requires-Dist: pyxnat ==1.5.* ; extra == 'airflow-sqlserver'
@@ -397,7 +397,7 @@ Requires-Dist: pydantic ~=2.11.0 ; extra == 'all'
397
397
  Requires-Dist: pypandoc ; extra == 'all'
398
398
  Requires-Dist: pypandoc-binary ; extra == 'all'
399
399
  Requires-Dist: pytest ; extra == 'all'
400
- Requires-Dist: python-dateutil ~=2.8.2 ; extra == 'all'
400
+ Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'all'
401
401
  Requires-Dist: python-docx ; extra == 'all'
402
402
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'all'
403
403
  Requires-Dist: pyxnat ==1.5.* ; extra == 'all'
@@ -460,7 +460,7 @@ Requires-Dist: pydantic ~=2.11.0 ; extra == 'ansible'
460
460
  Requires-Dist: pypandoc ; extra == 'ansible'
461
461
  Requires-Dist: pypandoc-binary ; extra == 'ansible'
462
462
  Requires-Dist: pytest ; extra == 'ansible'
463
- Requires-Dist: python-dateutil ~=2.8.2 ; extra == 'ansible'
463
+ Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'ansible'
464
464
  Requires-Dist: python-docx ; extra == 'ansible'
465
465
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'ansible'
466
466
  Requires-Dist: pyxnat ==1.5.* ; extra == 'ansible'
@@ -539,7 +539,7 @@ Requires-Dist: pytest-rerunfailures ; extra == 'dev'
539
539
  Requires-Dist: pytest-timeout ; extra == 'dev'
540
540
  Requires-Dist: pytest-xdist ; extra == 'dev'
541
541
  Requires-Dist: pytest >=5 ; extra == 'dev'
542
- Requires-Dist: python-dateutil ~=2.8.2 ; extra == 'dev'
542
+ Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'dev'
543
543
  Requires-Dist: python-docx ; extra == 'dev'
544
544
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'dev'
545
545
  Requires-Dist: pyxnat ==1.5.* ; extra == 'dev'
@@ -612,7 +612,7 @@ Requires-Dist: pydantic ~=2.11.0 ; extra == 'server'
612
612
  Requires-Dist: pypandoc ; extra == 'server'
613
613
  Requires-Dist: pypandoc-binary ; extra == 'server'
614
614
  Requires-Dist: pytest ; extra == 'server'
615
- Requires-Dist: python-dateutil ~=2.8.2 ; extra == 'server'
615
+ Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'server'
616
616
  Requires-Dist: python-docx ; extra == 'server'
617
617
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'server'
618
618
  Requires-Dist: pyxnat ==1.5.* ; extra == 'server'