regscale-cli 6.25.1.0__py3-none-any.whl → 6.26.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.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +150 -96
- regscale/models/integration_models/cisa_kev_data.json +154 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Unit tests for Wiz compliance report control ID normalization."""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from unittest.mock import MagicMock
|
|
7
|
+
|
|
8
|
+
from regscale.integrations.commercial.wizv2.compliance_report import WizComplianceReportItem
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestWizControlNormalization:
|
|
12
|
+
"""Test control ID normalization in WizComplianceReportItem."""
|
|
13
|
+
|
|
14
|
+
def test_normalize_base_control(self):
|
|
15
|
+
"""Test normalization of base control IDs with leading zeros."""
|
|
16
|
+
# Create a mock CSV row
|
|
17
|
+
csv_row = {"Compliance Check Name (Wiz Subcategory)": "Test", "Result": "Pass"}
|
|
18
|
+
item = WizComplianceReportItem(csv_row)
|
|
19
|
+
|
|
20
|
+
# Test cases
|
|
21
|
+
assert item._normalize_base_control("AC-01") == "AC-1"
|
|
22
|
+
assert item._normalize_base_control("AC-1") == "AC-1"
|
|
23
|
+
assert item._normalize_base_control("AU-003") == "AU-3"
|
|
24
|
+
assert item._normalize_base_control("SI-004") == "SI-4"
|
|
25
|
+
assert item._normalize_base_control("sc-7") == "SC-7" # lowercase
|
|
26
|
+
assert item._normalize_base_control("PM-10") == "PM-10" # no leading zero
|
|
27
|
+
|
|
28
|
+
def test_format_control_id_with_enhancement_normalization(self):
|
|
29
|
+
"""Test that enhancement numbers are normalized to remove leading zeros."""
|
|
30
|
+
csv_row = {"Compliance Check Name (Wiz Subcategory)": "Test", "Result": "Pass"}
|
|
31
|
+
item = WizComplianceReportItem(csv_row)
|
|
32
|
+
|
|
33
|
+
# Enhancement with leading zeros should be normalized
|
|
34
|
+
assert item._format_control_id("AC-1", "04") == "AC-1(4)"
|
|
35
|
+
assert item._format_control_id("AC-2", "001") == "AC-2(1)"
|
|
36
|
+
assert item._format_control_id("SI-4", "020") == "SI-4(20)"
|
|
37
|
+
|
|
38
|
+
# Enhancement without leading zeros should remain unchanged
|
|
39
|
+
assert item._format_control_id("AC-1", "4") == "AC-1(4)"
|
|
40
|
+
assert item._format_control_id("AC-2", "12") == "AC-2(12)"
|
|
41
|
+
|
|
42
|
+
# No enhancement
|
|
43
|
+
assert item._format_control_id("AC-1", "") == "AC-1"
|
|
44
|
+
assert item._format_control_id("AC-1", None) == "AC-1"
|
|
45
|
+
|
|
46
|
+
def test_get_all_control_ids_single_control(self):
|
|
47
|
+
"""Test extraction of single control ID."""
|
|
48
|
+
csv_row = {"Compliance Check Name (Wiz Subcategory)": "AC-3 Access Enforcement", "Result": "Pass"}
|
|
49
|
+
item = WizComplianceReportItem(csv_row)
|
|
50
|
+
|
|
51
|
+
control_ids = item.get_all_control_ids()
|
|
52
|
+
assert control_ids == ["AC-3"]
|
|
53
|
+
|
|
54
|
+
def test_get_all_control_ids_with_enhancement(self):
|
|
55
|
+
"""Test extraction of control ID with enhancement."""
|
|
56
|
+
csv_row = {
|
|
57
|
+
"Compliance Check Name (Wiz Subcategory)": "AC-2(4) Account Management | Automated Audit Actions",
|
|
58
|
+
"Result": "Pass",
|
|
59
|
+
}
|
|
60
|
+
item = WizComplianceReportItem(csv_row)
|
|
61
|
+
|
|
62
|
+
control_ids = item.get_all_control_ids()
|
|
63
|
+
assert control_ids == ["AC-2(4)"]
|
|
64
|
+
|
|
65
|
+
def test_get_all_control_ids_multiple_controls(self):
|
|
66
|
+
"""Test extraction of multiple control IDs from comma-separated list."""
|
|
67
|
+
csv_row = {
|
|
68
|
+
"Compliance Check Name (Wiz Subcategory)": "AC-2(4) Account Management | Automated Audit Actions, "
|
|
69
|
+
"AC-6(9) Least Privilege | Log Use of Privileged Functions, "
|
|
70
|
+
"AU-12 Audit Record Generation",
|
|
71
|
+
"Result": "Fail",
|
|
72
|
+
}
|
|
73
|
+
item = WizComplianceReportItem(csv_row)
|
|
74
|
+
|
|
75
|
+
control_ids = item.get_all_control_ids()
|
|
76
|
+
assert control_ids == ["AC-2(4)", "AC-6(9)", "AU-12"]
|
|
77
|
+
|
|
78
|
+
def test_get_all_control_ids_with_leading_zeros(self):
|
|
79
|
+
"""Test that control IDs with leading zeros are properly normalized."""
|
|
80
|
+
# Test with leading zeros in base control
|
|
81
|
+
csv_row = {"Compliance Check Name (Wiz Subcategory)": "AC-01 Access Control", "Result": "Pass"}
|
|
82
|
+
item = WizComplianceReportItem(csv_row)
|
|
83
|
+
assert item.get_all_control_ids() == ["AC-1"]
|
|
84
|
+
|
|
85
|
+
# Test with leading zeros in enhancement
|
|
86
|
+
csv_row = {"Compliance Check Name (Wiz Subcategory)": "AC-01(04) Access Control Enhancement", "Result": "Pass"}
|
|
87
|
+
item = WizComplianceReportItem(csv_row)
|
|
88
|
+
assert item.get_all_control_ids() == ["AC-1(4)"]
|
|
89
|
+
|
|
90
|
+
# Test multiple controls with various leading zero patterns
|
|
91
|
+
csv_row = {
|
|
92
|
+
"Compliance Check Name (Wiz Subcategory)": "AC-01(04) Access Control, AU-3(1) Audit Content, SI-04(020) Monitoring",
|
|
93
|
+
"Result": "Pass",
|
|
94
|
+
}
|
|
95
|
+
item = WizComplianceReportItem(csv_row)
|
|
96
|
+
assert item.get_all_control_ids() == ["AC-1(4)", "AU-3(1)", "SI-4(20)"]
|
|
97
|
+
|
|
98
|
+
def test_get_control_id_returns_first(self):
|
|
99
|
+
"""Test that get_control_id returns only the first control ID."""
|
|
100
|
+
csv_row = {
|
|
101
|
+
"Compliance Check Name (Wiz Subcategory)": "AC-2(4) Account Management, AC-6(9) Least Privilege, AU-12 Audit",
|
|
102
|
+
"Result": "Pass",
|
|
103
|
+
}
|
|
104
|
+
item = WizComplianceReportItem(csv_row)
|
|
105
|
+
|
|
106
|
+
# get_control_id should return only the first
|
|
107
|
+
assert item.get_control_id() == "AC-2(4)"
|
|
108
|
+
|
|
109
|
+
# get_all_control_ids should return all
|
|
110
|
+
assert item.get_all_control_ids() == ["AC-2(4)", "AC-6(9)", "AU-12"]
|
|
111
|
+
|
|
112
|
+
def test_affected_controls_property(self):
|
|
113
|
+
"""Test that affected_controls property returns comma-separated list."""
|
|
114
|
+
csv_row = {
|
|
115
|
+
"Compliance Check Name (Wiz Subcategory)": "AC-2(4) Account Management, AC-6(9) Least Privilege",
|
|
116
|
+
"Result": "Fail",
|
|
117
|
+
}
|
|
118
|
+
item = WizComplianceReportItem(csv_row)
|
|
119
|
+
|
|
120
|
+
# affected_controls should return all control IDs as comma-separated string
|
|
121
|
+
assert item.affected_controls == "AC-2(4),AC-6(9)"
|
|
122
|
+
|
|
123
|
+
def test_empty_compliance_check_name(self):
|
|
124
|
+
"""Test handling of empty compliance check name."""
|
|
125
|
+
csv_row = {"Compliance Check Name (Wiz Subcategory)": "", "Result": "Pass"}
|
|
126
|
+
item = WizComplianceReportItem(csv_row)
|
|
127
|
+
|
|
128
|
+
assert item.get_control_id() == ""
|
|
129
|
+
assert item.get_all_control_ids() == []
|
|
130
|
+
assert item.affected_controls == ""
|
|
131
|
+
|
|
132
|
+
def test_control_id_property(self):
|
|
133
|
+
"""Test the control_id property returns normalized first control."""
|
|
134
|
+
csv_row = {"Compliance Check Name (Wiz Subcategory)": "AC-01(04) Access Control, AU-3 Audit", "Result": "Pass"}
|
|
135
|
+
item = WizComplianceReportItem(csv_row)
|
|
136
|
+
|
|
137
|
+
# control_id property should return the first normalized control
|
|
138
|
+
assert item.control_id == "AC-1(4)"
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
from regscale.integrations.commercial.wizv2.issue import WizIssue
|
|
4
|
+
from regscale.integrations.commercial.wizv2.constants import WizVulnerabilityType
|
|
5
|
+
from regscale.models import IssueSeverity, IssueStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@patch("regscale.integrations.scanner_integration.ScannerIntegration.get_assessor_id")
|
|
9
|
+
def test_parse_finding_control_labels(mock_get_assessor):
|
|
10
|
+
"""Test that WizIssue correctly parses control labels from a Wiz issue."""
|
|
11
|
+
# Mock the assessor ID
|
|
12
|
+
mock_get_assessor.return_value = "test-assessor"
|
|
13
|
+
|
|
14
|
+
# Create test instance with a test plan ID
|
|
15
|
+
wiz_issue = WizIssue(plan_id=1)
|
|
16
|
+
|
|
17
|
+
# Sample Wiz issue data with security subcategories
|
|
18
|
+
test_issue = {
|
|
19
|
+
"id": "66013c7a-de84-4b46-a3c7-934521cb9e3b",
|
|
20
|
+
"sourceRule": {
|
|
21
|
+
"__typename": "Control",
|
|
22
|
+
"id": "wc-id-15",
|
|
23
|
+
"name": "Publicly exposed PaaS database server",
|
|
24
|
+
"controlDescription": "This database is exposed to the public internet.",
|
|
25
|
+
"resolutionRecommendation": "Limit external exposure",
|
|
26
|
+
"securitySubCategories": [
|
|
27
|
+
{
|
|
28
|
+
"title": "AC-4(21) Information Flow Enforcement | Physical or Logical Separation of Information Flows",
|
|
29
|
+
"externalId": "AC-4(21)",
|
|
30
|
+
"category": {"name": "AC Access Control", "framework": {"name": "NIST SP 800-53 Revision 5"}},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"title": "SC-7 Boundary Protection",
|
|
34
|
+
"externalId": "SC-7",
|
|
35
|
+
"category": {
|
|
36
|
+
"name": "SC System And Communications Protection",
|
|
37
|
+
"framework": {"name": "NIST SP 800-53 Revision 5"},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"title": "AC-3 Access Enforcement",
|
|
42
|
+
"externalId": "AC-3",
|
|
43
|
+
"category": {"name": "AC Access Control", "framework": {"name": "NIST SP 800-53 Revision 5"}},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
"severity": "HIGH",
|
|
48
|
+
"status": "OPEN",
|
|
49
|
+
"createdAt": "2024-02-21T08:22:22.696689Z",
|
|
50
|
+
"entitySnapshot": {
|
|
51
|
+
"id": "474c2882-b98a-5b2b-b2f5-40f6cbdbf04f",
|
|
52
|
+
"type": "DB_SERVER",
|
|
53
|
+
"name": "releasetest-sqlserver",
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Parse the finding
|
|
58
|
+
finding = wiz_issue.parse_finding(test_issue, WizVulnerabilityType.ISSUE)
|
|
59
|
+
|
|
60
|
+
# Expected control labels based on NIST SP 800-53 controls
|
|
61
|
+
expected_control_labels = ["ac-4.21", "sc-7", "ac-3"]
|
|
62
|
+
|
|
63
|
+
# Verify the finding attributes
|
|
64
|
+
assert finding is not None
|
|
65
|
+
assert finding.control_labels == expected_control_labels
|
|
66
|
+
assert finding.severity == IssueSeverity.High
|
|
67
|
+
assert finding.title == "Publicly exposed PaaS database server"
|
|
68
|
+
assert finding.asset_identifier == "474c2882-b98a-5b2b-b2f5-40f6cbdbf04f"
|
|
69
|
+
assert finding.external_id == "66013c7a-de84-4b46-a3c7-934521cb9e3b"
|
|
70
|
+
assert finding.plugin_name == "Wiz-Control-AC"
|
|
71
|
+
assert finding.source_rule_id == "Control-wc-id-15"
|
|
72
|
+
assert finding.vulnerability_type == WizVulnerabilityType.ISSUE.value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@patch("regscale.integrations.scanner_integration.ScannerIntegration.get_assessor_id")
|
|
76
|
+
def test_parse_finding_no_security_subcategories(mock_get_assessor):
|
|
77
|
+
"""Test that WizIssue handles issues without security subcategories."""
|
|
78
|
+
# Mock the assessor ID
|
|
79
|
+
mock_get_assessor.return_value = "test-assessor"
|
|
80
|
+
|
|
81
|
+
wiz_issue = WizIssue(plan_id=1)
|
|
82
|
+
|
|
83
|
+
test_issue = {
|
|
84
|
+
"id": "test-id",
|
|
85
|
+
"sourceRule": {
|
|
86
|
+
"__typename": "Control",
|
|
87
|
+
"id": "wc-id-1",
|
|
88
|
+
"name": "Test security configuration",
|
|
89
|
+
"controlDescription": "Test description",
|
|
90
|
+
"resolutionRecommendation": "Test recommendation",
|
|
91
|
+
"securitySubCategories": [],
|
|
92
|
+
},
|
|
93
|
+
"severity": "MEDIUM",
|
|
94
|
+
"status": "OPEN",
|
|
95
|
+
"createdAt": "2024-02-21T08:22:22Z",
|
|
96
|
+
"entitySnapshot": {"id": "test-entity-id", "type": "DB_SERVER", "name": "test-server"},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
finding = wiz_issue.parse_finding(test_issue, WizVulnerabilityType.VULNERABILITY)
|
|
100
|
+
|
|
101
|
+
assert finding is not None
|
|
102
|
+
assert finding.control_labels == []
|
|
103
|
+
assert finding.severity == IssueSeverity.Moderate
|
|
104
|
+
assert finding.title == "Test security configuration"
|
|
105
|
+
assert finding.plugin_name == "Wiz-Control-Test"
|
|
106
|
+
assert finding.source_rule_id == "Control-wc-id-1"
|
|
107
|
+
assert finding.vulnerability_type == WizVulnerabilityType.VULNERABILITY.value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@patch("regscale.integrations.scanner_integration.ScannerIntegration.get_assessor_id")
|
|
111
|
+
def test_parse_finding_non_nist_controls(mock_get_assessor):
|
|
112
|
+
"""Test that WizIssue correctly handles non-NIST controls."""
|
|
113
|
+
# Mock the assessor ID
|
|
114
|
+
mock_get_assessor.return_value = "test-assessor"
|
|
115
|
+
|
|
116
|
+
wiz_issue = WizIssue(plan_id=1)
|
|
117
|
+
|
|
118
|
+
test_issue = {
|
|
119
|
+
"id": "test-id",
|
|
120
|
+
"sourceRule": {
|
|
121
|
+
"__typename": "Control",
|
|
122
|
+
"id": "wc-id-1",
|
|
123
|
+
"name": "Database exposed to internet",
|
|
124
|
+
"controlDescription": "Test description",
|
|
125
|
+
"resolutionRecommendation": "Test recommendation",
|
|
126
|
+
"securitySubCategories": [
|
|
127
|
+
{
|
|
128
|
+
"title": "8.12 Data leakage prevention",
|
|
129
|
+
"externalId": "8.12",
|
|
130
|
+
"category": {"name": "Technological controls", "framework": {"name": "ISO/IEC 27001-2022"}},
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
"severity": "LOW",
|
|
135
|
+
"status": "OPEN",
|
|
136
|
+
"createdAt": "2024-02-21T08:22:22Z",
|
|
137
|
+
"entitySnapshot": {"id": "test-entity-id", "type": "DB_SERVER", "name": "test-server"},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
finding = wiz_issue.parse_finding(test_issue, WizVulnerabilityType.HOST_FINDING)
|
|
141
|
+
|
|
142
|
+
assert finding is not None
|
|
143
|
+
assert finding.control_labels == []
|
|
144
|
+
assert finding.severity == IssueSeverity.Low
|
|
145
|
+
assert finding.title == "Database exposed to internet"
|
|
146
|
+
assert finding.plugin_name == "Wiz-Control-Database"
|
|
147
|
+
assert finding.source_rule_id == "Control-wc-id-1"
|
|
148
|
+
assert finding.vulnerability_type == WizVulnerabilityType.HOST_FINDING.value
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@patch("regscale.integrations.scanner_integration.ScannerIntegration.get_assessor_id")
|
|
152
|
+
def test_parse_finding_cloud_config_rule(mock_get_assessor):
|
|
153
|
+
"""Test that WizIssue correctly parses a cloud configuration rule."""
|
|
154
|
+
# Mock the assessor ID
|
|
155
|
+
mock_get_assessor.return_value = "test-assessor"
|
|
156
|
+
|
|
157
|
+
wiz_issue = WizIssue(plan_id=1)
|
|
158
|
+
|
|
159
|
+
test_issue = {
|
|
160
|
+
"id": "test-id",
|
|
161
|
+
"sourceRule": {
|
|
162
|
+
"__typename": "CloudConfigurationRule",
|
|
163
|
+
"id": "ffcade8d-7961-4b71-93d3-0098d7e4b3e1",
|
|
164
|
+
"name": "App Configuration public network access should be disabled",
|
|
165
|
+
"cloudConfigurationRuleDescription": "Test description",
|
|
166
|
+
"remediationInstructions": "Test remediation",
|
|
167
|
+
"serviceType": "Azure",
|
|
168
|
+
},
|
|
169
|
+
"severity": "HIGH",
|
|
170
|
+
"status": "OPEN",
|
|
171
|
+
"createdAt": "2024-02-21T08:22:22Z",
|
|
172
|
+
"entitySnapshot": {"id": "test-entity-id", "type": "SERVICE_CONFIGURATION", "name": "test-config"},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
finding = wiz_issue.parse_finding(test_issue, WizVulnerabilityType.CONFIGURATION)
|
|
176
|
+
|
|
177
|
+
assert finding is not None
|
|
178
|
+
assert finding.plugin_name == "Wiz-Azure-AppConfiguration"
|
|
179
|
+
assert finding.severity == IssueSeverity.High
|
|
180
|
+
assert finding.title == "App Configuration public network access should be disabled"
|
|
181
|
+
assert finding.source_rule_id == "CloudConfigurationRule-Azure-ffcade8d-7961-4b71-93d3-0098d7e4b3e1"
|
|
182
|
+
assert finding.vulnerability_type == WizVulnerabilityType.CONFIGURATION.value
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@patch("regscale.integrations.scanner_integration.ScannerIntegration.get_assessor_id")
|
|
186
|
+
def test_parse_finding_cloud_event(mock_get_assessor):
|
|
187
|
+
"""Test that WizIssue correctly parses a cloud event rule."""
|
|
188
|
+
# Mock the assessor ID
|
|
189
|
+
mock_get_assessor.return_value = "test-assessor"
|
|
190
|
+
|
|
191
|
+
wiz_issue = WizIssue(plan_id=1)
|
|
192
|
+
|
|
193
|
+
test_issue = {
|
|
194
|
+
"id": "test-id",
|
|
195
|
+
"sourceRule": {
|
|
196
|
+
"__typename": "CloudEventRule",
|
|
197
|
+
"id": "event-1",
|
|
198
|
+
"name": "Suspicious activity detection in cloud resources",
|
|
199
|
+
"cloudEventRuleDescription": "Test description",
|
|
200
|
+
"sourceType": "test",
|
|
201
|
+
"type": "test",
|
|
202
|
+
"serviceType": "AWS",
|
|
203
|
+
},
|
|
204
|
+
"severity": "LOW",
|
|
205
|
+
"status": "OPEN",
|
|
206
|
+
"createdAt": "2024-02-21T08:22:22Z",
|
|
207
|
+
"entitySnapshot": {"id": "test-entity-id", "type": "CLOUD_ORGANIZATION", "name": "test-org"},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
finding = wiz_issue.parse_finding(test_issue, WizVulnerabilityType.DATA_FINDING)
|
|
211
|
+
|
|
212
|
+
assert finding is not None
|
|
213
|
+
assert finding.plugin_name == "Wiz-AWS-SuspiciousActivity"
|
|
214
|
+
assert finding.severity == IssueSeverity.Low
|
|
215
|
+
assert finding.title == "Suspicious activity detection in cloud resources"
|
|
216
|
+
assert finding.source_rule_id == "CloudEventRule-AWS-event-1"
|
|
217
|
+
assert finding.vulnerability_type == WizVulnerabilityType.DATA_FINDING.value
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@patch("regscale.integrations.scanner_integration.ScannerIntegration.get_assessor_id")
|
|
221
|
+
def test_title_based_consolidation(mock_get_assessor):
|
|
222
|
+
"""Test that issues with same title are consolidated regardless of asset."""
|
|
223
|
+
# Mock the assessor ID
|
|
224
|
+
mock_get_assessor.return_value = "test-assessor"
|
|
225
|
+
|
|
226
|
+
wiz_issue = WizIssue(plan_id=1)
|
|
227
|
+
|
|
228
|
+
# Create two issues with same title but different assets
|
|
229
|
+
issue1 = {
|
|
230
|
+
"id": "issue-576",
|
|
231
|
+
"sourceRule": {
|
|
232
|
+
"__typename": "CloudConfigurationRule",
|
|
233
|
+
"id": "rule-app-config",
|
|
234
|
+
"name": "App Configuration public network access should be disabled",
|
|
235
|
+
"serviceType": "Azure",
|
|
236
|
+
"resolutionRecommendation": "Disable public network access",
|
|
237
|
+
},
|
|
238
|
+
"severity": "HIGH",
|
|
239
|
+
"status": "OPEN",
|
|
240
|
+
"createdAt": "2024-01-15T10:00:00Z",
|
|
241
|
+
"entitySnapshot": {
|
|
242
|
+
"id": "asset-001",
|
|
243
|
+
"providerId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.AppConfiguration/configurationStores/store1",
|
|
244
|
+
"type": "SERVICE_CONFIGURATION",
|
|
245
|
+
"name": "store1",
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
issue2 = {
|
|
250
|
+
"id": "issue-663",
|
|
251
|
+
"sourceRule": {
|
|
252
|
+
"__typename": "CloudConfigurationRule",
|
|
253
|
+
"id": "rule-app-config",
|
|
254
|
+
"name": "App Configuration public network access should be disabled",
|
|
255
|
+
"serviceType": "Azure",
|
|
256
|
+
"resolutionRecommendation": "Disable public network access",
|
|
257
|
+
},
|
|
258
|
+
"severity": "MEDIUM",
|
|
259
|
+
"status": "RESOLVED",
|
|
260
|
+
"createdAt": "2024-01-20T10:00:00Z",
|
|
261
|
+
"entitySnapshot": {
|
|
262
|
+
"id": "asset-002",
|
|
263
|
+
"providerId": "/subscriptions/sub1/resourceGroups/rg2/providers/Microsoft.AppConfiguration/configurationStores/store2",
|
|
264
|
+
"type": "SERVICE_CONFIGURATION",
|
|
265
|
+
"name": "store2",
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Test grouping - should group by title only
|
|
270
|
+
groups = wiz_issue._group_issues_for_consolidation([issue1, issue2])
|
|
271
|
+
|
|
272
|
+
# Should have only 1 group with the same title
|
|
273
|
+
assert len(groups) == 1, f"Expected 1 group, got {len(groups)}"
|
|
274
|
+
|
|
275
|
+
group_key = "App Configuration public network access should be disabled"
|
|
276
|
+
assert group_key in groups, f"Expected to find '{group_key}' in groups"
|
|
277
|
+
assert len(groups[group_key]) == 2, f"Expected 2 issues in group, got {len(groups[group_key])}"
|
|
278
|
+
|
|
279
|
+
# Test consolidation
|
|
280
|
+
consolidated = wiz_issue._create_consolidated_finding(groups[group_key], WizVulnerabilityType.CONFIGURATION)
|
|
281
|
+
|
|
282
|
+
# Verify consolidation properties
|
|
283
|
+
assert consolidated.title == "App Configuration public network access should be disabled"
|
|
284
|
+
assert consolidated.severity == IssueSeverity.High # Should use highest severity
|
|
285
|
+
assert consolidated.status == IssueStatus.Open # Should use most urgent status
|
|
286
|
+
assert consolidated.asset_identifier == "asset-001" # Primary asset
|
|
287
|
+
|
|
288
|
+
# Should have both provider IDs
|
|
289
|
+
provider_ids = consolidated.issue_asset_identifier_value.split("\n")
|
|
290
|
+
assert len(provider_ids) == 2, f"Expected 2 provider IDs, got {len(provider_ids)}"
|
|
291
|
+
assert "/configurationStores/store1" in provider_ids[0]
|
|
292
|
+
assert "/configurationStores/store2" in provider_ids[1]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@patch("regscale.integrations.scanner_integration.ScannerIntegration.get_assessor_id")
|
|
296
|
+
def test_consolidation_priority_rules(mock_get_assessor):
|
|
297
|
+
"""Test that consolidation correctly applies priority rules for severity and status."""
|
|
298
|
+
mock_get_assessor.return_value = "test-assessor"
|
|
299
|
+
|
|
300
|
+
wiz_issue = WizIssue(plan_id=1)
|
|
301
|
+
|
|
302
|
+
# Create issues with different severities and statuses
|
|
303
|
+
issues = [
|
|
304
|
+
{
|
|
305
|
+
"id": "issue-1",
|
|
306
|
+
"sourceRule": {"name": "Test Rule", "__typename": "Control"},
|
|
307
|
+
"severity": "LOW",
|
|
308
|
+
"status": "RESOLVED",
|
|
309
|
+
"createdAt": "2024-01-20T10:00:00Z",
|
|
310
|
+
"entitySnapshot": {"id": "asset-1", "providerId": "provider-1"},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"id": "issue-2",
|
|
314
|
+
"sourceRule": {"name": "Test Rule", "__typename": "Control"},
|
|
315
|
+
"severity": "CRITICAL", # Highest severity
|
|
316
|
+
"status": "RESOLVED",
|
|
317
|
+
"createdAt": "2024-01-15T10:00:00Z", # Earlier date
|
|
318
|
+
"entitySnapshot": {"id": "asset-2", "providerId": "provider-2"},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"id": "issue-3",
|
|
322
|
+
"sourceRule": {"name": "Test Rule", "__typename": "Control"},
|
|
323
|
+
"severity": "MEDIUM",
|
|
324
|
+
"status": "OPEN", # Most urgent status
|
|
325
|
+
"createdAt": "2024-01-18T10:00:00Z",
|
|
326
|
+
"entitySnapshot": {"id": "asset-3", "providerId": "provider-3"},
|
|
327
|
+
},
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
consolidated = wiz_issue._create_consolidated_finding(issues, WizVulnerabilityType.ISSUE)
|
|
331
|
+
|
|
332
|
+
# Should use CRITICAL severity (highest)
|
|
333
|
+
assert consolidated.severity == IssueSeverity.Critical
|
|
334
|
+
|
|
335
|
+
# Should use OPEN status (most urgent)
|
|
336
|
+
assert consolidated.status == IssueStatus.Open
|
|
337
|
+
|
|
338
|
+
# Should use earliest date
|
|
339
|
+
assert "2024-01-15" in consolidated.date_created
|
|
340
|
+
|
|
341
|
+
# Should have all 3 provider IDs
|
|
342
|
+
provider_ids = consolidated.issue_asset_identifier_value.split("\n")
|
|
343
|
+
assert len(provider_ids) == 3
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Tests for Wiz Click command client_id parameter handling."""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from click.testing import CliRunner
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
from regscale.integrations.commercial.wizv2.click import wiz
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestWizClientIdHandling:
|
|
12
|
+
"""Test that client_id parameters are properly recognized when passed via CLI."""
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def runner(self):
|
|
16
|
+
"""Create a Click test runner."""
|
|
17
|
+
return CliRunner()
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def mock_wiz_variables(self):
|
|
21
|
+
"""Mock WizVariables to avoid environment dependencies."""
|
|
22
|
+
with patch("regscale.integrations.commercial.wizv2.click.WizVariables") as mock:
|
|
23
|
+
mock.wizClientId = "env_client_id"
|
|
24
|
+
mock.wizClientSecret = "env_client_secret"
|
|
25
|
+
mock.wizInventoryFilterBy = "{}"
|
|
26
|
+
mock.wizIssueFilterBy = "{}"
|
|
27
|
+
yield mock
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_scanner(self):
|
|
31
|
+
"""Mock the WizVulnerabilityIntegration scanner."""
|
|
32
|
+
with patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration") as mock:
|
|
33
|
+
instance = MagicMock()
|
|
34
|
+
mock.return_value = instance
|
|
35
|
+
yield instance
|
|
36
|
+
|
|
37
|
+
def test_inventory_cli_client_id_overrides_env(self, runner, mock_wiz_variables, mock_scanner):
|
|
38
|
+
"""Test that CLI-provided client_id overrides environment variable."""
|
|
39
|
+
result = runner.invoke(
|
|
40
|
+
wiz,
|
|
41
|
+
[
|
|
42
|
+
"inventory",
|
|
43
|
+
"--wiz_project_id",
|
|
44
|
+
"test-project",
|
|
45
|
+
"--regscale_ssp_id",
|
|
46
|
+
"2288",
|
|
47
|
+
"--client_id",
|
|
48
|
+
"cli_provided_client_id",
|
|
49
|
+
"--client_secret",
|
|
50
|
+
"cli_provided_secret",
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Check that the command ran
|
|
55
|
+
assert result.exit_code == 0 or "successfully" in result.output.lower()
|
|
56
|
+
|
|
57
|
+
# Verify that sync_assets was called with CLI-provided credentials
|
|
58
|
+
mock_scanner.sync_assets.assert_called_once()
|
|
59
|
+
call_kwargs = mock_scanner.sync_assets.call_args[1]
|
|
60
|
+
assert call_kwargs["client_id"] == "cli_provided_client_id"
|
|
61
|
+
assert call_kwargs["client_secret"] == "cli_provided_secret"
|
|
62
|
+
|
|
63
|
+
def test_inventory_uses_env_when_no_cli_args(self, runner, mock_wiz_variables, mock_scanner):
|
|
64
|
+
"""Test that environment variables are used when CLI args not provided."""
|
|
65
|
+
result = runner.invoke(
|
|
66
|
+
wiz,
|
|
67
|
+
[
|
|
68
|
+
"inventory",
|
|
69
|
+
"--wiz_project_id",
|
|
70
|
+
"test-project",
|
|
71
|
+
"--regscale_ssp_id",
|
|
72
|
+
"2288",
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Check that the command ran
|
|
77
|
+
assert result.exit_code == 0 or "successfully" in result.output.lower()
|
|
78
|
+
|
|
79
|
+
# Verify that sync_assets was called with environment credentials
|
|
80
|
+
mock_scanner.sync_assets.assert_called_once()
|
|
81
|
+
call_kwargs = mock_scanner.sync_assets.call_args[1]
|
|
82
|
+
assert call_kwargs["client_id"] == "env_client_id"
|
|
83
|
+
assert call_kwargs["client_secret"] == "env_client_secret"
|
|
84
|
+
|
|
85
|
+
@pytest.fixture
|
|
86
|
+
def mock_wiz_issue(self):
|
|
87
|
+
"""Mock the WizIssue scanner."""
|
|
88
|
+
with patch("regscale.integrations.commercial.wizv2.issue.WizIssue") as mock:
|
|
89
|
+
instance = MagicMock()
|
|
90
|
+
mock.return_value = instance
|
|
91
|
+
yield instance
|
|
92
|
+
|
|
93
|
+
@pytest.fixture
|
|
94
|
+
def mock_wiz_auth(self):
|
|
95
|
+
"""Mock wiz_authenticate."""
|
|
96
|
+
with patch("regscale.integrations.commercial.wizv2.wiz_auth.wiz_authenticate") as mock:
|
|
97
|
+
yield mock
|
|
98
|
+
|
|
99
|
+
@pytest.fixture
|
|
100
|
+
def mock_check_license(self):
|
|
101
|
+
"""Mock check_license."""
|
|
102
|
+
with patch("regscale.core.app.utils.app_utils.check_license") as mock:
|
|
103
|
+
yield mock
|
|
104
|
+
|
|
105
|
+
def test_issues_cli_client_id_overrides_env(
|
|
106
|
+
self,
|
|
107
|
+
runner,
|
|
108
|
+
mock_wiz_variables,
|
|
109
|
+
mock_wiz_issue,
|
|
110
|
+
mock_wiz_auth,
|
|
111
|
+
mock_check_license,
|
|
112
|
+
):
|
|
113
|
+
"""Test that CLI-provided client_id overrides environment variable for issues command."""
|
|
114
|
+
result = runner.invoke(
|
|
115
|
+
wiz,
|
|
116
|
+
[
|
|
117
|
+
"issues",
|
|
118
|
+
"--wiz_project_id",
|
|
119
|
+
"test-project",
|
|
120
|
+
"--regscale_ssp_id",
|
|
121
|
+
"2288",
|
|
122
|
+
"--client_id",
|
|
123
|
+
"cli_client_id",
|
|
124
|
+
"--client_secret",
|
|
125
|
+
"cli_secret",
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Check that the command ran
|
|
130
|
+
assert result.exit_code == 0 or "successfully" in result.output.lower()
|
|
131
|
+
|
|
132
|
+
# Verify that wiz_authenticate was called with CLI credentials
|
|
133
|
+
mock_wiz_auth.assert_called_once_with("cli_client_id", "cli_secret")
|
|
134
|
+
|
|
135
|
+
# Verify that sync_findings was called with CLI credentials
|
|
136
|
+
mock_wiz_issue.sync_findings.assert_called_once()
|
|
137
|
+
call_kwargs = mock_wiz_issue.sync_findings.call_args[1]
|
|
138
|
+
assert call_kwargs["client_id"] == "cli_client_id"
|
|
139
|
+
assert call_kwargs["client_secret"] == "cli_secret"
|
|
140
|
+
|
|
141
|
+
def test_vulnerabilities_cli_client_id_overrides_env(self, runner, mock_wiz_variables, mock_scanner):
|
|
142
|
+
"""Test that CLI-provided client_id overrides environment variable for vulnerabilities command."""
|
|
143
|
+
result = runner.invoke(
|
|
144
|
+
wiz,
|
|
145
|
+
[
|
|
146
|
+
"vulnerabilities",
|
|
147
|
+
"--wiz_project_id",
|
|
148
|
+
"test-project",
|
|
149
|
+
"--regscale_ssp_id",
|
|
150
|
+
"2288",
|
|
151
|
+
"--client_id",
|
|
152
|
+
"cli_vuln_client_id",
|
|
153
|
+
"--client_secret",
|
|
154
|
+
"cli_vuln_secret",
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Check that the command ran
|
|
159
|
+
assert result.exit_code == 0 or "successfully" in result.output.lower()
|
|
160
|
+
|
|
161
|
+
# Verify that sync_findings was called with CLI-provided credentials
|
|
162
|
+
mock_scanner.sync_findings.assert_called_once()
|
|
163
|
+
call_kwargs = mock_scanner.sync_findings.call_args[1]
|
|
164
|
+
assert call_kwargs["client_id"] == "cli_vuln_client_id"
|
|
165
|
+
assert call_kwargs["client_secret"] == "cli_vuln_secret"
|