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,333 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Due Date Handler for Scanner Integrations"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, Optional, Union
|
|
8
|
+
|
|
9
|
+
from regscale.core.app.application import Application
|
|
10
|
+
from regscale.core.utils.date import get_day_increment
|
|
11
|
+
from regscale.integrations.public.cisa import pull_cisa_kev
|
|
12
|
+
from regscale.models import regscale_models
|
|
13
|
+
from regscale.utils.threading import ThreadSafeDict
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("regscale")
|
|
16
|
+
|
|
17
|
+
# Date format constant for consistent datetime string formatting
|
|
18
|
+
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DueDateHandler:
|
|
22
|
+
"""
|
|
23
|
+
Handles due date calculations for scanner integrations based on:
|
|
24
|
+
1. Init.yaml timeline configurations per integration
|
|
25
|
+
2. KEV (Known Exploited Vulnerabilities) dates from CISA
|
|
26
|
+
3. Default severity-based timelines
|
|
27
|
+
4. Configurable past due date validation (noPastDueDates setting)
|
|
28
|
+
|
|
29
|
+
Configuration Options:
|
|
30
|
+
- Global setting: issues.noPastDueDates (default: true)
|
|
31
|
+
- Per-integration: issues.{integration_name}.noPastDueDates
|
|
32
|
+
|
|
33
|
+
When noPastDueDates=true (default):
|
|
34
|
+
- Due dates calculated in the past are automatically adjusted to future dates
|
|
35
|
+
- Prevents API validation errors for past due dates
|
|
36
|
+
|
|
37
|
+
When noPastDueDates=false:
|
|
38
|
+
- Original due dates are preserved, even if they're in the past
|
|
39
|
+
- Useful for historical data or when past dates are intentionally required
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, integration_name: str, config: Optional[Dict[str, Any]] = None):
|
|
43
|
+
"""
|
|
44
|
+
Initialize the DueDateHandler for a specific integration
|
|
45
|
+
|
|
46
|
+
:param str integration_name: Name of the integration (e.g., 'wiz', 'qualys', 'tenable')
|
|
47
|
+
:param Optional[Dict[str, Any]] config: Optional config override, uses Application config if None
|
|
48
|
+
"""
|
|
49
|
+
self.integration_name = integration_name.lower()
|
|
50
|
+
self.config = config or Application().config
|
|
51
|
+
self._kev_data: Optional[ThreadSafeDict] = None
|
|
52
|
+
|
|
53
|
+
# Default due date timelines (days)
|
|
54
|
+
self.default_timelines = {
|
|
55
|
+
regscale_models.IssueSeverity.Critical: 30,
|
|
56
|
+
"critical": 30,
|
|
57
|
+
regscale_models.IssueSeverity.High: 60,
|
|
58
|
+
"high": 60,
|
|
59
|
+
regscale_models.IssueSeverity.Moderate: 120,
|
|
60
|
+
"moderate": 120,
|
|
61
|
+
regscale_models.IssueSeverity.Low: 364,
|
|
62
|
+
"low": 364,
|
|
63
|
+
regscale_models.IssueSeverity.NotAssigned: 364, # Default to Low severity timeline
|
|
64
|
+
"notassigned": 364,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Load integration-specific timelines from config
|
|
68
|
+
self.integration_timelines = self._load_integration_timelines()
|
|
69
|
+
|
|
70
|
+
# Load noPastDueDates setting (defaults to True)
|
|
71
|
+
self.no_past_due_dates = self._load_no_past_due_dates_setting()
|
|
72
|
+
|
|
73
|
+
def _load_integration_timelines(self) -> Dict[regscale_models.IssueSeverity, int]:
|
|
74
|
+
"""
|
|
75
|
+
Load timeline configurations for this integration from init.yaml
|
|
76
|
+
mv
|
|
77
|
+
:return: Dictionary mapping severity to days
|
|
78
|
+
:rtype: Dict[regscale_models.IssueSeverity, int]
|
|
79
|
+
"""
|
|
80
|
+
timelines = self.default_timelines.copy()
|
|
81
|
+
|
|
82
|
+
issues_config = self.config.get("issues", {})
|
|
83
|
+
integration_config = issues_config.get(self.integration_name, {})
|
|
84
|
+
|
|
85
|
+
if integration_config:
|
|
86
|
+
logger.debug(f"Found timeline config for {self.integration_name}: {integration_config}")
|
|
87
|
+
|
|
88
|
+
# Map config keys to severity levels
|
|
89
|
+
severity_mapping = {
|
|
90
|
+
"critical": regscale_models.IssueSeverity.Critical,
|
|
91
|
+
"high": regscale_models.IssueSeverity.High,
|
|
92
|
+
"moderate": regscale_models.IssueSeverity.Moderate,
|
|
93
|
+
"medium": regscale_models.IssueSeverity.Moderate, # Some integrations use 'medium'
|
|
94
|
+
"low": regscale_models.IssueSeverity.Low,
|
|
95
|
+
"notassigned": regscale_models.IssueSeverity.NotAssigned, # Handle unassigned severities
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for config_key, severity in severity_mapping.items():
|
|
99
|
+
if config_key in integration_config:
|
|
100
|
+
timelines[severity] = integration_config[config_key]
|
|
101
|
+
|
|
102
|
+
return timelines
|
|
103
|
+
|
|
104
|
+
def _load_no_past_due_dates_setting(self) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Load noPastDueDates setting for this integration from init.yaml
|
|
107
|
+
|
|
108
|
+
Configuration hierarchy:
|
|
109
|
+
1. Integration-specific setting: issues.{integration_name}.noPastDueDates
|
|
110
|
+
2. Global setting: issues.noPastDueDates
|
|
111
|
+
3. Default: True
|
|
112
|
+
|
|
113
|
+
:return: True if past due dates should be automatically adjusted to future dates
|
|
114
|
+
:rtype: bool
|
|
115
|
+
"""
|
|
116
|
+
issues_config = self.config.get("issues", {})
|
|
117
|
+
integration_config = issues_config.get(self.integration_name, {})
|
|
118
|
+
|
|
119
|
+
# Check integration-specific setting first
|
|
120
|
+
if "noPastDueDates" in integration_config:
|
|
121
|
+
setting = integration_config["noPastDueDates"]
|
|
122
|
+
logger.debug(f"Using integration-specific noPastDueDates={setting} for {self.integration_name}")
|
|
123
|
+
return bool(setting)
|
|
124
|
+
|
|
125
|
+
# Fall back to global setting
|
|
126
|
+
if "noPastDueDates" in issues_config:
|
|
127
|
+
setting = issues_config["noPastDueDates"]
|
|
128
|
+
logger.debug(f"Using global noPastDueDates={setting} for {self.integration_name}")
|
|
129
|
+
return bool(setting)
|
|
130
|
+
|
|
131
|
+
# Default to True (prevent past due dates)
|
|
132
|
+
logger.debug(f"Using default noPastDueDates=True for {self.integration_name}")
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
def _get_kev_data(self) -> ThreadSafeDict:
|
|
136
|
+
"""
|
|
137
|
+
Get KEV data from CISA, using cache if available
|
|
138
|
+
|
|
139
|
+
:return: Thread-safe dictionary containing KEV data
|
|
140
|
+
:rtype: ThreadSafeDict
|
|
141
|
+
"""
|
|
142
|
+
if self._kev_data is None:
|
|
143
|
+
try:
|
|
144
|
+
kev_data = pull_cisa_kev()
|
|
145
|
+
self._kev_data = ThreadSafeDict()
|
|
146
|
+
self._kev_data.update(kev_data)
|
|
147
|
+
logger.debug("Loaded KEV data from CISA")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.warning(f"Failed to load KEV data: {e}")
|
|
150
|
+
self._kev_data = ThreadSafeDict()
|
|
151
|
+
|
|
152
|
+
return self._kev_data
|
|
153
|
+
|
|
154
|
+
def _should_use_kev(self) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Check if this integration should use KEV dates
|
|
157
|
+
|
|
158
|
+
:return: True if KEV should be used for this integration
|
|
159
|
+
:rtype: bool
|
|
160
|
+
"""
|
|
161
|
+
issues_config = self.config.get("issues", {})
|
|
162
|
+
integration_config = issues_config.get(self.integration_name, {})
|
|
163
|
+
return integration_config.get("useKev", True) # Default to True if not specified
|
|
164
|
+
|
|
165
|
+
def _get_kev_due_date(self, cve: str) -> Optional[str]:
|
|
166
|
+
"""
|
|
167
|
+
Get the KEV due date for a specific CVE
|
|
168
|
+
|
|
169
|
+
:param str cve: The CVE identifier
|
|
170
|
+
:return: KEV due date string if found, None otherwise
|
|
171
|
+
:rtype: Optional[str]
|
|
172
|
+
"""
|
|
173
|
+
if not self._should_use_kev() or not cve:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
kev_data = self._get_kev_data()
|
|
177
|
+
|
|
178
|
+
# Find the KEV entry for this CVE
|
|
179
|
+
kev_entry = next(
|
|
180
|
+
(entry for entry in kev_data.get("vulnerabilities", []) if entry.get("cveID", "").upper() == cve.upper()),
|
|
181
|
+
None,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if kev_entry:
|
|
185
|
+
kev_due_date = kev_entry.get("dueDate")
|
|
186
|
+
if kev_due_date:
|
|
187
|
+
logger.debug(f"Found KEV due date for {cve}: {kev_due_date}")
|
|
188
|
+
return kev_due_date
|
|
189
|
+
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def calculate_due_date(
|
|
193
|
+
self,
|
|
194
|
+
severity: Union[regscale_models.IssueSeverity, str],
|
|
195
|
+
created_date: str,
|
|
196
|
+
cve: Optional[str] = None,
|
|
197
|
+
title: Optional[str] = None,
|
|
198
|
+
) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Calculate the due date for an issue based on severity, KEV status, and integration config
|
|
201
|
+
|
|
202
|
+
:param Union[regscale_models.IssueSeverity, str] severity: The severity of the issue
|
|
203
|
+
:param str created_date: The creation date of the issue
|
|
204
|
+
:param Optional[str] cve: The CVE identifier (if applicable)
|
|
205
|
+
:param Optional[str] title: The title of the issue (for additional context)
|
|
206
|
+
:return: The calculated due date string
|
|
207
|
+
:rtype: str
|
|
208
|
+
"""
|
|
209
|
+
# First, check if this CVE has a KEV due date
|
|
210
|
+
if cve:
|
|
211
|
+
kev_due_date = self._get_kev_due_date(cve)
|
|
212
|
+
if kev_due_date:
|
|
213
|
+
# Parse the KEV due date and created date
|
|
214
|
+
try:
|
|
215
|
+
from dateutil.parser import parse as date_parse
|
|
216
|
+
|
|
217
|
+
kev_date = date_parse(kev_due_date).date()
|
|
218
|
+
created_dt = date_parse(created_date).date()
|
|
219
|
+
|
|
220
|
+
# If KEV due date is after creation date, use KEV date but ensure it's not in the past
|
|
221
|
+
# If KEV due date is before creation date, add the difference to creation date
|
|
222
|
+
if kev_date >= created_dt:
|
|
223
|
+
# Ensure KEV due date is not in the past
|
|
224
|
+
kev_due_validated = self._ensure_future_due_date(
|
|
225
|
+
kev_due_date, 30
|
|
226
|
+
) # Use 30 days as fallback for KEV
|
|
227
|
+
logger.debug(f"Using KEV due date {kev_due_validated} for CVE {cve}")
|
|
228
|
+
return kev_due_validated
|
|
229
|
+
else:
|
|
230
|
+
# KEV date has passed, calculate new due date from creation
|
|
231
|
+
days_diff = (created_dt - kev_date).days
|
|
232
|
+
# Give at least 30 days from creation for critical KEV items
|
|
233
|
+
adjusted_days = max(30, days_diff)
|
|
234
|
+
calculated_due_date_obj = get_day_increment(start=created_date, days=adjusted_days)
|
|
235
|
+
calculated_due_date = datetime.combine(calculated_due_date_obj, datetime.min.time()).strftime(
|
|
236
|
+
DATETIME_FORMAT
|
|
237
|
+
)
|
|
238
|
+
# Ensure the adjusted due date is not in the past
|
|
239
|
+
due_date = self._ensure_future_due_date(calculated_due_date, adjusted_days)
|
|
240
|
+
logger.debug(f"KEV date passed, using adjusted due date {due_date} for CVE {cve}")
|
|
241
|
+
return due_date
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.warning(f"Failed to parse KEV due date {kev_due_date}: {e}")
|
|
245
|
+
|
|
246
|
+
# Fall back to severity-based timeline from integration config
|
|
247
|
+
days = self.integration_timelines.get(severity, self.default_timelines[severity])
|
|
248
|
+
calculated_due_date_obj = get_day_increment(start=created_date, days=days)
|
|
249
|
+
calculated_due_date = datetime.combine(calculated_due_date_obj, datetime.min.time()).strftime(DATETIME_FORMAT)
|
|
250
|
+
|
|
251
|
+
# Ensure due date is never in the past (allow yesterday for timezone differences)
|
|
252
|
+
due_date = self._ensure_future_due_date(calculated_due_date, days)
|
|
253
|
+
|
|
254
|
+
logger.debug(
|
|
255
|
+
f"Using {self.integration_name} timeline: {severity.name if isinstance(severity, regscale_models.IssueSeverity) else severity} = {days} days, due date = {due_date}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return due_date
|
|
259
|
+
|
|
260
|
+
def _ensure_future_due_date(self, calculated_due_date: str, original_days: int) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Ensure the due date is not in the past. If it is, set it to today + original timeline days.
|
|
263
|
+
Behavior is controlled by the noPastDueDates configuration setting.
|
|
264
|
+
|
|
265
|
+
:param str calculated_due_date: The originally calculated due date
|
|
266
|
+
:param int original_days: The original number of days for this severity
|
|
267
|
+
:return: A due date that respects the noPastDueDates setting
|
|
268
|
+
:rtype: str
|
|
269
|
+
"""
|
|
270
|
+
# If noPastDueDates is disabled, return the original date without validation
|
|
271
|
+
if not self.no_past_due_dates:
|
|
272
|
+
logger.debug(
|
|
273
|
+
f"noPastDueDates=False for {self.integration_name}, returning original due date: {calculated_due_date}"
|
|
274
|
+
)
|
|
275
|
+
return calculated_due_date
|
|
276
|
+
|
|
277
|
+
from dateutil.parser import parse as date_parse
|
|
278
|
+
from datetime import date
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
calculated_date = date_parse(calculated_due_date).date()
|
|
282
|
+
today = date.today()
|
|
283
|
+
|
|
284
|
+
# Use > instead of >= to ensure due dates set to "today" are moved to tomorrow
|
|
285
|
+
# This prevents API validation errors when the time component (00:00:00) has already passed
|
|
286
|
+
if calculated_date > today:
|
|
287
|
+
return calculated_due_date
|
|
288
|
+
else:
|
|
289
|
+
# Due date is in the past or today, calculate new due date from today
|
|
290
|
+
# Use minimum 1 day to ensure it's always in the future
|
|
291
|
+
safe_days = max(1, original_days)
|
|
292
|
+
new_due_date_obj = get_day_increment(start=today, days=safe_days)
|
|
293
|
+
new_due_date = datetime.combine(new_due_date_obj, datetime.min.time()).strftime(DATETIME_FORMAT)
|
|
294
|
+
logger.debug(
|
|
295
|
+
f"Due date {calculated_due_date} was in the past or today for {self.integration_name}. "
|
|
296
|
+
f"Adjusted to {new_due_date} ({safe_days} days from today)."
|
|
297
|
+
)
|
|
298
|
+
return new_due_date
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.warning(f"Failed to validate due date {calculated_due_date}: {e}")
|
|
302
|
+
# If we can't parse the date, return a safe fallback only if noPastDueDates is enabled
|
|
303
|
+
if self.no_past_due_dates:
|
|
304
|
+
safe_days = max(1, original_days)
|
|
305
|
+
fallback_due_date_obj = get_day_increment(start=date.today(), days=safe_days)
|
|
306
|
+
return datetime.combine(fallback_due_date_obj, datetime.min.time()).strftime(DATETIME_FORMAT)
|
|
307
|
+
else:
|
|
308
|
+
return calculated_due_date
|
|
309
|
+
|
|
310
|
+
def get_integration_config(self) -> Dict[str, Any]:
|
|
311
|
+
"""
|
|
312
|
+
Get the full integration configuration from init.yaml
|
|
313
|
+
|
|
314
|
+
:return: Integration configuration dictionary
|
|
315
|
+
:rtype: Dict[str, Any]
|
|
316
|
+
"""
|
|
317
|
+
issues_config = self.config.get("issues", {})
|
|
318
|
+
return issues_config.get(self.integration_name, {})
|
|
319
|
+
|
|
320
|
+
def get_timeline_info(self) -> Dict[str, Any]:
|
|
321
|
+
"""
|
|
322
|
+
Get information about current timeline configuration
|
|
323
|
+
|
|
324
|
+
:return: Dictionary with timeline information
|
|
325
|
+
:rtype: Dict[str, Any]
|
|
326
|
+
"""
|
|
327
|
+
return {
|
|
328
|
+
"integration_name": self.integration_name,
|
|
329
|
+
"use_kev": self._should_use_kev(),
|
|
330
|
+
"no_past_due_dates": self.no_past_due_dates,
|
|
331
|
+
"timelines": {severity.name: days for severity, days in self.integration_timelines.items()},
|
|
332
|
+
"config_source": "init.yaml",
|
|
333
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Milestone Manager for Issue Tracking
|
|
5
|
+
|
|
6
|
+
Handles creation of milestones for issues based on status transitions (created, reopened, closed).
|
|
7
|
+
Also handles backfilling of missing milestones for existing issues.
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
11
|
+
|
|
12
|
+
from regscale.core.app.utils.app_utils import get_current_datetime
|
|
13
|
+
from regscale.integrations.variables import ScannerVariables
|
|
14
|
+
from regscale.models import regscale_models
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("regscale")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MilestoneManager:
|
|
23
|
+
"""
|
|
24
|
+
Manages milestone creation for issues based on status transitions.
|
|
25
|
+
|
|
26
|
+
Milestones are created when:
|
|
27
|
+
- A new issue is created
|
|
28
|
+
- An existing issue is reopened (Closed -> Open)
|
|
29
|
+
- An existing issue is closed (Open -> Closed)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, integration_title: str, assessor_id: str, scan_date: str):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the milestone manager.
|
|
35
|
+
|
|
36
|
+
:param str integration_title: Name of the integration (used in milestone titles)
|
|
37
|
+
:param str assessor_id: ID of the assessor/responsible person for milestones
|
|
38
|
+
:param str scan_date: Date of the scan (used for new issue milestones)
|
|
39
|
+
"""
|
|
40
|
+
self.integration_title = integration_title
|
|
41
|
+
self.assessor_id = assessor_id
|
|
42
|
+
self.scan_date = scan_date
|
|
43
|
+
|
|
44
|
+
def create_milestones_for_issue(
|
|
45
|
+
self,
|
|
46
|
+
issue: regscale_models.Issue,
|
|
47
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
48
|
+
existing_issue: Optional[regscale_models.Issue] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Create appropriate milestones for an issue based on status transitions.
|
|
52
|
+
|
|
53
|
+
:param regscale_models.Issue issue: The issue to create milestones for
|
|
54
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging/context)
|
|
55
|
+
:param Optional[regscale_models.Issue] existing_issue: Previous state of issue for comparison
|
|
56
|
+
"""
|
|
57
|
+
if not self._should_create_milestones(issue):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if self._should_create_reopened_milestone(existing_issue, issue):
|
|
61
|
+
self._create_reopened_milestone(issue, finding)
|
|
62
|
+
elif self._should_create_closed_milestone(existing_issue, issue):
|
|
63
|
+
self._create_closed_milestone(issue, finding)
|
|
64
|
+
elif not existing_issue:
|
|
65
|
+
self._create_new_issue_milestone(issue, finding)
|
|
66
|
+
else:
|
|
67
|
+
logger.debug(
|
|
68
|
+
"No milestone created for issue %s (no status transition detected)",
|
|
69
|
+
issue.id,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _should_create_milestones(self, issue: regscale_models.Issue) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if milestones should be created for this issue.
|
|
75
|
+
|
|
76
|
+
:param regscale_models.Issue issue: The issue to check
|
|
77
|
+
:return: True if milestones should be created
|
|
78
|
+
:rtype: bool
|
|
79
|
+
"""
|
|
80
|
+
if not ScannerVariables.useMilestones:
|
|
81
|
+
logger.debug("Milestone creation disabled (useMilestones=False)")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
if not issue.id:
|
|
85
|
+
logger.debug("Cannot create milestone - issue has no ID")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
def _should_create_reopened_milestone(
|
|
91
|
+
self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Check if a reopened milestone should be created.
|
|
95
|
+
|
|
96
|
+
:param Optional[regscale_models.Issue] existing_issue: The existing issue
|
|
97
|
+
:param regscale_models.Issue issue: The current issue
|
|
98
|
+
:return: True if reopened milestone should be created
|
|
99
|
+
:rtype: bool
|
|
100
|
+
"""
|
|
101
|
+
return (
|
|
102
|
+
existing_issue is not None
|
|
103
|
+
and existing_issue.status == regscale_models.IssueStatus.Closed
|
|
104
|
+
and issue.status == regscale_models.IssueStatus.Open
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _should_create_closed_milestone(
|
|
108
|
+
self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Check if a closed milestone should be created.
|
|
112
|
+
|
|
113
|
+
:param Optional[regscale_models.Issue] existing_issue: The existing issue
|
|
114
|
+
:param regscale_models.Issue issue: The current issue
|
|
115
|
+
:return: True if closed milestone should be created
|
|
116
|
+
:rtype: bool
|
|
117
|
+
"""
|
|
118
|
+
return (
|
|
119
|
+
existing_issue is not None
|
|
120
|
+
and existing_issue.status == regscale_models.IssueStatus.Open
|
|
121
|
+
and issue.status == regscale_models.IssueStatus.Closed
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _create_reopened_milestone(
|
|
125
|
+
self,
|
|
126
|
+
issue: regscale_models.Issue,
|
|
127
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Create a milestone for a reopened issue.
|
|
131
|
+
|
|
132
|
+
:param regscale_models.Issue issue: The issue being reopened
|
|
133
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
134
|
+
"""
|
|
135
|
+
milestone_date = get_current_datetime()
|
|
136
|
+
self._create_milestone(
|
|
137
|
+
issue=issue,
|
|
138
|
+
title=f"Issue reopened from {self.integration_title} scan",
|
|
139
|
+
milestone_date=milestone_date,
|
|
140
|
+
milestone_type="reopened",
|
|
141
|
+
finding=finding,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _create_closed_milestone(
|
|
145
|
+
self,
|
|
146
|
+
issue: regscale_models.Issue,
|
|
147
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Create a milestone for a closed issue.
|
|
151
|
+
|
|
152
|
+
:param regscale_models.Issue issue: The issue being closed
|
|
153
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
154
|
+
"""
|
|
155
|
+
milestone_date = issue.dateCompleted or get_current_datetime()
|
|
156
|
+
self._create_milestone(
|
|
157
|
+
issue=issue,
|
|
158
|
+
title=f"Issue closed from {self.integration_title} scan",
|
|
159
|
+
milestone_date=milestone_date,
|
|
160
|
+
milestone_type="closed",
|
|
161
|
+
finding=finding,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _create_new_issue_milestone(
|
|
165
|
+
self,
|
|
166
|
+
issue: regscale_models.Issue,
|
|
167
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Create a milestone for a newly created issue.
|
|
171
|
+
|
|
172
|
+
:param regscale_models.Issue issue: The newly created issue
|
|
173
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
174
|
+
"""
|
|
175
|
+
self._create_milestone(
|
|
176
|
+
issue=issue,
|
|
177
|
+
title=f"Issue created from {self.integration_title} scan",
|
|
178
|
+
milestone_date=self.scan_date,
|
|
179
|
+
milestone_type="new",
|
|
180
|
+
finding=finding,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _create_milestone(
|
|
184
|
+
self,
|
|
185
|
+
issue: regscale_models.Issue,
|
|
186
|
+
title: str,
|
|
187
|
+
milestone_date: str,
|
|
188
|
+
milestone_type: str,
|
|
189
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Create a milestone with error handling.
|
|
193
|
+
|
|
194
|
+
:param regscale_models.Issue issue: The issue to create milestone for
|
|
195
|
+
:param str title: Title of the milestone
|
|
196
|
+
:param str milestone_date: Date for the milestone
|
|
197
|
+
:param str milestone_type: Type of milestone (for logging: new, reopened, closed)
|
|
198
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
regscale_models.Milestone(
|
|
202
|
+
title=title,
|
|
203
|
+
milestoneDate=milestone_date,
|
|
204
|
+
dateCompleted=get_current_datetime(),
|
|
205
|
+
responsiblePersonId=self.assessor_id,
|
|
206
|
+
parentID=issue.id,
|
|
207
|
+
parentModule="issues",
|
|
208
|
+
).create_or_update()
|
|
209
|
+
|
|
210
|
+
logger.debug(f"Created {milestone_type} milestone for issue {issue.id}")
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.warning(f"Failed to create {milestone_type} milestone for issue {issue.id}: {e}")
|
|
214
|
+
|
|
215
|
+
def get_existing_milestones(self, issue: regscale_models.Issue) -> List[regscale_models.Milestone]:
|
|
216
|
+
"""
|
|
217
|
+
Get all existing milestones for an issue.
|
|
218
|
+
|
|
219
|
+
:param regscale_models.Issue issue: The issue to check
|
|
220
|
+
:return: List of existing milestones
|
|
221
|
+
:rtype: List[regscale_models.Milestone]
|
|
222
|
+
"""
|
|
223
|
+
if not issue.id:
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
milestones = regscale_models.Milestone.get_by_parent(parent_id=issue.id, parent_module="issues")
|
|
228
|
+
return milestones if milestones else []
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.debug(f"Could not retrieve milestones for issue {issue.id}: {e}")
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
def has_creation_milestone(self, issue: regscale_models.Issue) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Check if an issue has a creation milestone.
|
|
236
|
+
|
|
237
|
+
:param regscale_models.Issue issue: The issue to check
|
|
238
|
+
:return: True if creation milestone exists
|
|
239
|
+
:rtype: bool
|
|
240
|
+
"""
|
|
241
|
+
milestones = self.get_existing_milestones(issue)
|
|
242
|
+
|
|
243
|
+
# Check for creation milestone patterns
|
|
244
|
+
creation_patterns = [
|
|
245
|
+
"Issue created from",
|
|
246
|
+
"created from",
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
for milestone in milestones:
|
|
250
|
+
milestone_title = milestone.title.lower() if milestone.title else ""
|
|
251
|
+
if any(pattern.lower() in milestone_title for pattern in creation_patterns):
|
|
252
|
+
logger.debug(f"Found existing creation milestone for issue {issue.id}: {milestone.title}")
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def ensure_creation_milestone_exists(
|
|
258
|
+
self,
|
|
259
|
+
issue: regscale_models.Issue,
|
|
260
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Ensure an issue has a creation milestone, backfilling if necessary.
|
|
264
|
+
|
|
265
|
+
This method checks if an issue has a creation milestone. If not, it creates one
|
|
266
|
+
based on the issue's dateCreated field to backfill missing milestones.
|
|
267
|
+
|
|
268
|
+
:param regscale_models.Issue issue: The issue to check
|
|
269
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
270
|
+
"""
|
|
271
|
+
if not self._should_create_milestones(issue):
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
# Check if creation milestone already exists
|
|
275
|
+
if self.has_creation_milestone(issue):
|
|
276
|
+
logger.debug(f"Issue {issue.id} already has creation milestone, skipping backfill")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Backfill missing creation milestone
|
|
280
|
+
logger.debug(f"Backfilling missing creation milestone for issue {issue.id}")
|
|
281
|
+
|
|
282
|
+
# Use issue's dateCreated if available, otherwise use scan_date
|
|
283
|
+
milestone_date = issue.dateCreated if issue.dateCreated else self.scan_date
|
|
284
|
+
|
|
285
|
+
self._create_milestone(
|
|
286
|
+
issue=issue,
|
|
287
|
+
title=f"Issue created from {self.integration_title} scan",
|
|
288
|
+
milestone_date=milestone_date,
|
|
289
|
+
milestone_type="backfilled",
|
|
290
|
+
finding=finding,
|
|
291
|
+
)
|
|
@@ -17,6 +17,7 @@ from regscale.core.lazy_group import LazyGroup
|
|
|
17
17
|
"import_poam": "regscale.integrations.public.fedramp.click.import_fedramp_poam_template",
|
|
18
18
|
"import_drf": "regscale.integrations.public.fedramp.click.import_drf",
|
|
19
19
|
"import_cis_crm": "regscale.integrations.public.fedramp.click.import_ciscrm",
|
|
20
|
+
"export_poam_v5": "regscale.integrations.public.fedramp.click.export_poam_v5",
|
|
20
21
|
},
|
|
21
22
|
name="fedramp",
|
|
22
23
|
)
|
|
@@ -25,6 +26,19 @@ def fedramp():
|
|
|
25
26
|
pass
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
@click.group(
|
|
30
|
+
cls=LazyGroup,
|
|
31
|
+
lazy_subcommands={
|
|
32
|
+
"import_ssp": "regscale.integrations.public.csam.csam.import_ssp",
|
|
33
|
+
"import_poam": "regscale.integrations.public.csam.csam.import_poam",
|
|
34
|
+
},
|
|
35
|
+
name="csam",
|
|
36
|
+
)
|
|
37
|
+
def csam():
|
|
38
|
+
"""[BETA] Integration with DoJ's CSAM GRC Tool."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
@click.group(
|
|
29
43
|
cls=LazyGroup,
|
|
30
44
|
lazy_subcommands={
|