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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +2 -0
- regscale/integrations/commercial/__init__.py +1 -0
- regscale/integrations/commercial/jira.py +95 -22
- regscale/integrations/commercial/sarif/sarif_converter.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +132 -2
- regscale/integrations/commercial/wizv2/compliance_report.py +1574 -0
- regscale/integrations/commercial/wizv2/constants.py +72 -2
- regscale/integrations/commercial/wizv2/data_fetcher.py +61 -0
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +775 -27
- regscale/integrations/commercial/wizv2/policy_compliance.py +599 -181
- regscale/integrations/commercial/wizv2/reports.py +243 -0
- regscale/integrations/commercial/wizv2/scanner.py +668 -245
- regscale/integrations/compliance_integration.py +534 -56
- regscale/integrations/due_date_handler.py +210 -0
- regscale/integrations/public/cci_importer.py +444 -0
- regscale/integrations/scanner_integration.py +718 -153
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/cisa_kev_data.json +18 -3
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/control_implementation.py +13 -3
- regscale/models/regscale_models/form_field_value.py +1 -1
- regscale/models/regscale_models/milestone.py +1 -0
- regscale/models/regscale_models/regscale_model.py +225 -60
- regscale/models/regscale_models/security_plan.py +3 -2
- regscale/regscale.py +7 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/METADATA +17 -17
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/RECORD +45 -28
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/integrations/public/__init__.py +0 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +458 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +851 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_module_integration.py +582 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/entry_points.txt +0 -0
- {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
|
-
"
|
|
1299
|
-
"
|
|
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
|