regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.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 +19 -4
- regscale/core/app/internal/evidence.py +419 -2
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/jira.py +367 -126
- regscale/integrations/commercial/qualys/__init__.py +7 -8
- regscale/integrations/commercial/qualys/scanner.py +8 -3
- 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/synqly/assets.py +17 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +142 -1
- regscale/integrations/commercial/tenablev2/scanner.py +0 -1
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +64 -79
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
- regscale/integrations/commercial/wizv2/issue.py +1 -1
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +1 -1
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +39 -99
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +60 -41
- regscale/integrations/control_matcher.py +377 -0
- regscale/integrations/due_date_handler.py +14 -8
- 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/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +277 -153
- regscale/models/integration_models/cisa_kev_data.json +282 -9
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
- regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +47 -22
- regscale/models/regscale_models/issue.py +256 -95
- 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.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
- 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 +2204 -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 +1365 -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/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -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_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -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/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +1397 -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/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Comprehensive test suite for Wiz inventory integration with 100% coverage.
|
|
5
|
+
|
|
6
|
+
Tests cover:
|
|
7
|
+
- Generator function behavior for fetch_assets
|
|
8
|
+
- Asset parsing with all edge cases
|
|
9
|
+
- Progress tracking
|
|
10
|
+
- Authentication flow
|
|
11
|
+
- Filter construction
|
|
12
|
+
- Error handling
|
|
13
|
+
- Memory efficiency of generators
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import pytest
|
|
19
|
+
from typing import Any, Dict, List
|
|
20
|
+
from unittest.mock import MagicMock, Mock, patch, call
|
|
21
|
+
|
|
22
|
+
from regscale.integrations.commercial.wizv2.scanner import WizVulnerabilityIntegration
|
|
23
|
+
from regscale.integrations.scanner_integration import IntegrationAsset
|
|
24
|
+
from regscale.models.regscale_models import AssetCategory
|
|
25
|
+
from tests.fixtures.test_fixture import CLITestFixture
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestWizInventoryGenerators(CLITestFixture):
|
|
29
|
+
"""Test suite focusing on generator behavior for memory efficiency."""
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_scanner(self):
|
|
33
|
+
"""Create a mock scanner instance with mocked dependencies."""
|
|
34
|
+
# Mock the control implementation map to avoid API calls
|
|
35
|
+
with patch.object(WizVulnerabilityIntegration, "__init__", lambda self, *args, **kwargs: None):
|
|
36
|
+
scanner = WizVulnerabilityIntegration.__new__(WizVulnerabilityIntegration)
|
|
37
|
+
scanner.plan_id = 123
|
|
38
|
+
scanner.wiz_token = "mock_token"
|
|
39
|
+
scanner.num_assets_to_process = 0
|
|
40
|
+
scanner.num_findings_to_process = 0
|
|
41
|
+
# Mock progress trackers
|
|
42
|
+
scanner.asset_progress = MagicMock()
|
|
43
|
+
scanner.finding_progress = MagicMock()
|
|
44
|
+
return scanner
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def sample_wiz_nodes(self) -> List[Dict[str, Any]]:
|
|
48
|
+
"""Sample Wiz inventory nodes for testing."""
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
"id": "asset-1",
|
|
52
|
+
"name": "test-vm-1",
|
|
53
|
+
"type": "VIRTUAL_MACHINE",
|
|
54
|
+
"subscriptionId": "sub-123",
|
|
55
|
+
"subscriptionExternalId": "azure-sub-123",
|
|
56
|
+
"graphEntity": {
|
|
57
|
+
"id": "entity-1",
|
|
58
|
+
"providerUniqueId": "/subscriptions/123/vm-1",
|
|
59
|
+
"name": "test-vm-1",
|
|
60
|
+
"type": "VIRTUAL_MACHINE",
|
|
61
|
+
"projects": [{"id": "project-123"}],
|
|
62
|
+
"properties": {
|
|
63
|
+
"cloudPlatform": "Azure",
|
|
64
|
+
"region": "eastus",
|
|
65
|
+
"cpe": "cpe:/o:microsoft:windows_server:2019",
|
|
66
|
+
"publicExposure": "None",
|
|
67
|
+
},
|
|
68
|
+
"publicExposures": {"totalCount": 0},
|
|
69
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
70
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "asset-2",
|
|
75
|
+
"name": "test-container-1",
|
|
76
|
+
"type": "CONTAINER_IMAGE",
|
|
77
|
+
"subscriptionId": "sub-123",
|
|
78
|
+
"subscriptionExternalId": "aws-sub-456",
|
|
79
|
+
"graphEntity": {
|
|
80
|
+
"id": "entity-2",
|
|
81
|
+
"providerUniqueId": "docker.io/library/nginx:1.21",
|
|
82
|
+
"name": "nginx:1.21",
|
|
83
|
+
"type": "CONTAINER_IMAGE",
|
|
84
|
+
"projects": [{"id": "project-123"}],
|
|
85
|
+
"properties": {
|
|
86
|
+
"cloudPlatform": "AWS",
|
|
87
|
+
"region": "us-east-1",
|
|
88
|
+
"imageTags": ["nginx:1.21", "nginx:latest"],
|
|
89
|
+
},
|
|
90
|
+
"publicExposures": {"totalCount": 1},
|
|
91
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
92
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "asset-3",
|
|
97
|
+
"name": "test-bucket-1",
|
|
98
|
+
"type": "BUCKET",
|
|
99
|
+
"subscriptionId": "sub-123",
|
|
100
|
+
"subscriptionExternalId": "gcp-project-789",
|
|
101
|
+
"graphEntity": {
|
|
102
|
+
"id": "entity-3",
|
|
103
|
+
"providerUniqueId": "gs://my-test-bucket",
|
|
104
|
+
"name": "my-test-bucket",
|
|
105
|
+
"type": "BUCKET",
|
|
106
|
+
"projects": [{"id": "project-123"}],
|
|
107
|
+
"properties": {"cloudPlatform": "GCP", "region": "us-central1", "isPublic": True},
|
|
108
|
+
"publicExposures": {"totalCount": 5},
|
|
109
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
110
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
def test_fetch_assets_returns_generator(self, mock_scanner):
|
|
116
|
+
"""Test that fetch_assets returns a generator, not a list."""
|
|
117
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
118
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=[]
|
|
119
|
+
), patch.object(mock_scanner, "parse_asset", return_value=None):
|
|
120
|
+
result = mock_scanner.fetch_assets(
|
|
121
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Verify it's a generator
|
|
125
|
+
assert hasattr(result, "__iter__") and hasattr(result, "__next__"), "fetch_assets should return a generator"
|
|
126
|
+
|
|
127
|
+
def test_fetch_assets_yields_assets_lazily(self, mock_scanner, sample_wiz_nodes):
|
|
128
|
+
"""Test that fetch_assets yields assets one at a time (lazy evaluation)."""
|
|
129
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
130
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=sample_wiz_nodes
|
|
131
|
+
):
|
|
132
|
+
# Create a mock that tracks when parse_asset is called
|
|
133
|
+
parse_calls = []
|
|
134
|
+
|
|
135
|
+
def mock_parse(node):
|
|
136
|
+
parse_calls.append(node["id"])
|
|
137
|
+
return IntegrationAsset(
|
|
138
|
+
identifier=node["id"],
|
|
139
|
+
name=node["name"],
|
|
140
|
+
asset_type="Software",
|
|
141
|
+
asset_category=AssetCategory.Software,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
with patch.object(mock_scanner, "parse_asset", side_effect=mock_parse):
|
|
145
|
+
generator = mock_scanner.fetch_assets(
|
|
146
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Before consuming, no assets should be parsed
|
|
150
|
+
assert len(parse_calls) == 0, "Assets should not be parsed until consumed"
|
|
151
|
+
|
|
152
|
+
# Consume first asset
|
|
153
|
+
first_asset = next(generator)
|
|
154
|
+
assert len(parse_calls) == 1, "Only first asset should be parsed"
|
|
155
|
+
assert first_asset.identifier == "asset-1"
|
|
156
|
+
|
|
157
|
+
# Consume second asset
|
|
158
|
+
second_asset = next(generator)
|
|
159
|
+
assert len(parse_calls) == 2, "Only two assets should be parsed"
|
|
160
|
+
assert second_asset.identifier == "asset-2"
|
|
161
|
+
|
|
162
|
+
# Consume all remaining
|
|
163
|
+
remaining = list(generator)
|
|
164
|
+
assert len(parse_calls) == 3, "All assets should now be parsed"
|
|
165
|
+
assert len(remaining) == 1
|
|
166
|
+
|
|
167
|
+
def test_fetch_assets_memory_efficiency(self, mock_scanner, sample_wiz_nodes):
|
|
168
|
+
"""Test that fetch_assets doesn't hold all assets in memory."""
|
|
169
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
170
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=sample_wiz_nodes
|
|
171
|
+
):
|
|
172
|
+
assets_in_memory = []
|
|
173
|
+
|
|
174
|
+
def mock_parse(node):
|
|
175
|
+
asset = IntegrationAsset(
|
|
176
|
+
identifier=node["id"],
|
|
177
|
+
name=node["name"],
|
|
178
|
+
asset_type="Software",
|
|
179
|
+
asset_category=AssetCategory.Software,
|
|
180
|
+
)
|
|
181
|
+
assets_in_memory.append(asset)
|
|
182
|
+
return asset
|
|
183
|
+
|
|
184
|
+
with patch.object(mock_scanner, "parse_asset", side_effect=mock_parse):
|
|
185
|
+
generator = mock_scanner.fetch_assets(
|
|
186
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Process assets one at a time
|
|
190
|
+
for i, asset in enumerate(generator, 1):
|
|
191
|
+
# At any point, we should only have i assets parsed
|
|
192
|
+
assert len(assets_in_memory) == i
|
|
193
|
+
assert asset is not None
|
|
194
|
+
|
|
195
|
+
def test_fetch_assets_handles_empty_results(self, mock_scanner):
|
|
196
|
+
"""Test fetch_assets with no assets returned."""
|
|
197
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
198
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=[]
|
|
199
|
+
):
|
|
200
|
+
generator = mock_scanner.fetch_assets(
|
|
201
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
assets = list(generator)
|
|
205
|
+
assert assets == [], "Should return empty list for no assets"
|
|
206
|
+
assert mock_scanner.num_assets_to_process == 0
|
|
207
|
+
|
|
208
|
+
def test_fetch_assets_skips_invalid_nodes(self, mock_scanner, sample_wiz_nodes):
|
|
209
|
+
"""Test that fetch_assets skips nodes that can't be parsed."""
|
|
210
|
+
# Add an invalid node
|
|
211
|
+
invalid_nodes = sample_wiz_nodes + [
|
|
212
|
+
{
|
|
213
|
+
"id": "invalid-asset",
|
|
214
|
+
"name": "broken",
|
|
215
|
+
"type": "UNKNOWN",
|
|
216
|
+
"graphEntity": None, # This will cause parse_asset to return None
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
221
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=invalid_nodes
|
|
222
|
+
):
|
|
223
|
+
|
|
224
|
+
def mock_parse(node):
|
|
225
|
+
if node.get("graphEntity") is None:
|
|
226
|
+
return None
|
|
227
|
+
return IntegrationAsset(
|
|
228
|
+
identifier=node["id"],
|
|
229
|
+
name=node["name"],
|
|
230
|
+
asset_type="Software",
|
|
231
|
+
asset_category=AssetCategory.Software,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
with patch.object(mock_scanner, "parse_asset", side_effect=mock_parse):
|
|
235
|
+
generator = mock_scanner.fetch_assets(
|
|
236
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
assets = list(generator)
|
|
240
|
+
# Should only get 3 valid assets, skipping the invalid one
|
|
241
|
+
assert len(assets) == 3
|
|
242
|
+
assert all(a.identifier.startswith("asset-") for a in assets)
|
|
243
|
+
|
|
244
|
+
def test_fetch_assets_progress_tracking(self, mock_scanner, sample_wiz_nodes):
|
|
245
|
+
"""Test that fetch_assets properly tracks progress."""
|
|
246
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
247
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=sample_wiz_nodes
|
|
248
|
+
), patch.object(
|
|
249
|
+
mock_scanner,
|
|
250
|
+
"parse_asset",
|
|
251
|
+
side_effect=lambda n: IntegrationAsset(
|
|
252
|
+
identifier=n["id"], name=n["name"], asset_type="Software", asset_category=AssetCategory.Software
|
|
253
|
+
),
|
|
254
|
+
):
|
|
255
|
+
generator = mock_scanner.fetch_assets(
|
|
256
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Consume all assets
|
|
260
|
+
_ = list(generator)
|
|
261
|
+
|
|
262
|
+
# Verify progress tracking was called
|
|
263
|
+
assert mock_scanner.asset_progress.add_task.called
|
|
264
|
+
assert mock_scanner.asset_progress.update.called
|
|
265
|
+
assert mock_scanner.asset_progress.advance.called
|
|
266
|
+
|
|
267
|
+
# Verify asset count is set
|
|
268
|
+
assert mock_scanner.num_assets_to_process == len(sample_wiz_nodes)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestWizInventoryAssetParsing(CLITestFixture):
|
|
272
|
+
"""Test suite for asset parsing logic."""
|
|
273
|
+
|
|
274
|
+
@pytest.fixture
|
|
275
|
+
def mock_scanner(self):
|
|
276
|
+
"""Create a mock scanner instance."""
|
|
277
|
+
with patch.object(WizVulnerabilityIntegration, "__init__", lambda self, *args, **kwargs: None):
|
|
278
|
+
scanner = WizVulnerabilityIntegration.__new__(WizVulnerabilityIntegration)
|
|
279
|
+
scanner.plan_id = 123
|
|
280
|
+
return scanner
|
|
281
|
+
|
|
282
|
+
@pytest.mark.skip(reason="Integration test requiring live RegScale API")
|
|
283
|
+
def test_parse_asset_virtual_machine(self, mock_scanner):
|
|
284
|
+
"""Test parsing a virtual machine asset."""
|
|
285
|
+
node = {
|
|
286
|
+
"id": "vm-1",
|
|
287
|
+
"name": "test-vm",
|
|
288
|
+
"type": "VIRTUAL_MACHINE",
|
|
289
|
+
"subscriptionId": "sub-123",
|
|
290
|
+
"subscriptionExternalId": "azure-sub-123",
|
|
291
|
+
"graphEntity": {
|
|
292
|
+
"id": "entity-1",
|
|
293
|
+
"providerUniqueId": "/subscriptions/123/vm-1",
|
|
294
|
+
"name": "test-vm",
|
|
295
|
+
"type": "VIRTUAL_MACHINE",
|
|
296
|
+
"projects": [{"id": "project-123"}],
|
|
297
|
+
"properties": {
|
|
298
|
+
"cloudPlatform": "Azure",
|
|
299
|
+
"region": "eastus",
|
|
300
|
+
"cpe": "cpe:/o:microsoft:windows_server:2019",
|
|
301
|
+
"operatingSystem": "Windows Server 2019",
|
|
302
|
+
"ipAddress": "10.0.0.1",
|
|
303
|
+
"macAddress": "00:00:00:00:00:00",
|
|
304
|
+
},
|
|
305
|
+
"publicExposures": {"totalCount": 0},
|
|
306
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
307
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Mock create_asset_type to avoid API calls
|
|
312
|
+
with patch(
|
|
313
|
+
"regscale.integrations.commercial.wizv2.utils.main.regscale_models.Metadata.get_metadata_by_module_field",
|
|
314
|
+
return_value=[],
|
|
315
|
+
):
|
|
316
|
+
asset = mock_scanner.parse_asset(node)
|
|
317
|
+
|
|
318
|
+
assert asset is not None
|
|
319
|
+
assert asset.identifier == "vm-1"
|
|
320
|
+
assert asset.name == "/subscriptions/123/vm-1"
|
|
321
|
+
assert asset.asset_type == "Virtual Machine"
|
|
322
|
+
assert asset.location == "eastus"
|
|
323
|
+
assert asset.operating_system == "Windows Server 2019"
|
|
324
|
+
|
|
325
|
+
@pytest.mark.skip(reason="Integration test requiring live RegScale API")
|
|
326
|
+
def test_parse_asset_container_image(self, mock_scanner):
|
|
327
|
+
"""Test parsing a container image asset."""
|
|
328
|
+
node = {
|
|
329
|
+
"id": "container-1",
|
|
330
|
+
"name": "nginx:1.21",
|
|
331
|
+
"type": "CONTAINER_IMAGE",
|
|
332
|
+
"subscriptionId": "sub-123",
|
|
333
|
+
"subscriptionExternalId": "aws-sub-456",
|
|
334
|
+
"graphEntity": {
|
|
335
|
+
"id": "entity-2",
|
|
336
|
+
"providerUniqueId": "docker.io/library/nginx:1.21",
|
|
337
|
+
"name": "nginx:1.21",
|
|
338
|
+
"type": "CONTAINER_IMAGE",
|
|
339
|
+
"projects": [{"id": "project-123"}],
|
|
340
|
+
"properties": {
|
|
341
|
+
"cloudPlatform": "AWS",
|
|
342
|
+
"region": "us-east-1",
|
|
343
|
+
"imageTags": ["nginx:1.21", "nginx:latest"],
|
|
344
|
+
"cpe": "cpe:/a:nginx:nginx:1.21",
|
|
345
|
+
},
|
|
346
|
+
"publicExposures": {"totalCount": 1},
|
|
347
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
348
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
asset = mock_scanner.parse_asset(node)
|
|
353
|
+
|
|
354
|
+
assert asset is not None
|
|
355
|
+
assert asset.identifier == "container-1"
|
|
356
|
+
assert asset.software_name == "nginx"
|
|
357
|
+
assert "1.21" in (asset.software_version or "")
|
|
358
|
+
|
|
359
|
+
def test_parse_asset_missing_graph_entity(self, mock_scanner, caplog):
|
|
360
|
+
"""Test parsing asset with missing graphEntity returns None."""
|
|
361
|
+
node = {"id": "broken-1", "name": "broken-asset", "type": "VIRTUAL_MACHINE"}
|
|
362
|
+
|
|
363
|
+
with caplog.at_level(logging.WARNING):
|
|
364
|
+
asset = mock_scanner.parse_asset(node)
|
|
365
|
+
|
|
366
|
+
assert asset is None
|
|
367
|
+
assert "No graph entity found" in caplog.text
|
|
368
|
+
|
|
369
|
+
@pytest.mark.skip(reason="Integration test requiring live RegScale API")
|
|
370
|
+
def test_parse_asset_public_exposure(self, mock_scanner):
|
|
371
|
+
"""Test parsing asset with public exposure."""
|
|
372
|
+
node = {
|
|
373
|
+
"id": "public-1",
|
|
374
|
+
"name": "public-vm",
|
|
375
|
+
"type": "VIRTUAL_MACHINE",
|
|
376
|
+
"subscriptionId": "sub-123",
|
|
377
|
+
"subscriptionExternalId": "sub-123",
|
|
378
|
+
"graphEntity": {
|
|
379
|
+
"id": "entity-1",
|
|
380
|
+
"providerUniqueId": "/vm/public-1",
|
|
381
|
+
"name": "public-vm",
|
|
382
|
+
"type": "VIRTUAL_MACHINE",
|
|
383
|
+
"projects": [{"id": "project-123"}],
|
|
384
|
+
"properties": {"cloudPlatform": "Azure", "region": "eastus"},
|
|
385
|
+
"publicExposures": {"totalCount": 3},
|
|
386
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
387
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
asset = mock_scanner.parse_asset(node)
|
|
392
|
+
|
|
393
|
+
assert asset is not None
|
|
394
|
+
# The asset should be marked as publicly exposed
|
|
395
|
+
# (specific field depends on IntegrationAsset implementation)
|
|
396
|
+
|
|
397
|
+
@pytest.mark.skip(reason="Integration test requiring live RegScale API")
|
|
398
|
+
def test_parse_asset_with_installed_packages(self, mock_scanner):
|
|
399
|
+
"""Test parsing asset with installed packages."""
|
|
400
|
+
node = {
|
|
401
|
+
"id": "linux-1",
|
|
402
|
+
"name": "linux-server",
|
|
403
|
+
"type": "VIRTUAL_MACHINE",
|
|
404
|
+
"subscriptionId": "sub-123",
|
|
405
|
+
"subscriptionExternalId": "sub-123",
|
|
406
|
+
"graphEntity": {
|
|
407
|
+
"id": "entity-1",
|
|
408
|
+
"providerUniqueId": "/vm/linux-1",
|
|
409
|
+
"name": "linux-server",
|
|
410
|
+
"type": "VIRTUAL_MACHINE",
|
|
411
|
+
"projects": [{"id": "project-123"}],
|
|
412
|
+
"properties": {
|
|
413
|
+
"cloudPlatform": "GCP",
|
|
414
|
+
"region": "us-central1",
|
|
415
|
+
"cpe": "cpe:/o:ubuntu:linux:20.04",
|
|
416
|
+
"installedPackages": [
|
|
417
|
+
"nginx (1.18.0-0ubuntu1)",
|
|
418
|
+
"openssl (1.1.1f-1ubuntu2)",
|
|
419
|
+
"python3 (3.8.10-0ubuntu1)",
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
"publicExposures": {"totalCount": 0},
|
|
423
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
424
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
asset = mock_scanner.parse_asset(node)
|
|
429
|
+
|
|
430
|
+
assert asset is not None
|
|
431
|
+
# Verify software list is populated (implementation dependent)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class TestWizInventoryFilters(CLITestFixture):
|
|
435
|
+
"""Test suite for filter construction and handling."""
|
|
436
|
+
|
|
437
|
+
@pytest.fixture
|
|
438
|
+
def mock_scanner(self):
|
|
439
|
+
"""Create a mock scanner instance."""
|
|
440
|
+
with patch.object(WizVulnerabilityIntegration, "__init__", lambda self, *args, **kwargs: None):
|
|
441
|
+
scanner = WizVulnerabilityIntegration.__new__(WizVulnerabilityIntegration)
|
|
442
|
+
scanner.plan_id = 123
|
|
443
|
+
return scanner
|
|
444
|
+
|
|
445
|
+
def test_get_filter_by_with_project_id_only(self, mock_scanner):
|
|
446
|
+
"""Test filter construction with just project ID."""
|
|
447
|
+
filter_by = mock_scanner.get_filter_by(filter_by_override=None, wiz_project_id="project-123")
|
|
448
|
+
|
|
449
|
+
assert filter_by == {"project": "project-123"}
|
|
450
|
+
|
|
451
|
+
def test_get_filter_by_with_string_override(self, mock_scanner):
|
|
452
|
+
"""Test filter construction with JSON string override."""
|
|
453
|
+
override_str = '{"type": ["VIRTUAL_MACHINE"], "region": "eastus"}'
|
|
454
|
+
|
|
455
|
+
filter_by = mock_scanner.get_filter_by(filter_by_override=override_str, wiz_project_id="project-123")
|
|
456
|
+
|
|
457
|
+
assert filter_by["type"] == ["VIRTUAL_MACHINE"]
|
|
458
|
+
assert filter_by["region"] == "eastus"
|
|
459
|
+
|
|
460
|
+
def test_get_filter_by_with_dict_override(self, mock_scanner):
|
|
461
|
+
"""Test filter construction with dict override."""
|
|
462
|
+
override_dict = {"type": ["CONTAINER_IMAGE"], "subscriptionExternalId": ["aws-sub-123"]}
|
|
463
|
+
|
|
464
|
+
filter_by = mock_scanner.get_filter_by(filter_by_override=override_dict, wiz_project_id="project-123")
|
|
465
|
+
|
|
466
|
+
assert filter_by == override_dict
|
|
467
|
+
|
|
468
|
+
def test_get_filter_by_with_last_pull_date(self, mock_scanner):
|
|
469
|
+
"""Test filter includes updatedAt when last pull date is set."""
|
|
470
|
+
with patch("regscale.integrations.commercial.wizv2.scanner.WizVariables") as mock_vars:
|
|
471
|
+
mock_vars.wizLastInventoryPull = "2024-01-01T00:00:00Z"
|
|
472
|
+
mock_vars.wizFullPullLimitHours = None
|
|
473
|
+
|
|
474
|
+
filter_by = mock_scanner.get_filter_by(filter_by_override=None, wiz_project_id="project-123")
|
|
475
|
+
|
|
476
|
+
assert "updatedAt" in filter_by
|
|
477
|
+
assert filter_by["updatedAt"]["after"] == "2024-01-01T00:00:00Z"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class TestWizInventoryAuthentication(CLITestFixture):
|
|
481
|
+
"""Test suite for authentication flow."""
|
|
482
|
+
|
|
483
|
+
@pytest.fixture
|
|
484
|
+
def mock_scanner(self):
|
|
485
|
+
"""Create a mock scanner instance."""
|
|
486
|
+
with patch.object(WizVulnerabilityIntegration, "__init__", lambda self, *args, **kwargs: None):
|
|
487
|
+
scanner = WizVulnerabilityIntegration.__new__(WizVulnerabilityIntegration)
|
|
488
|
+
scanner.plan_id = 123
|
|
489
|
+
scanner.wiz_token = None
|
|
490
|
+
return scanner
|
|
491
|
+
|
|
492
|
+
def test_authenticate_with_provided_credentials(self, mock_scanner):
|
|
493
|
+
"""Test authentication with provided credentials."""
|
|
494
|
+
# Patch at the scanner module level where it's imported
|
|
495
|
+
with patch("regscale.integrations.commercial.wizv2.scanner.wiz_authenticate") as mock_auth:
|
|
496
|
+
mock_auth.return_value = "mock_token_123"
|
|
497
|
+
|
|
498
|
+
mock_scanner.authenticate(client_id="test_client_id", client_secret="test_client_secret")
|
|
499
|
+
|
|
500
|
+
mock_auth.assert_called_once_with("test_client_id", "test_client_secret")
|
|
501
|
+
assert mock_scanner.wiz_token == "mock_token_123"
|
|
502
|
+
|
|
503
|
+
def test_authenticate_with_env_vars(self, mock_scanner):
|
|
504
|
+
"""Test authentication using environment variables."""
|
|
505
|
+
# Patch at the scanner module level where they're imported
|
|
506
|
+
with patch("regscale.integrations.commercial.wizv2.scanner.wiz_authenticate") as mock_auth, patch(
|
|
507
|
+
"regscale.integrations.commercial.wizv2.scanner.WizVariables"
|
|
508
|
+
) as mock_vars:
|
|
509
|
+
mock_vars.wizClientId = "env_client_id"
|
|
510
|
+
mock_vars.wizClientSecret = "env_client_secret"
|
|
511
|
+
mock_auth.return_value = "mock_token_456"
|
|
512
|
+
|
|
513
|
+
mock_scanner.authenticate(client_id=None, client_secret=None)
|
|
514
|
+
|
|
515
|
+
mock_auth.assert_called_once_with("env_client_id", "env_client_secret")
|
|
516
|
+
assert mock_scanner.wiz_token == "mock_token_456"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class TestWizInventoryIntegration(CLITestFixture):
|
|
520
|
+
"""Integration tests for full inventory sync workflow."""
|
|
521
|
+
|
|
522
|
+
@pytest.fixture
|
|
523
|
+
def mock_scanner(self):
|
|
524
|
+
"""Create a fully mocked scanner for integration testing."""
|
|
525
|
+
with patch.object(WizVulnerabilityIntegration, "__init__", lambda self, *args, **kwargs: None):
|
|
526
|
+
scanner = WizVulnerabilityIntegration.__new__(WizVulnerabilityIntegration)
|
|
527
|
+
scanner.plan_id = 123
|
|
528
|
+
scanner.wiz_token = "mock_token"
|
|
529
|
+
scanner.num_assets_to_process = 0
|
|
530
|
+
scanner.asset_progress = MagicMock()
|
|
531
|
+
scanner.finding_progress = MagicMock()
|
|
532
|
+
return scanner
|
|
533
|
+
|
|
534
|
+
@pytest.fixture
|
|
535
|
+
def sample_nodes(self) -> List[Dict[str, Any]]:
|
|
536
|
+
"""Sample nodes for integration testing."""
|
|
537
|
+
return [
|
|
538
|
+
{
|
|
539
|
+
"id": f"asset-{i}",
|
|
540
|
+
"name": f"test-asset-{i}",
|
|
541
|
+
"type": "VIRTUAL_MACHINE",
|
|
542
|
+
"subscriptionId": "sub-123",
|
|
543
|
+
"subscriptionExternalId": "sub-123",
|
|
544
|
+
"graphEntity": {
|
|
545
|
+
"id": f"entity-{i}",
|
|
546
|
+
"providerUniqueId": f"/vm/asset-{i}",
|
|
547
|
+
"name": f"test-asset-{i}",
|
|
548
|
+
"type": "VIRTUAL_MACHINE",
|
|
549
|
+
"projects": [{"id": "project-123"}],
|
|
550
|
+
"properties": {"cloudPlatform": "Azure", "region": "eastus"},
|
|
551
|
+
"publicExposures": {"totalCount": 0},
|
|
552
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
553
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
for i in range(100) # Test with 100 assets
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
def test_full_inventory_sync_workflow(self, mock_scanner, sample_nodes):
|
|
560
|
+
"""Test complete inventory sync from fetch to parse."""
|
|
561
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
562
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=sample_nodes
|
|
563
|
+
), patch.object(
|
|
564
|
+
mock_scanner,
|
|
565
|
+
"parse_asset",
|
|
566
|
+
side_effect=lambda n: IntegrationAsset(
|
|
567
|
+
identifier=n["id"],
|
|
568
|
+
name=n["name"],
|
|
569
|
+
asset_type="Virtual Machine (VM)",
|
|
570
|
+
asset_category=AssetCategory.Software,
|
|
571
|
+
),
|
|
572
|
+
):
|
|
573
|
+
generator = mock_scanner.fetch_assets(
|
|
574
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# Simulate processing assets in batches (like real sync would do)
|
|
578
|
+
batch_size = 10
|
|
579
|
+
total_processed = 0
|
|
580
|
+
|
|
581
|
+
while True:
|
|
582
|
+
batch = []
|
|
583
|
+
try:
|
|
584
|
+
for _ in range(batch_size):
|
|
585
|
+
batch.append(next(generator))
|
|
586
|
+
except StopIteration:
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
total_processed += len(batch)
|
|
590
|
+
|
|
591
|
+
# Verify batch has expected assets
|
|
592
|
+
assert all(isinstance(a, IntegrationAsset) for a in batch)
|
|
593
|
+
|
|
594
|
+
# Verify we processed all 100 assets
|
|
595
|
+
assert total_processed == 100
|
|
596
|
+
assert mock_scanner.num_assets_to_process == 100
|
|
597
|
+
|
|
598
|
+
def test_inventory_sync_with_large_dataset(self, mock_scanner):
|
|
599
|
+
"""Test inventory sync with very large dataset (1000+ assets)."""
|
|
600
|
+
# Generate 1000 minimal nodes
|
|
601
|
+
large_dataset = [
|
|
602
|
+
{
|
|
603
|
+
"id": f"asset-{i}",
|
|
604
|
+
"name": f"asset-{i}",
|
|
605
|
+
"type": "VIRTUAL_MACHINE",
|
|
606
|
+
"subscriptionId": "sub-123",
|
|
607
|
+
"subscriptionExternalId": "sub-123",
|
|
608
|
+
"graphEntity": {
|
|
609
|
+
"id": f"entity-{i}",
|
|
610
|
+
"providerUniqueId": f"/vm/asset-{i}",
|
|
611
|
+
"name": f"asset-{i}",
|
|
612
|
+
"type": "VIRTUAL_MACHINE",
|
|
613
|
+
"projects": [{"id": "project-123"}],
|
|
614
|
+
"properties": {"cloudPlatform": "Azure"},
|
|
615
|
+
"publicExposures": {"totalCount": 0},
|
|
616
|
+
"firstSeen": "2024-01-01T00:00:00Z",
|
|
617
|
+
"lastSeen": "2024-01-10T00:00:00Z",
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
for i in range(1000)
|
|
621
|
+
]
|
|
622
|
+
|
|
623
|
+
with patch.object(mock_scanner, "authenticate"), patch.object(
|
|
624
|
+
mock_scanner, "fetch_wiz_data_if_needed", return_value=large_dataset
|
|
625
|
+
), patch.object(
|
|
626
|
+
mock_scanner,
|
|
627
|
+
"parse_asset",
|
|
628
|
+
side_effect=lambda n: IntegrationAsset(
|
|
629
|
+
identifier=n["id"],
|
|
630
|
+
name=n["name"],
|
|
631
|
+
asset_type="Virtual Machine (VM)",
|
|
632
|
+
asset_category=AssetCategory.Software,
|
|
633
|
+
),
|
|
634
|
+
):
|
|
635
|
+
generator = mock_scanner.fetch_assets(
|
|
636
|
+
client_id="test_id", client_secret="test_secret", wiz_project_id="project-123"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# Process all assets
|
|
640
|
+
assets = list(generator)
|
|
641
|
+
|
|
642
|
+
# Verify all were processed
|
|
643
|
+
assert len(assets) == 1000
|
|
644
|
+
assert mock_scanner.num_assets_to_process == 1000
|