regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/api.py +5 -2
- regscale/core/app/application.py +36 -6
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/evidence.py +727 -204
- regscale/core/app/internal/login.py +4 -2
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +86 -12
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/core/login.py +21 -4
- regscale/core/utils/async_graphql_client.py +363 -0
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/code_gen.py +109 -24
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +30 -2
- regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
- regscale/integrations/commercial/aws/cli.py +3107 -54
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
- regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
- regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +1072 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/jira.py +489 -153
- regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
- regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
- regscale/integrations/commercial/qualys/__init__.py +167 -68
- regscale/integrations/commercial/qualys/scanner.py +305 -39
- regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
- regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
- regscale/integrations/commercial/sicura/api.py +79 -42
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +83 -44
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +133 -16
- regscale/integrations/commercial/synqly/edr.py +2 -8
- regscale/integrations/commercial/synqly/query_builder.py +536 -0
- regscale/integrations/commercial/synqly/ticketing.py +27 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +165 -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 +146 -5
- regscale/integrations/commercial/tenablev2/scanner.py +1 -3
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +191 -76
- 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 +1592 -0
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
- 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} +66 -9
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +776 -28
- 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 +243 -0
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +1031 -441
- 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 +1036 -151
- regscale/integrations/control_matcher.py +432 -0
- regscale/integrations/due_date_handler.py +333 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +14 -0
- regscale/integrations/public/cci_importer.py +834 -0
- regscale/integrations/public/csam/__init__.py +0 -0
- regscale/integrations/public/csam/csam.py +938 -0
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +77 -6
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam/scanner.py +75 -7
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +1961 -430
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +805 -11
- regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
- 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 +87 -18
- regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
- regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +4 -2
- regscale/models/regscale_models/__init__.py +7 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/catalog.py +1 -1
- regscale/models/regscale_models/compliance_settings.py +251 -1
- regscale/models/regscale_models/component.py +1 -0
- regscale/models/regscale_models/control_implementation.py +236 -41
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/form_field_value.py +5 -3
- regscale/models/regscale_models/inheritance.py +44 -0
- regscale/models/regscale_models/issue.py +301 -102
- regscale/models/regscale_models/milestone.py +33 -14
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +310 -73
- regscale/models/regscale_models/security_plan.py +4 -2
- regscale/models/regscale_models/vulnerability.py +3 -3
- regscale/regscale.py +25 -4
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- regscale/validation/record.py +23 -1
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
- tests/core/__init__.py +0 -0
- tests/core/utils/__init__.py +0 -0
- tests/core/utils/test_async_graphql_client.py +472 -0
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
- tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
- tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -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 +3742 -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 +349 -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 +1218 -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/__init__.py +0 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +1053 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +1152 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
- tests/regscale/integrations/test_control_matcher.py +1421 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_issue.py +378 -1
- tests/regscale/models/test_module_integration.py +582 -0
- tests/regscale/models/test_tenable_integrations.py +811 -105
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
- regscale/integrations/public/fedramp/parts_mapper.py +0 -107
- /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
|
@@ -61,7 +61,62 @@ class Vulnerabilities(SynqlyModel):
|
|
|
61
61
|
self.can_fetch_vulns = "query_findings" in self.capabilities
|
|
62
62
|
|
|
63
63
|
@staticmethod
|
|
64
|
-
def
|
|
64
|
+
def _build_severity_filter(severity: Optional[str] = None) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Build severity filter string for Synqly queries
|
|
67
|
+
|
|
68
|
+
:param Optional[str] severity: Minimum severity level, if None, defaults to 'low'
|
|
69
|
+
:return: Comma-separated severity filter string
|
|
70
|
+
:rtype: str
|
|
71
|
+
"""
|
|
72
|
+
if not severity:
|
|
73
|
+
severity = "low"
|
|
74
|
+
severity_map = {
|
|
75
|
+
"critical": ["critical"],
|
|
76
|
+
"high": ["high", "critical"],
|
|
77
|
+
"medium": ["medium", "high", "critical"],
|
|
78
|
+
"low": ["low", "medium", "high", "critical"],
|
|
79
|
+
"info": ["info", "low", "medium", "high", "critical"],
|
|
80
|
+
}
|
|
81
|
+
if severity[0].lower() == "a":
|
|
82
|
+
mapped_severities = list(severity_map.keys())
|
|
83
|
+
else:
|
|
84
|
+
mapped_severities = severity_map.get(severity.lower(), severity)
|
|
85
|
+
severity_filter = f"severity[in]{','.join(mapped_severities)}"
|
|
86
|
+
return severity_filter
|
|
87
|
+
|
|
88
|
+
def _translate_asset_filter(self, replace: str, replace_with: str, asset_filters: Optional[list[str]]) -> list[str]:
|
|
89
|
+
"""
|
|
90
|
+
Translate asset filters to the correct format for the integration
|
|
91
|
+
|
|
92
|
+
:param str replace: The string to replace
|
|
93
|
+
:param str replace_with: The string to replace with
|
|
94
|
+
:param list[str] asset_filters: The asset filters to translate
|
|
95
|
+
:return: The translated asset filters
|
|
96
|
+
:rtype: list[str]
|
|
97
|
+
"""
|
|
98
|
+
translated_asset_filters = []
|
|
99
|
+
for asset_filter in asset_filters:
|
|
100
|
+
# Remove outer double quotes if present
|
|
101
|
+
cleaned_filter = asset_filter
|
|
102
|
+
if cleaned_filter.startswith('"') and cleaned_filter.endswith('"') and len(cleaned_filter) > 1:
|
|
103
|
+
cleaned_filter = cleaned_filter[1:-1]
|
|
104
|
+
if cleaned_filter.startswith("'") and cleaned_filter.endswith("'") and len(cleaned_filter) > 1:
|
|
105
|
+
cleaned_filter = cleaned_filter[1:-1]
|
|
106
|
+
|
|
107
|
+
if replace_with in cleaned_filter:
|
|
108
|
+
possible_filter = cleaned_filter
|
|
109
|
+
elif replace in cleaned_filter:
|
|
110
|
+
possible_filter = cleaned_filter.replace(replace, replace_with)
|
|
111
|
+
self.logger.debug(f"Translated filter: from {cleaned_filter} to {possible_filter}")
|
|
112
|
+
else:
|
|
113
|
+
continue
|
|
114
|
+
valid_filter, _ = self.filter_parser.validate_filter(self.integration_id, possible_filter)
|
|
115
|
+
if valid_filter:
|
|
116
|
+
translated_asset_filters.append(possible_filter)
|
|
117
|
+
return translated_asset_filters
|
|
118
|
+
|
|
119
|
+
def _handle_scan_date_options(self, regscale_ssp_id: int, **kwargs) -> list[str]:
|
|
65
120
|
"""
|
|
66
121
|
Handle scan date options for the integration sync process
|
|
67
122
|
|
|
@@ -71,21 +126,22 @@ class Vulnerabilities(SynqlyModel):
|
|
|
71
126
|
"""
|
|
72
127
|
from regscale.integrations.commercial.tenablev2.utils import get_last_pull_epoch
|
|
73
128
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
129
|
+
vuln_filter = [self._build_severity_filter(kwargs.get("minimum_severity_filter"))]
|
|
130
|
+
|
|
131
|
+
if asset_filters := kwargs.get("filter", []):
|
|
132
|
+
vuln_filter.extend(
|
|
133
|
+
self._translate_asset_filter(replace="device.", replace_with="resources.", asset_filters=asset_filters)
|
|
134
|
+
)
|
|
135
|
+
|
|
80
136
|
if kwargs.get("all_scans"):
|
|
81
137
|
vuln_filter.append("finding.last_seen_time[gte]915148800") # Friday, January 1, 1999 12:00:00 AM UTC
|
|
82
138
|
elif scan_date := kwargs.get("scan_date"):
|
|
83
|
-
from regscale.core.utils.date import
|
|
139
|
+
from regscale.core.utils.date import datetime_obj
|
|
84
140
|
|
|
85
|
-
if scan_date :=
|
|
86
|
-
vuln_filter.append(f"finding.last_seen_time[gte]{scan_date.
|
|
141
|
+
if scan_date := datetime_obj(scan_date):
|
|
142
|
+
vuln_filter.append(f"finding.last_seen_time[gte]{int(scan_date.timestamp())}")
|
|
87
143
|
else:
|
|
88
|
-
|
|
144
|
+
vuln_filter.append(f"finding.last_seen_time[gte]{get_last_pull_epoch(regscale_ssp_id)}")
|
|
89
145
|
else:
|
|
90
146
|
vuln_filter.append(f"finding.last_seen_time[gte]{get_last_pull_epoch(regscale_ssp_id)}")
|
|
91
147
|
return vuln_filter
|
|
@@ -99,22 +155,35 @@ class Vulnerabilities(SynqlyModel):
|
|
|
99
155
|
"""
|
|
100
156
|
vuln_filter = self._handle_scan_date_options(regscale_ssp_id=regscale_ssp_id, **kwargs)
|
|
101
157
|
self.logger.debug(f"Vulnerability filter: {vuln_filter}")
|
|
158
|
+
|
|
159
|
+
# Pop the filter from kwargs so it doesn't get passed to query_findings
|
|
160
|
+
if asset_filter := kwargs.pop("filter", []):
|
|
161
|
+
asset_filter = self._translate_asset_filter(
|
|
162
|
+
replace="resources.", replace_with="device.", asset_filters=asset_filter
|
|
163
|
+
)
|
|
164
|
+
self.logger.debug(f"Asset filter: {asset_filter}")
|
|
165
|
+
|
|
166
|
+
self.logger.info(f"Fetching asset data from {self.integration_name}...")
|
|
167
|
+
assets = (
|
|
168
|
+
self.fetch_integration_data(
|
|
169
|
+
func=self.tenant.engine_client.vulnerabilities.query_assets,
|
|
170
|
+
filter=asset_filter, # Field-based filters only for assets
|
|
171
|
+
**kwargs,
|
|
172
|
+
)
|
|
173
|
+
if self.can_fetch_assets
|
|
174
|
+
else []
|
|
175
|
+
)
|
|
176
|
+
|
|
102
177
|
self.logger.info(f"Fetching vulnerability data from {self.integration_name}...")
|
|
103
178
|
findings = (
|
|
104
179
|
self.fetch_integration_data(
|
|
105
180
|
func=self.tenant.engine_client.vulnerabilities.query_findings,
|
|
106
|
-
filter=vuln_filter,
|
|
181
|
+
filter=vuln_filter, # Only severity/date filters for findings
|
|
107
182
|
**kwargs,
|
|
108
183
|
)
|
|
109
184
|
if self.can_fetch_vulns
|
|
110
185
|
else []
|
|
111
186
|
)
|
|
112
|
-
self.logger.info(f"Fetching asset data from {self.integration_name}...")
|
|
113
|
-
assets = (
|
|
114
|
-
self.fetch_integration_data(func=self.tenant.engine_client.vulnerabilities.query_assets, **kwargs)
|
|
115
|
-
if self.can_fetch_assets
|
|
116
|
-
else []
|
|
117
|
-
)
|
|
118
187
|
|
|
119
188
|
self.scanner_integration = VulnerabilitiesIntegration(plan_id=regscale_ssp_id)
|
|
120
189
|
self.logger.info(f"Mapping {self.provider} asset(s) data to RegScale asset(s)...")
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized parser for extracting filter definitions from Synqly capabilities.
|
|
3
|
+
Used by both code generation and query builder.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
9
|
+
import importlib.resources as pkg_resources
|
|
10
|
+
|
|
11
|
+
from regscale.models.integration_models.synqly_models.connector_types import ConnectorType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FilterParser:
|
|
15
|
+
"""Parser for Synqly filter definitions from capabilities.json"""
|
|
16
|
+
|
|
17
|
+
# Define which connectors support filtering
|
|
18
|
+
FILTERABLE_CONNECTORS: Set[str] = {ConnectorType.Assets.value, ConnectorType.Vulnerabilities.value}
|
|
19
|
+
|
|
20
|
+
def __init__(self, capabilities_data: Optional[List[dict]] = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the filter parser with provided or loaded capabilities.
|
|
23
|
+
|
|
24
|
+
:param Optional[List[dict]] capabilities_data: Pre-loaded capabilities data.
|
|
25
|
+
If None, will load from package resources.
|
|
26
|
+
"""
|
|
27
|
+
if capabilities_data is None:
|
|
28
|
+
self.capabilities_data = self._load_capabilities()
|
|
29
|
+
else:
|
|
30
|
+
self.capabilities_data = capabilities_data
|
|
31
|
+
self.filter_mapping = self._build_filter_mapping()
|
|
32
|
+
|
|
33
|
+
def _load_capabilities(self) -> List[dict]:
|
|
34
|
+
"""
|
|
35
|
+
Load capabilities.json from package resources.
|
|
36
|
+
|
|
37
|
+
:return: List of capability definitions
|
|
38
|
+
:rtype: List[dict]
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
files = pkg_resources.files("regscale.models.integration_models.synqly_models")
|
|
42
|
+
capabilities_file = files / "capabilities.json"
|
|
43
|
+
with capabilities_file.open("r") as file:
|
|
44
|
+
data = json.load(file)
|
|
45
|
+
return data.get("result", [])
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print(f"Error loading capabilities.json: {e}")
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
def _build_filter_mapping(self) -> Dict[str, Dict[str, List[dict]]]:
|
|
51
|
+
"""
|
|
52
|
+
Build comprehensive filter mapping for all providers.
|
|
53
|
+
|
|
54
|
+
Structure:
|
|
55
|
+
{
|
|
56
|
+
'assets_armis_centrix': {
|
|
57
|
+
'query_devices': [
|
|
58
|
+
{
|
|
59
|
+
'name': 'device.ip',
|
|
60
|
+
'type': 'string',
|
|
61
|
+
'operators': ['eq', 'ne', 'in', 'not_in'],
|
|
62
|
+
'values': [] # For enum types
|
|
63
|
+
},
|
|
64
|
+
...
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
'vulnerabilities_qualys': {
|
|
68
|
+
'query_findings': [...],
|
|
69
|
+
'query_assets': [...]
|
|
70
|
+
},
|
|
71
|
+
...
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:return: Mapping of provider IDs to their operations and filters
|
|
75
|
+
:rtype: Dict[str, Dict[str, List[dict]]]
|
|
76
|
+
"""
|
|
77
|
+
filter_mapping = {}
|
|
78
|
+
|
|
79
|
+
for provider in self.capabilities_data:
|
|
80
|
+
provider_id = provider.get("id", "")
|
|
81
|
+
connector_type = provider.get("connector", "")
|
|
82
|
+
|
|
83
|
+
# Only process filterable connector types
|
|
84
|
+
if connector_type not in self.FILTERABLE_CONNECTORS:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Skip mock providers if desired (they end with _mock)
|
|
88
|
+
# if provider_id.endswith('_mock'):
|
|
89
|
+
# continue
|
|
90
|
+
|
|
91
|
+
operations = provider.get("operations", [])
|
|
92
|
+
for operation in operations:
|
|
93
|
+
# Only process supported operations with filters
|
|
94
|
+
if not operation.get("supported", False):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
filters = operation.get("filters", [])
|
|
98
|
+
if filters:
|
|
99
|
+
if provider_id not in filter_mapping:
|
|
100
|
+
filter_mapping[provider_id] = {}
|
|
101
|
+
|
|
102
|
+
operation_name = operation.get("name", "")
|
|
103
|
+
filter_mapping[provider_id][operation_name] = filters
|
|
104
|
+
|
|
105
|
+
return filter_mapping
|
|
106
|
+
|
|
107
|
+
def get_filters_for_provider(self, provider_id: str, operation: Optional[str] = None) -> List[dict]:
|
|
108
|
+
"""
|
|
109
|
+
Get filters for a specific provider and optionally a specific operation.
|
|
110
|
+
|
|
111
|
+
:param str provider_id: Provider ID (e.g., 'assets_armis_centrix')
|
|
112
|
+
:param Optional[str] operation: Operation name (e.g., 'query_devices')
|
|
113
|
+
:return: List of filter definitions
|
|
114
|
+
:rtype: List[dict]
|
|
115
|
+
"""
|
|
116
|
+
provider_filters = self.filter_mapping.get(provider_id, {})
|
|
117
|
+
|
|
118
|
+
if operation:
|
|
119
|
+
return provider_filters.get(operation, [])
|
|
120
|
+
|
|
121
|
+
# Return all filters for all operations if no specific operation
|
|
122
|
+
all_filters = []
|
|
123
|
+
seen_fields = set() # Avoid duplicates
|
|
124
|
+
|
|
125
|
+
for op_filters in provider_filters.values():
|
|
126
|
+
for filter_def in op_filters:
|
|
127
|
+
field_name = filter_def.get("name", "")
|
|
128
|
+
if field_name not in seen_fields:
|
|
129
|
+
all_filters.append(filter_def)
|
|
130
|
+
seen_fields.add(field_name)
|
|
131
|
+
|
|
132
|
+
return all_filters
|
|
133
|
+
|
|
134
|
+
def get_providers_with_filters(self, connector_type: str) -> List[str]:
|
|
135
|
+
"""
|
|
136
|
+
Get list of providers that support filtering for a connector type.
|
|
137
|
+
|
|
138
|
+
:param str connector_type: Connector type (e.g., 'assets', 'vulnerabilities')
|
|
139
|
+
:return: List of provider IDs that have filters
|
|
140
|
+
:rtype: List[str]
|
|
141
|
+
"""
|
|
142
|
+
providers = []
|
|
143
|
+
|
|
144
|
+
for provider_id, operations in self.filter_mapping.items():
|
|
145
|
+
# Check if provider matches connector type and has filters
|
|
146
|
+
if provider_id.startswith(f"{connector_type}_") and operations:
|
|
147
|
+
providers.append(provider_id)
|
|
148
|
+
|
|
149
|
+
# Sort for consistent ordering
|
|
150
|
+
return sorted(providers)
|
|
151
|
+
|
|
152
|
+
def has_filters(self, provider_id: str) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Check if a provider has any filters defined.
|
|
155
|
+
|
|
156
|
+
:param str provider_id: Provider ID to check
|
|
157
|
+
:return: True if provider has filters
|
|
158
|
+
:rtype: bool
|
|
159
|
+
"""
|
|
160
|
+
return provider_id in self.filter_mapping and bool(self.filter_mapping[provider_id])
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def format_filter_string(field: str, operator: str, value: str) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Convert user input to Synqly filter format.
|
|
166
|
+
|
|
167
|
+
:param str field: Field name (e.g., 'device.ip')
|
|
168
|
+
:param str operator: Operator (e.g., 'eq', 'gte')
|
|
169
|
+
:param str value: Filter value
|
|
170
|
+
:return: Formatted filter string
|
|
171
|
+
:rtype: str
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
format_filter_string('device.ip', 'eq', '192.168.1.1')
|
|
175
|
+
Returns: 'device.ip[eq]192.168.1.1'
|
|
176
|
+
"""
|
|
177
|
+
return f"{field}[{operator}]{value}"
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def parse_filter_string(filter_string: str) -> Optional[Tuple[str, str, str]]:
|
|
181
|
+
"""
|
|
182
|
+
Parse a filter string into its components.
|
|
183
|
+
|
|
184
|
+
:param str filter_string: Filter in format 'field[operator]value'
|
|
185
|
+
:return: Tuple of (field, operator, value) or None if invalid
|
|
186
|
+
:rtype: Optional[Tuple[str, str, str]]
|
|
187
|
+
"""
|
|
188
|
+
match = re.match(r"^([a-z._]+)\[([a-z_]+)\](.+)$", filter_string, re.IGNORECASE)
|
|
189
|
+
if match:
|
|
190
|
+
return match.groups()
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def validate_filter(self, provider_id: str, filter_string: str) -> Tuple[bool, str]:
|
|
194
|
+
"""
|
|
195
|
+
Validate a filter string against provider capabilities.
|
|
196
|
+
|
|
197
|
+
:param str provider_id: Provider ID (e.g., 'assets_armis_centrix')
|
|
198
|
+
:param str filter_string: Filter in format 'field[operator]value'
|
|
199
|
+
:return: Tuple of (is_valid, error_message)
|
|
200
|
+
:rtype: Tuple[bool, str]
|
|
201
|
+
"""
|
|
202
|
+
# Parse the filter string
|
|
203
|
+
parsed = self.parse_filter_string(filter_string)
|
|
204
|
+
if not parsed:
|
|
205
|
+
return False, f"Invalid filter format: {filter_string}. Expected format: field[operator]value"
|
|
206
|
+
|
|
207
|
+
field, operator, value = parsed
|
|
208
|
+
|
|
209
|
+
# Get all filters for this provider
|
|
210
|
+
provider_filters = self.get_filters_for_provider(provider_id)
|
|
211
|
+
|
|
212
|
+
if not provider_filters:
|
|
213
|
+
return False, f"Provider '{provider_id}' does not support filtering"
|
|
214
|
+
|
|
215
|
+
# Check if field exists
|
|
216
|
+
field_filter = None
|
|
217
|
+
for f in provider_filters:
|
|
218
|
+
if f.get("name") == field:
|
|
219
|
+
field_filter = f
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
if not field_filter:
|
|
223
|
+
available_fields = [f.get("name", "") for f in provider_filters]
|
|
224
|
+
return (
|
|
225
|
+
False,
|
|
226
|
+
f"Field '{field}' not supported by {provider_id}. Available fields: {', '.join(available_fields)}",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Check if operator is valid for this field
|
|
230
|
+
valid_operators = field_filter.get("operators", [])
|
|
231
|
+
if operator not in valid_operators:
|
|
232
|
+
return (
|
|
233
|
+
False,
|
|
234
|
+
f"Operator '{operator}' not valid for field '{field}'. Valid operators: {', '.join(valid_operators)}",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Optionally validate value type and format
|
|
238
|
+
field_type = field_filter.get("type", "string")
|
|
239
|
+
|
|
240
|
+
# Handle comma-separated values for 'in' and 'not_in' operators
|
|
241
|
+
if operator in ["in", "not_in"]:
|
|
242
|
+
values_to_check = [v.strip() for v in value.split(",")]
|
|
243
|
+
else:
|
|
244
|
+
values_to_check = [value]
|
|
245
|
+
|
|
246
|
+
for val in values_to_check:
|
|
247
|
+
if field_type == "number":
|
|
248
|
+
try:
|
|
249
|
+
float(val)
|
|
250
|
+
except ValueError:
|
|
251
|
+
return False, f"Value '{val}' is not a valid number for field '{field}'"
|
|
252
|
+
elif field_type == "enum":
|
|
253
|
+
valid_values = field_filter.get("values", [])
|
|
254
|
+
if valid_values and val not in valid_values:
|
|
255
|
+
return (
|
|
256
|
+
False,
|
|
257
|
+
f"Value '{val}' not valid for field '{field}'. Valid values: {', '.join(valid_values)}",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return True, ""
|
|
261
|
+
|
|
262
|
+
def get_operator_display_name(self, operator: str) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Get human-friendly display name for an operator.
|
|
265
|
+
|
|
266
|
+
:param str operator: Operator code
|
|
267
|
+
:return: Display name
|
|
268
|
+
:rtype: str
|
|
269
|
+
"""
|
|
270
|
+
operator_map = {
|
|
271
|
+
"eq": "equals",
|
|
272
|
+
"ne": "not equals",
|
|
273
|
+
"in": "in list",
|
|
274
|
+
"not_in": "not in list",
|
|
275
|
+
"like": "matches pattern",
|
|
276
|
+
"not_like": "does not match pattern",
|
|
277
|
+
"gt": "greater than",
|
|
278
|
+
"gte": "greater than or equal to",
|
|
279
|
+
"lt": "less than",
|
|
280
|
+
"lte": "less than or equal to",
|
|
281
|
+
}
|
|
282
|
+
return operator_map.get(operator, operator)
|
|
283
|
+
|
|
284
|
+
def get_field_display_name(self, field: str) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Convert field name to human-friendly display name.
|
|
287
|
+
|
|
288
|
+
:param str field: Field name (e.g., 'device.hw_info.serial_number')
|
|
289
|
+
:return: Display name (e.g., 'Device Hardware Info Serial Number')
|
|
290
|
+
:rtype: str
|
|
291
|
+
"""
|
|
292
|
+
# Replace dots and underscores with spaces, then title case
|
|
293
|
+
display = field.replace(".", " ").replace("_", " ").title()
|
|
294
|
+
return display
|
|
295
|
+
|
|
296
|
+
def get_connector_operations(self, connector_type: str) -> Dict[str, List[str]]:
|
|
297
|
+
"""
|
|
298
|
+
Get all operations that support filtering for a connector type.
|
|
299
|
+
|
|
300
|
+
:param str connector_type: Connector type (e.g., 'assets')
|
|
301
|
+
:return: Dict mapping provider IDs to their filterable operations
|
|
302
|
+
:rtype: Dict[str, List[str]]
|
|
303
|
+
"""
|
|
304
|
+
operations_map = {}
|
|
305
|
+
|
|
306
|
+
for provider_id, operations in self.filter_mapping.items():
|
|
307
|
+
if provider_id.startswith(f"{connector_type}_"):
|
|
308
|
+
operations_map[provider_id] = list(operations.keys())
|
|
309
|
+
|
|
310
|
+
return operations_map
|
|
311
|
+
|
|
312
|
+
def get_stats(self) -> dict:
|
|
313
|
+
"""
|
|
314
|
+
Get statistics about loaded filters.
|
|
315
|
+
|
|
316
|
+
:return: Dictionary with filter statistics
|
|
317
|
+
:rtype: dict
|
|
318
|
+
"""
|
|
319
|
+
stats = {
|
|
320
|
+
"total_providers": len(self.capabilities_data),
|
|
321
|
+
"providers_with_filters": len(self.filter_mapping),
|
|
322
|
+
"total_filters": 0,
|
|
323
|
+
"by_connector": {},
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for connector in self.FILTERABLE_CONNECTORS:
|
|
327
|
+
providers = self.get_providers_with_filters(connector)
|
|
328
|
+
filter_count = sum(len(self.get_filters_for_provider(p)) for p in providers)
|
|
329
|
+
stats["by_connector"][connector] = {"providers": len(providers), "filters": filter_count}
|
|
330
|
+
stats["total_filters"] += filter_count
|
|
331
|
+
|
|
332
|
+
return stats
|