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,792 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Unit tests for AWS Security Hub collector."""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from botocore.exceptions import ClientError
|
|
10
|
+
|
|
11
|
+
from regscale.integrations.commercial.aws.inventory.resources.securityhub import SecurityHubCollector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestSecurityHubCollector:
|
|
15
|
+
"""Test suite for AWS Security Hub collector."""
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_session(self):
|
|
19
|
+
"""Create a mock AWS session."""
|
|
20
|
+
session = MagicMock()
|
|
21
|
+
return session
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_client(self):
|
|
25
|
+
"""Create a mock Security Hub client."""
|
|
26
|
+
client = MagicMock()
|
|
27
|
+
return client
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def collector_no_account(self, mock_session):
|
|
31
|
+
"""Create a SecurityHub collector without account_id."""
|
|
32
|
+
return SecurityHubCollector(session=mock_session, region="us-east-1", account_id=None)
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def collector_with_account(self, mock_session):
|
|
36
|
+
"""Create a SecurityHub collector with account_id."""
|
|
37
|
+
return SecurityHubCollector(session=mock_session, region="us-east-1", account_id="123456789012")
|
|
38
|
+
|
|
39
|
+
def test_initialization_without_account_id(self, mock_session):
|
|
40
|
+
"""Test initialization without account_id."""
|
|
41
|
+
collector = SecurityHubCollector(session=mock_session, region="us-west-2")
|
|
42
|
+
|
|
43
|
+
assert collector.session == mock_session
|
|
44
|
+
assert collector.region == "us-west-2"
|
|
45
|
+
assert collector.account_id is None
|
|
46
|
+
|
|
47
|
+
def test_initialization_with_account_id(self, mock_session):
|
|
48
|
+
"""Test initialization with account_id."""
|
|
49
|
+
collector = SecurityHubCollector(session=mock_session, region="us-west-2", account_id="123456789012")
|
|
50
|
+
|
|
51
|
+
assert collector.session == mock_session
|
|
52
|
+
assert collector.region == "us-west-2"
|
|
53
|
+
assert collector.account_id == "123456789012"
|
|
54
|
+
|
|
55
|
+
@patch.object(SecurityHubCollector, "_get_client")
|
|
56
|
+
@patch.object(SecurityHubCollector, "_describe_hub")
|
|
57
|
+
@patch.object(SecurityHubCollector, "_get_enabled_standards")
|
|
58
|
+
@patch.object(SecurityHubCollector, "_describe_standards")
|
|
59
|
+
@patch.object(SecurityHubCollector, "_list_security_controls")
|
|
60
|
+
@patch.object(SecurityHubCollector, "_get_findings")
|
|
61
|
+
@patch.object(SecurityHubCollector, "_get_insights")
|
|
62
|
+
@patch.object(SecurityHubCollector, "_list_members")
|
|
63
|
+
def test_collect_success(
|
|
64
|
+
self,
|
|
65
|
+
mock_list_members,
|
|
66
|
+
mock_get_insights,
|
|
67
|
+
mock_get_findings,
|
|
68
|
+
mock_list_controls,
|
|
69
|
+
mock_describe_standards,
|
|
70
|
+
mock_get_enabled_standards,
|
|
71
|
+
mock_describe_hub,
|
|
72
|
+
mock_get_client,
|
|
73
|
+
collector_no_account,
|
|
74
|
+
mock_client,
|
|
75
|
+
):
|
|
76
|
+
"""Test successful collection of all Security Hub resources."""
|
|
77
|
+
mock_get_client.return_value = mock_client
|
|
78
|
+
|
|
79
|
+
# Setup mock returns
|
|
80
|
+
mock_hub_config = {"HubArn": "arn:aws:securityhub:us-east-1:123456789012:hub/default", "Region": "us-east-1"}
|
|
81
|
+
mock_describe_hub.return_value = mock_hub_config
|
|
82
|
+
|
|
83
|
+
mock_enabled_stds = [
|
|
84
|
+
{
|
|
85
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
mock_get_enabled_standards.return_value = mock_enabled_stds
|
|
89
|
+
|
|
90
|
+
mock_standards = [
|
|
91
|
+
{
|
|
92
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0",
|
|
93
|
+
"Name": "AWS Foundational Security Best Practices",
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
mock_describe_standards.return_value = mock_standards
|
|
97
|
+
|
|
98
|
+
mock_controls = [
|
|
99
|
+
{"SecurityControlId": "IAM.1", "Title": "IAM policies should not allow full '*' administrative privileges"}
|
|
100
|
+
]
|
|
101
|
+
mock_list_controls.return_value = mock_controls
|
|
102
|
+
|
|
103
|
+
mock_findings = [{"Id": "finding-1", "Title": "Test Finding", "Region": "us-east-1"}]
|
|
104
|
+
mock_get_findings.return_value = mock_findings
|
|
105
|
+
|
|
106
|
+
mock_insights = [
|
|
107
|
+
{
|
|
108
|
+
"InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/abc123",
|
|
109
|
+
"Name": "Test Insight",
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
mock_get_insights.return_value = mock_insights
|
|
113
|
+
|
|
114
|
+
mock_members = [{"AccountId": "123456789012", "Email": "test@example.com"}]
|
|
115
|
+
mock_list_members.return_value = mock_members
|
|
116
|
+
|
|
117
|
+
# Execute
|
|
118
|
+
result = collector_no_account.collect()
|
|
119
|
+
|
|
120
|
+
# Verify structure
|
|
121
|
+
assert "Findings" in result
|
|
122
|
+
assert "Standards" in result
|
|
123
|
+
assert "EnabledStandards" in result
|
|
124
|
+
assert "SecurityControls" in result
|
|
125
|
+
assert "HubConfiguration" in result
|
|
126
|
+
assert "Members" in result
|
|
127
|
+
assert "Insights" in result
|
|
128
|
+
|
|
129
|
+
# Verify content
|
|
130
|
+
assert result["HubConfiguration"] == mock_hub_config
|
|
131
|
+
assert result["EnabledStandards"] == mock_enabled_stds
|
|
132
|
+
assert result["Standards"] == mock_standards
|
|
133
|
+
assert result["SecurityControls"] == mock_controls
|
|
134
|
+
assert result["Findings"] == mock_findings
|
|
135
|
+
assert result["Insights"] == mock_insights
|
|
136
|
+
assert result["Members"] == mock_members
|
|
137
|
+
|
|
138
|
+
# Verify method calls
|
|
139
|
+
mock_get_client.assert_called_once_with("securityhub")
|
|
140
|
+
mock_describe_hub.assert_called_once()
|
|
141
|
+
mock_get_enabled_standards.assert_called_once()
|
|
142
|
+
mock_describe_standards.assert_called_once()
|
|
143
|
+
mock_list_controls.assert_called_once()
|
|
144
|
+
mock_get_findings.assert_called_once()
|
|
145
|
+
mock_get_insights.assert_called_once()
|
|
146
|
+
mock_list_members.assert_called_once()
|
|
147
|
+
|
|
148
|
+
def test_describe_hub_success(self, collector_no_account, mock_client):
|
|
149
|
+
"""Test successful hub configuration retrieval."""
|
|
150
|
+
mock_response = {
|
|
151
|
+
"HubArn": "arn:aws:securityhub:us-east-1:123456789012:hub/default",
|
|
152
|
+
"SubscribedAt": datetime(2024, 1, 1, 12, 0, 0),
|
|
153
|
+
"AutoEnableControls": True,
|
|
154
|
+
"ControlFindingGenerator": "SECURITY_CONTROL",
|
|
155
|
+
}
|
|
156
|
+
mock_client.describe_hub.return_value = mock_response
|
|
157
|
+
|
|
158
|
+
result = collector_no_account._describe_hub(mock_client)
|
|
159
|
+
|
|
160
|
+
assert result["Region"] == "us-east-1"
|
|
161
|
+
assert result["HubArn"] == "arn:aws:securityhub:us-east-1:123456789012:hub/default"
|
|
162
|
+
assert result["AutoEnableControls"] is True
|
|
163
|
+
assert result["ControlFindingGenerator"] == "SECURITY_CONTROL"
|
|
164
|
+
assert "2024-01-01" in result["SubscribedAt"]
|
|
165
|
+
mock_client.describe_hub.assert_called_once()
|
|
166
|
+
|
|
167
|
+
def test_describe_hub_invalid_access_exception(self, collector_no_account, mock_client):
|
|
168
|
+
"""Test hub configuration retrieval with InvalidAccessException."""
|
|
169
|
+
mock_client.describe_hub.side_effect = ClientError(
|
|
170
|
+
{"Error": {"Code": "InvalidAccessException", "Message": "Account is not subscribed"}},
|
|
171
|
+
"DescribeHub",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
result = collector_no_account._describe_hub(mock_client)
|
|
175
|
+
|
|
176
|
+
assert result == {}
|
|
177
|
+
mock_client.describe_hub.assert_called_once()
|
|
178
|
+
|
|
179
|
+
def test_describe_hub_resource_not_found_exception(self, collector_no_account, mock_client):
|
|
180
|
+
"""Test hub configuration retrieval with ResourceNotFoundException."""
|
|
181
|
+
mock_client.describe_hub.side_effect = ClientError(
|
|
182
|
+
{"Error": {"Code": "ResourceNotFoundException", "Message": "Hub not found"}},
|
|
183
|
+
"DescribeHub",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
result = collector_no_account._describe_hub(mock_client)
|
|
187
|
+
|
|
188
|
+
assert result == {}
|
|
189
|
+
mock_client.describe_hub.assert_called_once()
|
|
190
|
+
|
|
191
|
+
def test_describe_hub_other_error(self, collector_no_account, mock_client):
|
|
192
|
+
"""Test hub configuration retrieval with other ClientError."""
|
|
193
|
+
mock_client.describe_hub.side_effect = ClientError(
|
|
194
|
+
{"Error": {"Code": "InternalError", "Message": "Internal error"}},
|
|
195
|
+
"DescribeHub",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
result = collector_no_account._describe_hub(mock_client)
|
|
199
|
+
|
|
200
|
+
assert result == {}
|
|
201
|
+
mock_client.describe_hub.assert_called_once()
|
|
202
|
+
|
|
203
|
+
def test_get_enabled_standards_success_single_page(self, collector_no_account, mock_client):
|
|
204
|
+
"""Test successful enabled standards retrieval (single page)."""
|
|
205
|
+
mock_response = {
|
|
206
|
+
"StandardsSubscriptions": [
|
|
207
|
+
{
|
|
208
|
+
"StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0",
|
|
209
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0",
|
|
210
|
+
"StandardsInput": {},
|
|
211
|
+
"StandardsStatus": "READY",
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
mock_client.get_enabled_standards.return_value = mock_response
|
|
216
|
+
|
|
217
|
+
result = collector_no_account._get_enabled_standards(mock_client)
|
|
218
|
+
|
|
219
|
+
assert len(result) == 1
|
|
220
|
+
assert result[0]["Region"] == "us-east-1"
|
|
221
|
+
assert result[0]["StandardsStatus"] == "READY"
|
|
222
|
+
assert "StandardsArn" in result[0]
|
|
223
|
+
mock_client.get_enabled_standards.assert_called_once_with()
|
|
224
|
+
|
|
225
|
+
def test_get_enabled_standards_pagination(self, collector_no_account, mock_client):
|
|
226
|
+
"""Test enabled standards retrieval with pagination."""
|
|
227
|
+
mock_response_page1 = {
|
|
228
|
+
"StandardsSubscriptions": [
|
|
229
|
+
{
|
|
230
|
+
"StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/standard1",
|
|
231
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard1",
|
|
232
|
+
"StandardsInput": {},
|
|
233
|
+
"StandardsStatus": "READY",
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"NextToken": "token123",
|
|
237
|
+
}
|
|
238
|
+
mock_response_page2 = {
|
|
239
|
+
"StandardsSubscriptions": [
|
|
240
|
+
{
|
|
241
|
+
"StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/standard2",
|
|
242
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard2",
|
|
243
|
+
"StandardsInput": {},
|
|
244
|
+
"StandardsStatus": "READY",
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
mock_client.get_enabled_standards.side_effect = [mock_response_page1, mock_response_page2]
|
|
249
|
+
|
|
250
|
+
result = collector_no_account._get_enabled_standards(mock_client)
|
|
251
|
+
|
|
252
|
+
assert len(result) == 2
|
|
253
|
+
assert result[0]["StandardsArn"] == "arn:aws:securityhub:us-east-1::standards/standard1"
|
|
254
|
+
assert result[1]["StandardsArn"] == "arn:aws:securityhub:us-east-1::standards/standard2"
|
|
255
|
+
assert mock_client.get_enabled_standards.call_count == 2
|
|
256
|
+
|
|
257
|
+
def test_get_enabled_standards_invalid_access_exception(self, collector_no_account, mock_client):
|
|
258
|
+
"""Test enabled standards retrieval with InvalidAccessException."""
|
|
259
|
+
mock_client.get_enabled_standards.side_effect = ClientError(
|
|
260
|
+
{"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
|
|
261
|
+
"GetEnabledStandards",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
result = collector_no_account._get_enabled_standards(mock_client)
|
|
265
|
+
|
|
266
|
+
assert result == []
|
|
267
|
+
mock_client.get_enabled_standards.assert_called_once()
|
|
268
|
+
|
|
269
|
+
def test_describe_standards_success_single_page(self, collector_no_account, mock_client):
|
|
270
|
+
"""Test successful standards description (single page)."""
|
|
271
|
+
mock_response = {
|
|
272
|
+
"Standards": [
|
|
273
|
+
{
|
|
274
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0",
|
|
275
|
+
"Name": "AWS Foundational Security Best Practices",
|
|
276
|
+
"Description": "AWS Foundational Security Best Practices standard",
|
|
277
|
+
"EnabledByDefault": True,
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
mock_client.describe_standards.return_value = mock_response
|
|
282
|
+
|
|
283
|
+
result = collector_no_account._describe_standards(mock_client)
|
|
284
|
+
|
|
285
|
+
assert len(result) == 1
|
|
286
|
+
assert result[0]["Region"] == "us-east-1"
|
|
287
|
+
assert result[0]["Name"] == "AWS Foundational Security Best Practices"
|
|
288
|
+
assert result[0]["EnabledByDefault"] is True
|
|
289
|
+
mock_client.describe_standards.assert_called_once_with()
|
|
290
|
+
|
|
291
|
+
def test_describe_standards_pagination(self, collector_no_account, mock_client):
|
|
292
|
+
"""Test standards description with pagination."""
|
|
293
|
+
mock_response_page1 = {
|
|
294
|
+
"Standards": [
|
|
295
|
+
{
|
|
296
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard1",
|
|
297
|
+
"Name": "Standard 1",
|
|
298
|
+
"Description": "First standard",
|
|
299
|
+
"EnabledByDefault": True,
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
"NextToken": "token123",
|
|
303
|
+
}
|
|
304
|
+
mock_response_page2 = {
|
|
305
|
+
"Standards": [
|
|
306
|
+
{
|
|
307
|
+
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard2",
|
|
308
|
+
"Name": "Standard 2",
|
|
309
|
+
"Description": "Second standard",
|
|
310
|
+
"EnabledByDefault": False,
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
mock_client.describe_standards.side_effect = [mock_response_page1, mock_response_page2]
|
|
315
|
+
|
|
316
|
+
result = collector_no_account._describe_standards(mock_client)
|
|
317
|
+
|
|
318
|
+
assert len(result) == 2
|
|
319
|
+
assert result[0]["Name"] == "Standard 1"
|
|
320
|
+
assert result[1]["Name"] == "Standard 2"
|
|
321
|
+
assert mock_client.describe_standards.call_count == 2
|
|
322
|
+
|
|
323
|
+
def test_describe_standards_invalid_access_exception(self, collector_no_account, mock_client):
|
|
324
|
+
"""Test standards description with InvalidAccessException."""
|
|
325
|
+
mock_client.describe_standards.side_effect = ClientError(
|
|
326
|
+
{"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
|
|
327
|
+
"DescribeStandards",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
result = collector_no_account._describe_standards(mock_client)
|
|
331
|
+
|
|
332
|
+
assert result == []
|
|
333
|
+
mock_client.describe_standards.assert_called_once()
|
|
334
|
+
|
|
335
|
+
def test_list_security_controls_success_single_page(self, collector_no_account, mock_client):
|
|
336
|
+
"""Test successful security controls listing (single page)."""
|
|
337
|
+
mock_response = {
|
|
338
|
+
"SecurityControlDefinitions": [
|
|
339
|
+
{
|
|
340
|
+
"SecurityControlId": "IAM.1",
|
|
341
|
+
"Title": "IAM policies should not allow full '*' administrative privileges",
|
|
342
|
+
"Description": "This control checks whether the IAM policies allow full '*' administrative privileges",
|
|
343
|
+
"RemediationUrl": "https://docs.aws.amazon.com/console/securityhub/IAM.1/remediation",
|
|
344
|
+
"SeverityRating": "HIGH",
|
|
345
|
+
"CurrentRegionAvailability": "AVAILABLE",
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
mock_client.list_security_control_definitions.return_value = mock_response
|
|
350
|
+
|
|
351
|
+
result = collector_no_account._list_security_controls(mock_client)
|
|
352
|
+
|
|
353
|
+
assert len(result) == 1
|
|
354
|
+
assert result[0]["Region"] == "us-east-1"
|
|
355
|
+
assert result[0]["SecurityControlId"] == "IAM.1"
|
|
356
|
+
assert result[0]["SeverityRating"] == "HIGH"
|
|
357
|
+
mock_client.list_security_control_definitions.assert_called_once_with(MaxResults=100)
|
|
358
|
+
|
|
359
|
+
def test_list_security_controls_pagination(self, collector_no_account, mock_client):
|
|
360
|
+
"""Test security controls listing with pagination."""
|
|
361
|
+
mock_response_page1 = {
|
|
362
|
+
"SecurityControlDefinitions": [
|
|
363
|
+
{
|
|
364
|
+
"SecurityControlId": "IAM.1",
|
|
365
|
+
"Title": "Control 1",
|
|
366
|
+
"Description": "Description 1",
|
|
367
|
+
"RemediationUrl": "https://example.com/1",
|
|
368
|
+
"SeverityRating": "HIGH",
|
|
369
|
+
"CurrentRegionAvailability": "AVAILABLE",
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
"NextToken": "token123",
|
|
373
|
+
}
|
|
374
|
+
mock_response_page2 = {
|
|
375
|
+
"SecurityControlDefinitions": [
|
|
376
|
+
{
|
|
377
|
+
"SecurityControlId": "IAM.2",
|
|
378
|
+
"Title": "Control 2",
|
|
379
|
+
"Description": "Description 2",
|
|
380
|
+
"RemediationUrl": "https://example.com/2",
|
|
381
|
+
"SeverityRating": "MEDIUM",
|
|
382
|
+
"CurrentRegionAvailability": "AVAILABLE",
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
mock_client.list_security_control_definitions.side_effect = [mock_response_page1, mock_response_page2]
|
|
387
|
+
|
|
388
|
+
result = collector_no_account._list_security_controls(mock_client)
|
|
389
|
+
|
|
390
|
+
assert len(result) == 2
|
|
391
|
+
assert result[0]["SecurityControlId"] == "IAM.1"
|
|
392
|
+
assert result[1]["SecurityControlId"] == "IAM.2"
|
|
393
|
+
assert mock_client.list_security_control_definitions.call_count == 2
|
|
394
|
+
|
|
395
|
+
def test_list_security_controls_invalid_access_exception(self, collector_no_account, mock_client):
|
|
396
|
+
"""Test security controls listing with InvalidAccessException."""
|
|
397
|
+
mock_client.list_security_control_definitions.side_effect = ClientError(
|
|
398
|
+
{"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
|
|
399
|
+
"ListSecurityControlDefinitions",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
result = collector_no_account._list_security_controls(mock_client)
|
|
403
|
+
|
|
404
|
+
assert result == []
|
|
405
|
+
mock_client.list_security_control_definitions.assert_called_once_with(MaxResults=100)
|
|
406
|
+
|
|
407
|
+
def test_get_findings_success_single_page_no_account_filter(self, collector_no_account, mock_client):
|
|
408
|
+
"""Test successful findings retrieval (single page, no account filter)."""
|
|
409
|
+
mock_response = {
|
|
410
|
+
"Findings": [
|
|
411
|
+
{
|
|
412
|
+
"Id": "finding-1",
|
|
413
|
+
"Title": "Test Finding 1",
|
|
414
|
+
"Description": "Test Description",
|
|
415
|
+
"Severity": {"Label": "HIGH"},
|
|
416
|
+
}
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
mock_client.get_findings.return_value = mock_response
|
|
420
|
+
|
|
421
|
+
result = collector_no_account._get_findings(mock_client)
|
|
422
|
+
|
|
423
|
+
assert len(result) == 1
|
|
424
|
+
assert result[0]["Id"] == "finding-1"
|
|
425
|
+
assert result[0]["Region"] == "us-east-1" # Verify Region tagging
|
|
426
|
+
mock_client.get_findings.assert_called_once_with(MaxResults=100)
|
|
427
|
+
|
|
428
|
+
def test_get_findings_success_with_account_filter(self, collector_with_account, mock_client):
|
|
429
|
+
"""Test successful findings retrieval with account filter."""
|
|
430
|
+
mock_response = {
|
|
431
|
+
"Findings": [
|
|
432
|
+
{
|
|
433
|
+
"Id": "finding-1",
|
|
434
|
+
"Title": "Test Finding 1",
|
|
435
|
+
"AwsAccountId": "123456789012",
|
|
436
|
+
"Severity": {"Label": "HIGH"},
|
|
437
|
+
}
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
mock_client.get_findings.return_value = mock_response
|
|
441
|
+
|
|
442
|
+
result = collector_with_account._get_findings(mock_client)
|
|
443
|
+
|
|
444
|
+
assert len(result) == 1
|
|
445
|
+
assert result[0]["Region"] == "us-east-1" # Verify Region tagging
|
|
446
|
+
# Verify account filter was applied
|
|
447
|
+
call_args = mock_client.get_findings.call_args
|
|
448
|
+
assert "Filters" in call_args[1]
|
|
449
|
+
assert call_args[1]["Filters"]["AwsAccountId"][0]["Value"] == "123456789012"
|
|
450
|
+
assert call_args[1]["Filters"]["AwsAccountId"][0]["Comparison"] == "EQUALS"
|
|
451
|
+
|
|
452
|
+
def test_get_findings_pagination(self, collector_no_account, mock_client):
|
|
453
|
+
"""Test findings retrieval with pagination."""
|
|
454
|
+
mock_response_page1 = {
|
|
455
|
+
"Findings": [{"Id": "finding-1", "Title": "Finding 1"}],
|
|
456
|
+
"NextToken": "token123",
|
|
457
|
+
}
|
|
458
|
+
mock_response_page2 = {"Findings": [{"Id": "finding-2", "Title": "Finding 2"}]}
|
|
459
|
+
mock_client.get_findings.side_effect = [mock_response_page1, mock_response_page2]
|
|
460
|
+
|
|
461
|
+
result = collector_no_account._get_findings(mock_client)
|
|
462
|
+
|
|
463
|
+
assert len(result) == 2
|
|
464
|
+
assert result[0]["Id"] == "finding-1"
|
|
465
|
+
assert result[0]["Region"] == "us-east-1" # Verify Region tagging
|
|
466
|
+
assert result[1]["Id"] == "finding-2"
|
|
467
|
+
assert result[1]["Region"] == "us-east-1" # Verify Region tagging
|
|
468
|
+
assert mock_client.get_findings.call_count == 2
|
|
469
|
+
|
|
470
|
+
def test_get_findings_invalid_access_exception(self, collector_no_account, mock_client):
|
|
471
|
+
"""Test findings retrieval with InvalidAccessException."""
|
|
472
|
+
mock_client.get_findings.side_effect = ClientError(
|
|
473
|
+
{"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
|
|
474
|
+
"GetFindings",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
result = collector_no_account._get_findings(mock_client)
|
|
478
|
+
|
|
479
|
+
assert result == []
|
|
480
|
+
mock_client.get_findings.assert_called_once()
|
|
481
|
+
|
|
482
|
+
def test_get_insights_success_single_page(self, collector_no_account, mock_client):
|
|
483
|
+
"""Test successful insights retrieval (single page)."""
|
|
484
|
+
mock_response = {
|
|
485
|
+
"Insights": [
|
|
486
|
+
{
|
|
487
|
+
"InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/abc123",
|
|
488
|
+
"Name": "Test Insight",
|
|
489
|
+
"Filters": {"SeverityLabel": [{"Value": "CRITICAL", "Comparison": "EQUALS"}]},
|
|
490
|
+
"GroupByAttribute": "ResourceType",
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
mock_client.get_insights.return_value = mock_response
|
|
495
|
+
|
|
496
|
+
result = collector_no_account._get_insights(mock_client)
|
|
497
|
+
|
|
498
|
+
assert len(result) == 1
|
|
499
|
+
assert result[0]["Region"] == "us-east-1"
|
|
500
|
+
assert result[0]["Name"] == "Test Insight"
|
|
501
|
+
assert result[0]["GroupByAttribute"] == "ResourceType"
|
|
502
|
+
mock_client.get_insights.assert_called_once_with(MaxResults=100)
|
|
503
|
+
|
|
504
|
+
def test_get_insights_pagination(self, collector_no_account, mock_client):
|
|
505
|
+
"""Test insights retrieval with pagination."""
|
|
506
|
+
mock_response_page1 = {
|
|
507
|
+
"Insights": [
|
|
508
|
+
{
|
|
509
|
+
"InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/abc123",
|
|
510
|
+
"Name": "Insight 1",
|
|
511
|
+
"Filters": {},
|
|
512
|
+
"GroupByAttribute": "ResourceType",
|
|
513
|
+
}
|
|
514
|
+
],
|
|
515
|
+
"NextToken": "token123",
|
|
516
|
+
}
|
|
517
|
+
mock_response_page2 = {
|
|
518
|
+
"Insights": [
|
|
519
|
+
{
|
|
520
|
+
"InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/def456",
|
|
521
|
+
"Name": "Insight 2",
|
|
522
|
+
"Filters": {},
|
|
523
|
+
"GroupByAttribute": "SeverityLabel",
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
mock_client.get_insights.side_effect = [mock_response_page1, mock_response_page2]
|
|
528
|
+
|
|
529
|
+
result = collector_no_account._get_insights(mock_client)
|
|
530
|
+
|
|
531
|
+
assert len(result) == 2
|
|
532
|
+
assert result[0]["Name"] == "Insight 1"
|
|
533
|
+
assert result[1]["Name"] == "Insight 2"
|
|
534
|
+
assert mock_client.get_insights.call_count == 2
|
|
535
|
+
|
|
536
|
+
def test_get_insights_invalid_access_exception(self, collector_no_account, mock_client):
|
|
537
|
+
"""Test insights retrieval with InvalidAccessException."""
|
|
538
|
+
mock_client.get_insights.side_effect = ClientError(
|
|
539
|
+
{"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
|
|
540
|
+
"GetInsights",
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
result = collector_no_account._get_insights(mock_client)
|
|
544
|
+
|
|
545
|
+
assert result == []
|
|
546
|
+
mock_client.get_insights.assert_called_once()
|
|
547
|
+
|
|
548
|
+
def test_list_members_success_single_page_no_account_filter(self, collector_no_account, mock_client):
|
|
549
|
+
"""Test successful members listing (single page, no account filter)."""
|
|
550
|
+
mock_response = {
|
|
551
|
+
"Members": [
|
|
552
|
+
{
|
|
553
|
+
"AccountId": "123456789012",
|
|
554
|
+
"Email": "account1@example.com",
|
|
555
|
+
"MasterId": "999999999999",
|
|
556
|
+
"AdministratorId": "999999999999",
|
|
557
|
+
"MemberStatus": "ENABLED",
|
|
558
|
+
"InvitedAt": datetime(2024, 1, 1, 12, 0, 0),
|
|
559
|
+
"UpdatedAt": datetime(2024, 1, 2, 12, 0, 0),
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
}
|
|
563
|
+
mock_client.list_members.return_value = mock_response
|
|
564
|
+
|
|
565
|
+
result = collector_no_account._list_members(mock_client)
|
|
566
|
+
|
|
567
|
+
assert len(result) == 1
|
|
568
|
+
assert result[0]["Region"] == "us-east-1"
|
|
569
|
+
assert result[0]["AccountId"] == "123456789012"
|
|
570
|
+
assert result[0]["MemberStatus"] == "ENABLED"
|
|
571
|
+
assert "2024-01-01" in result[0]["InvitedAt"]
|
|
572
|
+
assert "2024-01-02" in result[0]["UpdatedAt"]
|
|
573
|
+
mock_client.list_members.assert_called_once_with(MaxResults=50)
|
|
574
|
+
|
|
575
|
+
def test_list_members_with_account_filter(self, collector_with_account, mock_client):
|
|
576
|
+
"""Test members listing with account filter."""
|
|
577
|
+
mock_response = {
|
|
578
|
+
"Members": [
|
|
579
|
+
{
|
|
580
|
+
"AccountId": "123456789012",
|
|
581
|
+
"Email": "account1@example.com",
|
|
582
|
+
"MemberStatus": "ENABLED",
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"AccountId": "999999999999",
|
|
586
|
+
"Email": "account2@example.com",
|
|
587
|
+
"MemberStatus": "ENABLED",
|
|
588
|
+
},
|
|
589
|
+
]
|
|
590
|
+
}
|
|
591
|
+
mock_client.list_members.return_value = mock_response
|
|
592
|
+
|
|
593
|
+
result = collector_with_account._list_members(mock_client)
|
|
594
|
+
|
|
595
|
+
# Only the account matching the filter should be returned
|
|
596
|
+
assert len(result) == 1
|
|
597
|
+
assert result[0]["AccountId"] == "123456789012"
|
|
598
|
+
assert result[0]["Region"] == "us-east-1"
|
|
599
|
+
|
|
600
|
+
def test_list_members_pagination(self, collector_no_account, mock_client):
|
|
601
|
+
"""Test members listing with pagination."""
|
|
602
|
+
mock_response_page1 = {
|
|
603
|
+
"Members": [
|
|
604
|
+
{
|
|
605
|
+
"AccountId": "123456789012",
|
|
606
|
+
"Email": "account1@example.com",
|
|
607
|
+
"MemberStatus": "ENABLED",
|
|
608
|
+
}
|
|
609
|
+
],
|
|
610
|
+
"NextToken": "token123",
|
|
611
|
+
}
|
|
612
|
+
mock_response_page2 = {
|
|
613
|
+
"Members": [
|
|
614
|
+
{
|
|
615
|
+
"AccountId": "999999999999",
|
|
616
|
+
"Email": "account2@example.com",
|
|
617
|
+
"MemberStatus": "ENABLED",
|
|
618
|
+
}
|
|
619
|
+
]
|
|
620
|
+
}
|
|
621
|
+
mock_client.list_members.side_effect = [mock_response_page1, mock_response_page2]
|
|
622
|
+
|
|
623
|
+
result = collector_no_account._list_members(mock_client)
|
|
624
|
+
|
|
625
|
+
assert len(result) == 2
|
|
626
|
+
assert result[0]["AccountId"] == "123456789012"
|
|
627
|
+
assert result[1]["AccountId"] == "999999999999"
|
|
628
|
+
assert mock_client.list_members.call_count == 2
|
|
629
|
+
|
|
630
|
+
def test_list_members_invalid_access_exception(self, collector_no_account, mock_client):
|
|
631
|
+
"""Test members listing with InvalidAccessException."""
|
|
632
|
+
mock_client.list_members.side_effect = ClientError(
|
|
633
|
+
{"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
|
|
634
|
+
"ListMembers",
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
result = collector_no_account._list_members(mock_client)
|
|
638
|
+
|
|
639
|
+
assert result == []
|
|
640
|
+
mock_client.list_members.assert_called_once()
|
|
641
|
+
|
|
642
|
+
def test_list_members_no_invited_at(self, collector_no_account, mock_client):
|
|
643
|
+
"""Test members listing with no InvitedAt or UpdatedAt fields."""
|
|
644
|
+
mock_response = {
|
|
645
|
+
"Members": [
|
|
646
|
+
{
|
|
647
|
+
"AccountId": "123456789012",
|
|
648
|
+
"Email": "account1@example.com",
|
|
649
|
+
"MemberStatus": "ENABLED",
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
mock_client.list_members.return_value = mock_response
|
|
654
|
+
|
|
655
|
+
result = collector_no_account._list_members(mock_client)
|
|
656
|
+
|
|
657
|
+
assert len(result) == 1
|
|
658
|
+
assert result[0]["InvitedAt"] is None
|
|
659
|
+
assert result[0]["UpdatedAt"] is None
|
|
660
|
+
|
|
661
|
+
@patch.object(SecurityHubCollector, "_get_client")
|
|
662
|
+
@patch.object(SecurityHubCollector, "_describe_hub")
|
|
663
|
+
@patch.object(SecurityHubCollector, "_get_enabled_standards")
|
|
664
|
+
@patch.object(SecurityHubCollector, "_describe_standards")
|
|
665
|
+
@patch.object(SecurityHubCollector, "_list_security_controls")
|
|
666
|
+
@patch.object(SecurityHubCollector, "_get_findings")
|
|
667
|
+
@patch.object(SecurityHubCollector, "_get_insights")
|
|
668
|
+
@patch.object(SecurityHubCollector, "_list_members")
|
|
669
|
+
@patch.object(SecurityHubCollector, "_handle_error")
|
|
670
|
+
def test_collect_handles_client_error(
|
|
671
|
+
self,
|
|
672
|
+
mock_handle_error,
|
|
673
|
+
mock_list_members,
|
|
674
|
+
mock_get_insights,
|
|
675
|
+
mock_get_findings,
|
|
676
|
+
mock_list_controls,
|
|
677
|
+
mock_describe_standards,
|
|
678
|
+
mock_get_enabled_standards,
|
|
679
|
+
mock_describe_hub,
|
|
680
|
+
mock_get_client,
|
|
681
|
+
collector_no_account,
|
|
682
|
+
):
|
|
683
|
+
"""Test that collect properly handles ClientError."""
|
|
684
|
+
mock_client = MagicMock()
|
|
685
|
+
mock_get_client.return_value = mock_client
|
|
686
|
+
|
|
687
|
+
error = ClientError(
|
|
688
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}},
|
|
689
|
+
"DescribeHub",
|
|
690
|
+
)
|
|
691
|
+
mock_describe_hub.side_effect = error
|
|
692
|
+
|
|
693
|
+
# Mock other methods to return empty data
|
|
694
|
+
mock_get_enabled_standards.return_value = []
|
|
695
|
+
mock_describe_standards.return_value = []
|
|
696
|
+
mock_list_controls.return_value = []
|
|
697
|
+
mock_get_findings.return_value = []
|
|
698
|
+
mock_get_insights.return_value = []
|
|
699
|
+
mock_list_members.return_value = []
|
|
700
|
+
|
|
701
|
+
result = collector_no_account.collect()
|
|
702
|
+
|
|
703
|
+
# Should return empty structure
|
|
704
|
+
assert "Findings" in result
|
|
705
|
+
assert "Standards" in result
|
|
706
|
+
assert result["Findings"] == []
|
|
707
|
+
mock_handle_error.assert_called_once()
|
|
708
|
+
|
|
709
|
+
@patch.object(SecurityHubCollector, "_get_client")
|
|
710
|
+
@patch.object(SecurityHubCollector, "_describe_hub")
|
|
711
|
+
@patch.object(SecurityHubCollector, "_get_enabled_standards")
|
|
712
|
+
@patch.object(SecurityHubCollector, "_describe_standards")
|
|
713
|
+
@patch.object(SecurityHubCollector, "_list_security_controls")
|
|
714
|
+
@patch.object(SecurityHubCollector, "_get_findings")
|
|
715
|
+
@patch.object(SecurityHubCollector, "_get_insights")
|
|
716
|
+
@patch.object(SecurityHubCollector, "_list_members")
|
|
717
|
+
def test_collect_handles_unexpected_error(
|
|
718
|
+
self,
|
|
719
|
+
mock_list_members,
|
|
720
|
+
mock_get_insights,
|
|
721
|
+
mock_get_findings,
|
|
722
|
+
mock_list_controls,
|
|
723
|
+
mock_describe_standards,
|
|
724
|
+
mock_get_enabled_standards,
|
|
725
|
+
mock_describe_hub,
|
|
726
|
+
mock_get_client,
|
|
727
|
+
collector_no_account,
|
|
728
|
+
):
|
|
729
|
+
"""Test that collect properly handles unexpected errors."""
|
|
730
|
+
mock_client = MagicMock()
|
|
731
|
+
mock_get_client.return_value = mock_client
|
|
732
|
+
|
|
733
|
+
# Cause an unexpected error
|
|
734
|
+
mock_describe_hub.side_effect = ValueError("Unexpected error")
|
|
735
|
+
|
|
736
|
+
# Mock other methods to return empty data
|
|
737
|
+
mock_get_enabled_standards.return_value = []
|
|
738
|
+
mock_describe_standards.return_value = []
|
|
739
|
+
mock_list_controls.return_value = []
|
|
740
|
+
mock_get_findings.return_value = []
|
|
741
|
+
mock_get_insights.return_value = []
|
|
742
|
+
mock_list_members.return_value = []
|
|
743
|
+
|
|
744
|
+
result = collector_no_account.collect()
|
|
745
|
+
|
|
746
|
+
# Should return empty structure and not raise
|
|
747
|
+
assert "Findings" in result
|
|
748
|
+
assert "Standards" in result
|
|
749
|
+
assert result["Findings"] == []
|
|
750
|
+
|
|
751
|
+
def test_region_tagging_in_all_methods(self, collector_no_account, mock_client):
|
|
752
|
+
"""Test that all methods properly tag resources with Region."""
|
|
753
|
+
# Test _describe_hub
|
|
754
|
+
mock_client.describe_hub.return_value = {"HubArn": "arn:test"}
|
|
755
|
+
hub_result = collector_no_account._describe_hub(mock_client)
|
|
756
|
+
assert hub_result["Region"] == "us-east-1"
|
|
757
|
+
|
|
758
|
+
# Test _get_enabled_standards
|
|
759
|
+
mock_client.get_enabled_standards.return_value = {
|
|
760
|
+
"StandardsSubscriptions": [{"StandardsArn": "arn:test", "StandardsStatus": "READY"}]
|
|
761
|
+
}
|
|
762
|
+
standards_result = collector_no_account._get_enabled_standards(mock_client)
|
|
763
|
+
assert all(s["Region"] == "us-east-1" for s in standards_result)
|
|
764
|
+
|
|
765
|
+
# Test _describe_standards
|
|
766
|
+
mock_client.describe_standards.return_value = {"Standards": [{"StandardsArn": "arn:test", "Name": "Test"}]}
|
|
767
|
+
desc_standards_result = collector_no_account._describe_standards(mock_client)
|
|
768
|
+
assert all(s["Region"] == "us-east-1" for s in desc_standards_result)
|
|
769
|
+
|
|
770
|
+
# Test _list_security_controls
|
|
771
|
+
mock_client.list_security_control_definitions.return_value = {
|
|
772
|
+
"SecurityControlDefinitions": [{"SecurityControlId": "TEST.1", "Title": "Test"}]
|
|
773
|
+
}
|
|
774
|
+
controls_result = collector_no_account._list_security_controls(mock_client)
|
|
775
|
+
assert all(c["Region"] == "us-east-1" for c in controls_result)
|
|
776
|
+
|
|
777
|
+
# Test _get_findings
|
|
778
|
+
mock_client.get_findings.return_value = {"Findings": [{"Id": "finding-1"}]}
|
|
779
|
+
findings_result = collector_no_account._get_findings(mock_client)
|
|
780
|
+
assert all(f["Region"] == "us-east-1" for f in findings_result)
|
|
781
|
+
|
|
782
|
+
# Test _get_insights
|
|
783
|
+
mock_client.get_insights.return_value = {"Insights": [{"InsightArn": "arn:test", "Name": "Test"}]}
|
|
784
|
+
insights_result = collector_no_account._get_insights(mock_client)
|
|
785
|
+
assert all(i["Region"] == "us-east-1" for i in insights_result)
|
|
786
|
+
|
|
787
|
+
# Test _list_members
|
|
788
|
+
mock_client.list_members.return_value = {
|
|
789
|
+
"Members": [{"AccountId": "123456789012", "Email": "test@example.com"}]
|
|
790
|
+
}
|
|
791
|
+
members_result = collector_no_account._list_members(mock_client)
|
|
792
|
+
assert all(m["Region"] == "us-east-1" for m in members_result)
|