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,879 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""AWS KMS Evidence Integration for RegScale CLI."""
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from io import BytesIO
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import boto3
|
|
16
|
+
from botocore.exceptions import ClientError
|
|
17
|
+
|
|
18
|
+
from regscale.core.app.api import Api
|
|
19
|
+
from regscale.core.app.utils.app_utils import get_current_datetime
|
|
20
|
+
from regscale.integrations.commercial.aws.kms_control_mappings import KMSControlMapper
|
|
21
|
+
from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
|
|
22
|
+
from regscale.models import regscale_models
|
|
23
|
+
from regscale.models.regscale_models.evidence import Evidence
|
|
24
|
+
from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
|
|
25
|
+
from regscale.models.regscale_models.file import File
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("regscale")
|
|
28
|
+
|
|
29
|
+
# Constants for file paths and cache TTL
|
|
30
|
+
KMS_CACHE_FILE = os.path.join("artifacts", "aws", "kms_data.json")
|
|
31
|
+
CACHE_TTL_SECONDS = 4 * 60 * 60 # 4 hours in seconds
|
|
32
|
+
|
|
33
|
+
# HTML tag constants to avoid duplication
|
|
34
|
+
HTML_STRONG_OPEN = "<strong>"
|
|
35
|
+
HTML_STRONG_CLOSE = "</strong>"
|
|
36
|
+
HTML_P_OPEN = "<p>"
|
|
37
|
+
HTML_P_CLOSE = "</p>"
|
|
38
|
+
HTML_UL_OPEN = "<ul>"
|
|
39
|
+
HTML_UL_CLOSE = "</ul>"
|
|
40
|
+
HTML_LI_OPEN = "<li>"
|
|
41
|
+
HTML_LI_CLOSE = "</li>"
|
|
42
|
+
HTML_H2_OPEN = "<h2>"
|
|
43
|
+
HTML_H2_CLOSE = "</h2>"
|
|
44
|
+
HTML_H3_OPEN = "<h3>"
|
|
45
|
+
HTML_H3_CLOSE = "</h3>"
|
|
46
|
+
HTML_BR = "<br>"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class KMSEvidenceConfig:
|
|
51
|
+
"""Configuration for AWS KMS evidence collection."""
|
|
52
|
+
|
|
53
|
+
plan_id: int
|
|
54
|
+
region: str = "us-east-1"
|
|
55
|
+
framework: str = "NIST800-53R5"
|
|
56
|
+
create_issues: bool = True
|
|
57
|
+
update_control_status: bool = True
|
|
58
|
+
create_poams: bool = False
|
|
59
|
+
parent_module: str = "securityplans"
|
|
60
|
+
collect_evidence: bool = False
|
|
61
|
+
evidence_as_attachments: bool = True
|
|
62
|
+
evidence_control_ids: Optional[List[str]] = None
|
|
63
|
+
evidence_frequency: int = 30
|
|
64
|
+
force_refresh: bool = False
|
|
65
|
+
account_id: Optional[str] = None
|
|
66
|
+
tags: Optional[Dict[str, str]] = None
|
|
67
|
+
profile: Optional[str] = None
|
|
68
|
+
aws_access_key_id: Optional[str] = None
|
|
69
|
+
aws_secret_access_key: Optional[str] = None
|
|
70
|
+
aws_session_token: Optional[str] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class KMSComplianceItem(ComplianceItem):
|
|
74
|
+
"""
|
|
75
|
+
Compliance item representing a single KMS key assessment.
|
|
76
|
+
|
|
77
|
+
Maps KMS key attributes to compliance control requirements.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, key_data: Dict[str, Any], control_mapper: KMSControlMapper):
|
|
81
|
+
"""
|
|
82
|
+
Initialize KMS compliance item from key data.
|
|
83
|
+
|
|
84
|
+
:param Dict[str, Any] key_data: KMS key metadata and attributes
|
|
85
|
+
:param KMSControlMapper control_mapper: Control mapper for compliance assessment
|
|
86
|
+
"""
|
|
87
|
+
self.key_data = key_data
|
|
88
|
+
self.control_mapper = control_mapper
|
|
89
|
+
|
|
90
|
+
# Extract key attributes
|
|
91
|
+
self._key_id = key_data.get("KeyId", "")
|
|
92
|
+
self._key_arn = key_data.get("Arn", "")
|
|
93
|
+
self._key_state = key_data.get("KeyState", "Unknown")
|
|
94
|
+
self._rotation_enabled = key_data.get("RotationEnabled", False)
|
|
95
|
+
self._key_manager = key_data.get("KeyManager", "CUSTOMER")
|
|
96
|
+
self._description = key_data.get("Description", "")
|
|
97
|
+
self._tags = key_data.get("Tags", [])
|
|
98
|
+
|
|
99
|
+
# Assess compliance for all mapped controls
|
|
100
|
+
self._compliance_results = control_mapper.assess_key_compliance(key_data)
|
|
101
|
+
|
|
102
|
+
# Extract region and account from ARN
|
|
103
|
+
self._region = self._extract_region_from_arn(self._key_arn)
|
|
104
|
+
self._account_id = self._extract_account_from_arn(self._key_arn)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def resource_id(self) -> str:
|
|
108
|
+
"""Unique identifier for the KMS key."""
|
|
109
|
+
return self._key_id
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def resource_name(self) -> str:
|
|
113
|
+
"""Human-readable name of the KMS key."""
|
|
114
|
+
# Try to get alias from tags or description
|
|
115
|
+
for tag in self._tags:
|
|
116
|
+
if tag.get("TagKey") == "Name":
|
|
117
|
+
return f"{tag.get('TagValue')} ({self._key_id[:8]}...)"
|
|
118
|
+
|
|
119
|
+
if self._description:
|
|
120
|
+
return f"{self._description[:50]} ({self._key_id[:8]}...)"
|
|
121
|
+
|
|
122
|
+
return f"KMS Key {self._key_id[:12]}..."
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def control_id(self) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Primary control identifier for this key assessment.
|
|
128
|
+
|
|
129
|
+
Returns the first failing control, or first passing control if all pass.
|
|
130
|
+
"""
|
|
131
|
+
# Return first failing control for issue creation
|
|
132
|
+
for control_id, result in self._compliance_results.items():
|
|
133
|
+
if result == "FAIL":
|
|
134
|
+
return control_id
|
|
135
|
+
|
|
136
|
+
# If all pass, return first control
|
|
137
|
+
return list(self._compliance_results.keys())[0] if self._compliance_results else "SC-12"
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def compliance_result(self) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Overall compliance result for this key.
|
|
143
|
+
|
|
144
|
+
Returns FAIL if any control fails, PASS if all pass.
|
|
145
|
+
"""
|
|
146
|
+
if not self._compliance_results:
|
|
147
|
+
return "PASS"
|
|
148
|
+
|
|
149
|
+
# If ANY control fails, the key fails overall
|
|
150
|
+
if "FAIL" in self._compliance_results.values():
|
|
151
|
+
return "FAIL"
|
|
152
|
+
|
|
153
|
+
return "PASS"
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def severity(self) -> Optional[str]:
|
|
157
|
+
"""Severity level based on which controls are failing."""
|
|
158
|
+
if self.compliance_result == "PASS":
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# SC-12 failures (rotation) are HIGH severity
|
|
162
|
+
if self._compliance_results.get("SC-12") == "FAIL":
|
|
163
|
+
return "HIGH"
|
|
164
|
+
|
|
165
|
+
# SC-13 failures (crypto protection) are MEDIUM severity
|
|
166
|
+
if self._compliance_results.get("SC-13") == "FAIL":
|
|
167
|
+
return "MEDIUM"
|
|
168
|
+
|
|
169
|
+
# SC-28 failures (data at rest) are MEDIUM severity
|
|
170
|
+
if self._compliance_results.get("SC-28") == "FAIL":
|
|
171
|
+
return "MEDIUM"
|
|
172
|
+
|
|
173
|
+
return "MEDIUM"
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def description(self) -> str:
|
|
177
|
+
"""Detailed description of the KMS key compliance assessment."""
|
|
178
|
+
desc_parts = self._build_key_details()
|
|
179
|
+
|
|
180
|
+
if self._description:
|
|
181
|
+
desc_parts.extend(self._build_description_section())
|
|
182
|
+
|
|
183
|
+
desc_parts.extend(self._build_compliance_results_section())
|
|
184
|
+
|
|
185
|
+
if self.compliance_result == "FAIL":
|
|
186
|
+
desc_parts.extend(self._build_remediation_section())
|
|
187
|
+
|
|
188
|
+
return "\n".join(desc_parts)
|
|
189
|
+
|
|
190
|
+
def _build_key_details(self) -> List[str]:
|
|
191
|
+
"""Build the key details section of the description."""
|
|
192
|
+
rotation_status = "Yes" if self._rotation_enabled else "No"
|
|
193
|
+
return [
|
|
194
|
+
f"{HTML_H3_OPEN}AWS KMS Key Compliance Assessment{HTML_H3_CLOSE}",
|
|
195
|
+
HTML_P_OPEN,
|
|
196
|
+
f"{HTML_STRONG_OPEN}Key ID:{HTML_STRONG_CLOSE} {self._key_id}{HTML_BR}",
|
|
197
|
+
f"{HTML_STRONG_OPEN}Key ARN:{HTML_STRONG_CLOSE} {self._key_arn}{HTML_BR}",
|
|
198
|
+
f"{HTML_STRONG_OPEN}Key State:{HTML_STRONG_CLOSE} {self._key_state}{HTML_BR}",
|
|
199
|
+
f"{HTML_STRONG_OPEN}Key Manager:{HTML_STRONG_CLOSE} {self._key_manager}{HTML_BR}",
|
|
200
|
+
f"{HTML_STRONG_OPEN}Rotation Enabled:{HTML_STRONG_CLOSE} {rotation_status}{HTML_BR}",
|
|
201
|
+
f"{HTML_STRONG_OPEN}Region:{HTML_STRONG_CLOSE} {self._region}",
|
|
202
|
+
HTML_P_CLOSE,
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
def _build_description_section(self) -> List[str]:
|
|
206
|
+
"""Build the optional description section."""
|
|
207
|
+
return [
|
|
208
|
+
HTML_P_OPEN,
|
|
209
|
+
f"{HTML_STRONG_OPEN}Description:{HTML_STRONG_CLOSE} {self._description}",
|
|
210
|
+
HTML_P_CLOSE,
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
def _build_compliance_results_section(self) -> List[str]:
|
|
214
|
+
"""Build the compliance results section."""
|
|
215
|
+
section_parts = [
|
|
216
|
+
f"{HTML_H3_OPEN}Control Compliance Results{HTML_H3_CLOSE}",
|
|
217
|
+
HTML_UL_OPEN,
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
for control_id, result in self._compliance_results.items():
|
|
221
|
+
result_item = self._format_compliance_result(control_id, result)
|
|
222
|
+
section_parts.append(result_item)
|
|
223
|
+
|
|
224
|
+
section_parts.append(HTML_UL_CLOSE)
|
|
225
|
+
return section_parts
|
|
226
|
+
|
|
227
|
+
def _format_compliance_result(self, control_id: str, result: str) -> str:
|
|
228
|
+
"""Format a single compliance result item."""
|
|
229
|
+
result_color = "#d32f2f" if result == "FAIL" else "#2e7d32"
|
|
230
|
+
control_desc = self.control_mapper.get_control_description(control_id)
|
|
231
|
+
return (
|
|
232
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
|
|
233
|
+
f"<span style='color: {result_color};'>{result}</span> - {control_desc}{HTML_LI_CLOSE}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def _build_remediation_section(self) -> List[str]:
|
|
237
|
+
"""Build remediation guidance for failed controls."""
|
|
238
|
+
section_parts = [
|
|
239
|
+
f"{HTML_H3_OPEN}Remediation Guidance{HTML_H3_CLOSE}",
|
|
240
|
+
HTML_UL_OPEN,
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
section_parts.extend(self._get_sc12_remediation())
|
|
244
|
+
section_parts.extend(self._get_sc13_remediation())
|
|
245
|
+
section_parts.extend(self._get_sc28_remediation())
|
|
246
|
+
|
|
247
|
+
section_parts.append(HTML_UL_CLOSE)
|
|
248
|
+
return section_parts
|
|
249
|
+
|
|
250
|
+
def _get_sc12_remediation(self) -> List[str]:
|
|
251
|
+
"""Get remediation steps for SC-12 control failures."""
|
|
252
|
+
items = []
|
|
253
|
+
if self._compliance_results.get("SC-12") == "FAIL":
|
|
254
|
+
if not self._rotation_enabled and self._key_manager == "CUSTOMER":
|
|
255
|
+
items.append(
|
|
256
|
+
f"{HTML_LI_OPEN}Enable automatic key rotation for this customer-managed key{HTML_LI_CLOSE}"
|
|
257
|
+
)
|
|
258
|
+
if self._key_state in ["PendingDeletion", "Disabled"]:
|
|
259
|
+
items.append(f"{HTML_LI_OPEN}Key is {self._key_state} - review key lifecycle{HTML_LI_CLOSE}")
|
|
260
|
+
return items
|
|
261
|
+
|
|
262
|
+
def _get_sc13_remediation(self) -> List[str]:
|
|
263
|
+
"""Get remediation steps for SC-13 control failures."""
|
|
264
|
+
items = []
|
|
265
|
+
if self._compliance_results.get("SC-13") == "FAIL":
|
|
266
|
+
key_spec = self.key_data.get("KeySpec", "Unknown")
|
|
267
|
+
items.append(
|
|
268
|
+
f"{HTML_LI_OPEN}Review key specification ({key_spec}) - ensure FIPS-validated "
|
|
269
|
+
f"algorithms are used{HTML_LI_CLOSE}"
|
|
270
|
+
)
|
|
271
|
+
return items
|
|
272
|
+
|
|
273
|
+
def _get_sc28_remediation(self) -> List[str]:
|
|
274
|
+
"""Get remediation steps for SC-28 control failures."""
|
|
275
|
+
items = []
|
|
276
|
+
if self._compliance_results.get("SC-28") == "FAIL":
|
|
277
|
+
items.append(
|
|
278
|
+
f"{HTML_LI_OPEN}Ensure key is enabled and available for data-at-rest encryption{HTML_LI_CLOSE}"
|
|
279
|
+
)
|
|
280
|
+
return items
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def framework(self) -> str:
|
|
284
|
+
"""Compliance framework used for assessment."""
|
|
285
|
+
return self.control_mapper.framework
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def _extract_region_from_arn(arn: str) -> str:
|
|
289
|
+
"""Extract AWS region from KMS key ARN."""
|
|
290
|
+
try:
|
|
291
|
+
# ARN format: arn:aws:kms:region:account:key/key-id
|
|
292
|
+
return arn.split(":")[3]
|
|
293
|
+
except (IndexError, AttributeError):
|
|
294
|
+
return "unknown"
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _extract_account_from_arn(arn: str) -> str:
|
|
298
|
+
"""Extract AWS account ID from KMS key ARN."""
|
|
299
|
+
try:
|
|
300
|
+
# ARN format: arn:aws:kms:region:account:key/key-id
|
|
301
|
+
return arn.split(":")[4]
|
|
302
|
+
except (IndexError, AttributeError):
|
|
303
|
+
return "unknown"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class AWSKMSEvidenceIntegration(ComplianceIntegration):
|
|
307
|
+
"""Process AWS KMS key data and create evidence/compliance records in RegScale."""
|
|
308
|
+
|
|
309
|
+
def __init__(self, config: KMSEvidenceConfig):
|
|
310
|
+
"""
|
|
311
|
+
Initialize AWS KMS evidence integration.
|
|
312
|
+
|
|
313
|
+
:param KMSEvidenceConfig config: Configuration object containing all parameters
|
|
314
|
+
"""
|
|
315
|
+
super().__init__(
|
|
316
|
+
plan_id=config.plan_id,
|
|
317
|
+
framework=config.framework,
|
|
318
|
+
create_issues=config.create_issues,
|
|
319
|
+
update_control_status=config.update_control_status,
|
|
320
|
+
create_poams=config.create_poams,
|
|
321
|
+
parent_module=config.parent_module,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Initialize API for file operations
|
|
325
|
+
self.api = Api()
|
|
326
|
+
|
|
327
|
+
self.region = config.region
|
|
328
|
+
self.title = "AWS KMS"
|
|
329
|
+
self.framework = config.framework
|
|
330
|
+
|
|
331
|
+
# Evidence collection parameters
|
|
332
|
+
self.collect_evidence = config.collect_evidence
|
|
333
|
+
self.evidence_as_attachments = config.evidence_as_attachments
|
|
334
|
+
self.evidence_control_ids = config.evidence_control_ids
|
|
335
|
+
self.evidence_frequency = config.evidence_frequency
|
|
336
|
+
|
|
337
|
+
# Cache control
|
|
338
|
+
self.force_refresh = config.force_refresh
|
|
339
|
+
|
|
340
|
+
# Filtering parameters
|
|
341
|
+
self.account_id = config.account_id
|
|
342
|
+
self.tags = config.tags or {}
|
|
343
|
+
|
|
344
|
+
# Initialize control mapper
|
|
345
|
+
self.control_mapper = KMSControlMapper(framework=config.framework)
|
|
346
|
+
|
|
347
|
+
# Extract AWS credentials from config
|
|
348
|
+
profile = config.profile
|
|
349
|
+
aws_access_key_id = config.aws_access_key_id
|
|
350
|
+
aws_secret_access_key = config.aws_secret_access_key
|
|
351
|
+
aws_session_token = config.aws_session_token
|
|
352
|
+
|
|
353
|
+
# INFO-level logging for credential resolution
|
|
354
|
+
if aws_access_key_id and aws_secret_access_key:
|
|
355
|
+
logger.info("Initializing AWS KMS client with explicit credentials")
|
|
356
|
+
self.session = boto3.Session(
|
|
357
|
+
region_name=config.region,
|
|
358
|
+
aws_access_key_id=aws_access_key_id,
|
|
359
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
360
|
+
aws_session_token=aws_session_token,
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
logger.info(f"Initializing AWS KMS client with profile: {profile if profile else 'default'}")
|
|
364
|
+
self.session = boto3.Session(profile_name=profile, region_name=config.region)
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
self.client = self.session.client("kms")
|
|
368
|
+
logger.info("Successfully created AWS KMS client")
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error(f"Failed to create AWS KMS client: {e}")
|
|
371
|
+
raise
|
|
372
|
+
|
|
373
|
+
# Store raw KMS data for evidence generation
|
|
374
|
+
self.raw_kms_data: List[Dict[str, Any]] = []
|
|
375
|
+
|
|
376
|
+
def _is_cache_valid(self) -> bool:
|
|
377
|
+
"""
|
|
378
|
+
Check if the cache file exists and is within the TTL.
|
|
379
|
+
|
|
380
|
+
:return: True if cache is valid, False otherwise
|
|
381
|
+
:rtype: bool
|
|
382
|
+
"""
|
|
383
|
+
if not os.path.exists(KMS_CACHE_FILE):
|
|
384
|
+
logger.debug("Cache file does not exist")
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
file_age = time.time() - os.path.getmtime(KMS_CACHE_FILE)
|
|
388
|
+
is_valid = file_age < CACHE_TTL_SECONDS
|
|
389
|
+
|
|
390
|
+
if is_valid:
|
|
391
|
+
hours_old = file_age / 3600
|
|
392
|
+
logger.info(f"Using cached KMS data (age: {hours_old:.1f} hours)")
|
|
393
|
+
else:
|
|
394
|
+
hours_old = file_age / 3600
|
|
395
|
+
logger.debug(f"Cache expired (age: {hours_old:.1f} hours, TTL: {CACHE_TTL_SECONDS / 3600} hours)")
|
|
396
|
+
|
|
397
|
+
return is_valid
|
|
398
|
+
|
|
399
|
+
def _load_cached_data(self) -> List[Dict[str, Any]]:
|
|
400
|
+
"""
|
|
401
|
+
Load KMS data from cache file.
|
|
402
|
+
|
|
403
|
+
:return: List of raw KMS key data from cache
|
|
404
|
+
:rtype: List[Dict[str, Any]]
|
|
405
|
+
"""
|
|
406
|
+
try:
|
|
407
|
+
with open(KMS_CACHE_FILE, encoding="utf-8") as file:
|
|
408
|
+
cached_data = json.load(file)
|
|
409
|
+
|
|
410
|
+
# Validate cache format - must be a list
|
|
411
|
+
if not isinstance(cached_data, list):
|
|
412
|
+
logger.warning("Invalid cache format detected (not a list). Invalidating cache.")
|
|
413
|
+
return []
|
|
414
|
+
|
|
415
|
+
# Check if items are dicts
|
|
416
|
+
if cached_data and not isinstance(cached_data[0], dict):
|
|
417
|
+
logger.warning("Invalid cache format detected (items not dicts). Invalidating cache.")
|
|
418
|
+
return []
|
|
419
|
+
|
|
420
|
+
logger.info(f"Loaded {len(cached_data)} KMS keys from cache")
|
|
421
|
+
return cached_data
|
|
422
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
423
|
+
logger.warning(f"Error reading cache file: {e}. Fetching fresh data.")
|
|
424
|
+
return []
|
|
425
|
+
|
|
426
|
+
def _save_to_cache(self, kms_data: List[Dict[str, Any]]) -> None:
|
|
427
|
+
"""
|
|
428
|
+
Save KMS data to cache file.
|
|
429
|
+
|
|
430
|
+
:param List[Dict[str, Any]] kms_data: Data to cache
|
|
431
|
+
:return: None
|
|
432
|
+
:rtype: None
|
|
433
|
+
"""
|
|
434
|
+
try:
|
|
435
|
+
# Ensure the artifacts directory exists
|
|
436
|
+
os.makedirs(os.path.dirname(KMS_CACHE_FILE), exist_ok=True)
|
|
437
|
+
|
|
438
|
+
with open(KMS_CACHE_FILE, "w", encoding="utf-8") as file:
|
|
439
|
+
json.dump(kms_data, file, indent=2, default=str)
|
|
440
|
+
|
|
441
|
+
logger.info(f"Cached {len(kms_data)} KMS keys to {KMS_CACHE_FILE}")
|
|
442
|
+
except IOError as e:
|
|
443
|
+
logger.warning(f"Error writing to cache file: {e}")
|
|
444
|
+
|
|
445
|
+
def _fetch_fresh_kms_data(self) -> List[Dict[str, Any]]:
|
|
446
|
+
"""
|
|
447
|
+
Fetch fresh KMS data from AWS.
|
|
448
|
+
|
|
449
|
+
:return: List of KMS key data
|
|
450
|
+
:rtype: List[Dict[str, Any]]
|
|
451
|
+
"""
|
|
452
|
+
logger.info("Fetching KMS data from AWS...")
|
|
453
|
+
|
|
454
|
+
# Log filtering parameters
|
|
455
|
+
if self.account_id:
|
|
456
|
+
logger.info(f"Filtering KMS keys by account ID: {self.account_id}")
|
|
457
|
+
if self.tags:
|
|
458
|
+
logger.info(f"Filtering KMS keys by tags: {self.tags}")
|
|
459
|
+
|
|
460
|
+
# Use inventory collector for consistency
|
|
461
|
+
from regscale.integrations.commercial.aws.inventory.resources.kms import KMSCollector
|
|
462
|
+
|
|
463
|
+
collector = KMSCollector(session=self.session, region=self.region, account_id=self.account_id, tags=self.tags)
|
|
464
|
+
|
|
465
|
+
inventory = collector.collect()
|
|
466
|
+
keys = inventory.get("Keys", [])
|
|
467
|
+
|
|
468
|
+
logger.info(f"Fetched {len(keys)} KMS keys from AWS (after filtering)")
|
|
469
|
+
return keys
|
|
470
|
+
|
|
471
|
+
def fetch_compliance_data(self) -> List[Dict[str, Any]]:
|
|
472
|
+
"""
|
|
473
|
+
Fetch raw KMS data from AWS.
|
|
474
|
+
|
|
475
|
+
Uses cached data if available and not expired (4-hour TTL), unless force_refresh is True.
|
|
476
|
+
|
|
477
|
+
:return: List of raw KMS key data
|
|
478
|
+
:rtype: List[Dict[str, Any]]
|
|
479
|
+
"""
|
|
480
|
+
# Check if we should use cached data
|
|
481
|
+
if not self.force_refresh and self._is_cache_valid():
|
|
482
|
+
cached_data = self._load_cached_data()
|
|
483
|
+
if cached_data:
|
|
484
|
+
self.raw_kms_data = cached_data
|
|
485
|
+
return cached_data
|
|
486
|
+
|
|
487
|
+
# Force refresh requested or no valid cache, fetch fresh data from AWS
|
|
488
|
+
if self.force_refresh:
|
|
489
|
+
logger.info("Force refresh requested, bypassing cache and fetching fresh data from AWS KMS...")
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
kms_data = self._fetch_fresh_kms_data()
|
|
493
|
+
self.raw_kms_data = kms_data
|
|
494
|
+
self._save_to_cache(kms_data)
|
|
495
|
+
return kms_data
|
|
496
|
+
except ClientError as e:
|
|
497
|
+
logger.error(f"Error fetching KMS data from AWS: {e}")
|
|
498
|
+
return []
|
|
499
|
+
|
|
500
|
+
def create_compliance_item(self, raw_data: Dict[str, Any]) -> ComplianceItem:
|
|
501
|
+
"""
|
|
502
|
+
Create a ComplianceItem from raw KMS key data.
|
|
503
|
+
|
|
504
|
+
:param Dict[str, Any] raw_data: Raw KMS key data
|
|
505
|
+
:return: KMSComplianceItem instance
|
|
506
|
+
:rtype: ComplianceItem
|
|
507
|
+
"""
|
|
508
|
+
return KMSComplianceItem(raw_data, self.control_mapper)
|
|
509
|
+
|
|
510
|
+
def _map_resource_type_to_asset_type(self, compliance_item: ComplianceItem) -> str:
|
|
511
|
+
"""
|
|
512
|
+
Map KMS key to RegScale asset type.
|
|
513
|
+
|
|
514
|
+
:param ComplianceItem compliance_item: Compliance item
|
|
515
|
+
:return: Asset type string
|
|
516
|
+
:rtype: str
|
|
517
|
+
"""
|
|
518
|
+
return "AWS KMS Key"
|
|
519
|
+
|
|
520
|
+
def sync_compliance(self) -> None:
|
|
521
|
+
"""
|
|
522
|
+
Main method to sync KMS compliance data.
|
|
523
|
+
|
|
524
|
+
Extends base sync_compliance to add evidence collection support.
|
|
525
|
+
|
|
526
|
+
:return: None
|
|
527
|
+
:rtype: None
|
|
528
|
+
"""
|
|
529
|
+
# Call the base class sync_compliance to handle control assessments and issues
|
|
530
|
+
super().sync_compliance()
|
|
531
|
+
|
|
532
|
+
# If evidence collection is enabled, collect evidence after compliance sync
|
|
533
|
+
if self.collect_evidence:
|
|
534
|
+
logger.info("Evidence collection enabled, starting evidence collection...")
|
|
535
|
+
self._collect_kms_evidence()
|
|
536
|
+
|
|
537
|
+
def _collect_kms_evidence(self) -> None:
|
|
538
|
+
"""
|
|
539
|
+
Collect KMS evidence and create Evidence records or SSP attachments.
|
|
540
|
+
|
|
541
|
+
:return: None
|
|
542
|
+
:rtype: None
|
|
543
|
+
"""
|
|
544
|
+
if not self.raw_kms_data:
|
|
545
|
+
logger.warning("No KMS data available for evidence collection")
|
|
546
|
+
return
|
|
547
|
+
|
|
548
|
+
scan_date = get_current_datetime(dt_format="%Y-%m-%d")
|
|
549
|
+
|
|
550
|
+
if self.evidence_as_attachments:
|
|
551
|
+
logger.info("Creating SSP file attachment with KMS evidence...")
|
|
552
|
+
self._create_ssp_attachment(scan_date)
|
|
553
|
+
else:
|
|
554
|
+
logger.info("Creating Evidence record with KMS evidence...")
|
|
555
|
+
self._create_evidence_record(scan_date)
|
|
556
|
+
|
|
557
|
+
def _create_ssp_attachment(self, scan_date: str) -> None:
|
|
558
|
+
"""
|
|
559
|
+
Create SSP file attachment with KMS evidence data.
|
|
560
|
+
|
|
561
|
+
:param str scan_date: Scan date string
|
|
562
|
+
:return: None
|
|
563
|
+
:rtype: None
|
|
564
|
+
"""
|
|
565
|
+
try:
|
|
566
|
+
# Check for existing evidence to avoid duplicates
|
|
567
|
+
date_str = datetime.now().strftime("%Y%m%d")
|
|
568
|
+
account_suffix = f"_{self.account_id}" if self.account_id else ""
|
|
569
|
+
file_name_pattern = f"kms_evidence{account_suffix}_{date_str}"
|
|
570
|
+
|
|
571
|
+
if self.check_for_existing_evidence(file_name_pattern):
|
|
572
|
+
logger.info("Evidence file for KMS already exists for today. Skipping upload to avoid duplicates.")
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
# Add timestamp to make filename unique if run multiple times per day
|
|
576
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
577
|
+
file_name = f"kms_evidence{account_suffix}_{timestamp}.jsonl.gz"
|
|
578
|
+
|
|
579
|
+
# Prepare JSONL content with compliance results
|
|
580
|
+
jsonl_lines = []
|
|
581
|
+
for key_data in self.raw_kms_data:
|
|
582
|
+
compliance_item = self.create_compliance_item(key_data)
|
|
583
|
+
evidence_entry = {
|
|
584
|
+
**key_data,
|
|
585
|
+
"compliance_assessment": {
|
|
586
|
+
"overall_result": compliance_item.compliance_result,
|
|
587
|
+
"control_results": compliance_item._compliance_results,
|
|
588
|
+
"assessed_controls": list(compliance_item._compliance_results.keys()),
|
|
589
|
+
"assessment_date": scan_date,
|
|
590
|
+
},
|
|
591
|
+
}
|
|
592
|
+
jsonl_lines.append(json.dumps(evidence_entry, default=str))
|
|
593
|
+
|
|
594
|
+
jsonl_content = "\n".join(jsonl_lines)
|
|
595
|
+
|
|
596
|
+
# Compress the JSONL content
|
|
597
|
+
compressed_buffer = BytesIO()
|
|
598
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
599
|
+
gz_file.write(jsonl_content)
|
|
600
|
+
|
|
601
|
+
compressed_data = compressed_buffer.getvalue()
|
|
602
|
+
compressed_size_mb = len(compressed_data) / (1024 * 1024)
|
|
603
|
+
uncompressed_size_mb = len(jsonl_content.encode("utf-8")) / (1024 * 1024)
|
|
604
|
+
compression_ratio = (1 - (len(compressed_data) / len(jsonl_content.encode("utf-8")))) * 100
|
|
605
|
+
|
|
606
|
+
logger.info(
|
|
607
|
+
"Compressed KMS evidence: %.2f MB -> %.2f MB (%.1f%% reduction)",
|
|
608
|
+
uncompressed_size_mb,
|
|
609
|
+
compressed_size_mb,
|
|
610
|
+
compression_ratio,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
# Upload to SSP
|
|
614
|
+
api = Api()
|
|
615
|
+
success = File.upload_file_to_regscale(
|
|
616
|
+
file_name=file_name,
|
|
617
|
+
parent_id=self.plan_id,
|
|
618
|
+
parent_module=self.parent_module,
|
|
619
|
+
api=api,
|
|
620
|
+
file_data=compressed_data,
|
|
621
|
+
tags="aws,kms,encryption,automated",
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
if success:
|
|
625
|
+
logger.info(f"Successfully uploaded KMS evidence file to SSP {self.plan_id}: {file_name}")
|
|
626
|
+
else:
|
|
627
|
+
logger.error(f"Failed to upload KMS evidence file to SSP {self.plan_id}")
|
|
628
|
+
|
|
629
|
+
except Exception as e:
|
|
630
|
+
logger.error(f"Error creating SSP attachment for KMS evidence: {e}", exc_info=True)
|
|
631
|
+
|
|
632
|
+
def _create_evidence_record(self, scan_date: str) -> None:
|
|
633
|
+
"""
|
|
634
|
+
Create Evidence record with KMS evidence data.
|
|
635
|
+
|
|
636
|
+
:param str scan_date: Scan date string
|
|
637
|
+
:return: None
|
|
638
|
+
:rtype: None
|
|
639
|
+
"""
|
|
640
|
+
try:
|
|
641
|
+
# Build evidence title and description
|
|
642
|
+
title = f"AWS KMS Evidence - {scan_date}"
|
|
643
|
+
description = self._build_evidence_description(scan_date)
|
|
644
|
+
|
|
645
|
+
# Calculate due date
|
|
646
|
+
due_date = (datetime.now() + timedelta(days=self.evidence_frequency)).isoformat()
|
|
647
|
+
|
|
648
|
+
# Create Evidence record
|
|
649
|
+
evidence = Evidence(
|
|
650
|
+
title=title,
|
|
651
|
+
description=description,
|
|
652
|
+
status="Collected",
|
|
653
|
+
updateFrequency=self.evidence_frequency,
|
|
654
|
+
dueDate=due_date,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
created_evidence = evidence.create()
|
|
658
|
+
if not created_evidence or not created_evidence.id:
|
|
659
|
+
logger.error("Failed to create evidence record")
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
logger.info(f"Created evidence record {created_evidence.id}: {title}")
|
|
663
|
+
|
|
664
|
+
# Upload compressed evidence file
|
|
665
|
+
self._upload_evidence_file(created_evidence.id, scan_date)
|
|
666
|
+
|
|
667
|
+
# Link evidence to SSP
|
|
668
|
+
self._link_evidence_to_ssp(created_evidence.id)
|
|
669
|
+
|
|
670
|
+
# Link to controls if specified
|
|
671
|
+
if self.evidence_control_ids:
|
|
672
|
+
self._link_evidence_to_controls(created_evidence.id, is_attachment=False)
|
|
673
|
+
|
|
674
|
+
except Exception as e:
|
|
675
|
+
logger.error(f"Error creating evidence record for KMS: {e}", exc_info=True)
|
|
676
|
+
|
|
677
|
+
def _build_evidence_description(self, scan_date: str) -> str:
|
|
678
|
+
"""
|
|
679
|
+
Build HTML-formatted evidence description.
|
|
680
|
+
|
|
681
|
+
:param str scan_date: Scan date string
|
|
682
|
+
:return: HTML description
|
|
683
|
+
:rtype: str
|
|
684
|
+
"""
|
|
685
|
+
# Gather statistics
|
|
686
|
+
kms_stats = self._calculate_kms_statistics()
|
|
687
|
+
control_stats = self._calculate_control_compliance_stats()
|
|
688
|
+
|
|
689
|
+
# Build description
|
|
690
|
+
desc_parts = self._build_evidence_header(scan_date)
|
|
691
|
+
desc_parts.extend(self._build_filter_info())
|
|
692
|
+
desc_parts.extend(self._build_kms_summary(kms_stats))
|
|
693
|
+
desc_parts.extend(self._build_control_compliance_summary(control_stats))
|
|
694
|
+
|
|
695
|
+
return "\n".join(desc_parts)
|
|
696
|
+
|
|
697
|
+
def _calculate_kms_statistics(self) -> Dict[str, Any]:
|
|
698
|
+
"""Calculate KMS key statistics."""
|
|
699
|
+
total_keys = len(self.raw_kms_data)
|
|
700
|
+
rotation_enabled_count = sum(1 for k in self.raw_kms_data if k.get("RotationEnabled", False))
|
|
701
|
+
customer_managed_count = sum(1 for k in self.raw_kms_data if k.get("KeyManager") == "CUSTOMER")
|
|
702
|
+
|
|
703
|
+
rotation_pct = rotation_enabled_count / max(total_keys, 1) * 100
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
"total": total_keys,
|
|
707
|
+
"rotation_enabled": rotation_enabled_count,
|
|
708
|
+
"rotation_pct": rotation_pct,
|
|
709
|
+
"customer_managed": customer_managed_count,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
def _calculate_control_compliance_stats(self) -> Dict[str, Dict[str, int]]:
|
|
713
|
+
"""Calculate compliance statistics by control."""
|
|
714
|
+
control_stats = {control_id: {"pass": 0, "fail": 0} for control_id in self.control_mapper.get_mapped_controls()}
|
|
715
|
+
|
|
716
|
+
for key_data in self.raw_kms_data:
|
|
717
|
+
compliance_item = self.create_compliance_item(key_data)
|
|
718
|
+
self._update_control_stats(control_stats, compliance_item._compliance_results)
|
|
719
|
+
|
|
720
|
+
return control_stats
|
|
721
|
+
|
|
722
|
+
def _update_control_stats(
|
|
723
|
+
self, control_stats: Dict[str, Dict[str, int]], compliance_results: Dict[str, str]
|
|
724
|
+
) -> None:
|
|
725
|
+
"""Update control statistics with compliance results."""
|
|
726
|
+
for control_id, result in compliance_results.items():
|
|
727
|
+
if result == "PASS":
|
|
728
|
+
control_stats[control_id]["pass"] += 1
|
|
729
|
+
else:
|
|
730
|
+
control_stats[control_id]["fail"] += 1
|
|
731
|
+
|
|
732
|
+
def _build_evidence_header(self, scan_date: str) -> List[str]:
|
|
733
|
+
"""Build the evidence header section."""
|
|
734
|
+
return [
|
|
735
|
+
"<h1>AWS KMS Evidence</h1>",
|
|
736
|
+
f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Assessment Date:{HTML_STRONG_CLOSE} {scan_date}{HTML_P_CLOSE}",
|
|
737
|
+
f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Region:{HTML_STRONG_CLOSE} {self.region}{HTML_P_CLOSE}",
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
def _build_filter_info(self) -> List[str]:
|
|
741
|
+
"""Build filter information section."""
|
|
742
|
+
filter_parts = []
|
|
743
|
+
|
|
744
|
+
if self.account_id:
|
|
745
|
+
filter_parts.append(
|
|
746
|
+
f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Filtered by Account ID:{HTML_STRONG_CLOSE} {self.account_id}{HTML_P_CLOSE}"
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
if self.tags:
|
|
750
|
+
tags_str = ", ".join([f"{k}={v}" for k, v in self.tags.items()])
|
|
751
|
+
filter_parts.append(
|
|
752
|
+
f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Filtered by Tags:{HTML_STRONG_CLOSE} {tags_str}{HTML_P_CLOSE}"
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
return filter_parts
|
|
756
|
+
|
|
757
|
+
def _build_kms_summary(self, kms_stats: Dict[str, Any]) -> List[str]:
|
|
758
|
+
"""Build KMS summary section."""
|
|
759
|
+
return [
|
|
760
|
+
f"{HTML_H2_OPEN}Summary{HTML_H2_CLOSE}",
|
|
761
|
+
HTML_UL_OPEN,
|
|
762
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Total Keys:{HTML_STRONG_CLOSE} {kms_stats['total']}{HTML_LI_CLOSE}",
|
|
763
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Customer-Managed Keys:{HTML_STRONG_CLOSE} "
|
|
764
|
+
f"{kms_stats['customer_managed']}{HTML_LI_CLOSE}",
|
|
765
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Rotation Enabled:{HTML_STRONG_CLOSE} {kms_stats['rotation_enabled']} "
|
|
766
|
+
f"({kms_stats['rotation_pct']:.1f}%){HTML_LI_CLOSE}",
|
|
767
|
+
HTML_UL_CLOSE,
|
|
768
|
+
]
|
|
769
|
+
|
|
770
|
+
def _build_control_compliance_summary(self, control_stats: Dict[str, Dict[str, int]]) -> List[str]:
|
|
771
|
+
"""Build control compliance summary section."""
|
|
772
|
+
section_parts = [
|
|
773
|
+
f"{HTML_H2_OPEN}Control Compliance Results{HTML_H2_CLOSE}",
|
|
774
|
+
HTML_UL_OPEN,
|
|
775
|
+
]
|
|
776
|
+
|
|
777
|
+
for control_id in sorted(control_stats.keys()):
|
|
778
|
+
control_line = self._format_control_stats(control_id, control_stats[control_id])
|
|
779
|
+
section_parts.append(control_line)
|
|
780
|
+
|
|
781
|
+
section_parts.append(HTML_UL_CLOSE)
|
|
782
|
+
return section_parts
|
|
783
|
+
|
|
784
|
+
def _format_control_stats(self, control_id: str, stats: Dict[str, int]) -> str:
|
|
785
|
+
"""Format control statistics for display."""
|
|
786
|
+
total = stats["pass"] + stats["fail"]
|
|
787
|
+
pass_pct = stats["pass"] / max(total, 1) * 100
|
|
788
|
+
control_desc = self.control_mapper.get_control_description(control_id)
|
|
789
|
+
|
|
790
|
+
return (
|
|
791
|
+
f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
|
|
792
|
+
f"{stats['pass']} PASS / {stats['fail']} FAIL ({pass_pct:.1f}% compliant) - {control_desc}{HTML_LI_CLOSE}"
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
def _upload_evidence_file(self, evidence_id: int, scan_date: str) -> None:
|
|
796
|
+
"""
|
|
797
|
+
Upload compressed JSONL evidence file to Evidence record.
|
|
798
|
+
|
|
799
|
+
:param int evidence_id: Evidence record ID
|
|
800
|
+
:param str scan_date: Scan date string
|
|
801
|
+
:return: None
|
|
802
|
+
:rtype: None
|
|
803
|
+
"""
|
|
804
|
+
try:
|
|
805
|
+
# Prepare JSONL content
|
|
806
|
+
jsonl_lines = []
|
|
807
|
+
for key_data in self.raw_kms_data:
|
|
808
|
+
compliance_item = self.create_compliance_item(key_data)
|
|
809
|
+
evidence_entry = {
|
|
810
|
+
**key_data,
|
|
811
|
+
"compliance_assessment": {
|
|
812
|
+
"overall_result": compliance_item.compliance_result,
|
|
813
|
+
"control_results": compliance_item._compliance_results,
|
|
814
|
+
"assessed_controls": list(compliance_item._compliance_results.keys()),
|
|
815
|
+
"assessment_date": scan_date,
|
|
816
|
+
},
|
|
817
|
+
}
|
|
818
|
+
jsonl_lines.append(json.dumps(evidence_entry, default=str))
|
|
819
|
+
|
|
820
|
+
jsonl_content = "\n".join(jsonl_lines)
|
|
821
|
+
|
|
822
|
+
# Compress
|
|
823
|
+
compressed_buffer = BytesIO()
|
|
824
|
+
with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
|
|
825
|
+
gz_file.write(jsonl_content)
|
|
826
|
+
|
|
827
|
+
compressed_data = compressed_buffer.getvalue()
|
|
828
|
+
|
|
829
|
+
# Upload
|
|
830
|
+
file_name = f"kms_evidence_{scan_date}.jsonl.gz"
|
|
831
|
+
api = Api()
|
|
832
|
+
success = File.upload_file_to_regscale(
|
|
833
|
+
file_name=file_name,
|
|
834
|
+
parent_id=evidence_id,
|
|
835
|
+
parent_module="evidence",
|
|
836
|
+
api=api,
|
|
837
|
+
file_data=compressed_data,
|
|
838
|
+
tags="aws,kms,encryption",
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if success:
|
|
842
|
+
logger.info(f"Uploaded KMS evidence file to Evidence {evidence_id}")
|
|
843
|
+
else:
|
|
844
|
+
logger.warning(f"Failed to upload KMS evidence file to Evidence {evidence_id}")
|
|
845
|
+
|
|
846
|
+
except Exception as e:
|
|
847
|
+
logger.error(f"Error uploading evidence file: {e}", exc_info=True)
|
|
848
|
+
|
|
849
|
+
def _link_evidence_to_ssp(self, evidence_id: int) -> None:
|
|
850
|
+
"""
|
|
851
|
+
Link evidence to Security Plan.
|
|
852
|
+
|
|
853
|
+
:param int evidence_id: Evidence record ID
|
|
854
|
+
:return: None
|
|
855
|
+
:rtype: None
|
|
856
|
+
"""
|
|
857
|
+
try:
|
|
858
|
+
mapping = EvidenceMapping(evidenceID=evidence_id, mappedID=self.plan_id, mappingType=self.parent_module)
|
|
859
|
+
mapping.create()
|
|
860
|
+
logger.info(f"Linked evidence {evidence_id} to SSP {self.plan_id}")
|
|
861
|
+
except Exception as ex:
|
|
862
|
+
logger.warning(f"Failed to link evidence to SSP: {ex}")
|
|
863
|
+
|
|
864
|
+
def _link_evidence_to_controls(self, evidence_id: int, is_attachment: bool = False) -> None:
|
|
865
|
+
"""
|
|
866
|
+
Link evidence to specified control IDs.
|
|
867
|
+
|
|
868
|
+
:param int evidence_id: Evidence or attachment ID
|
|
869
|
+
:param bool is_attachment: True if linking attachment, False for evidence record
|
|
870
|
+
"""
|
|
871
|
+
try:
|
|
872
|
+
for control_id in self.evidence_control_ids:
|
|
873
|
+
if is_attachment:
|
|
874
|
+
self.api.link_ssp_attachment_to_control(self.plan_id, evidence_id, control_id)
|
|
875
|
+
else:
|
|
876
|
+
self.api.link_evidence_to_control(evidence_id, control_id)
|
|
877
|
+
logger.info(f"Linked evidence {evidence_id} to control {control_id}")
|
|
878
|
+
except Exception as e:
|
|
879
|
+
logger.error(f"Failed to link evidence to controls: {e}", exc_info=True)
|