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
|
@@ -13,6 +13,7 @@ from collections import defaultdict
|
|
|
13
13
|
from typing import Dict, List, Optional, Any, Iterator
|
|
14
14
|
|
|
15
15
|
from regscale.core.app.utils.app_utils import get_current_datetime, regscale_string_to_datetime
|
|
16
|
+
from regscale.integrations.control_matcher import ControlMatcher
|
|
16
17
|
from regscale.integrations.scanner_integration import (
|
|
17
18
|
ScannerIntegration,
|
|
18
19
|
IntegrationAsset,
|
|
@@ -166,6 +167,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
166
167
|
self._compliance_settings = None
|
|
167
168
|
self._security_plan = None
|
|
168
169
|
|
|
170
|
+
# Initialize control matcher for robust control ID matching
|
|
171
|
+
self._control_matcher = ControlMatcher()
|
|
172
|
+
|
|
169
173
|
def is_poam(self, finding: IntegrationFinding) -> bool: # type: ignore[override]
|
|
170
174
|
"""
|
|
171
175
|
Determines if an issue should be considered a POAM for compliance integrations.
|
|
@@ -1230,7 +1234,6 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1230
1234
|
processed_impl_today.add(impl.id)
|
|
1231
1235
|
|
|
1232
1236
|
self._record_control_mapping(control_id, impl.id)
|
|
1233
|
-
self._map_assets_to_control_component(sec_control, items)
|
|
1234
1237
|
return 1
|
|
1235
1238
|
except Exception as e: # noqa: BLE001
|
|
1236
1239
|
logger.error(f"Error processing control assessment for '{control_id}': {e}")
|
|
@@ -1245,13 +1248,22 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1245
1248
|
"""
|
|
1246
1249
|
Find matching implementation and security control for a control ID.
|
|
1247
1250
|
|
|
1251
|
+
Uses ControlMatcher for robust control ID matching with leading zero normalization.
|
|
1252
|
+
|
|
1248
1253
|
:param str control_id: Control identifier to match
|
|
1249
1254
|
:param List[ControlImplementation] implementations: Available implementations
|
|
1250
1255
|
:return: Tuple of matching implementation and security control, or (None, None)
|
|
1251
1256
|
:rtype: tuple[Optional[ControlImplementation], Optional[SecurityControl]]
|
|
1252
1257
|
"""
|
|
1258
|
+
# Generate all variations of the search control ID for matching
|
|
1259
|
+
search_variations = self._control_matcher._get_control_id_variations(control_id)
|
|
1260
|
+
if not search_variations:
|
|
1261
|
+
logger.debug(f"Could not generate control ID variations for: {control_id}")
|
|
1262
|
+
return None, None
|
|
1263
|
+
|
|
1253
1264
|
matching_implementation = None
|
|
1254
1265
|
matching_security_control = None
|
|
1266
|
+
|
|
1255
1267
|
for implementation in implementations:
|
|
1256
1268
|
try:
|
|
1257
1269
|
security_control = SecurityControl.get_object(object_id=implementation.controlID)
|
|
@@ -1264,10 +1276,16 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1264
1276
|
if not security_control_id:
|
|
1265
1277
|
logger.debug(f"Security control {security_control.id} has no controlId")
|
|
1266
1278
|
continue
|
|
1279
|
+
|
|
1280
|
+
# Get variations of the security control ID
|
|
1281
|
+
control_variations = self._control_matcher._get_control_id_variations(security_control_id)
|
|
1282
|
+
|
|
1267
1283
|
logger.debug(
|
|
1268
|
-
f"Comparing
|
|
1284
|
+
f"Comparing control '{control_id}' variations {list(search_variations)[:3]} with RegScale control '{security_control_id}' variations {list(control_variations)[:3]} (impl: {implementation.id})"
|
|
1269
1285
|
)
|
|
1270
|
-
|
|
1286
|
+
|
|
1287
|
+
# Check if any variation matches (set intersection)
|
|
1288
|
+
if search_variations & control_variations:
|
|
1271
1289
|
matching_implementation = implementation
|
|
1272
1290
|
matching_security_control = security_control
|
|
1273
1291
|
logger.info(
|
|
@@ -1351,44 +1369,6 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1351
1369
|
except Exception:
|
|
1352
1370
|
pass
|
|
1353
1371
|
|
|
1354
|
-
def _map_assets_to_control_component(self, sec_control: SecurityControl, items: List[ComplianceItem]) -> None:
|
|
1355
|
-
"""
|
|
1356
|
-
Map assets to control-specific components for organization.
|
|
1357
|
-
|
|
1358
|
-
:param SecurityControl sec_control: Security control to create component for
|
|
1359
|
-
:param List[ComplianceItem] items: Compliance items with asset references
|
|
1360
|
-
:return: None
|
|
1361
|
-
:rtype: None
|
|
1362
|
-
"""
|
|
1363
|
-
try:
|
|
1364
|
-
component_title = f"Control {sec_control.controlId}"
|
|
1365
|
-
component = self.components_by_title.get(component_title) if hasattr(self, "components_by_title") else None
|
|
1366
|
-
if not component:
|
|
1367
|
-
component = regscale_models.Component(
|
|
1368
|
-
title=component_title,
|
|
1369
|
-
componentType=regscale_models.ComponentType.Hardware,
|
|
1370
|
-
securityPlansId=self.plan_id,
|
|
1371
|
-
description=component_title,
|
|
1372
|
-
componentOwnerId=self.get_assessor_id(),
|
|
1373
|
-
).get_or_create()
|
|
1374
|
-
regscale_models.ComponentMapping(
|
|
1375
|
-
componentId=component.id,
|
|
1376
|
-
securityPlanId=self.plan_id,
|
|
1377
|
-
).get_or_create()
|
|
1378
|
-
if hasattr(self, "components_by_title"):
|
|
1379
|
-
self.components_by_title[component_title] = component
|
|
1380
|
-
|
|
1381
|
-
for item in items:
|
|
1382
|
-
asset = self.get_asset_by_identifier(getattr(item, "resource_id", ""))
|
|
1383
|
-
if not asset:
|
|
1384
|
-
continue
|
|
1385
|
-
regscale_models.AssetMapping(
|
|
1386
|
-
assetId=asset.id,
|
|
1387
|
-
componentId=component.id,
|
|
1388
|
-
).get_or_create_with_status()
|
|
1389
|
-
except Exception as map_exc: # noqa: BLE001
|
|
1390
|
-
logger.debug(f"Control-to-asset mapping skipped due to: {map_exc}")
|
|
1391
|
-
|
|
1392
1372
|
@staticmethod
|
|
1393
1373
|
def _parse_control_id(control_id: str) -> tuple[str, Optional[str]]:
|
|
1394
1374
|
"""
|
|
@@ -2050,6 +2030,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
2050
2030
|
"""
|
|
2051
2031
|
Create or update an issue from a finding, using cache to prevent duplicates.
|
|
2052
2032
|
|
|
2033
|
+
Properly handles milestone creation for compliance integrations.
|
|
2034
|
+
|
|
2053
2035
|
:param str title: Issue title
|
|
2054
2036
|
:param IntegrationFinding finding: The finding to create issue from
|
|
2055
2037
|
:return: Created or updated issue
|
|
@@ -2068,6 +2050,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
2068
2050
|
f"Found existing issue {existing_issue.id} (other_identifier: '{existing_issue.otherIdentifier}') for lookup external_id '{external_id}', updating instead of creating"
|
|
2069
2051
|
)
|
|
2070
2052
|
|
|
2053
|
+
# Store original status for milestone comparison
|
|
2054
|
+
original_status = existing_issue.status
|
|
2055
|
+
|
|
2071
2056
|
# Update existing issue with new finding data
|
|
2072
2057
|
existing_issue.title = title
|
|
2073
2058
|
existing_issue.description = finding.description
|
|
@@ -2095,8 +2080,42 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
2095
2080
|
existing_issue.orgId = self.determine_issue_organization_id(existing_issue.issueOwnerId)
|
|
2096
2081
|
existing_issue.save()
|
|
2097
2082
|
|
|
2083
|
+
# Create milestone if status changed
|
|
2084
|
+
# Reconstruct original issue state for comparison
|
|
2085
|
+
original_issue = regscale_models.Issue()
|
|
2086
|
+
original_issue.status = original_status
|
|
2087
|
+
self._create_milestones_for_updated_issue(existing_issue, finding, original_issue)
|
|
2088
|
+
|
|
2098
2089
|
return existing_issue
|
|
2099
2090
|
else:
|
|
2100
2091
|
# No existing issue found, create new one using parent method
|
|
2101
2092
|
logger.debug(f"No existing issue found for external_id {external_id}, creating new issue")
|
|
2102
2093
|
return super().create_or_update_issue_from_finding(title, finding)
|
|
2094
|
+
|
|
2095
|
+
def _create_milestones_for_updated_issue(
|
|
2096
|
+
self,
|
|
2097
|
+
issue: regscale_models.Issue,
|
|
2098
|
+
finding: IntegrationFinding,
|
|
2099
|
+
original_issue: regscale_models.Issue,
|
|
2100
|
+
) -> None:
|
|
2101
|
+
"""
|
|
2102
|
+
Create milestones for an updated issue in compliance integration.
|
|
2103
|
+
|
|
2104
|
+
This method handles both status transition milestones and backfilling of missing
|
|
2105
|
+
creation milestones for existing issues.
|
|
2106
|
+
|
|
2107
|
+
:param regscale_models.Issue issue: The updated issue
|
|
2108
|
+
:param IntegrationFinding finding: The finding data
|
|
2109
|
+
:param regscale_models.Issue original_issue: Original state for comparison
|
|
2110
|
+
"""
|
|
2111
|
+
milestone_manager = self.get_milestone_manager()
|
|
2112
|
+
|
|
2113
|
+
# First, ensure the issue has a creation milestone (backfill if missing)
|
|
2114
|
+
milestone_manager.ensure_creation_milestone_exists(issue=issue, finding=finding)
|
|
2115
|
+
|
|
2116
|
+
# Then, handle status transition milestones
|
|
2117
|
+
milestone_manager.create_milestones_for_issue(
|
|
2118
|
+
issue=issue,
|
|
2119
|
+
finding=finding,
|
|
2120
|
+
existing_issue=original_issue,
|
|
2121
|
+
)
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Control Matcher - A utility class for identifying and matching control implementations
|
|
5
|
+
across different RegScale entities based on control ID strings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from regscale.core.app.api import Api
|
|
13
|
+
from regscale.core.app.application import Application
|
|
14
|
+
from regscale.models.regscale_models.control_implementation import ControlImplementation
|
|
15
|
+
from regscale.models.regscale_models.security_control import SecurityControl
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("regscale")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ControlMatcher:
|
|
21
|
+
"""
|
|
22
|
+
A class to identify control IDs and match them across different RegScale entities.
|
|
23
|
+
|
|
24
|
+
This class provides control matching capabilities:
|
|
25
|
+
- Parse control ID strings to extract NIST control identifiers
|
|
26
|
+
- Match controls from catalogs to security plans
|
|
27
|
+
- Find control implementations based on control IDs
|
|
28
|
+
- Support multiple control ID formats (e.g., AC-1, AC-1(1), AC-1.1)
|
|
29
|
+
|
|
30
|
+
Note: This class is focused on finding and matching existing controls only.
|
|
31
|
+
Control creation/modification should be handled by calling code.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, app: Optional[Application] = None):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the ControlMatcher.
|
|
37
|
+
|
|
38
|
+
:param Optional[Application] app: RegScale Application instance
|
|
39
|
+
"""
|
|
40
|
+
self.app = app or Application()
|
|
41
|
+
self.api = Api()
|
|
42
|
+
self._catalog_cache: Dict[int, List[SecurityControl]] = {}
|
|
43
|
+
self._control_impl_cache: Dict[Tuple[int, str], Dict[str, ControlImplementation]] = {}
|
|
44
|
+
|
|
45
|
+
def parse_control_id(self, control_string: str) -> Optional[str]:
|
|
46
|
+
"""
|
|
47
|
+
Parse a control ID string and extract the standardized control identifier.
|
|
48
|
+
|
|
49
|
+
Handles various formats:
|
|
50
|
+
- NIST format: AC-1, AC-1(1), AC-1.1
|
|
51
|
+
- With leading zeros: AC-01, AC-17(02)
|
|
52
|
+
- With spaces: AC-1 (1), AC-02 (04)
|
|
53
|
+
- With text: "Access Control AC-1"
|
|
54
|
+
- Multiple controls: "AC-1, AC-2"
|
|
55
|
+
|
|
56
|
+
:param str control_string: Raw control ID string
|
|
57
|
+
:return: Standardized control ID or None if not found
|
|
58
|
+
:rtype: Optional[str]
|
|
59
|
+
"""
|
|
60
|
+
if not control_string:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# Clean the string
|
|
64
|
+
control_string = control_string.strip().upper()
|
|
65
|
+
|
|
66
|
+
# Common NIST control ID pattern
|
|
67
|
+
# Matches: AC-1, AC-01, AC-1(1), AC-1(01), AC-1 (1), AC-1.1, AC-1.01, etc.
|
|
68
|
+
# Allows optional whitespace before and inside parentheses
|
|
69
|
+
pattern = r"([A-Z]{2,3}-\d+(?:\s*\(\s*\d+\s*\)|\.\d+)?)"
|
|
70
|
+
|
|
71
|
+
matches = re.findall(pattern, control_string)
|
|
72
|
+
if matches:
|
|
73
|
+
# Normalize parentheses to dots for consistency and remove spaces
|
|
74
|
+
control_id = matches[0]
|
|
75
|
+
control_id = control_id.replace(" ", "") # Remove all spaces
|
|
76
|
+
control_id = control_id.replace("(", ".").replace(")", "")
|
|
77
|
+
|
|
78
|
+
# Normalize leading zeros (e.g., AC-01.02 -> AC-1.2)
|
|
79
|
+
parts = control_id.split("-")
|
|
80
|
+
if len(parts) == 2:
|
|
81
|
+
family = parts[0]
|
|
82
|
+
number_part = parts[1]
|
|
83
|
+
|
|
84
|
+
if "." in number_part:
|
|
85
|
+
main_num, enhancement = number_part.split(".", 1)
|
|
86
|
+
main_num = str(int(main_num))
|
|
87
|
+
enhancement = str(int(enhancement))
|
|
88
|
+
control_id = f"{family}-{main_num}.{enhancement}"
|
|
89
|
+
else:
|
|
90
|
+
main_num = str(int(number_part))
|
|
91
|
+
control_id = f"{family}-{main_num}"
|
|
92
|
+
|
|
93
|
+
return control_id
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def find_control_in_catalog(self, control_id: str, catalog_id: int) -> Optional[SecurityControl]:
|
|
98
|
+
"""
|
|
99
|
+
Find a security control in a specific catalog by control ID.
|
|
100
|
+
|
|
101
|
+
:param str control_id: The control ID to search for
|
|
102
|
+
:param int catalog_id: The catalog ID to search in
|
|
103
|
+
:return: SecurityControl object if found, None otherwise
|
|
104
|
+
:rtype: Optional[SecurityControl]
|
|
105
|
+
"""
|
|
106
|
+
controls = self._get_catalog_controls(catalog_id)
|
|
107
|
+
|
|
108
|
+
# Generate all possible variations of the control ID
|
|
109
|
+
search_ids = self._get_control_id_variations(control_id)
|
|
110
|
+
|
|
111
|
+
# Try exact match with any variation
|
|
112
|
+
for control in controls:
|
|
113
|
+
if control.controlId in search_ids:
|
|
114
|
+
return control
|
|
115
|
+
|
|
116
|
+
# Try matching control variations against search variations
|
|
117
|
+
for control in controls:
|
|
118
|
+
control_variations = self._get_control_id_variations(control.controlId)
|
|
119
|
+
if control_variations & search_ids: # Set intersection
|
|
120
|
+
return control
|
|
121
|
+
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def find_control_implementation(
|
|
125
|
+
self, control_id: str, parent_id: int, parent_module: str = "securityplans", catalog_id: Optional[int] = None
|
|
126
|
+
) -> Optional[ControlImplementation]:
|
|
127
|
+
"""
|
|
128
|
+
Find a control implementation based on control ID and parent context.
|
|
129
|
+
|
|
130
|
+
:param str control_id: The control ID to match
|
|
131
|
+
:param int parent_id: Parent entity ID (e.g., security plan ID)
|
|
132
|
+
:param str parent_module: Parent module type (default: securityplans)
|
|
133
|
+
:param Optional[int] catalog_id: Optional catalog ID for better matching
|
|
134
|
+
:return: ControlImplementation if found, None otherwise
|
|
135
|
+
:rtype: Optional[ControlImplementation]
|
|
136
|
+
"""
|
|
137
|
+
# Get control implementations for the parent
|
|
138
|
+
implementations = self._get_control_implementations(parent_id, parent_module)
|
|
139
|
+
|
|
140
|
+
# Get all variations of the control ID for matching
|
|
141
|
+
search_variations = self._get_control_id_variations(control_id)
|
|
142
|
+
if not search_variations:
|
|
143
|
+
logger.warning(f"Could not parse control ID: {control_id}")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
# Try to find matching implementation with variation matching
|
|
147
|
+
for impl_key, impl in implementations.items():
|
|
148
|
+
impl_variations = self._get_control_id_variations(impl_key)
|
|
149
|
+
if impl_variations & search_variations: # Set intersection
|
|
150
|
+
return impl
|
|
151
|
+
|
|
152
|
+
# If catalog ID provided, try to find via security control
|
|
153
|
+
if catalog_id:
|
|
154
|
+
control = self.find_control_in_catalog(control_id, catalog_id)
|
|
155
|
+
if control:
|
|
156
|
+
# Search implementations by control ID
|
|
157
|
+
for impl in implementations.values():
|
|
158
|
+
if impl.controlID == control.id:
|
|
159
|
+
return impl
|
|
160
|
+
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def match_controls_to_implementations(
|
|
164
|
+
self,
|
|
165
|
+
control_ids: List[str],
|
|
166
|
+
parent_id: int,
|
|
167
|
+
parent_module: str = "securityplans",
|
|
168
|
+
catalog_id: Optional[int] = None,
|
|
169
|
+
) -> Dict[str, Optional[ControlImplementation]]:
|
|
170
|
+
"""
|
|
171
|
+
Match multiple control IDs to their implementations.
|
|
172
|
+
|
|
173
|
+
:param List[str] control_ids: List of control ID strings
|
|
174
|
+
:param int parent_id: Parent entity ID
|
|
175
|
+
:param str parent_module: Parent module type
|
|
176
|
+
:param Optional[int] catalog_id: Optional catalog ID
|
|
177
|
+
:return: Dictionary mapping control IDs to implementations
|
|
178
|
+
:rtype: Dict[str, Optional[ControlImplementation]]
|
|
179
|
+
"""
|
|
180
|
+
results = {}
|
|
181
|
+
|
|
182
|
+
for control_id in control_ids:
|
|
183
|
+
impl = self.find_control_implementation(control_id, parent_id, parent_module, catalog_id)
|
|
184
|
+
results[control_id] = impl
|
|
185
|
+
|
|
186
|
+
return results
|
|
187
|
+
|
|
188
|
+
def get_security_plan_controls(self, security_plan_id: int) -> Dict[str, ControlImplementation]:
|
|
189
|
+
"""
|
|
190
|
+
Get all control implementations for a security plan.
|
|
191
|
+
|
|
192
|
+
:param int security_plan_id: The security plan ID
|
|
193
|
+
:return: Dictionary of control implementations keyed by control ID
|
|
194
|
+
:rtype: Dict[str, ControlImplementation]
|
|
195
|
+
"""
|
|
196
|
+
return self._get_control_implementations(security_plan_id, "securityplans")
|
|
197
|
+
|
|
198
|
+
def find_controls_by_pattern(self, pattern: str, catalog_id: int) -> List[SecurityControl]:
|
|
199
|
+
"""
|
|
200
|
+
Find all controls in a catalog matching a pattern.
|
|
201
|
+
|
|
202
|
+
:param str pattern: Regex pattern or substring to match
|
|
203
|
+
:param int catalog_id: Catalog ID to search in
|
|
204
|
+
:return: List of matching SecurityControl objects
|
|
205
|
+
:rtype: List[SecurityControl]
|
|
206
|
+
"""
|
|
207
|
+
controls = self._get_catalog_controls(catalog_id)
|
|
208
|
+
matched = []
|
|
209
|
+
|
|
210
|
+
for control in controls:
|
|
211
|
+
if (re.search(pattern, control.controlId, re.IGNORECASE)) or (
|
|
212
|
+
control.title and re.search(pattern, control.title, re.IGNORECASE)
|
|
213
|
+
):
|
|
214
|
+
matched.append(control)
|
|
215
|
+
|
|
216
|
+
return matched
|
|
217
|
+
|
|
218
|
+
def bulk_match_controls(
|
|
219
|
+
self,
|
|
220
|
+
control_mappings: Dict[str, str],
|
|
221
|
+
parent_id: int,
|
|
222
|
+
parent_module: str = "securityplans",
|
|
223
|
+
catalog_id: Optional[int] = None,
|
|
224
|
+
) -> Dict[str, Optional[ControlImplementation]]:
|
|
225
|
+
"""
|
|
226
|
+
Bulk match control IDs to their implementations.
|
|
227
|
+
|
|
228
|
+
:param Dict[str, str] control_mappings: Dict of {external_id: control_id}
|
|
229
|
+
:param int parent_id: Parent entity ID
|
|
230
|
+
:param str parent_module: Parent module type
|
|
231
|
+
:param Optional[int] catalog_id: Catalog ID for controls
|
|
232
|
+
:return: Dictionary mapping external IDs to ControlImplementations (None if not found)
|
|
233
|
+
:rtype: Dict[str, Optional[ControlImplementation]]
|
|
234
|
+
"""
|
|
235
|
+
results = {}
|
|
236
|
+
|
|
237
|
+
for external_id, control_id in control_mappings.items():
|
|
238
|
+
impl = self.find_control_implementation(control_id, parent_id, parent_module, catalog_id)
|
|
239
|
+
results[external_id] = impl
|
|
240
|
+
|
|
241
|
+
return results
|
|
242
|
+
|
|
243
|
+
def _get_catalog_controls(self, catalog_id: int) -> List[SecurityControl]:
|
|
244
|
+
"""
|
|
245
|
+
Get all controls for a catalog (with caching).
|
|
246
|
+
|
|
247
|
+
:param int catalog_id: Catalog ID
|
|
248
|
+
:return: List of SecurityControl objects
|
|
249
|
+
:rtype: List[SecurityControl]
|
|
250
|
+
"""
|
|
251
|
+
if catalog_id not in self._catalog_cache:
|
|
252
|
+
try:
|
|
253
|
+
controls = SecurityControl.get_list_by_catalog(catalog_id)
|
|
254
|
+
self._catalog_cache[catalog_id] = controls
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"Failed to get controls for catalog {catalog_id}: {e}")
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
return self._catalog_cache.get(catalog_id, [])
|
|
260
|
+
|
|
261
|
+
def _normalize_control_id(self, control_id: str) -> Optional[str]:
|
|
262
|
+
"""
|
|
263
|
+
Normalize a control ID by removing leading zeros from all numeric parts.
|
|
264
|
+
|
|
265
|
+
Examples:
|
|
266
|
+
- AC-01 -> AC-1
|
|
267
|
+
- AC-17(02) -> AC-17.2
|
|
268
|
+
- AC-1.01 -> AC-1.1
|
|
269
|
+
|
|
270
|
+
:param str control_id: The control ID to normalize
|
|
271
|
+
:return: Normalized control ID or None if invalid
|
|
272
|
+
:rtype: Optional[str]
|
|
273
|
+
"""
|
|
274
|
+
parsed = self.parse_control_id(control_id)
|
|
275
|
+
if not parsed:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
# Split by '-' to get family and number parts
|
|
279
|
+
parts = parsed.split("-")
|
|
280
|
+
if len(parts) != 2:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
family = parts[0]
|
|
284
|
+
number_part = parts[1]
|
|
285
|
+
|
|
286
|
+
# Handle enhancement notation (both . and parentheses are normalized to .)
|
|
287
|
+
if "." in number_part:
|
|
288
|
+
main_num, enhancement = number_part.split(".", 1)
|
|
289
|
+
# Remove leading zeros from both parts
|
|
290
|
+
main_num = str(int(main_num))
|
|
291
|
+
enhancement = str(int(enhancement))
|
|
292
|
+
return f"{family}-{main_num}.{enhancement}"
|
|
293
|
+
else:
|
|
294
|
+
# Just main control number
|
|
295
|
+
main_num = str(int(number_part))
|
|
296
|
+
return f"{family}-{main_num}"
|
|
297
|
+
|
|
298
|
+
def _get_control_id_variations(self, control_id: str) -> set:
|
|
299
|
+
"""
|
|
300
|
+
Generate all valid variations of a control ID (with and without leading zeros).
|
|
301
|
+
|
|
302
|
+
Examples:
|
|
303
|
+
- AC-1 -> {AC-1, AC-01}
|
|
304
|
+
- AC-17.2 -> {AC-17.2, AC-17.02, AC-17(2), AC-17(02)}
|
|
305
|
+
|
|
306
|
+
:param str control_id: The control ID to generate variations for
|
|
307
|
+
:return: Set of all valid variations
|
|
308
|
+
:rtype: set
|
|
309
|
+
"""
|
|
310
|
+
parsed = self.parse_control_id(control_id)
|
|
311
|
+
if not parsed:
|
|
312
|
+
return set()
|
|
313
|
+
|
|
314
|
+
variations = set()
|
|
315
|
+
|
|
316
|
+
# Split by '-' to get family and number parts
|
|
317
|
+
parts = parsed.split("-")
|
|
318
|
+
if len(parts) != 2:
|
|
319
|
+
return set()
|
|
320
|
+
|
|
321
|
+
family = parts[0]
|
|
322
|
+
number_part = parts[1]
|
|
323
|
+
|
|
324
|
+
# Handle enhancement notation
|
|
325
|
+
if "." in number_part:
|
|
326
|
+
main_num, enhancement = number_part.split(".", 1)
|
|
327
|
+
main_int = int(main_num)
|
|
328
|
+
enh_int = int(enhancement)
|
|
329
|
+
|
|
330
|
+
# Generate all combinations: with/without leading zeros, dot/parenthesis notation
|
|
331
|
+
for main_fmt in [str(main_int), f"{main_int:02d}"]:
|
|
332
|
+
for enh_fmt in [str(enh_int), f"{enh_int:02d}"]:
|
|
333
|
+
variations.add(f"{family}-{main_fmt}.{enh_fmt}")
|
|
334
|
+
variations.add(f"{family}-{main_fmt}({enh_fmt})")
|
|
335
|
+
else:
|
|
336
|
+
# Just main control number
|
|
337
|
+
main_int = int(number_part)
|
|
338
|
+
variations.add(f"{family}-{main_int}")
|
|
339
|
+
variations.add(f"{family}-{main_int:02d}")
|
|
340
|
+
|
|
341
|
+
# Add uppercase versions to ensure consistency
|
|
342
|
+
return {v.upper() for v in variations}
|
|
343
|
+
|
|
344
|
+
def _get_control_implementations(self, parent_id: int, parent_module: str) -> Dict[str, ControlImplementation]:
|
|
345
|
+
"""
|
|
346
|
+
Get control implementations for a parent (with caching).
|
|
347
|
+
|
|
348
|
+
:param int parent_id: Parent ID
|
|
349
|
+
:param str parent_module: Parent module
|
|
350
|
+
:return: Dict of implementations keyed by control ID
|
|
351
|
+
:rtype: Dict[str, ControlImplementation]
|
|
352
|
+
"""
|
|
353
|
+
cache_key = (parent_id, parent_module)
|
|
354
|
+
|
|
355
|
+
if cache_key not in self._control_impl_cache:
|
|
356
|
+
try:
|
|
357
|
+
# Get the label map which maps control IDs to implementation IDs
|
|
358
|
+
label_map = ControlImplementation.get_control_label_map_by_parent(parent_id, parent_module)
|
|
359
|
+
|
|
360
|
+
implementations = {}
|
|
361
|
+
for control_label, impl_id in label_map.items():
|
|
362
|
+
impl = ControlImplementation.get_object(impl_id)
|
|
363
|
+
if impl:
|
|
364
|
+
implementations[control_label] = impl
|
|
365
|
+
|
|
366
|
+
self._control_impl_cache[cache_key] = implementations
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Failed to get implementations for {parent_module}/{parent_id}: {e}")
|
|
369
|
+
return {}
|
|
370
|
+
|
|
371
|
+
return self._control_impl_cache.get(cache_key, {})
|
|
372
|
+
|
|
373
|
+
def clear_cache(self):
|
|
374
|
+
"""Clear all cached data."""
|
|
375
|
+
self._catalog_cache.clear()
|
|
376
|
+
self._control_impl_cache.clear()
|
|
377
|
+
logger.info("Cleared control matcher cache")
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
7
|
+
from typing import Any, Dict, Optional, Union
|
|
8
8
|
|
|
9
9
|
from regscale.core.app.application import Application
|
|
10
10
|
from regscale.core.utils.date import get_day_increment
|
|
@@ -53,9 +53,13 @@ class DueDateHandler:
|
|
|
53
53
|
# Default due date timelines (days)
|
|
54
54
|
self.default_timelines = {
|
|
55
55
|
regscale_models.IssueSeverity.Critical: 30,
|
|
56
|
+
"critical": 30,
|
|
56
57
|
regscale_models.IssueSeverity.High: 60,
|
|
58
|
+
"high": 60,
|
|
57
59
|
regscale_models.IssueSeverity.Moderate: 120,
|
|
60
|
+
"moderate": 120,
|
|
58
61
|
regscale_models.IssueSeverity.Low: 364,
|
|
62
|
+
"low": 364,
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
# Load integration-specific timelines from config
|
|
@@ -66,10 +70,10 @@ class DueDateHandler:
|
|
|
66
70
|
|
|
67
71
|
def _load_integration_timelines(self) -> Dict[regscale_models.IssueSeverity, int]:
|
|
68
72
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
Load timeline configurations for this integration from init.yaml
|
|
74
|
+
mv
|
|
75
|
+
:return: Dictionary mapping severity to days
|
|
76
|
+
:rtype: Dict[regscale_models.IssueSeverity, int]
|
|
73
77
|
"""
|
|
74
78
|
timelines = self.default_timelines.copy()
|
|
75
79
|
|
|
@@ -184,7 +188,7 @@ class DueDateHandler:
|
|
|
184
188
|
|
|
185
189
|
def calculate_due_date(
|
|
186
190
|
self,
|
|
187
|
-
severity: regscale_models.IssueSeverity,
|
|
191
|
+
severity: Union[regscale_models.IssueSeverity, str],
|
|
188
192
|
created_date: str,
|
|
189
193
|
cve: Optional[str] = None,
|
|
190
194
|
title: Optional[str] = None,
|
|
@@ -192,7 +196,7 @@ class DueDateHandler:
|
|
|
192
196
|
"""
|
|
193
197
|
Calculate the due date for an issue based on severity, KEV status, and integration config
|
|
194
198
|
|
|
195
|
-
:param regscale_models.IssueSeverity severity: The severity of the issue
|
|
199
|
+
:param Union[regscale_models.IssueSeverity, str] severity: The severity of the issue
|
|
196
200
|
:param str created_date: The creation date of the issue
|
|
197
201
|
:param Optional[str] cve: The CVE identifier (if applicable)
|
|
198
202
|
:param Optional[str] title: The title of the issue (for additional context)
|
|
@@ -244,7 +248,9 @@ class DueDateHandler:
|
|
|
244
248
|
# Ensure due date is never in the past (allow yesterday for timezone differences)
|
|
245
249
|
due_date = self._ensure_future_due_date(calculated_due_date, days)
|
|
246
250
|
|
|
247
|
-
logger.debug(
|
|
251
|
+
logger.debug(
|
|
252
|
+
f"Using {self.integration_name} timeline: {severity.name if isinstance(severity, regscale_models.IssueSeverity) else severity} = {days} days, due date = {due_date}"
|
|
253
|
+
)
|
|
248
254
|
|
|
249
255
|
return due_date
|
|
250
256
|
|