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,888 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
FedRAMP Rev 5 POAM Export
|
|
4
|
+
|
|
5
|
+
This module provides FedRAMP Rev 5 POAM Excel export functionality with advanced features:
|
|
6
|
+
- Dynamic POAM ID generation based on source file path properties
|
|
7
|
+
- KEV date determination from CISA KEV catalog
|
|
8
|
+
- Deviation status mapping (Approved/Pending/Rejected)
|
|
9
|
+
- Custom milestone and comment generation
|
|
10
|
+
- Excel formatting optimized for FedRAMP Rev 5 template
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from html import unescape
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
import openpyxl
|
|
23
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
24
|
+
|
|
25
|
+
from regscale.core.app.api import Api
|
|
26
|
+
from regscale.core.app.application import Application
|
|
27
|
+
from regscale.core.utils.date import datetime_obj
|
|
28
|
+
from regscale.integrations.public.cisa import pull_cisa_kev
|
|
29
|
+
from regscale.models.regscale_models import (
|
|
30
|
+
Asset,
|
|
31
|
+
Deviation,
|
|
32
|
+
File,
|
|
33
|
+
Issue,
|
|
34
|
+
IssueSeverity,
|
|
35
|
+
IssueStatus,
|
|
36
|
+
Link,
|
|
37
|
+
Property,
|
|
38
|
+
ScanHistory,
|
|
39
|
+
SecurityPlan,
|
|
40
|
+
VulnerabilityMapping,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger("regscale")
|
|
44
|
+
|
|
45
|
+
# FedRAMP POAM Export Constants
|
|
46
|
+
POAM_CLOSED_DATE_ROUNDING_DAY = 25 # FedRAMP requirement: round closed dates to 25th of month
|
|
47
|
+
EXCEL_TEMPLATE_HEADER_ROWS = 6 # Number of header rows in FedRAMP template before data starts
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@functools.lru_cache(maxsize=1)
|
|
51
|
+
def get_cached_cisa_kev():
|
|
52
|
+
"""
|
|
53
|
+
Pull the CISA KEV with caching
|
|
54
|
+
|
|
55
|
+
:return: CISA KEV data
|
|
56
|
+
:rtype: dict
|
|
57
|
+
"""
|
|
58
|
+
return pull_cisa_kev()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def set_short_date(date_str: str) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Convert datetime string to short date format (MM/DD/YY)
|
|
64
|
+
|
|
65
|
+
:param str date_str: Date string to convert
|
|
66
|
+
:return: Formatted date string
|
|
67
|
+
:rtype: str
|
|
68
|
+
"""
|
|
69
|
+
return datetime_obj(date_str).strftime("%m/%d/%y")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def strip_html(input_str: str) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Strip HTML tags from input string
|
|
75
|
+
|
|
76
|
+
:param str input_str: String with HTML tags
|
|
77
|
+
:return: String with HTML removed
|
|
78
|
+
:rtype: str
|
|
79
|
+
"""
|
|
80
|
+
if not input_str:
|
|
81
|
+
return ""
|
|
82
|
+
no_html = re.sub("<[^>]*>", "", input_str) # Use negated character class instead of reluctant quantifier
|
|
83
|
+
return unescape(no_html)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def convert_to_list(asset_identifier: str) -> List[str]:
|
|
87
|
+
"""
|
|
88
|
+
Convert asset identifier string to list, supporting multiple formats
|
|
89
|
+
|
|
90
|
+
Data could be <p> tag delimited, tab delimited, or newline delimited
|
|
91
|
+
|
|
92
|
+
:param str asset_identifier: Asset identifier string
|
|
93
|
+
:return: List of asset identifiers
|
|
94
|
+
:rtype: List[str]
|
|
95
|
+
"""
|
|
96
|
+
if not asset_identifier:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
# Check for <p> tags and split by them
|
|
100
|
+
if "<p>" in asset_identifier and "</p>" in asset_identifier:
|
|
101
|
+
return re.findall(r"<p>([^<]*)</p>", asset_identifier)
|
|
102
|
+
# Check for tab characters and split by them
|
|
103
|
+
if "\t" in asset_identifier:
|
|
104
|
+
return asset_identifier.split("\t")
|
|
105
|
+
# Otherwise, split by newlines
|
|
106
|
+
return asset_identifier.splitlines()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def determine_kev_date(cve: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Determine KEV due date from CISA KEV catalog
|
|
112
|
+
|
|
113
|
+
:param str cve: CVE identifier
|
|
114
|
+
:return: KEV due date or "N/A"
|
|
115
|
+
:rtype: str
|
|
116
|
+
"""
|
|
117
|
+
if not cve:
|
|
118
|
+
return "N/A"
|
|
119
|
+
|
|
120
|
+
kev_data = get_cached_cisa_kev()
|
|
121
|
+
for item in kev_data.get("vulnerabilities", []):
|
|
122
|
+
if item.get("cveID", "").lower() == cve.lower():
|
|
123
|
+
logger.info("Matched CVE: %s. KEV due date: %s", item.get("cveID"), item.get("dueDate"))
|
|
124
|
+
due_date = item.get("dueDate")
|
|
125
|
+
return set_short_date(due_date)
|
|
126
|
+
return "N/A"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def determine_poam_id(poam: Issue, props: List[Property]) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Determine POAM ID based on source file path patterns
|
|
132
|
+
|
|
133
|
+
Maps source file path keywords to POAM prefixes:
|
|
134
|
+
- pdf -> DC
|
|
135
|
+
- signatures -> CPT
|
|
136
|
+
- campaign -> ALM
|
|
137
|
+
- learning manager -> CCD
|
|
138
|
+
- cce -> CCE
|
|
139
|
+
|
|
140
|
+
:param Issue poam: POAM issue object
|
|
141
|
+
:param List[Property] props: Properties for the POAM
|
|
142
|
+
:return: Generated POAM ID
|
|
143
|
+
:rtype: str
|
|
144
|
+
"""
|
|
145
|
+
# Define mapping from file path keywords to POAM prefixes
|
|
146
|
+
source_path_mappings = {
|
|
147
|
+
"pdf": "DC",
|
|
148
|
+
"signatures": "CPT",
|
|
149
|
+
"campaign": "ALM",
|
|
150
|
+
"learning manager": "CCD",
|
|
151
|
+
"cce": "CCE",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Look for source_file_path property
|
|
155
|
+
source_file_path = None
|
|
156
|
+
for prop in props:
|
|
157
|
+
if prop.key == "source_file_path":
|
|
158
|
+
source_file_path = prop.value.lower()
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
if source_file_path:
|
|
162
|
+
# Check each mapping pattern
|
|
163
|
+
for keyword, prefix in source_path_mappings.items():
|
|
164
|
+
if keyword in source_file_path:
|
|
165
|
+
return f"{prefix}-{poam.id}"
|
|
166
|
+
|
|
167
|
+
return f"UNK-{poam.id}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def determine_poam_service_name(_poam: Issue, props: List[Property]) -> str:
|
|
171
|
+
"""
|
|
172
|
+
Determine service name from source file path
|
|
173
|
+
|
|
174
|
+
:param Issue _poam: POAM issue object (unused, kept for API consistency)
|
|
175
|
+
:param List[Property] props: Properties for the POAM
|
|
176
|
+
:return: Service name
|
|
177
|
+
:rtype: str
|
|
178
|
+
"""
|
|
179
|
+
for prop in props:
|
|
180
|
+
if prop.key == "source_file_path":
|
|
181
|
+
value_lower = prop.value.lower()
|
|
182
|
+
if "pdf" in value_lower:
|
|
183
|
+
return "PDF Services"
|
|
184
|
+
if "signatures" in value_lower:
|
|
185
|
+
return "Signatures"
|
|
186
|
+
return "UNKNOWN"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def lookup_scan_date(poam: Issue, assets: List[Asset]) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Lookup the scan date from vulnerability mappings
|
|
192
|
+
|
|
193
|
+
:param Issue poam: POAM issue object
|
|
194
|
+
:param List[Asset] assets: List of assets
|
|
195
|
+
:return: Scan date string
|
|
196
|
+
:rtype: str
|
|
197
|
+
"""
|
|
198
|
+
poam_assets = convert_to_list(poam.assetIdentifier)
|
|
199
|
+
for asset_name in poam_assets:
|
|
200
|
+
matching_asset = [asset for asset in assets if asset.name == asset_name]
|
|
201
|
+
if matching_asset:
|
|
202
|
+
vulns = VulnerabilityMapping.find_by_asset(matching_asset[0].id)
|
|
203
|
+
scans = [vuln.scanId for vuln in vulns]
|
|
204
|
+
if scans:
|
|
205
|
+
scan_date = ScanHistory.get_object(scans[0]).scanDate
|
|
206
|
+
return set_short_date(scan_date)
|
|
207
|
+
return set_short_date(poam.dateLastUpdated)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def determine_poam_comment(poam: Issue, assets: List[Asset]) -> str: # pylint: disable=unused-argument
|
|
211
|
+
"""
|
|
212
|
+
Determine and update POAM comment with appropriate status-based messages
|
|
213
|
+
|
|
214
|
+
:param Issue poam: POAM issue object
|
|
215
|
+
:param List[Asset] assets: List of assets
|
|
216
|
+
:return: Updated POAM comment
|
|
217
|
+
:rtype: str
|
|
218
|
+
"""
|
|
219
|
+
# Comment templates
|
|
220
|
+
closed_comment_template = (
|
|
221
|
+
"Per review of the latest scan report on %s, (TGRC) can confirm that this issue "
|
|
222
|
+
"no longer persists. This POAM will be submitted for closure."
|
|
223
|
+
)
|
|
224
|
+
open_comment_template = "POAM entry added"
|
|
225
|
+
|
|
226
|
+
if not poam.dateFirstDetected:
|
|
227
|
+
return "N/A"
|
|
228
|
+
|
|
229
|
+
original_comment = poam.poamComments
|
|
230
|
+
current_comment = poam.poamComments or ""
|
|
231
|
+
detection_date = set_short_date(poam.dateFirstDetected)
|
|
232
|
+
|
|
233
|
+
# Determine new comment based on POAM status
|
|
234
|
+
if poam.dateCompleted:
|
|
235
|
+
# Closed POAM: Add closure comment if not already present
|
|
236
|
+
updated_comment = _generate_closed_poam_comment(
|
|
237
|
+
poam, current_comment, closed_comment_template, open_comment_template
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
# Open POAM: Add detection/creation comment
|
|
241
|
+
updated_comment = _generate_open_poam_comment(current_comment, detection_date, open_comment_template)
|
|
242
|
+
|
|
243
|
+
# Save POAM if comment changed
|
|
244
|
+
if updated_comment != original_comment:
|
|
245
|
+
logger.info("Updating POAM comment for POAM #%s", poam.id)
|
|
246
|
+
poam.poamComments = updated_comment
|
|
247
|
+
poam.save()
|
|
248
|
+
|
|
249
|
+
return updated_comment or "N/A"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _generate_closed_poam_comment(poam: Issue, current_comment: str, template: str, _open_template: str) -> str:
|
|
253
|
+
"""
|
|
254
|
+
Generate comment for closed POAMs
|
|
255
|
+
|
|
256
|
+
:param Issue poam: POAM issue object
|
|
257
|
+
:param str current_comment: Current comment text
|
|
258
|
+
:param str template: Template for closed comment
|
|
259
|
+
:param str _open_template: Template for open comment (unused, kept for API consistency)
|
|
260
|
+
:return: Generated comment
|
|
261
|
+
:rtype: str
|
|
262
|
+
"""
|
|
263
|
+
closed_blurb = "This POAM will be submitted for closure"
|
|
264
|
+
open_blurb = "POAM entry added"
|
|
265
|
+
|
|
266
|
+
if open_blurb not in current_comment:
|
|
267
|
+
current_comment = f"{set_short_date(poam.dateCreated)}: {open_blurb}"
|
|
268
|
+
if closed_blurb in current_comment:
|
|
269
|
+
return current_comment # Already has closed comment
|
|
270
|
+
|
|
271
|
+
return template % set_short_date(poam.dateCompleted) + "\n" + current_comment
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _generate_open_poam_comment(current_comment: str, detection_date: str, template: str) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Generate comment for open POAMs
|
|
277
|
+
|
|
278
|
+
:param str current_comment: Current comment text
|
|
279
|
+
:param str detection_date: Detection date string
|
|
280
|
+
:param str template: Template for open comment
|
|
281
|
+
:return: Generated comment
|
|
282
|
+
:rtype: str
|
|
283
|
+
"""
|
|
284
|
+
# If comment already has "entry added", return unchanged
|
|
285
|
+
if current_comment and "entry added" in current_comment:
|
|
286
|
+
return current_comment
|
|
287
|
+
|
|
288
|
+
new_entry = f"{detection_date}: {template}"
|
|
289
|
+
|
|
290
|
+
# If there's existing comment (without "entry added"), prepend new entry
|
|
291
|
+
if current_comment:
|
|
292
|
+
return f"{new_entry}\n{current_comment}"
|
|
293
|
+
|
|
294
|
+
# No existing comment, return just the new entry
|
|
295
|
+
return new_entry
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def set_milestones(poam: Issue, index: int, sheet: Worksheet, column_l_date: str, all_milestones: List[dict]) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Set milestones in the worksheet
|
|
301
|
+
|
|
302
|
+
:param Issue poam: POAM issue object
|
|
303
|
+
:param int index: Row index
|
|
304
|
+
:param Worksheet sheet: Worksheet object
|
|
305
|
+
:param str column_l_date: Scheduled completion date
|
|
306
|
+
:param List[dict] all_milestones: All milestones
|
|
307
|
+
"""
|
|
308
|
+
milestones = [milestone for milestone in all_milestones if milestone.get("parent_id", 0) == poam.id]
|
|
309
|
+
milestone_text = f"{column_l_date}: System will be updated as part of the monthly patching cycle.\n".join(
|
|
310
|
+
[set_short_date(milestone.get("MilestoneDate", "")) for milestone in milestones]
|
|
311
|
+
)
|
|
312
|
+
if milestone_text:
|
|
313
|
+
sheet[f"M{index}"].value = milestone_text
|
|
314
|
+
else:
|
|
315
|
+
sheet[f"M{index}"].value = f"{column_l_date}: System will be updated as part of the monthly patching cycle."
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def set_status(poam: Issue, index: int, sheet: Worksheet) -> None:
|
|
319
|
+
"""
|
|
320
|
+
Set status completion date with rounding logic for closed POAMs
|
|
321
|
+
|
|
322
|
+
Closed dates are rounded to the 25th of the month:
|
|
323
|
+
- If closed on or before the 25th, use the 25th of that month
|
|
324
|
+
- If closed after the 25th, use the 25th of the next month
|
|
325
|
+
|
|
326
|
+
:param Issue poam: POAM issue object
|
|
327
|
+
:param int index: Row index
|
|
328
|
+
:param Worksheet sheet: Worksheet object
|
|
329
|
+
"""
|
|
330
|
+
if poam.status == "Closed" and poam.dateCompleted:
|
|
331
|
+
day_of_month = datetime_obj(poam.dateCompleted).day
|
|
332
|
+
if day_of_month <= POAM_CLOSED_DATE_ROUNDING_DAY:
|
|
333
|
+
new_date_completed = datetime_obj(poam.dateCompleted).replace(day=POAM_CLOSED_DATE_ROUNDING_DAY)
|
|
334
|
+
else:
|
|
335
|
+
# Move to 25th of next month
|
|
336
|
+
next_month = datetime_obj(poam.dateCompleted) + timedelta(days=31)
|
|
337
|
+
new_date_completed = next_month.replace(day=POAM_CLOSED_DATE_ROUNDING_DAY)
|
|
338
|
+
sheet[f"O{index}"].value = set_short_date(new_date_completed)
|
|
339
|
+
elif poam.status == "Closed":
|
|
340
|
+
sheet[f"O{index}"].value = ""
|
|
341
|
+
if poam.status == "Open" and poam.dateLastUpdated:
|
|
342
|
+
sheet[f"O{index}"].value = set_short_date(poam.dateLastUpdated)
|
|
343
|
+
elif poam.status == "Open":
|
|
344
|
+
sheet[f"O{index}"].value = ""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def set_vendor_info(poam: Issue, index: int, sheet: Worksheet) -> None:
|
|
348
|
+
"""
|
|
349
|
+
Set vendor dependency information
|
|
350
|
+
|
|
351
|
+
:param Issue poam: POAM issue object
|
|
352
|
+
:param int index: Row index
|
|
353
|
+
:param Worksheet sheet: Worksheet object
|
|
354
|
+
"""
|
|
355
|
+
sheet[f"P{index}"].value = poam.vendorDependency or "No"
|
|
356
|
+
sheet[f"R{index}"].value = poam.vendorName if poam.vendorName else "N/A"
|
|
357
|
+
if sheet[f"P{index}"].value == "No":
|
|
358
|
+
sheet[f"Q{index}"].value = "N/A"
|
|
359
|
+
elif poam.vendorLastUpdate:
|
|
360
|
+
sheet[f"Q{index}"].value = set_short_date(poam.vendorLastUpdate)
|
|
361
|
+
else:
|
|
362
|
+
sheet[f"Q{index}"].value = ""
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def set_risk_info(poam: Issue, index: int, sheet: Worksheet) -> None:
|
|
366
|
+
"""
|
|
367
|
+
Set risk adjustment and deviation information
|
|
368
|
+
|
|
369
|
+
Maps deviation status to Yes/Pending/No based on approval state
|
|
370
|
+
|
|
371
|
+
:param Issue poam: POAM issue object
|
|
372
|
+
:param int index: Row index
|
|
373
|
+
:param Worksheet sheet: Worksheet object
|
|
374
|
+
"""
|
|
375
|
+
deviation_map = {"Approved": "Yes", "Pending": "Pending", "Rejected": "No"}
|
|
376
|
+
deviation_status = ""
|
|
377
|
+
deviation_obj = Deviation.get_by_issue(poam.id)
|
|
378
|
+
if deviation_obj:
|
|
379
|
+
deviation_status = deviation_obj.deviationStatus
|
|
380
|
+
deviation_rationale = strip_html(poam.deviationRationale)
|
|
381
|
+
|
|
382
|
+
original_risk_rating = (
|
|
383
|
+
poam.originalRiskRating if poam.originalRiskRating else IssueSeverity(poam.severityLevel).name
|
|
384
|
+
)
|
|
385
|
+
sheet[f"S{index}"].value = poam.originalRiskRating if poam.originalRiskRating else original_risk_rating
|
|
386
|
+
|
|
387
|
+
# Set defaults
|
|
388
|
+
sheet[f"T{index}"].value = poam.adjustedRiskRating or "N/A"
|
|
389
|
+
sheet[f"U{index}"].value = poam.riskAdjustment or "No"
|
|
390
|
+
sheet[f"V{index}"].value = poam.falsePositive or "No"
|
|
391
|
+
sheet[f"W{index}"].value = "No"
|
|
392
|
+
|
|
393
|
+
if poam.operationalRequirement or poam.riskAdjustment or poam.falsePositive:
|
|
394
|
+
sheet[f"X{index}"].value = deviation_rationale
|
|
395
|
+
|
|
396
|
+
if poam.falsePositive in ["Yes", "Pending"]:
|
|
397
|
+
sheet[f"V{index}"].value = deviation_map.get(deviation_status, "No")
|
|
398
|
+
|
|
399
|
+
if poam.riskAdjustment in ["Yes", "Pending"]:
|
|
400
|
+
sheet[f"U{index}"].value = deviation_map.get(deviation_status, "No")
|
|
401
|
+
|
|
402
|
+
if poam.operationalRequirement in ["Yes", "Pending"]:
|
|
403
|
+
sheet[f"W{index}"].value = deviation_map.get(deviation_status, "No")
|
|
404
|
+
if poam.operationalRequirement == "Yes" and deviation_map.get(deviation_status, "No") == "Pending":
|
|
405
|
+
sheet[f"U{index}"].value = "No"
|
|
406
|
+
sheet[f"V{index}"].value = "No"
|
|
407
|
+
sheet[f"W{index}"].value = "Pending"
|
|
408
|
+
|
|
409
|
+
if not deviation_rationale:
|
|
410
|
+
sheet[f"X{index}"].value = "N/A"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def set_end_columns(
|
|
414
|
+
_ssp: SecurityPlan,
|
|
415
|
+
poam: Issue,
|
|
416
|
+
index: int,
|
|
417
|
+
sheet: Worksheet,
|
|
418
|
+
props: List[Property],
|
|
419
|
+
assets: List[Asset],
|
|
420
|
+
all_links: List[dict],
|
|
421
|
+
all_files: List[dict],
|
|
422
|
+
):
|
|
423
|
+
"""
|
|
424
|
+
Set end columns including links, files, KEV data, and service names
|
|
425
|
+
|
|
426
|
+
:param SecurityPlan _ssp: Security plan object (unused, kept for API consistency)
|
|
427
|
+
:param Issue poam: POAM issue object
|
|
428
|
+
:param int index: Row index
|
|
429
|
+
:param Worksheet sheet: Worksheet object
|
|
430
|
+
:param List[Property] props: Properties list
|
|
431
|
+
:param List[Asset] assets: Assets list
|
|
432
|
+
:param List[dict] all_links: All links
|
|
433
|
+
:param List[dict] all_files: All files
|
|
434
|
+
"""
|
|
435
|
+
grouped_links = [link for link in all_links if link.parentID == poam.id]
|
|
436
|
+
grouped_files = [file for file in all_files if file.parentId == poam.id]
|
|
437
|
+
|
|
438
|
+
aggregate_link_txt = "".join([f"\t{lin['Title']}: {lin['URL']};\n" for lin in grouped_links])
|
|
439
|
+
aggregate_file_txt = "".join([f"\t{fil['TrustedDisplayName']};\n" for fil in grouped_files])
|
|
440
|
+
|
|
441
|
+
sheet[f"Y{index}"].value = "N/A"
|
|
442
|
+
if grouped_links:
|
|
443
|
+
sheet[f"Y{index}"].value = "Links:\n" + aggregate_link_txt
|
|
444
|
+
if grouped_files:
|
|
445
|
+
sheet[f"Y{index}"].value = "\nFiles:\n" + aggregate_file_txt
|
|
446
|
+
|
|
447
|
+
sheet[f"Z{index}"].value = determine_poam_comment(poam, assets)
|
|
448
|
+
sheet[f"AA{index}"].value = poam.autoApproved
|
|
449
|
+
sheet[f"AB{index}"].value = poam.kevList if poam.kevList == "Yes" else "No"
|
|
450
|
+
sheet[f"AC{index}"].value = determine_kev_date(poam.cve)
|
|
451
|
+
sheet[f"AD{index}"].value = poam.cve or "N/A"
|
|
452
|
+
service_name = determine_poam_service_name(poam, props)
|
|
453
|
+
sheet[f"AE{index}"].value = service_name
|
|
454
|
+
sheet[f"I{index}"].value = service_name
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _normalize_source_report(poam: Issue) -> None:
|
|
458
|
+
"""
|
|
459
|
+
Normalize source report name (e.g., SAP Concur -> Tenable SC)
|
|
460
|
+
|
|
461
|
+
:param Issue poam: POAM issue object
|
|
462
|
+
"""
|
|
463
|
+
if poam.sourceReport == "SAP Concur":
|
|
464
|
+
poam.sourceReport = "Tenable SC"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _populate_basic_poam_columns(sheet: Worksheet, index: int, poam: Issue, point_of_contact: str) -> None:
|
|
468
|
+
"""
|
|
469
|
+
Populate basic POAM columns B-I (control, title, description, assets, POC, service)
|
|
470
|
+
|
|
471
|
+
:param Worksheet sheet: Worksheet object
|
|
472
|
+
:param int index: Row index
|
|
473
|
+
:param Issue poam: POAM issue object
|
|
474
|
+
:param str point_of_contact: Point of Contact name
|
|
475
|
+
"""
|
|
476
|
+
sheet[f"B{index}"].value = "RA-5"
|
|
477
|
+
title = poam.title or poam.cve
|
|
478
|
+
sheet[f"C{index}"].value = title
|
|
479
|
+
sheet[f"D{index}"].value = strip_html(poam.description)
|
|
480
|
+
sheet[f"G{index}"].value = "\n".join(convert_to_list(poam.assetIdentifier))
|
|
481
|
+
sheet[f"H{index}"].value = point_of_contact if point_of_contact else ""
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _populate_date_and_milestone_columns(sheet: Worksheet, index: int, poam: Issue, all_milestones: List[dict]) -> None:
|
|
485
|
+
"""
|
|
486
|
+
Populate date and milestone columns K-M (detection date, due date, milestones)
|
|
487
|
+
|
|
488
|
+
:param Worksheet sheet: Worksheet object
|
|
489
|
+
:param int index: Row index
|
|
490
|
+
:param Issue poam: POAM issue object
|
|
491
|
+
:param List[dict] all_milestones: All milestones
|
|
492
|
+
"""
|
|
493
|
+
sheet[f"K{index}"].value = set_short_date(poam.dateFirstDetected)
|
|
494
|
+
column_l_date = (
|
|
495
|
+
(datetime_obj(poam.dueDate) + timedelta(days=-1)).strftime("%m/%d/%y") if datetime_obj(poam.dueDate) else ""
|
|
496
|
+
)
|
|
497
|
+
sheet[f"L{index}"].value = column_l_date
|
|
498
|
+
set_milestones(poam, index, sheet, column_l_date, all_milestones)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def map_weakness_detector_and_id_for_rev5_issues(
|
|
502
|
+
worksheet: Worksheet, column1: str, column2: str, row_number: int, issue: Issue
|
|
503
|
+
):
|
|
504
|
+
"""
|
|
505
|
+
Map weakness detector (column E) and source ID (column F)
|
|
506
|
+
|
|
507
|
+
:param Worksheet worksheet: Worksheet object
|
|
508
|
+
:param str column1: First column letter (E)
|
|
509
|
+
:param str column2: Second column letter (F)
|
|
510
|
+
:param int row_number: Row number
|
|
511
|
+
:param Issue issue: Issue object
|
|
512
|
+
"""
|
|
513
|
+
worksheet[f"{column1}{row_number}"] = issue.sourceReport or ""
|
|
514
|
+
worksheet[f"{column2}{row_number}"] = issue.cve or issue.pluginId or issue.title
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def process_row(
|
|
518
|
+
ssp: SecurityPlan,
|
|
519
|
+
poam: Issue,
|
|
520
|
+
index: int,
|
|
521
|
+
sheet: Worksheet,
|
|
522
|
+
assets: List[Asset],
|
|
523
|
+
all_milestones: List[dict],
|
|
524
|
+
all_links: List[dict],
|
|
525
|
+
all_files: List[dict],
|
|
526
|
+
point_of_contact: str = "",
|
|
527
|
+
):
|
|
528
|
+
"""
|
|
529
|
+
Process a single POAM row in the worksheet
|
|
530
|
+
|
|
531
|
+
:param SecurityPlan ssp: Security plan object
|
|
532
|
+
:param Issue poam: POAM issue object
|
|
533
|
+
:param int index: Row index
|
|
534
|
+
:param Worksheet sheet: Worksheet object
|
|
535
|
+
:param List[Asset] assets: Assets list
|
|
536
|
+
:param List[dict] all_milestones: All milestones
|
|
537
|
+
:param List[dict] all_links: All links
|
|
538
|
+
:param List[dict] all_files: All files
|
|
539
|
+
:param str point_of_contact: Point of Contact name for POAMs
|
|
540
|
+
"""
|
|
541
|
+
index = EXCEL_TEMPLATE_HEADER_ROWS + index # Adjust for header rows
|
|
542
|
+
|
|
543
|
+
if not index or index < EXCEL_TEMPLATE_HEADER_ROWS:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
props = Property.get_all_by_parent(parent_id=poam.id, parent_module="issues")
|
|
548
|
+
|
|
549
|
+
# Normalize source report name
|
|
550
|
+
_normalize_source_report(poam)
|
|
551
|
+
|
|
552
|
+
# Populate basic columns (B-I)
|
|
553
|
+
_populate_basic_poam_columns(sheet, index, poam, point_of_contact)
|
|
554
|
+
|
|
555
|
+
# Map weakness detector and source ID (E-F)
|
|
556
|
+
map_weakness_detector_and_id_for_rev5_issues(
|
|
557
|
+
worksheet=sheet, column1="E", column2="F", row_number=index, issue=poam
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Populate remediation and date columns (J-M)
|
|
561
|
+
sheet[f"J{index}"].value = strip_html(poam.remediationDescription)
|
|
562
|
+
_populate_date_and_milestone_columns(sheet, index, poam, all_milestones)
|
|
563
|
+
|
|
564
|
+
# Populate changes and status columns (N-R)
|
|
565
|
+
sheet[f"N{index}"].value = strip_html(poam.changes)
|
|
566
|
+
set_status(poam, index, sheet)
|
|
567
|
+
set_vendor_info(poam, index, sheet)
|
|
568
|
+
|
|
569
|
+
# Populate risk and deviation columns (S-X)
|
|
570
|
+
set_risk_info(poam, index, sheet)
|
|
571
|
+
|
|
572
|
+
# Populate end columns (Y-AE)
|
|
573
|
+
set_end_columns(ssp, poam, index, sheet, props, assets, all_links, all_files)
|
|
574
|
+
|
|
575
|
+
# Set POAM ID (column A)
|
|
576
|
+
new_poam_id = determine_poam_id(poam, props)
|
|
577
|
+
logger.info("Generated POAM ID For POAM #%s: %s", poam.id, new_poam_id)
|
|
578
|
+
sheet[f"A{index}"].value = new_poam_id
|
|
579
|
+
|
|
580
|
+
except (KeyError, AttributeError, ValueError, TypeError) as e:
|
|
581
|
+
logger.error("Error processing POAM #%s: %s", poam.id, e)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def update_column_widths(ws: Worksheet) -> None:
|
|
585
|
+
"""
|
|
586
|
+
Update column widths and formatting for the worksheet
|
|
587
|
+
|
|
588
|
+
:param Worksheet ws: Worksheet to format
|
|
589
|
+
"""
|
|
590
|
+
# Define specific column widths
|
|
591
|
+
fixed_widths = {
|
|
592
|
+
"A": 15, # POAM ID
|
|
593
|
+
"B": 10, # Control
|
|
594
|
+
"C": 40, # Title
|
|
595
|
+
"D": 50, # Description
|
|
596
|
+
"E": 20, # Source Report
|
|
597
|
+
"F": 20, # Plugin ID/CVE
|
|
598
|
+
"G": 30, # Asset Identifier
|
|
599
|
+
"H": 15, # Point of Contact
|
|
600
|
+
"I": 50, # Service Name
|
|
601
|
+
"J": 15, # Remediation
|
|
602
|
+
"K": 15, # Detection Date
|
|
603
|
+
"L": 15, # Due Date
|
|
604
|
+
"M": 15, # Milestones
|
|
605
|
+
"N": 30, # Changes
|
|
606
|
+
"O": 15, # Completion Date
|
|
607
|
+
"P": 15, # Vendor Dependency
|
|
608
|
+
"Q": 15, # Vendor Last Update
|
|
609
|
+
"R": 20, # Vendor Name
|
|
610
|
+
"S": 15, # Original Risk
|
|
611
|
+
"T": 15, # Adjusted Risk
|
|
612
|
+
"U": 15, # Risk Adjustment
|
|
613
|
+
"V": 15, # False Positive
|
|
614
|
+
"W": 15, # Operational Requirement
|
|
615
|
+
"X": 30, # Deviation Rationale
|
|
616
|
+
"Y": 40, # Links and Files
|
|
617
|
+
"Z": 50, # POAM Comments
|
|
618
|
+
"AA": 15, # Auto Approved
|
|
619
|
+
"AB": 15, # KEV List
|
|
620
|
+
"AC": 15, # KEV Due Date
|
|
621
|
+
"AD": 20, # CVE
|
|
622
|
+
"AE": 30, # Service Name
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# Apply fixed widths
|
|
626
|
+
for col, width in fixed_widths.items():
|
|
627
|
+
ws.column_dimensions[col].width = width
|
|
628
|
+
|
|
629
|
+
# Enable text wrapping for specific columns
|
|
630
|
+
wrap_columns = ["C", "D", "I", "X", "Y", "Z"]
|
|
631
|
+
for col in wrap_columns:
|
|
632
|
+
for cell in ws[col]:
|
|
633
|
+
if not isinstance(cell, openpyxl.cell.cell.MergedCell) and cell.value:
|
|
634
|
+
cell.alignment = openpyxl.styles.Alignment(wrap_text=True)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def align_column(column_letter: str, worksheet: Worksheet) -> None:
|
|
638
|
+
"""
|
|
639
|
+
Align column text to the left and wrap text
|
|
640
|
+
|
|
641
|
+
:param str column_letter: Column letter to align
|
|
642
|
+
:param Worksheet worksheet: Worksheet object
|
|
643
|
+
"""
|
|
644
|
+
for cell in worksheet[column_letter]:
|
|
645
|
+
cell.alignment = openpyxl.styles.Alignment(wrap_text=True, horizontal="left")
|
|
646
|
+
cell.value = cell.value.strip() if cell.value else ""
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def update_header(ssp: SecurityPlan, sheet: Worksheet) -> Worksheet:
|
|
650
|
+
"""
|
|
651
|
+
Update the header rows of the worksheet with SSP information
|
|
652
|
+
|
|
653
|
+
:param SecurityPlan ssp: Security plan object
|
|
654
|
+
:param Worksheet sheet: Worksheet object
|
|
655
|
+
:return: Updated worksheet
|
|
656
|
+
:rtype: Worksheet
|
|
657
|
+
"""
|
|
658
|
+
sheet["A3"] = ssp.cspOrgName or "N/A"
|
|
659
|
+
sheet["B3"] = ssp.systemName
|
|
660
|
+
sheet["C3"] = ssp.overallCategorization
|
|
661
|
+
sheet["D3"] = datetime.now().strftime("%m/%d/%Y")
|
|
662
|
+
return sheet
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def get_all_poams(ssp_id: str) -> List[Issue]:
|
|
666
|
+
"""
|
|
667
|
+
Get all POAMs for the given SSP ID, including those from child assets
|
|
668
|
+
|
|
669
|
+
:param str ssp_id: SSP ID
|
|
670
|
+
:return: List of POAM issues
|
|
671
|
+
:rtype: List[Issue]
|
|
672
|
+
"""
|
|
673
|
+
logger.info("Getting POAMs for SSP %s", ssp_id)
|
|
674
|
+
poams = [iss for iss in Issue.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans") if iss.isPoam]
|
|
675
|
+
|
|
676
|
+
assets = Asset.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans")
|
|
677
|
+
unique_poams = {
|
|
678
|
+
(
|
|
679
|
+
poam.otherIdentifier,
|
|
680
|
+
poam.assetIdentifier,
|
|
681
|
+
poam.cve,
|
|
682
|
+
poam.pluginId,
|
|
683
|
+
poam.title,
|
|
684
|
+
)
|
|
685
|
+
for poam in poams
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
for asset in assets:
|
|
689
|
+
asset_poams = [iss for iss in Issue.get_all_by_parent(parent_id=asset.id, parent_module="assets") if iss.isPoam]
|
|
690
|
+
for asset_poam in asset_poams:
|
|
691
|
+
if not asset_poam.otherIdentifier:
|
|
692
|
+
continue
|
|
693
|
+
poam_tuple = (
|
|
694
|
+
asset_poam.otherIdentifier,
|
|
695
|
+
asset_poam.assetIdentifier,
|
|
696
|
+
asset_poam.cve,
|
|
697
|
+
asset_poam.pluginId,
|
|
698
|
+
asset_poam.title,
|
|
699
|
+
)
|
|
700
|
+
if poam_tuple not in unique_poams:
|
|
701
|
+
poams.append(asset_poam)
|
|
702
|
+
unique_poams.add(poam_tuple)
|
|
703
|
+
|
|
704
|
+
logger.info("Found %s POAMs", len(poams))
|
|
705
|
+
return poams
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def gen_links(all_poams: List[Issue]) -> List[dict]:
|
|
709
|
+
"""
|
|
710
|
+
Generate list of links for all POAMs
|
|
711
|
+
|
|
712
|
+
:param List[Issue] all_poams: All POAM issues
|
|
713
|
+
:return: List of link dicts
|
|
714
|
+
:rtype: List[dict]
|
|
715
|
+
"""
|
|
716
|
+
logger.info("Building list of links")
|
|
717
|
+
res = [Link.get_all_by_parent(parent_id=iss.id, parent_module="issues") for iss in all_poams]
|
|
718
|
+
return [link for sublist in res for link in sublist]
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def gen_files(all_poams: List[Issue], api: Api) -> List[dict]:
|
|
722
|
+
"""
|
|
723
|
+
Generate list of files for all POAMs
|
|
724
|
+
|
|
725
|
+
:param List[Issue] all_poams: All POAM issues
|
|
726
|
+
:param Api api: API client
|
|
727
|
+
:return: List of file dicts
|
|
728
|
+
:rtype: List[dict]
|
|
729
|
+
"""
|
|
730
|
+
logger.info("Building list of files")
|
|
731
|
+
res = [
|
|
732
|
+
File.get_files_for_parent_from_regscale(parent_id=iss.id, parent_module="issues", api=api) for iss in all_poams
|
|
733
|
+
]
|
|
734
|
+
return [file for sublist in res for file in sublist]
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def gen_milestones(all_poams: List[Issue], api: Api, app: Application) -> List[dict]:
|
|
738
|
+
"""
|
|
739
|
+
Generate list of milestones for all POAMs
|
|
740
|
+
|
|
741
|
+
:param List[Issue] all_poams: All POAM issues
|
|
742
|
+
:param Api api: API client
|
|
743
|
+
:param Application app: Application object
|
|
744
|
+
:return: List of milestone dicts
|
|
745
|
+
:rtype: List[dict]
|
|
746
|
+
"""
|
|
747
|
+
logger.info("Building list of milestones")
|
|
748
|
+
milestones = []
|
|
749
|
+
url = app.config["domain"] + "/api/milestones/getAllByParent/"
|
|
750
|
+
for iss in all_poams:
|
|
751
|
+
dat = api.get(f"{url}{iss.id}/issues").json()
|
|
752
|
+
milestones.extend(dat)
|
|
753
|
+
return milestones
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def process_worksheet(
|
|
757
|
+
ssp: SecurityPlan,
|
|
758
|
+
sheet_name: str,
|
|
759
|
+
workbook_path: Path,
|
|
760
|
+
all_poams: List[Issue],
|
|
761
|
+
all_milestones: List[dict],
|
|
762
|
+
all_links: List[dict],
|
|
763
|
+
all_files: List[dict],
|
|
764
|
+
point_of_contact: str = "",
|
|
765
|
+
):
|
|
766
|
+
"""
|
|
767
|
+
Process a single worksheet (Open or Closed POAMs)
|
|
768
|
+
|
|
769
|
+
:param SecurityPlan ssp: Security plan object
|
|
770
|
+
:param str sheet_name: Worksheet name ("Open POA&M Items" or "Closed POA&M Items")
|
|
771
|
+
:param Path workbook_path: Path to workbook file
|
|
772
|
+
:param List[Issue] all_poams: All POAM issues
|
|
773
|
+
:param str point_of_contact: Point of Contact name for POAMs
|
|
774
|
+
:param List[dict] all_milestones: All milestones
|
|
775
|
+
:param List[dict] all_links: All links
|
|
776
|
+
:param List[dict] all_files: All files
|
|
777
|
+
"""
|
|
778
|
+
logger.info("Processing worksheet: %s", sheet_name)
|
|
779
|
+
|
|
780
|
+
wb = openpyxl.load_workbook(workbook_path)
|
|
781
|
+
sheet = wb[sheet_name]
|
|
782
|
+
|
|
783
|
+
status = IssueStatus.Closed if sheet_name == "Closed POA&M Items" else IssueStatus.Open
|
|
784
|
+
|
|
785
|
+
sheet = update_header(ssp=ssp, sheet=sheet)
|
|
786
|
+
|
|
787
|
+
assets = Asset.get_all_by_parent(parent_id=ssp.id, parent_module="securityplans")
|
|
788
|
+
|
|
789
|
+
# Process POAMs matching the status
|
|
790
|
+
matching_poams = [poam for poam in sorted(all_poams, key=lambda x: x.id) if poam.status == status]
|
|
791
|
+
|
|
792
|
+
for ix, poam in enumerate(matching_poams):
|
|
793
|
+
process_row(
|
|
794
|
+
ssp=ssp,
|
|
795
|
+
poam=poam,
|
|
796
|
+
index=ix,
|
|
797
|
+
sheet=sheet,
|
|
798
|
+
assets=assets,
|
|
799
|
+
all_milestones=all_milestones,
|
|
800
|
+
all_links=all_links,
|
|
801
|
+
all_files=all_files,
|
|
802
|
+
point_of_contact=point_of_contact,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
logger.info("Processed %s %s POAMs out of %s Total POAMs", len(matching_poams), status, len(all_poams))
|
|
806
|
+
|
|
807
|
+
# Format worksheet
|
|
808
|
+
update_column_widths(sheet)
|
|
809
|
+
align_column("G", sheet)
|
|
810
|
+
|
|
811
|
+
# Format date column
|
|
812
|
+
for cell in sheet["L"]:
|
|
813
|
+
if cell.row >= 6:
|
|
814
|
+
cell.number_format = "mm/dd/yyyy"
|
|
815
|
+
|
|
816
|
+
wb.save(workbook_path)
|
|
817
|
+
logger.info("Saved worksheet: %s", sheet_name)
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def export_poam_v5(ssp_id: str, output_file: str, template_path: Optional[Path] = None, point_of_contact: str = ""):
|
|
821
|
+
"""
|
|
822
|
+
Export FedRAMP Rev 5 POAM Excel file
|
|
823
|
+
|
|
824
|
+
:param str ssp_id: SSP ID
|
|
825
|
+
:param str output_file: Output file path
|
|
826
|
+
:param Optional[Path] template_path: Path to FedRAMP POAM template
|
|
827
|
+
:param str point_of_contact: Point of Contact name for POAMs (defaults to empty string)
|
|
828
|
+
"""
|
|
829
|
+
logger.info("Starting FedRAMP Rev 5 POAM export for SSP %s", ssp_id)
|
|
830
|
+
|
|
831
|
+
app = Application()
|
|
832
|
+
api = Api()
|
|
833
|
+
|
|
834
|
+
# Get SSP info
|
|
835
|
+
ssp = SecurityPlan.get_object(ssp_id)
|
|
836
|
+
if not ssp:
|
|
837
|
+
logger.error("SSP %s not found", ssp_id)
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
logger.info("Exporting POAMs for SSP: %s", ssp.systemName)
|
|
841
|
+
|
|
842
|
+
# Get all POAMs
|
|
843
|
+
all_poams = get_all_poams(ssp_id)
|
|
844
|
+
if not all_poams:
|
|
845
|
+
logger.warning("No POAMs found for SSP %s", ssp_id)
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
# Get related data
|
|
849
|
+
all_links = gen_links(all_poams)
|
|
850
|
+
all_files = gen_files(all_poams, api)
|
|
851
|
+
all_milestones = gen_milestones(all_poams, api, app)
|
|
852
|
+
|
|
853
|
+
# Copy template to output location
|
|
854
|
+
if not template_path:
|
|
855
|
+
import importlib.resources as pkg_resources
|
|
856
|
+
from regscale import templates
|
|
857
|
+
|
|
858
|
+
files = pkg_resources.files(templates)
|
|
859
|
+
template_path = files / "FedRAMP-POAM-Template.xlsx"
|
|
860
|
+
# Look for template in templates directory first, then current directory
|
|
861
|
+
template_path = Path(template_path)
|
|
862
|
+
|
|
863
|
+
if not template_path.exists():
|
|
864
|
+
logger.error("Template file not found: %s", template_path)
|
|
865
|
+
logger.error("Please provide a FedRAMP POAM template Excel file or place it in ./templates/ directory")
|
|
866
|
+
return
|
|
867
|
+
|
|
868
|
+
output_path = Path(output_file)
|
|
869
|
+
if output_path.suffix != ".xlsx":
|
|
870
|
+
output_path = output_path.with_suffix(".xlsx")
|
|
871
|
+
|
|
872
|
+
shutil.copy(template_path, output_path)
|
|
873
|
+
logger.info("Copied template to: %s", output_path)
|
|
874
|
+
|
|
875
|
+
# Process both worksheets
|
|
876
|
+
for sheet_name in ["Open POA&M Items", "Closed POA&M Items"]:
|
|
877
|
+
process_worksheet(
|
|
878
|
+
ssp=ssp,
|
|
879
|
+
sheet_name=sheet_name,
|
|
880
|
+
workbook_path=output_path,
|
|
881
|
+
all_poams=all_poams,
|
|
882
|
+
all_milestones=all_milestones,
|
|
883
|
+
all_links=all_links,
|
|
884
|
+
all_files=all_files,
|
|
885
|
+
point_of_contact=point_of_contact,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
logger.info("POAMs exported successfully to: %s", output_path.absolute())
|