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
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Test CISA Integrations"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from unittest import TestCase
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
from urllib.error import URLError
|
|
9
|
+
import dateutil.parser as dparser
|
|
10
|
+
from requests.exceptions import RequestException
|
|
11
|
+
|
|
12
|
+
from bs4 import Tag, BeautifulSoup
|
|
13
|
+
import pytest
|
|
14
|
+
from regscale.integrations.public.cisa import (
|
|
15
|
+
build_threat,
|
|
16
|
+
convert_date_string,
|
|
17
|
+
filter_elements,
|
|
18
|
+
fuzzy_find_date,
|
|
19
|
+
gen_soup,
|
|
20
|
+
insert_or_upd_threat,
|
|
21
|
+
is_url,
|
|
22
|
+
merge_old,
|
|
23
|
+
parse_html,
|
|
24
|
+
process_element,
|
|
25
|
+
process_params,
|
|
26
|
+
process_threats,
|
|
27
|
+
unique,
|
|
28
|
+
update_regscale_links,
|
|
29
|
+
alerts,
|
|
30
|
+
parse_details,
|
|
31
|
+
pull_cisa_kev,
|
|
32
|
+
update_regscale,
|
|
33
|
+
update_regscale_threats,
|
|
34
|
+
)
|
|
35
|
+
from regscale.models import Threat, Link
|
|
36
|
+
from regscale.core.app.application import Application
|
|
37
|
+
from tests import CLITestFixture
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestCisa(CLITestFixture, TestCase):
|
|
41
|
+
"""Test CISA Integrations"""
|
|
42
|
+
|
|
43
|
+
@patch("regscale.integrations.public.cisa.Link.batch_update", return_value=None)
|
|
44
|
+
def test_update_regscale_links_no_threats(self, mock_batch_update):
|
|
45
|
+
"""Test update_regscale_links function with no threats"""
|
|
46
|
+
threats = []
|
|
47
|
+
update_regscale_links(threats)
|
|
48
|
+
mock_batch_update.assert_called_once_with(threats)
|
|
49
|
+
|
|
50
|
+
@patch("regscale.integrations.public.cisa.Link.batch_update", return_value=None)
|
|
51
|
+
def test_update_regscale_links(self, mock_batch_update):
|
|
52
|
+
"""Test update_regscale_links function"""
|
|
53
|
+
threats = [
|
|
54
|
+
Threat(
|
|
55
|
+
id=1,
|
|
56
|
+
threatType="Vulnerability",
|
|
57
|
+
status="Under Investigation",
|
|
58
|
+
source="Open Source",
|
|
59
|
+
title="Threat 1",
|
|
60
|
+
targetType="Target Type",
|
|
61
|
+
description="Description https://example.com",
|
|
62
|
+
)
|
|
63
|
+
]
|
|
64
|
+
links = [Link(parentID=1, parentModule="threats", url="https://example.com", title="Threat 1")]
|
|
65
|
+
|
|
66
|
+
update_regscale_links(threats)
|
|
67
|
+
mock_batch_update.assert_called_once_with(links)
|
|
68
|
+
|
|
69
|
+
def test_process_threats_without_threats(self):
|
|
70
|
+
"""Test process_threats with no threats"""
|
|
71
|
+
threats = []
|
|
72
|
+
unique_threats = set()
|
|
73
|
+
reg_threats = []
|
|
74
|
+
insert_threats, update_threats = process_threats(threats, unique_threats, reg_threats)
|
|
75
|
+
assert insert_threats == []
|
|
76
|
+
assert update_threats == []
|
|
77
|
+
|
|
78
|
+
def test_process_threats_insert(self):
|
|
79
|
+
"""Test process_threats with insertions"""
|
|
80
|
+
threats = [
|
|
81
|
+
Threat(
|
|
82
|
+
id=1,
|
|
83
|
+
threatType="Vulnerability",
|
|
84
|
+
status="Under Investigation",
|
|
85
|
+
source="Open Source",
|
|
86
|
+
title="Threat 1",
|
|
87
|
+
)
|
|
88
|
+
]
|
|
89
|
+
unique_threats = set()
|
|
90
|
+
reg_threats = []
|
|
91
|
+
insert_threats, update_threats = process_threats(threats, unique_threats, reg_threats)
|
|
92
|
+
assert insert_threats == [threats[0].dict()]
|
|
93
|
+
assert update_threats == []
|
|
94
|
+
|
|
95
|
+
def test_process_threats_update(self):
|
|
96
|
+
"""Test process_threats with updates"""
|
|
97
|
+
threats = [
|
|
98
|
+
Threat(
|
|
99
|
+
id=1,
|
|
100
|
+
threatType="Vulnerability",
|
|
101
|
+
status="Closed",
|
|
102
|
+
source="Open Source",
|
|
103
|
+
title="Threat 1",
|
|
104
|
+
description="Description https://example.com",
|
|
105
|
+
)
|
|
106
|
+
]
|
|
107
|
+
unique_threats = set(["Description https://example.com"])
|
|
108
|
+
reg_threats = [
|
|
109
|
+
Threat(
|
|
110
|
+
id=1,
|
|
111
|
+
threatType="Vulnerability",
|
|
112
|
+
status="Under Investigation",
|
|
113
|
+
source="Open Source",
|
|
114
|
+
title="Threat 1",
|
|
115
|
+
description="Description https://example.com",
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
insert_threats, update_threats = process_threats(threats, unique_threats, reg_threats)
|
|
119
|
+
assert insert_threats == []
|
|
120
|
+
assert update_threats == [reg_threats[0].dict()]
|
|
121
|
+
|
|
122
|
+
@patch("regscale.integrations.public.cisa.parse_details")
|
|
123
|
+
def test_build_empty_threat(self, mock_parse_details):
|
|
124
|
+
"""Test build_threat function with empty threat"""
|
|
125
|
+
mock_parse_details.return_value = None
|
|
126
|
+
app = MagicMock()
|
|
127
|
+
threat = build_threat(app, "https://example.com", "Description https://example.com", "Threat 1")
|
|
128
|
+
assert threat is None
|
|
129
|
+
|
|
130
|
+
@patch("regscale.integrations.public.cisa.parse_details")
|
|
131
|
+
@patch("regscale.integrations.public.cisa.Threat")
|
|
132
|
+
def test_build_threat(self, mock_threat_class, mock_parse_details):
|
|
133
|
+
"""Test build_threat function"""
|
|
134
|
+
app = MagicMock()
|
|
135
|
+
app.config = {"userId": "1"}
|
|
136
|
+
|
|
137
|
+
mock_threat_instance = MagicMock()
|
|
138
|
+
mock_threat_class.return_value = mock_threat_instance
|
|
139
|
+
mock_threat_class.xstr.return_value = ""
|
|
140
|
+
|
|
141
|
+
dat = (
|
|
142
|
+
"2025-05-26",
|
|
143
|
+
["Vulnerability 1"],
|
|
144
|
+
["Mitigation 1"],
|
|
145
|
+
["Note 1"],
|
|
146
|
+
)
|
|
147
|
+
mock_parse_details.return_value = dat
|
|
148
|
+
|
|
149
|
+
threat = build_threat(app, "https://example.com", "Description https://example.com", "Threat 1")
|
|
150
|
+
|
|
151
|
+
mock_threat_class.assert_called_once_with(
|
|
152
|
+
uuid=Threat.xstr(None),
|
|
153
|
+
title="Threat 1",
|
|
154
|
+
threatType="Specific",
|
|
155
|
+
threatOwnerId="1",
|
|
156
|
+
dateIdentified="2025-05-26",
|
|
157
|
+
targetType="Other",
|
|
158
|
+
source="Open Source",
|
|
159
|
+
description="Description https://example.com",
|
|
160
|
+
vulnerabilityAnalysis="Vulnerability 1",
|
|
161
|
+
mitigations="Mitigation 1",
|
|
162
|
+
notes="Note 1",
|
|
163
|
+
dateCreated="2025-05-26",
|
|
164
|
+
status="Initial Report/Notification",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
assert threat is not None
|
|
168
|
+
assert threat == mock_threat_instance
|
|
169
|
+
|
|
170
|
+
def test_filter_elements_filter_list(self):
|
|
171
|
+
"""Test filter_elements function blocked by filter list"""
|
|
172
|
+
filter_list = [
|
|
173
|
+
"c-figure__media",
|
|
174
|
+
"c-product-survey__text-area",
|
|
175
|
+
"l-full__footer",
|
|
176
|
+
"usa-navbar",
|
|
177
|
+
]
|
|
178
|
+
for cls in filter_list:
|
|
179
|
+
element = Tag(name="p", attrs={"class": [cls]})
|
|
180
|
+
assert filter_elements(element) is None
|
|
181
|
+
|
|
182
|
+
def test_filter_elements_tags(self):
|
|
183
|
+
"""Test filter_elements function"""
|
|
184
|
+
tags = ["p", "li", "div", "table"]
|
|
185
|
+
for tag in tags:
|
|
186
|
+
element = Tag(name=tag)
|
|
187
|
+
assert filter_elements(element) == element
|
|
188
|
+
|
|
189
|
+
def test_filter_elements_bad_tag(self):
|
|
190
|
+
"""Test filter_elements function with bad tag"""
|
|
191
|
+
element = Tag(name="h1") # tag not in list
|
|
192
|
+
assert filter_elements(element) is None
|
|
193
|
+
|
|
194
|
+
def test_process_params_filtered_element(self):
|
|
195
|
+
"""Test process_params function with filtered element"""
|
|
196
|
+
element = Tag(name="p", attrs={"class": ["c-figure__media"]})
|
|
197
|
+
vulnerability, mitigation, notes = process_params(element, "", [], [], [])
|
|
198
|
+
assert vulnerability == []
|
|
199
|
+
assert mitigation == []
|
|
200
|
+
assert notes == []
|
|
201
|
+
|
|
202
|
+
def test_process_params_summary(self):
|
|
203
|
+
"""Test process_params function with summary"""
|
|
204
|
+
element = Tag(name="p")
|
|
205
|
+
element.string = "Lorem ipsum or something"
|
|
206
|
+
|
|
207
|
+
# check summary is appended to notes
|
|
208
|
+
vulnerability, mitigation, notes = process_params(element, "summary", [], [], [])
|
|
209
|
+
assert vulnerability == []
|
|
210
|
+
assert mitigation == []
|
|
211
|
+
assert notes == ["<p>Lorem ipsum or something</p>"]
|
|
212
|
+
|
|
213
|
+
# check summary is not appended to notes if already in notes
|
|
214
|
+
vulnerability, mitigation, notes = process_params(element, "summary", [], [], notes)
|
|
215
|
+
assert vulnerability == []
|
|
216
|
+
assert mitigation == []
|
|
217
|
+
assert notes == ["<p>Lorem ipsum or something</p>"]
|
|
218
|
+
|
|
219
|
+
def test_process_params_vulnerability(self):
|
|
220
|
+
"""Test process_params function with vulnerability"""
|
|
221
|
+
element = Tag(name="p")
|
|
222
|
+
element.string = "Lorem ipsum or something"
|
|
223
|
+
|
|
224
|
+
# check vulnerability is appended to vulnerability
|
|
225
|
+
vulnerability, mitigation, notes = process_params(element, "technical details", [], [], [])
|
|
226
|
+
assert vulnerability == ["<p>Lorem ipsum or something</p>"]
|
|
227
|
+
assert mitigation == []
|
|
228
|
+
assert notes == []
|
|
229
|
+
|
|
230
|
+
# check vulnerability is not appended to vulnerability if already in vulnerability
|
|
231
|
+
vulnerability, mitigation, notes = process_params(element, "technical details", vulnerability, [], [])
|
|
232
|
+
assert vulnerability == ["<p>Lorem ipsum or something</p>"]
|
|
233
|
+
assert mitigation == []
|
|
234
|
+
assert notes == []
|
|
235
|
+
|
|
236
|
+
def test_process_params_mitigation(self):
|
|
237
|
+
"""Test process_params function with mitigation"""
|
|
238
|
+
element = Tag(name="p")
|
|
239
|
+
element.string = "Lorem ipsum or something"
|
|
240
|
+
|
|
241
|
+
# check mitigation is appended to mitigation
|
|
242
|
+
vulnerability, mitigation, notes = process_params(element, "mitigations", [], [], [])
|
|
243
|
+
assert vulnerability == []
|
|
244
|
+
assert mitigation == ["<p>Lorem ipsum or something</p>"]
|
|
245
|
+
assert notes == []
|
|
246
|
+
|
|
247
|
+
# check mitigation is not appended to mitigation if already in mitigation
|
|
248
|
+
vulnerability, mitigation, notes = process_params(element, "mitigations", [], mitigation, [])
|
|
249
|
+
assert vulnerability == []
|
|
250
|
+
assert mitigation == ["<p>Lorem ipsum or something</p>"]
|
|
251
|
+
assert notes == []
|
|
252
|
+
|
|
253
|
+
def test_process_element_basic_non_header(self):
|
|
254
|
+
"""Test process_element with a basic non-header element"""
|
|
255
|
+
dat = Tag(name="p")
|
|
256
|
+
dat.string = "Some content"
|
|
257
|
+
last_header = {"type": "h2", "title": "Previous Header"}
|
|
258
|
+
last_h3 = "Previous H3"
|
|
259
|
+
nav_string = "technical details"
|
|
260
|
+
div_list = ["technical details", "mitigations", "summary"]
|
|
261
|
+
args = (
|
|
262
|
+
dat,
|
|
263
|
+
last_header,
|
|
264
|
+
last_h3,
|
|
265
|
+
nav_string,
|
|
266
|
+
div_list,
|
|
267
|
+
[], # vulnerability
|
|
268
|
+
[], # mitigation
|
|
269
|
+
[], # notes
|
|
270
|
+
)
|
|
271
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
272
|
+
assert new_last_header == last_header
|
|
273
|
+
assert new_last_h3 == last_h3
|
|
274
|
+
assert new_nav_string == nav_string
|
|
275
|
+
|
|
276
|
+
def test_process_element_header_update(self):
|
|
277
|
+
"""Test process_element updates headers for h1-h6 elements"""
|
|
278
|
+
for header_type in ["h1", "h2", "h3", "h4", "h5", "h6"]:
|
|
279
|
+
dat = Tag(name=header_type)
|
|
280
|
+
dat.string = f"New {header_type} Header"
|
|
281
|
+
last_header = {"type": "h2", "title": "Previous Header"}
|
|
282
|
+
last_h3 = "Previous H3"
|
|
283
|
+
args = (
|
|
284
|
+
dat,
|
|
285
|
+
last_header,
|
|
286
|
+
last_h3,
|
|
287
|
+
"", # nav_string
|
|
288
|
+
[], # div_list
|
|
289
|
+
[], # vulnerability
|
|
290
|
+
[], # mitigation
|
|
291
|
+
[], # notes
|
|
292
|
+
)
|
|
293
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
294
|
+
assert new_last_header == {"type": header_type, "title": f"New {header_type} Header"}
|
|
295
|
+
# Only h3 should update last_h3
|
|
296
|
+
assert new_last_h3 == (f"New {header_type} Header" if header_type == "h3" else last_h3)
|
|
297
|
+
assert new_nav_string == ""
|
|
298
|
+
|
|
299
|
+
def test_process_element_nav_string_update(self):
|
|
300
|
+
"""Test process_element updates nav_string when text matches div_list"""
|
|
301
|
+
div_list = ["technical details", "mitigations", "summary"]
|
|
302
|
+
for nav_text in div_list:
|
|
303
|
+
dat = Tag(name="p")
|
|
304
|
+
dat.string = nav_text
|
|
305
|
+
last_header = {"type": "h2", "title": "Some Header"}
|
|
306
|
+
last_h3 = "Some H3"
|
|
307
|
+
args = (
|
|
308
|
+
dat,
|
|
309
|
+
last_header,
|
|
310
|
+
last_h3,
|
|
311
|
+
"", # nav_string
|
|
312
|
+
div_list,
|
|
313
|
+
[], # vulnerability
|
|
314
|
+
[], # mitigation
|
|
315
|
+
[], # notes
|
|
316
|
+
)
|
|
317
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
318
|
+
assert new_last_header == last_header
|
|
319
|
+
assert new_last_h3 == last_h3
|
|
320
|
+
assert new_nav_string == nav_text
|
|
321
|
+
|
|
322
|
+
def test_process_element_process_params_conditions(self):
|
|
323
|
+
"""Test process_element calls process_params when all conditions are met"""
|
|
324
|
+
dat = Tag(name="p")
|
|
325
|
+
dat.string = "Content to process"
|
|
326
|
+
last_header = {"type": "h2", "title": "Some Header"}
|
|
327
|
+
last_h3 = "technical details" # matches div_list
|
|
328
|
+
nav_string = "technical details" # matches div_list
|
|
329
|
+
div_list = ["technical details", "mitigations", "summary"]
|
|
330
|
+
vulnerability = []
|
|
331
|
+
mitigation = []
|
|
332
|
+
notes = []
|
|
333
|
+
args = (
|
|
334
|
+
dat,
|
|
335
|
+
last_header,
|
|
336
|
+
last_h3,
|
|
337
|
+
nav_string,
|
|
338
|
+
div_list,
|
|
339
|
+
vulnerability,
|
|
340
|
+
mitigation,
|
|
341
|
+
notes,
|
|
342
|
+
)
|
|
343
|
+
with patch("regscale.integrations.public.cisa.process_params") as mock_process_params:
|
|
344
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
345
|
+
mock_process_params.assert_called_once_with(dat, nav_string, vulnerability, mitigation, notes)
|
|
346
|
+
assert new_last_header == last_header
|
|
347
|
+
assert new_last_h3 == last_h3
|
|
348
|
+
assert new_nav_string == nav_string
|
|
349
|
+
|
|
350
|
+
def test_process_element_no_process_params_missing_last_h3(self):
|
|
351
|
+
"""Test process_element doesn't call process_params when last_h3 is missing"""
|
|
352
|
+
dat = Tag(name="p")
|
|
353
|
+
dat.string = "Content to process"
|
|
354
|
+
last_header = {"type": "h2", "title": "Some Header"}
|
|
355
|
+
last_h3 = None # Missing last_h3
|
|
356
|
+
nav_string = "technical details"
|
|
357
|
+
div_list = ["technical details", "mitigations", "summary"]
|
|
358
|
+
args = (
|
|
359
|
+
dat,
|
|
360
|
+
last_header,
|
|
361
|
+
last_h3,
|
|
362
|
+
nav_string,
|
|
363
|
+
div_list,
|
|
364
|
+
[], # vulnerability
|
|
365
|
+
[], # mitigation
|
|
366
|
+
[], # notes
|
|
367
|
+
)
|
|
368
|
+
with patch("regscale.integrations.public.cisa.process_params") as mock_process_params:
|
|
369
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
370
|
+
mock_process_params.assert_not_called()
|
|
371
|
+
assert new_last_header == last_header
|
|
372
|
+
assert new_last_h3 == last_h3
|
|
373
|
+
assert new_nav_string == nav_string
|
|
374
|
+
|
|
375
|
+
def test_process_element_no_process_params_missing_nav_string(self):
|
|
376
|
+
"""Test process_element doesn't call process_params when nav_string is missing"""
|
|
377
|
+
dat = Tag(name="p")
|
|
378
|
+
dat.string = "Content to process"
|
|
379
|
+
last_header = {"type": "h2", "title": "Some Header"}
|
|
380
|
+
last_h3 = "technical details"
|
|
381
|
+
nav_string = "" # Missing nav_string
|
|
382
|
+
div_list = ["technical details", "mitigations", "summary"]
|
|
383
|
+
args = (
|
|
384
|
+
dat,
|
|
385
|
+
last_header,
|
|
386
|
+
last_h3,
|
|
387
|
+
nav_string,
|
|
388
|
+
div_list,
|
|
389
|
+
[], # vulnerability
|
|
390
|
+
[], # mitigation
|
|
391
|
+
[], # notes
|
|
392
|
+
)
|
|
393
|
+
with patch("regscale.integrations.public.cisa.process_params") as mock_process_params:
|
|
394
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
395
|
+
mock_process_params.assert_not_called()
|
|
396
|
+
assert new_last_header == last_header
|
|
397
|
+
assert new_last_h3 == last_h3
|
|
398
|
+
assert new_nav_string == nav_string
|
|
399
|
+
|
|
400
|
+
def test_process_element_no_process_params_h3_not_in_div_list(self):
|
|
401
|
+
"""Test process_element doesn't call process_params when last_h3 doesn't match div_list"""
|
|
402
|
+
dat = Tag(name="p")
|
|
403
|
+
dat.string = "Content to process"
|
|
404
|
+
last_header = {"type": "h2", "title": "Some Header"}
|
|
405
|
+
last_h3 = "unmatched h3" # Doesn't match div_list
|
|
406
|
+
nav_string = "technical details"
|
|
407
|
+
div_list = ["technical details", "mitigations", "summary"]
|
|
408
|
+
args = (
|
|
409
|
+
dat,
|
|
410
|
+
last_header,
|
|
411
|
+
last_h3,
|
|
412
|
+
nav_string,
|
|
413
|
+
div_list,
|
|
414
|
+
[], # vulnerability
|
|
415
|
+
[], # mitigation
|
|
416
|
+
[], # notes
|
|
417
|
+
)
|
|
418
|
+
with patch("regscale.integrations.public.cisa.process_params") as mock_process_params:
|
|
419
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
420
|
+
mock_process_params.assert_not_called()
|
|
421
|
+
assert new_last_header == last_header
|
|
422
|
+
assert new_last_h3 == last_h3
|
|
423
|
+
assert new_nav_string == nav_string
|
|
424
|
+
|
|
425
|
+
def test_process_element_no_process_params_content_in_div_list(self):
|
|
426
|
+
"""Test process_element doesn't call process_params when content matches div_list"""
|
|
427
|
+
dat = Tag(name="p")
|
|
428
|
+
dat.string = "technical details" # Matches div_list
|
|
429
|
+
last_header = {"type": "h2", "title": "Some Header"}
|
|
430
|
+
last_h3 = "technical details"
|
|
431
|
+
nav_string = "technical details"
|
|
432
|
+
div_list = ["technical details", "mitigations", "summary"]
|
|
433
|
+
args = (
|
|
434
|
+
dat,
|
|
435
|
+
last_header,
|
|
436
|
+
last_h3,
|
|
437
|
+
nav_string,
|
|
438
|
+
div_list,
|
|
439
|
+
[], # vulnerability
|
|
440
|
+
[], # mitigation
|
|
441
|
+
[], # notes
|
|
442
|
+
)
|
|
443
|
+
with patch("regscale.integrations.public.cisa.process_params") as mock_process_params:
|
|
444
|
+
new_last_header, new_last_h3, new_nav_string = process_element(args)
|
|
445
|
+
mock_process_params.assert_not_called()
|
|
446
|
+
assert new_last_header == last_header
|
|
447
|
+
assert new_last_h3 == last_h3
|
|
448
|
+
assert new_nav_string == nav_string
|
|
449
|
+
|
|
450
|
+
@patch("regscale.integrations.public.cisa.fuzzy_find_date", return_value="2023-01-01T00:00:00")
|
|
451
|
+
@patch("regscale.integrations.public.cisa.gen_soup")
|
|
452
|
+
def test_parse_details_with_content(self, mock_gen_soup, mock_find_date):
|
|
453
|
+
"""Test parse_details function with actual content processing"""
|
|
454
|
+
html = """
|
|
455
|
+
<div class="l-full__main">
|
|
456
|
+
<h2>Technical Details</h2>
|
|
457
|
+
<h3>technical details</h3>
|
|
458
|
+
<p>Vulnerability content</p>
|
|
459
|
+
<h2>Mitigations</h2>
|
|
460
|
+
<p>Mitigation content</p>
|
|
461
|
+
<h2>Summary</h2>
|
|
462
|
+
<p>Summary content</p>
|
|
463
|
+
</div>
|
|
464
|
+
"""
|
|
465
|
+
mock_gen_soup.return_value = BeautifulSoup(html, "html.parser")
|
|
466
|
+
|
|
467
|
+
result = parse_details("https://example.com")
|
|
468
|
+
|
|
469
|
+
assert result is not None
|
|
470
|
+
assert result[0] == "2023-01-01T00:00:00"
|
|
471
|
+
assert result[1] == ["<p>Vulnerability content</p>"]
|
|
472
|
+
assert result[2] == ["<p>Mitigation content</p>"]
|
|
473
|
+
assert result[3] == ["<p>Summary content</p>"]
|
|
474
|
+
|
|
475
|
+
@patch("regscale.integrations.public.cisa.fuzzy_find_date", return_value="2023-01-01T00:00:00")
|
|
476
|
+
@patch("regscale.integrations.public.cisa.gen_soup")
|
|
477
|
+
def test_parse_details_empty_content(self, mock_gen_soup, mock_find_date):
|
|
478
|
+
"""Test parse_details function with no content to process"""
|
|
479
|
+
html = """
|
|
480
|
+
<div class="l-full__main">
|
|
481
|
+
<h2>Some Header</h2>
|
|
482
|
+
<p>Some content that doesn't match any sections</p>
|
|
483
|
+
</div>
|
|
484
|
+
"""
|
|
485
|
+
mock_gen_soup.return_value = BeautifulSoup(html, "html.parser")
|
|
486
|
+
|
|
487
|
+
result = parse_details("https://example.com")
|
|
488
|
+
|
|
489
|
+
assert result is not None
|
|
490
|
+
assert result[0] == "2023-01-01T00:00:00"
|
|
491
|
+
assert result[1] == ["See Link for details."]
|
|
492
|
+
assert result[2] == ["See Link for details."]
|
|
493
|
+
assert result[3] == ["See Link for details."]
|
|
494
|
+
|
|
495
|
+
@patch("regscale.integrations.public.cisa.fuzzy_find_date", return_value=None)
|
|
496
|
+
@patch("regscale.integrations.public.cisa.gen_soup", return_value=MagicMock())
|
|
497
|
+
def test_parse_details_no_date(self, mock_gen_soup, mock_find_date):
|
|
498
|
+
"""Test parse_details function with no date"""
|
|
499
|
+
result = parse_details("https://example.com")
|
|
500
|
+
assert result is None
|
|
501
|
+
|
|
502
|
+
def test_fuzzy_find_date_first_regex(self):
|
|
503
|
+
"""Test fuzzy_find_date function with first call match"""
|
|
504
|
+
html1 = """<div class="c-field__content">Last Revised: January 15, 2024</div>"""
|
|
505
|
+
html2 = """<div class="c-field__content">Release Date: January 15, 2024</div>"""
|
|
506
|
+
soup1 = BeautifulSoup(html1, "html.parser")
|
|
507
|
+
soup2 = BeautifulSoup(html2, "html.parser")
|
|
508
|
+
assert fuzzy_find_date(soup1) == "2024-01-15T00:00:00"
|
|
509
|
+
assert fuzzy_find_date(soup2) == "2024-01-15T00:00:00"
|
|
510
|
+
|
|
511
|
+
def test_fuzzy_find_date(self):
|
|
512
|
+
"""Test fuzzy_find_date's recursive functionality"""
|
|
513
|
+
html1 = """
|
|
514
|
+
<div class="c-field__content">Some other content</div>
|
|
515
|
+
<div class="c-field__content">More content</div>
|
|
516
|
+
<div class="c-field__content">January 15, 2024</div>
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
for i in range(0, 5):
|
|
520
|
+
with patch(
|
|
521
|
+
"regscale.integrations.public.cisa.fuzzy_find_date", wraps=fuzzy_find_date
|
|
522
|
+
) as mock_fuzzy_find_date:
|
|
523
|
+
html1 = '<div class="c-field__content">Content</div>' + html1 if i > 0 else html1
|
|
524
|
+
soup1 = BeautifulSoup(html1, "html.parser")
|
|
525
|
+
assert fuzzy_find_date(soup1) == "2024-01-15T00:00:00"
|
|
526
|
+
assert mock_fuzzy_find_date.call_count == i
|
|
527
|
+
|
|
528
|
+
def test_fuzzy_find_date_not_found(self):
|
|
529
|
+
"""Test fuzzy_find_date function when we run out of attempts"""
|
|
530
|
+
html1 = """<div class="c-field__content">Invalid Date</div>"""
|
|
531
|
+
soup1 = BeautifulSoup(html1, "html.parser")
|
|
532
|
+
with patch("regscale.integrations.public.cisa.fuzzy_find_date", wraps=fuzzy_find_date) as mock_fuzzy_find_date:
|
|
533
|
+
# throw parser error to skip date parsing and force next attempt
|
|
534
|
+
with patch("regscale.integrations.public.cisa.BeautifulSoup.find_all", side_effect=dparser.ParserError):
|
|
535
|
+
assert fuzzy_find_date(soup1) is None
|
|
536
|
+
assert mock_fuzzy_find_date.call_count == 5 # Should try all 5 attempts
|
|
537
|
+
|
|
538
|
+
def test_gen_soup(self):
|
|
539
|
+
"""Test gen_soup function"""
|
|
540
|
+
html = "<html><body>Test</body></html>"
|
|
541
|
+
mock_response = MagicMock()
|
|
542
|
+
mock_response.content = html
|
|
543
|
+
mock_response.raise_for_status = MagicMock()
|
|
544
|
+
|
|
545
|
+
with patch("regscale.integrations.public.cisa.Api.get", return_value=mock_response) as mock_api_get:
|
|
546
|
+
soup = gen_soup("https://example.com")
|
|
547
|
+
assert soup is not None
|
|
548
|
+
mock_api_get.assert_called_once_with("https://example.com")
|
|
549
|
+
mock_response.raise_for_status.assert_called_once()
|
|
550
|
+
|
|
551
|
+
def test_gen_soup_tuple(self):
|
|
552
|
+
"""Test gen_soup function with tuple"""
|
|
553
|
+
html = "<html><body>Test</body></html>"
|
|
554
|
+
mock_response = MagicMock()
|
|
555
|
+
mock_response.content = html
|
|
556
|
+
mock_response.raise_for_status = MagicMock()
|
|
557
|
+
with patch("regscale.integrations.public.cisa.Api.get", return_value=mock_response) as mock_api_get:
|
|
558
|
+
soup = gen_soup(("https://example.com", "https://example2.com"))
|
|
559
|
+
assert soup is not None
|
|
560
|
+
mock_api_get.assert_called_once_with("https://example.com")
|
|
561
|
+
mock_response.raise_for_status.assert_called_once()
|
|
562
|
+
|
|
563
|
+
def test_gen_soup_invalid_url(self):
|
|
564
|
+
"""Test gen_soup function with invalid url"""
|
|
565
|
+
# Test with a string that's not a URL
|
|
566
|
+
with pytest.raises(URLError) as context:
|
|
567
|
+
gen_soup("not_a_url")
|
|
568
|
+
assert context.type == URLError
|
|
569
|
+
assert context.value.reason == "URL is invalid, exiting..."
|
|
570
|
+
|
|
571
|
+
def test_pull_cisa_kev_integration(self):
|
|
572
|
+
"""Test pull_cisa_kev with actual call"""
|
|
573
|
+
# Clear any cached data
|
|
574
|
+
if hasattr(pull_cisa_kev, "_cached_data"):
|
|
575
|
+
delattr(pull_cisa_kev, "_cached_data")
|
|
576
|
+
|
|
577
|
+
data = pull_cisa_kev()
|
|
578
|
+
assert "title" in data.keys()
|
|
579
|
+
assert data["title"] == "CISA Catalog of Known Exploited Vulnerabilities"
|
|
580
|
+
|
|
581
|
+
@patch("regscale.integrations.public.cisa.Api.get")
|
|
582
|
+
def test_pull_cisa_kev_success(self, mock_get):
|
|
583
|
+
"""Test pull_cisa_kev successful API call"""
|
|
584
|
+
# Clear any cached data
|
|
585
|
+
if hasattr(pull_cisa_kev, "_cached_data"):
|
|
586
|
+
delattr(pull_cisa_kev, "_cached_data")
|
|
587
|
+
|
|
588
|
+
mock_response = MagicMock()
|
|
589
|
+
mock_response.json.return_value = {"title": "Test Data"}
|
|
590
|
+
mock_get.return_value = mock_response
|
|
591
|
+
|
|
592
|
+
# First call should hit the API
|
|
593
|
+
data = pull_cisa_kev()
|
|
594
|
+
assert data == {"title": "Test Data"}
|
|
595
|
+
mock_get.assert_called_once()
|
|
596
|
+
|
|
597
|
+
# Second call should use cached data
|
|
598
|
+
mock_get.reset_mock()
|
|
599
|
+
data = pull_cisa_kev()
|
|
600
|
+
assert data == {"title": "Test Data"}
|
|
601
|
+
mock_get.assert_not_called()
|
|
602
|
+
|
|
603
|
+
@patch("regscale.integrations.public.cisa.Api.get")
|
|
604
|
+
def test_pull_cisa_kev_fallback(self, mock_get):
|
|
605
|
+
"""Test pull_cisa_kev falls back to package data on error"""
|
|
606
|
+
# Clear any cached data
|
|
607
|
+
if hasattr(pull_cisa_kev, "_cached_data"):
|
|
608
|
+
delattr(pull_cisa_kev, "_cached_data")
|
|
609
|
+
|
|
610
|
+
mock_get.side_effect = RequestException("API Error")
|
|
611
|
+
|
|
612
|
+
data = pull_cisa_kev()
|
|
613
|
+
assert "title" in data
|
|
614
|
+
assert data["title"] == "CISA Catalog of Known Exploited Vulnerabilities"
|
|
615
|
+
|
|
616
|
+
@patch("regscale.integrations.public.cisa.Api.get")
|
|
617
|
+
def test_pull_cisa_kev_config_url(self, mock_get):
|
|
618
|
+
"""Test pull_cisa_kev with custom URL from config"""
|
|
619
|
+
# Clear any cached data
|
|
620
|
+
if hasattr(pull_cisa_kev, "_cached_data"):
|
|
621
|
+
delattr(pull_cisa_kev, "_cached_data")
|
|
622
|
+
|
|
623
|
+
mock_response = MagicMock()
|
|
624
|
+
mock_response.json.return_value = {"title": "Custom Data"}
|
|
625
|
+
mock_get.return_value = mock_response
|
|
626
|
+
|
|
627
|
+
# Set up custom URL in config
|
|
628
|
+
app = Application()
|
|
629
|
+
kev_url = app.config["cisaKev"] or None
|
|
630
|
+
app.config["cisaKev"] = "https://custom.url"
|
|
631
|
+
app.save_config(app.config)
|
|
632
|
+
app.logger.info(f"cisaKev: {app.config['cisaKev']}")
|
|
633
|
+
|
|
634
|
+
data = pull_cisa_kev()
|
|
635
|
+
assert data == {"title": "Custom Data"}
|
|
636
|
+
mock_get.assert_called_once_with(url="https://custom.url", headers={}, retry_login=False)
|
|
637
|
+
|
|
638
|
+
# cleanup
|
|
639
|
+
if kev_url:
|
|
640
|
+
app.config["cisaKev"] = kev_url
|
|
641
|
+
else:
|
|
642
|
+
app.config.pop("cisaKev")
|
|
643
|
+
app.save_config(app.config)
|
|
644
|
+
|
|
645
|
+
@patch("regscale.integrations.public.cisa.Api.get")
|
|
646
|
+
def test_pull_cisa_kev_var_url(self, mock_get):
|
|
647
|
+
"""Test pull_cisa_kev with default cisa url from integration"""
|
|
648
|
+
# Clear any cached data
|
|
649
|
+
if hasattr(pull_cisa_kev, "_cached_data"):
|
|
650
|
+
delattr(pull_cisa_kev, "_cached_data")
|
|
651
|
+
|
|
652
|
+
mock_response = MagicMock()
|
|
653
|
+
mock_response.json.return_value = {"title": "Custom Data"}
|
|
654
|
+
mock_get.return_value = mock_response
|
|
655
|
+
|
|
656
|
+
# Remove url from config
|
|
657
|
+
app = Application()
|
|
658
|
+
kev_url = app.config["cisaKev"] or None
|
|
659
|
+
app.config.pop("cisaKev")
|
|
660
|
+
app.save_config(app.config)
|
|
661
|
+
|
|
662
|
+
data = pull_cisa_kev()
|
|
663
|
+
assert data == {"title": "Custom Data"}
|
|
664
|
+
mock_get.assert_called_once()
|
|
665
|
+
|
|
666
|
+
# cleanup
|
|
667
|
+
if kev_url:
|
|
668
|
+
app.config["cisaKev"] = kev_url
|
|
669
|
+
app.save_config(app.config)
|
|
670
|
+
|
|
671
|
+
def test_merge_old(self):
|
|
672
|
+
"""Test merge_old function"""
|
|
673
|
+
update_vuln = {"id": 2, "name": "Test Threat", "title": "New Title", "description": "New Description"}
|
|
674
|
+
old_vuln = {
|
|
675
|
+
"id": 1,
|
|
676
|
+
"uuid": "123",
|
|
677
|
+
"status": "Active",
|
|
678
|
+
"source": "CISA",
|
|
679
|
+
"threatType": "Specific",
|
|
680
|
+
"threatOwnerId": 456,
|
|
681
|
+
"notes": "Old Notes",
|
|
682
|
+
"targetType": "Other",
|
|
683
|
+
"dateCreated": "2024-01-01",
|
|
684
|
+
"isPublic": True,
|
|
685
|
+
"investigated": False,
|
|
686
|
+
"investigationResults": "Closed",
|
|
687
|
+
}
|
|
688
|
+
expected = {
|
|
689
|
+
"id": 1,
|
|
690
|
+
"name": "Test Threat",
|
|
691
|
+
"title": "New Title",
|
|
692
|
+
"description": "New Description",
|
|
693
|
+
"uuid": "123",
|
|
694
|
+
"status": "Active",
|
|
695
|
+
"source": "CISA",
|
|
696
|
+
"threatType": "Specific",
|
|
697
|
+
"threatOwnerId": 456,
|
|
698
|
+
"notes": "Old Notes",
|
|
699
|
+
"targetType": "Other",
|
|
700
|
+
"dateCreated": "2024-01-01",
|
|
701
|
+
"isPublic": True,
|
|
702
|
+
"investigated": False,
|
|
703
|
+
"investigationResults": "Closed",
|
|
704
|
+
}
|
|
705
|
+
assert merge_old(update_vuln, old_vuln) == expected
|
|
706
|
+
assert merge_old(update_vuln, {}) == update_vuln
|
|
707
|
+
assert merge_old({}, old_vuln) == {
|
|
708
|
+
"id": 1,
|
|
709
|
+
"uuid": "123",
|
|
710
|
+
"status": "Active",
|
|
711
|
+
"source": "CISA",
|
|
712
|
+
"threatType": "Specific",
|
|
713
|
+
"threatOwnerId": 456,
|
|
714
|
+
"notes": "Old Notes",
|
|
715
|
+
"targetType": "Other",
|
|
716
|
+
"dateCreated": "2024-01-01",
|
|
717
|
+
"isPublic": True,
|
|
718
|
+
"investigated": False,
|
|
719
|
+
"investigationResults": "Closed",
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
@patch("regscale.integrations.public.cisa.Api.post", return_value=None)
|
|
723
|
+
def test_insert_or_upd_threat_insert(self, mock_post):
|
|
724
|
+
"""Test insert_or_upd_threat function"""
|
|
725
|
+
threat = {}
|
|
726
|
+
app = MagicMock()
|
|
727
|
+
insert_or_upd_threat(threat, app)
|
|
728
|
+
mock_post.assert_called_once()
|
|
729
|
+
|
|
730
|
+
@patch("regscale.integrations.public.cisa.Api.put", return_value=None)
|
|
731
|
+
def test_insert_or_upd_threat_update(self, mock_put):
|
|
732
|
+
"""Test insert_or_upd_threat function"""
|
|
733
|
+
threat = {}
|
|
734
|
+
app = MagicMock()
|
|
735
|
+
insert_or_upd_threat(threat, app, 1)
|
|
736
|
+
mock_put.assert_called_once()
|
|
737
|
+
|
|
738
|
+
@patch("regscale.integrations.public.cisa.Threat.bulk_update", return_value=None)
|
|
739
|
+
def test_update_regscale_threats(self, mock_bulk_update):
|
|
740
|
+
"""Test update_regscale_threats function"""
|
|
741
|
+
threats = [Threat()]
|
|
742
|
+
update_regscale_threats(threats)
|
|
743
|
+
mock_bulk_update.assert_called_once_with(None, threats)
|
|
744
|
+
|
|
745
|
+
@patch("regscale.integrations.public.cisa.Threat.bulk_update", return_value=None)
|
|
746
|
+
def test_update_regscale_threats_no_threats(self, mock_bulk_update):
|
|
747
|
+
"""Test update_regscale_threats function with no threats"""
|
|
748
|
+
update_regscale_threats()
|
|
749
|
+
update_regscale_threats([])
|
|
750
|
+
mock_bulk_update.assert_not_called()
|
|
751
|
+
|
|
752
|
+
def test_convert_date_string(self):
|
|
753
|
+
"""Test convert_date_string function"""
|
|
754
|
+
date_str = "2022-11-03"
|
|
755
|
+
assert convert_date_string(date_str) == "2022-11-03T00:00:00.000Z"
|
|
756
|
+
|
|
757
|
+
def test_unique(self):
|
|
758
|
+
"""Test unique function"""
|
|
759
|
+
test_list = ["a", "b", "c", "a", "b", "c"]
|
|
760
|
+
assert unique(test_list) == ["a", "b", "c"]
|
|
761
|
+
|
|
762
|
+
def test_is_url(self):
|
|
763
|
+
"""Test is_url function"""
|
|
764
|
+
assert is_url("https://example.com")
|
|
765
|
+
assert not is_url("not_a_url")
|
|
766
|
+
assert not is_url("")
|
|
767
|
+
|
|
768
|
+
def test_is_url_value_error(self):
|
|
769
|
+
"""Test is_url function raising a ValueError"""
|
|
770
|
+
with patch("regscale.integrations.public.cisa.urlparse", side_effect=ValueError):
|
|
771
|
+
assert not is_url("not_a_url")
|
|
772
|
+
|
|
773
|
+
@patch("regscale.integrations.public.cisa.update_regscale_threats")
|
|
774
|
+
@patch("regscale.integrations.public.cisa.Threat.bulk_insert")
|
|
775
|
+
@patch("regscale.integrations.public.cisa.Threat.fetch_all_threats")
|
|
776
|
+
@patch("regscale.integrations.public.cisa.Application")
|
|
777
|
+
def test_update_regscale(self, mock_app, mock_fetch_threats, mock_bulk_insert, mock_update_threats):
|
|
778
|
+
"""Test update_regscale function with both insert and update"""
|
|
779
|
+
# Setup mocks
|
|
780
|
+
mock_app_instance = MagicMock()
|
|
781
|
+
mock_app_instance.config = {"userId": "123"}
|
|
782
|
+
mock_app.return_value = mock_app_instance
|
|
783
|
+
|
|
784
|
+
# Mock existing threats
|
|
785
|
+
existing_threat = Threat(
|
|
786
|
+
id=1,
|
|
787
|
+
description="Qualcomm Multiple Chipsets Incorrect Authorization Vulnerability",
|
|
788
|
+
investigationResults="Some results",
|
|
789
|
+
threatType="Specific",
|
|
790
|
+
title="CVE-2025-21479",
|
|
791
|
+
threatOwnerId="123",
|
|
792
|
+
targetType="Other",
|
|
793
|
+
source="Open Source",
|
|
794
|
+
dateCreated=datetime.now().isoformat(),
|
|
795
|
+
status="Initial Report/Notification",
|
|
796
|
+
)
|
|
797
|
+
mock_fetch_threats.return_value = [existing_threat]
|
|
798
|
+
|
|
799
|
+
data = {
|
|
800
|
+
"title": "CISA Catalog of Known Exploited Vulnerabilities",
|
|
801
|
+
"catalogVersion": "2025.06.03",
|
|
802
|
+
"dateReleased": "2025-06-03T16:48:39.9414Z",
|
|
803
|
+
"count": 3,
|
|
804
|
+
"vulnerabilities": [
|
|
805
|
+
{
|
|
806
|
+
"cveID": "CVE-2025-21479",
|
|
807
|
+
"vendorProject": "Qualcomm",
|
|
808
|
+
"product": "Multiple Chipsets",
|
|
809
|
+
"vulnerabilityName": "Qualcomm Multiple Chipsets Incorrect Authorization Vulnerability",
|
|
810
|
+
"dateAdded": "2025-06-03",
|
|
811
|
+
"shortDescription": "Multiple Qualcomm chipsets contain an incorrect authorization vulnerability.",
|
|
812
|
+
"requiredAction": "Apply mitigations per vendor instructions",
|
|
813
|
+
"notes": "Please check with specific vendors",
|
|
814
|
+
"cwes": ["CWE-863"],
|
|
815
|
+
"dueDate": "2025-06-24",
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
"cveID": "CVE-2025-27038",
|
|
819
|
+
"vendorProject": "Qualcomm",
|
|
820
|
+
"product": "Multiple Chipsets",
|
|
821
|
+
"vulnerabilityName": "Qualcomm Multiple Chipsets Use-After-Free Vulnerability",
|
|
822
|
+
"dateAdded": "2025-06-03",
|
|
823
|
+
"shortDescription": "Multiple Qualcomm chipsets contain a use-after-free vulnerability.",
|
|
824
|
+
"requiredAction": "Apply mitigations per vendor instructions",
|
|
825
|
+
"notes": "Please check with specific vendors",
|
|
826
|
+
"cwes": ["CWE-416"],
|
|
827
|
+
"dueDate": "2025-06-24",
|
|
828
|
+
},
|
|
829
|
+
],
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
try:
|
|
833
|
+
update_regscale(data)
|
|
834
|
+
except Exception as e:
|
|
835
|
+
assert False, "update_regscale raised exception: {}".format(e)
|
|
836
|
+
|
|
837
|
+
# Verify new threats were inserted
|
|
838
|
+
mock_bulk_insert.assert_called_once()
|
|
839
|
+
inserted_threats = mock_bulk_insert.call_args[0][1]
|
|
840
|
+
assert len(inserted_threats) == 1 # should only insert second vuln
|
|
841
|
+
assert inserted_threats[0]["title"] == "CVE-2025-27038"
|
|
842
|
+
|
|
843
|
+
# Verify existing threats were updated
|
|
844
|
+
mock_update_threats.assert_called_once()
|
|
845
|
+
updated_threats = mock_update_threats.call_args[1]["json_list"]
|
|
846
|
+
assert len(updated_threats) == 1 # only update first vuln
|
|
847
|
+
assert updated_threats[0]["title"] == "CVE-2025-21479"
|
|
848
|
+
assert "investigationResults" in updated_threats[0] # preserve investigation results
|
|
849
|
+
|
|
850
|
+
def test_parsing(self):
|
|
851
|
+
"""Test link parse method"""
|
|
852
|
+
links = [
|
|
853
|
+
"https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-039a",
|
|
854
|
+
"https://www.cisa.gov/news-events/cybersecurity-advisories/aa22-249a",
|
|
855
|
+
"https://www.cisa.gov/news-events/cybersecurity-advisories/aa22-131a",
|
|
856
|
+
]
|
|
857
|
+
for link in links:
|
|
858
|
+
dat = parse_details(link)
|
|
859
|
+
assert dat is not None
|
|
860
|
+
|
|
861
|
+
@patch("regscale.core.app.api.Api.get")
|
|
862
|
+
def test_load_from_package(self, mock_pull_cisa_kev):
|
|
863
|
+
mock_pull_cisa_kev.return_value = None
|
|
864
|
+
data = pull_cisa_kev()
|
|
865
|
+
assert data is not None
|
|
866
|
+
|
|
867
|
+
@patch("regscale.integrations.public.cisa.is_valid", return_value=False)
|
|
868
|
+
def test_alerts_bad_app(self, mock_is_valid):
|
|
869
|
+
"""Test alerts function with bad app"""
|
|
870
|
+
with pytest.raises(SystemExit) as e:
|
|
871
|
+
alerts(1)
|
|
872
|
+
mock_is_valid.assert_called_once()
|
|
873
|
+
assert e.type == SystemExit
|
|
874
|
+
assert e.value.code == 1
|
|
875
|
+
|
|
876
|
+
@patch("regscale.integrations.public.cisa.parse_html")
|
|
877
|
+
@patch("regscale.integrations.public.cisa.update_regscale_links")
|
|
878
|
+
@patch("regscale.integrations.public.cisa.update_regscale_threats")
|
|
879
|
+
@patch("regscale.integrations.public.cisa.Threat.fetch_all_threats")
|
|
880
|
+
@patch("regscale.integrations.public.cisa.process_threats")
|
|
881
|
+
def test_alerts_no_threats(
|
|
882
|
+
self,
|
|
883
|
+
mock_process_threats,
|
|
884
|
+
mock_fetch_threats,
|
|
885
|
+
mock_update_regscale_links,
|
|
886
|
+
mock_update_regscale_threats,
|
|
887
|
+
mock_parse_html,
|
|
888
|
+
):
|
|
889
|
+
"""Test alerts function with no threats from CISA"""
|
|
890
|
+
mock_fetch_threats.return_value = []
|
|
891
|
+
mock_parse_html.return_value = []
|
|
892
|
+
alerts(1)
|
|
893
|
+
mock_parse_html.assert_called_once()
|
|
894
|
+
mock_process_threats.assert_not_called()
|
|
895
|
+
mock_update_regscale_links.assert_not_called()
|
|
896
|
+
mock_update_regscale_threats.assert_not_called()
|
|
897
|
+
|
|
898
|
+
@patch("regscale.integrations.public.cisa.parse_html")
|
|
899
|
+
@patch("regscale.integrations.public.cisa.update_regscale_links")
|
|
900
|
+
@patch("regscale.integrations.public.cisa.update_regscale_threats")
|
|
901
|
+
@patch("regscale.integrations.public.cisa.Threat.fetch_all_threats")
|
|
902
|
+
@patch("regscale.integrations.public.cisa.process_threats")
|
|
903
|
+
@patch("regscale.integrations.public.cisa.Threat.bulk_insert")
|
|
904
|
+
def test_alerts_insert(
|
|
905
|
+
self,
|
|
906
|
+
mock_bulk_insert,
|
|
907
|
+
mock_process_threats,
|
|
908
|
+
mock_fetch_threats,
|
|
909
|
+
mock_update_regscale_threats,
|
|
910
|
+
mock_update_regscale_links,
|
|
911
|
+
mock_parse_html,
|
|
912
|
+
):
|
|
913
|
+
"""Test alerts function with inserts"""
|
|
914
|
+
insert_threats = [
|
|
915
|
+
{
|
|
916
|
+
"title": "CVE-2025-21479",
|
|
917
|
+
"description": "Qualcomm Multiple Chipsets Incorrect Authorization Vulnerability",
|
|
918
|
+
"threatType": "Specific",
|
|
919
|
+
"targetType": "Other",
|
|
920
|
+
"source": "Open Source",
|
|
921
|
+
"status": "Initial Report/Notification",
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
mock_parse_html.return_value = ["Threat"]
|
|
925
|
+
mock_process_threats.return_value = (insert_threats, [])
|
|
926
|
+
alerts(1)
|
|
927
|
+
mock_process_threats.assert_called_once()
|
|
928
|
+
mock_bulk_insert.assert_called_once()
|
|
929
|
+
mock_update_regscale_links.assert_called_once()
|
|
930
|
+
mock_update_regscale_threats.assert_not_called()
|
|
931
|
+
|
|
932
|
+
@patch("regscale.integrations.public.cisa.parse_html")
|
|
933
|
+
@patch("regscale.integrations.public.cisa.update_regscale_links")
|
|
934
|
+
@patch("regscale.integrations.public.cisa.update_regscale_threats")
|
|
935
|
+
@patch("regscale.integrations.public.cisa.Threat.fetch_all_threats")
|
|
936
|
+
@patch("regscale.integrations.public.cisa.process_threats")
|
|
937
|
+
@patch("regscale.integrations.public.cisa.Threat.bulk_insert")
|
|
938
|
+
def test_alerts_update(
|
|
939
|
+
self,
|
|
940
|
+
mock_bulk_insert,
|
|
941
|
+
mock_process_threats,
|
|
942
|
+
mock_fetch_threats,
|
|
943
|
+
mock_update_regscale_threats,
|
|
944
|
+
mock_update_regscale_links,
|
|
945
|
+
mock_parse_html,
|
|
946
|
+
):
|
|
947
|
+
"""Test alerts function with updates"""
|
|
948
|
+
update_threats = [
|
|
949
|
+
{
|
|
950
|
+
"title": "CVE-2025-21479",
|
|
951
|
+
"description": "Qualcomm Multiple Chipsets Incorrect Authorization Vulnerability",
|
|
952
|
+
"threatType": "Specific",
|
|
953
|
+
"targetType": "Other",
|
|
954
|
+
"source": "Open Source",
|
|
955
|
+
"status": "Initial Report/Notification",
|
|
956
|
+
}
|
|
957
|
+
]
|
|
958
|
+
mock_parse_html.return_value = ["Threat"]
|
|
959
|
+
mock_process_threats.return_value = ([], update_threats)
|
|
960
|
+
alerts(1)
|
|
961
|
+
mock_process_threats.assert_called_once()
|
|
962
|
+
mock_update_regscale_threats.assert_called_once()
|
|
963
|
+
mock_bulk_insert.assert_not_called()
|
|
964
|
+
mock_update_regscale_links.assert_not_called()
|
|
965
|
+
|
|
966
|
+
@pytest.mark.skip("Skipping alerts integration test due to forbidden url error")
|
|
967
|
+
@patch("regscale.integrations.public.cisa.Threat.fetch_all_threats", wraps=Threat.fetch_all_threats)
|
|
968
|
+
@patch("regscale.integrations.public.cisa.parse_html", wraps=parse_html)
|
|
969
|
+
def test_alerts(self, mock_parse_html, mock_fetch_threats):
|
|
970
|
+
"""Integration test for alerts function"""
|
|
971
|
+
alerts(2021)
|
|
972
|
+
|
|
973
|
+
# Verify core operations were called
|
|
974
|
+
mock_fetch_threats.assert_called_once()
|
|
975
|
+
mock_parse_html.assert_called_once()
|
|
976
|
+
|
|
977
|
+
@pytest.mark.skip("Skipping kev integration test to due api error")
|
|
978
|
+
def test_cisa_integration(self):
|
|
979
|
+
"""Full integration test of CISA KEV ingestion"""
|
|
980
|
+
data = pull_cisa_kev()
|
|
981
|
+
assert data is not None
|
|
982
|
+
assert "title" in data.keys()
|
|
983
|
+
assert data["title"] == "CISA Catalog of Known Exploited Vulnerabilities"
|
|
984
|
+
assert "vulnerabilities" in data
|
|
985
|
+
|
|
986
|
+
update_regscale(data)
|
|
987
|
+
|
|
988
|
+
reg_threats = Threat.fetch_all_threats()
|
|
989
|
+
assert len(reg_threats) > 0
|
|
990
|
+
|
|
991
|
+
@patch("regscale.integrations.public.cisa.build_threat")
|
|
992
|
+
@patch("regscale.integrations.public.cisa.gen_soup")
|
|
993
|
+
def test_parse_html(self, mock_gen_soup, mock_build_threat):
|
|
994
|
+
"""Test parse_html function's core parsing logic"""
|
|
995
|
+
mock_soup = MagicMock()
|
|
996
|
+
mock_article = MagicMock()
|
|
997
|
+
mock_article.text = "Some Title | CISA Alert"
|
|
998
|
+
mock_link = MagicMock()
|
|
999
|
+
mock_link.__getitem__.return_value = "/some/path" # This handles the href access
|
|
1000
|
+
mock_article.find_all.return_value = [mock_link]
|
|
1001
|
+
|
|
1002
|
+
# First call returns one article, second call returns empty list
|
|
1003
|
+
mock_soup.find_all.side_effect = [[mock_article], []]
|
|
1004
|
+
mock_gen_soup.return_value = mock_soup
|
|
1005
|
+
|
|
1006
|
+
mock_threat = Threat(
|
|
1007
|
+
title="CISA Alert",
|
|
1008
|
+
description="Test Description",
|
|
1009
|
+
threatType="Specific",
|
|
1010
|
+
targetType="Other",
|
|
1011
|
+
source="Open Source",
|
|
1012
|
+
status="Initial Report/Notification",
|
|
1013
|
+
)
|
|
1014
|
+
mock_build_threat.return_value = mock_threat
|
|
1015
|
+
|
|
1016
|
+
app = Application()
|
|
1017
|
+
result = parse_html("https://example.com", app)
|
|
1018
|
+
|
|
1019
|
+
assert mock_gen_soup.call_count == 2
|
|
1020
|
+
assert mock_build_threat.call_count == 2
|
|
1021
|
+
assert result == [mock_threat]
|