regscale-cli 6.23.0.1__py3-none-any.whl → 6.24.0.1__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 (45) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +2 -0
  3. regscale/integrations/commercial/__init__.py +1 -0
  4. regscale/integrations/commercial/jira.py +95 -22
  5. regscale/integrations/commercial/sarif/sarif_converter.py +1 -1
  6. regscale/integrations/commercial/wizv2/click.py +132 -2
  7. regscale/integrations/commercial/wizv2/compliance_report.py +1574 -0
  8. regscale/integrations/commercial/wizv2/constants.py +72 -2
  9. regscale/integrations/commercial/wizv2/data_fetcher.py +61 -0
  10. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  11. regscale/integrations/commercial/wizv2/issue.py +775 -27
  12. regscale/integrations/commercial/wizv2/policy_compliance.py +599 -181
  13. regscale/integrations/commercial/wizv2/reports.py +243 -0
  14. regscale/integrations/commercial/wizv2/scanner.py +668 -245
  15. regscale/integrations/compliance_integration.py +534 -56
  16. regscale/integrations/due_date_handler.py +210 -0
  17. regscale/integrations/public/cci_importer.py +444 -0
  18. regscale/integrations/scanner_integration.py +718 -153
  19. regscale/models/integration_models/CCI_List.xml +1 -0
  20. regscale/models/integration_models/cisa_kev_data.json +18 -3
  21. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  22. regscale/models/regscale_models/control_implementation.py +13 -3
  23. regscale/models/regscale_models/form_field_value.py +1 -1
  24. regscale/models/regscale_models/milestone.py +1 -0
  25. regscale/models/regscale_models/regscale_model.py +225 -60
  26. regscale/models/regscale_models/security_plan.py +3 -2
  27. regscale/regscale.py +7 -0
  28. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/METADATA +17 -17
  29. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/RECORD +45 -28
  30. tests/fixtures/test_fixture.py +13 -8
  31. tests/regscale/integrations/public/__init__.py +0 -0
  32. tests/regscale/integrations/public/test_alienvault.py +220 -0
  33. tests/regscale/integrations/public/test_cci.py +458 -0
  34. tests/regscale/integrations/public/test_cisa.py +1021 -0
  35. tests/regscale/integrations/public/test_emass.py +518 -0
  36. tests/regscale/integrations/public/test_fedramp.py +851 -0
  37. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  38. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  39. tests/regscale/integrations/public/test_oscal.py +453 -0
  40. tests/regscale/models/test_form_field_value_integration.py +304 -0
  41. tests/regscale/models/test_module_integration.py +582 -0
  42. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/LICENSE +0 -0
  43. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/WHEEL +0 -0
  44. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/entry_points.txt +0 -0
  45. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/top_level.txt +0 -0
@@ -545,6 +545,76 @@ CREATE_REPORT_QUERY = """
545
545
  }
546
546
  }
547
547
  """
548
+
549
+
550
+ def get_compliance_report_variables(
551
+ project_id: str, run_starts_at: Optional[str] = None, framework_ids: Optional[List[str]] = None
552
+ ) -> dict:
553
+ """Get compliance report variables with dynamic projectId and runStartsAt.
554
+
555
+ :param str project_id: The Wiz project ID
556
+ :param Optional[str] run_starts_at: ISO timestamp for when the report should start, defaults to current time
557
+ :param Optional[List[str]] framework_ids: List of framework IDs to include, defaults to NIST SP 800-53 Rev 5
558
+ :return: Variables for compliance report creation
559
+ :rtype: dict
560
+ """
561
+ from datetime import datetime, timezone
562
+
563
+ if not run_starts_at:
564
+ run_starts_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
565
+
566
+ if not framework_ids:
567
+ # Default to NIST SP 800-53 Revision 5
568
+ framework_ids = ["wf-id-4"]
569
+
570
+ return {
571
+ "input": {
572
+ "name": "Compliance Report",
573
+ "type": "COMPLIANCE_ASSESSMENTS",
574
+ "compressionMethod": "GZIP",
575
+ "runIntervalHours": 168,
576
+ "runStartsAt": run_starts_at,
577
+ "csvDelimiter": "US",
578
+ "projectId": project_id,
579
+ "complianceAssessmentsParams": {
580
+ "securityFrameworkIds": framework_ids,
581
+ },
582
+ "emailTargetParams": None,
583
+ "exportDestinations": None,
584
+ "columnSelection": [
585
+ "Assessed At",
586
+ "Category",
587
+ "Cloud Provider",
588
+ "Cloud Provider ID",
589
+ "Compliance Check Name (Wiz Subcategory)",
590
+ "Created At",
591
+ "Framework",
592
+ "Ignore Reason",
593
+ "Issue/Finding ID",
594
+ "Native Type",
595
+ "Object Type",
596
+ "Policy Description",
597
+ "Policy ID",
598
+ "Policy Name",
599
+ "Policy Short Name",
600
+ "Policy Type",
601
+ "Projects",
602
+ "Remediation Steps",
603
+ "Resource Cloud Platform",
604
+ "Resource Group Name",
605
+ "Resource ID",
606
+ "Resource Name",
607
+ "Resource Region",
608
+ "Result",
609
+ "Severity",
610
+ "Subscription",
611
+ "Subscription Name",
612
+ "Subscription Provider ID",
613
+ ],
614
+ }
615
+ }
616
+
617
+
548
618
  REPORTS_QUERY = """
549
619
  query ReportsTable($filterBy: ReportFilters, $first: Int, $after: String) {
550
620
  reports(first: $first, after: $after, filterBy: $filterBy) {
@@ -1295,8 +1365,8 @@ def get_wiz_vulnerability_queries(project_id: str, filter_by: Optional[dict] = N
1295
1365
  "first": 200,
1296
1366
  "quick": True,
1297
1367
  "filterBy": {
1298
- "rule": {},
1299
- "resource": {"projectId": [project_id]},
1368
+ "resource": {"projectId": project_id},
1369
+ "status": ["OPEN", "IN_PROGRESS"],
1300
1370
  },
1301
1371
  },
1302
1372
  },
@@ -333,6 +333,9 @@ class PolicyAssessmentFetcher:
333
333
  logger.debug("Async client failed, falling back to requests")
334
334
  nodes = self._fetch_with_requests()
335
335
 
336
+ # Clean data (trim whitespace from externalId values)
337
+ nodes = self._clean_node_data(nodes)
338
+
336
339
  # Filter to framework
337
340
  filtered_nodes = self._filter_nodes_to_framework(nodes)
338
341
 
@@ -399,3 +402,61 @@ class PolicyAssessmentFetcher:
399
402
  filtered_nodes.append(node)
400
403
 
401
404
  return filtered_nodes
405
+
406
+ def _clean_node_data(self, nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
407
+ """
408
+ Clean node data by trimming whitespace from externalId values.
409
+
410
+ This fixes issues where Wiz API returns control IDs with trailing/leading
411
+ whitespace (e.g., "AC-14 " instead of "AC-14") which prevents proper matching
412
+ with RegScale control implementations.
413
+
414
+ :param nodes: Raw assessment nodes from Wiz API
415
+ :return: Cleaned nodes with trimmed externalId values
416
+ """
417
+ cleaned_nodes = []
418
+
419
+ for node in nodes:
420
+ try:
421
+ cleaned_node = self._clean_single_node(node)
422
+ cleaned_nodes.append(cleaned_node)
423
+ except Exception:
424
+ # On error, include the original node unchanged (defensive)
425
+ cleaned_nodes.append(node)
426
+
427
+ return cleaned_nodes
428
+
429
+ def _clean_single_node(self, node: Dict[str, Any]) -> Dict[str, Any]:
430
+ """Clean a single node's data."""
431
+ # Deep copy the node to avoid modifying the original
432
+ cleaned_node = dict(node)
433
+
434
+ policy = cleaned_node.get("policy")
435
+ if self._should_clean_policy(policy):
436
+ cleaned_node["policy"] = self._clean_policy_subcategories(policy)
437
+
438
+ return cleaned_node
439
+
440
+ def _should_clean_policy(self, policy: Dict[str, Any]) -> bool:
441
+ """Check if policy should be cleaned."""
442
+ return policy and "securitySubCategories" in policy
443
+
444
+ def _clean_policy_subcategories(self, policy: Dict[str, Any]) -> Dict[str, Any]:
445
+ """Clean security subcategories in policy."""
446
+ subcategories = policy["securitySubCategories"]
447
+ cleaned_subcategories = [self._clean_subcategory(subcat) for subcat in subcategories]
448
+
449
+ cleaned_policy = dict(policy)
450
+ cleaned_policy["securitySubCategories"] = cleaned_subcategories
451
+ return cleaned_policy
452
+
453
+ def _clean_subcategory(self, subcat: Dict[str, Any]) -> Dict[str, Any]:
454
+ """Clean a single subcategory's externalId."""
455
+ cleaned_subcat = dict(subcat)
456
+
457
+ if "externalId" in cleaned_subcat:
458
+ original_id = cleaned_subcat["externalId"]
459
+ cleaned_id = original_id.strip() if isinstance(original_id, str) else original_id
460
+ cleaned_subcat["externalId"] = cleaned_id
461
+
462
+ return cleaned_subcat
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """File cleanup utilities for Wiz integrations."""
4
+
5
+ import logging
6
+ import os
7
+ from typing import List, Tuple
8
+
9
+ logger = logging.getLogger("regscale")
10
+
11
+
12
+ class ReportFileCleanup:
13
+ """Utility class for cleaning up old report files."""
14
+
15
+ @staticmethod
16
+ def cleanup_old_files(directory: str, file_prefix: str, extensions: List[str] = None, keep_count: int = 5) -> None:
17
+ """
18
+ Keep the most recent files matching the pattern, delete older ones.
19
+
20
+ :param str directory: Directory containing files to clean
21
+ :param str file_prefix: File name prefix to match (e.g., 'compliance_report_')
22
+ :param List[str] extensions: List of extensions to clean (e.g., ['.csv', '.json']), defaults to None
23
+ :param int keep_count: Number of most recent files per extension to keep, defaults to 5
24
+ :return: None
25
+ :rtype: None
26
+ """
27
+ if extensions is None:
28
+ extensions = [".csv", ".json", ".jsonl"]
29
+
30
+ try:
31
+ if not os.path.exists(directory):
32
+ return
33
+
34
+ matching_entries = ReportFileCleanup._find_matching_files(directory, file_prefix, extensions)
35
+ files_by_extension = ReportFileCleanup._group_files_by_extension(matching_entries)
36
+ files_deleted = ReportFileCleanup._cleanup_files_by_extension(files_by_extension, keep_count)
37
+
38
+ if files_deleted > 0:
39
+ logger.info(f"Cleaned up {files_deleted} old report files from {directory}")
40
+
41
+ except Exception as e:
42
+ logger.warning(f"Error during file cleanup in {directory}: {e}")
43
+
44
+ @staticmethod
45
+ def _find_matching_files(directory: str, file_prefix: str, extensions: List[str]) -> List[Tuple[str, str, str]]:
46
+ """
47
+ Find all files matching the prefix and extensions.
48
+
49
+ :param str directory: Directory to search for files
50
+ :param str file_prefix: File name prefix to match
51
+ :param List[str] extensions: List of file extensions to match
52
+ :return: List of tuples containing (filename, file_path, extension)
53
+ :rtype: List[Tuple[str, str, str]]
54
+ """
55
+ entries = []
56
+ for filename in os.listdir(directory):
57
+ if filename.startswith(file_prefix):
58
+ for ext in extensions:
59
+ if filename.endswith(ext):
60
+ file_path = os.path.join(directory, filename)
61
+ entries.append((filename, file_path, ext))
62
+ break
63
+ return entries
64
+
65
+ @staticmethod
66
+ def _group_files_by_extension(entries: List[Tuple[str, str, str]]) -> dict:
67
+ """
68
+ Group files by their extensions.
69
+
70
+ :param List[Tuple[str, str, str]] entries: List of file entries (filename, file_path, extension)
71
+ :return: Dictionary mapping extensions to lists of (filename, file_path) tuples
72
+ :rtype: dict
73
+ """
74
+ by_extension = {}
75
+ for filename, file_path, ext in entries:
76
+ if ext not in by_extension:
77
+ by_extension[ext] = []
78
+ by_extension[ext].append((filename, file_path))
79
+ return by_extension
80
+
81
+ @staticmethod
82
+ def _cleanup_files_by_extension(files_by_extension: dict, keep_count: int) -> int:
83
+ """
84
+ Clean up files for each extension, keeping the most recent ones.
85
+
86
+ :param dict files_by_extension: Dictionary mapping extensions to lists of (filename, file_path) tuples
87
+ :param int keep_count: Number of most recent files to keep for each extension
88
+ :return: Total number of files deleted
89
+ :rtype: int
90
+ """
91
+ files_deleted = 0
92
+
93
+ for ext, files in files_by_extension.items():
94
+ files.sort(key=lambda x: os.path.getmtime(x[1]), reverse=True)
95
+
96
+ for filename, file_path in files[keep_count:]:
97
+ try:
98
+ os.remove(file_path)
99
+ files_deleted += 1
100
+ logger.debug(f"Deleted old report file: {filename}")
101
+ except Exception as e:
102
+ logger.warning(f"Failed to delete file {filename}: {e}")
103
+
104
+ return files_deleted