regscale-cli 6.16.0.0__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.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/__init__.py +1 -0
- regscale/airflow/__init__.py +9 -0
- regscale/airflow/azure/__init__.py +9 -0
- regscale/airflow/azure/cli.py +89 -0
- regscale/airflow/azure/upload_dags.py +116 -0
- regscale/airflow/click_dags.py +127 -0
- regscale/airflow/click_mixins.py +82 -0
- regscale/airflow/config.py +25 -0
- regscale/airflow/factories/__init__.py +0 -0
- regscale/airflow/factories/connections.py +58 -0
- regscale/airflow/factories/workflows.py +78 -0
- regscale/airflow/hierarchy.py +88 -0
- regscale/airflow/operators/__init__.py +0 -0
- regscale/airflow/operators/click.py +36 -0
- regscale/airflow/sensors/__init__.py +0 -0
- regscale/airflow/sensors/sql.py +107 -0
- regscale/airflow/sessions/__init__.py +0 -0
- regscale/airflow/sessions/sql/__init__.py +3 -0
- regscale/airflow/sessions/sql/queries.py +64 -0
- regscale/airflow/sessions/sql/sql_server_queries.py +248 -0
- regscale/airflow/tasks/__init__.py +0 -0
- regscale/airflow/tasks/branches.py +22 -0
- regscale/airflow/tasks/cli.py +116 -0
- regscale/airflow/tasks/click.py +73 -0
- regscale/airflow/tasks/debugging.py +9 -0
- regscale/airflow/tasks/groups.py +116 -0
- regscale/airflow/tasks/init.py +60 -0
- regscale/airflow/tasks/states.py +47 -0
- regscale/airflow/tasks/workflows.py +36 -0
- regscale/ansible/__init__.py +9 -0
- regscale/core/__init__.py +0 -0
- regscale/core/app/__init__.py +3 -0
- regscale/core/app/api.py +571 -0
- regscale/core/app/application.py +665 -0
- regscale/core/app/internal/__init__.py +136 -0
- regscale/core/app/internal/admin_actions.py +230 -0
- regscale/core/app/internal/assessments_editor.py +873 -0
- regscale/core/app/internal/catalog.py +316 -0
- regscale/core/app/internal/comparison.py +459 -0
- regscale/core/app/internal/control_editor.py +571 -0
- regscale/core/app/internal/encrypt.py +79 -0
- regscale/core/app/internal/evidence.py +1240 -0
- regscale/core/app/internal/file_uploads.py +151 -0
- regscale/core/app/internal/healthcheck.py +66 -0
- regscale/core/app/internal/login.py +305 -0
- regscale/core/app/internal/migrations.py +240 -0
- regscale/core/app/internal/model_editor.py +1701 -0
- regscale/core/app/internal/poam_editor.py +632 -0
- regscale/core/app/internal/workflow.py +105 -0
- regscale/core/app/logz.py +74 -0
- regscale/core/app/utils/XMLIR.py +258 -0
- regscale/core/app/utils/__init__.py +0 -0
- regscale/core/app/utils/api_handler.py +358 -0
- regscale/core/app/utils/app_utils.py +1110 -0
- regscale/core/app/utils/catalog_utils/__init__.py +0 -0
- regscale/core/app/utils/catalog_utils/common.py +91 -0
- regscale/core/app/utils/catalog_utils/compare_catalog.py +193 -0
- regscale/core/app/utils/catalog_utils/diagnostic_catalog.py +97 -0
- regscale/core/app/utils/catalog_utils/download_catalog.py +103 -0
- regscale/core/app/utils/catalog_utils/update_catalog.py +718 -0
- regscale/core/app/utils/catalog_utils/update_catalog_v2.py +1378 -0
- regscale/core/app/utils/catalog_utils/update_catalog_v3.py +1272 -0
- regscale/core/app/utils/catalog_utils/update_plans.py +334 -0
- regscale/core/app/utils/file_utils.py +238 -0
- regscale/core/app/utils/parser_utils.py +81 -0
- regscale/core/app/utils/pickle_file_handler.py +57 -0
- regscale/core/app/utils/regscale_utils.py +319 -0
- regscale/core/app/utils/report_utils.py +119 -0
- regscale/core/app/utils/variables.py +226 -0
- regscale/core/decorators.py +31 -0
- regscale/core/lazy_group.py +65 -0
- regscale/core/login.py +63 -0
- regscale/core/server/__init__.py +0 -0
- regscale/core/server/flask_api.py +473 -0
- regscale/core/server/helpers.py +373 -0
- regscale/core/server/rest.py +64 -0
- regscale/core/server/static/css/bootstrap.css +6030 -0
- regscale/core/server/static/css/bootstrap.min.css +6 -0
- regscale/core/server/static/css/main.css +176 -0
- regscale/core/server/static/images/regscale-cli.svg +49 -0
- regscale/core/server/static/images/regscale.svg +38 -0
- regscale/core/server/templates/base.html +74 -0
- regscale/core/server/templates/index.html +43 -0
- regscale/core/server/templates/login.html +28 -0
- regscale/core/server/templates/make_base64.html +22 -0
- regscale/core/server/templates/upload_STIG.html +109 -0
- regscale/core/server/templates/upload_STIG_result.html +26 -0
- regscale/core/server/templates/upload_ssp.html +144 -0
- regscale/core/server/templates/upload_ssp_result.html +128 -0
- regscale/core/static/__init__.py +0 -0
- regscale/core/static/regex.py +14 -0
- regscale/core/utils/__init__.py +117 -0
- regscale/core/utils/click_utils.py +13 -0
- regscale/core/utils/date.py +238 -0
- regscale/core/utils/graphql.py +254 -0
- regscale/core/utils/urls.py +23 -0
- regscale/dev/__init__.py +6 -0
- regscale/dev/analysis.py +454 -0
- regscale/dev/cli.py +235 -0
- regscale/dev/code_gen.py +492 -0
- regscale/dev/dirs.py +69 -0
- regscale/dev/docs.py +384 -0
- regscale/dev/monitoring.py +26 -0
- regscale/dev/profiling.py +216 -0
- regscale/exceptions/__init__.py +4 -0
- regscale/exceptions/license_exception.py +7 -0
- regscale/exceptions/validation_exception.py +9 -0
- regscale/integrations/__init__.py +1 -0
- regscale/integrations/commercial/__init__.py +486 -0
- regscale/integrations/commercial/ad.py +433 -0
- regscale/integrations/commercial/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/common.py +106 -0
- regscale/integrations/commercial/aqua/__init__.py +0 -0
- regscale/integrations/commercial/aqua/aqua.py +91 -0
- regscale/integrations/commercial/aws/__init__.py +6 -0
- regscale/integrations/commercial/aws/cli.py +322 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +110 -0
- regscale/integrations/commercial/aws/inventory/base.py +64 -0
- regscale/integrations/commercial/aws/inventory/resources/__init__.py +19 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +113 -0
- regscale/integrations/commercial/aws/inventory/resources/database.py +101 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +237 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +240 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +91 -0
- regscale/integrations/commercial/aws/scanner.py +823 -0
- regscale/integrations/commercial/azure/__init__.py +0 -0
- regscale/integrations/commercial/azure/common.py +32 -0
- regscale/integrations/commercial/azure/intune.py +488 -0
- regscale/integrations/commercial/azure/scanner.py +49 -0
- regscale/integrations/commercial/burp.py +78 -0
- regscale/integrations/commercial/cpe.py +144 -0
- regscale/integrations/commercial/crowdstrike.py +1117 -0
- regscale/integrations/commercial/defender.py +1511 -0
- regscale/integrations/commercial/dependabot.py +210 -0
- regscale/integrations/commercial/durosuite/__init__.py +0 -0
- regscale/integrations/commercial/durosuite/api.py +1546 -0
- regscale/integrations/commercial/durosuite/process_devices.py +101 -0
- regscale/integrations/commercial/durosuite/scanner.py +637 -0
- regscale/integrations/commercial/durosuite/variables.py +21 -0
- regscale/integrations/commercial/ecr.py +90 -0
- regscale/integrations/commercial/gcp/__init__.py +237 -0
- regscale/integrations/commercial/gcp/auth.py +96 -0
- regscale/integrations/commercial/gcp/control_tests.py +238 -0
- regscale/integrations/commercial/gcp/variables.py +18 -0
- regscale/integrations/commercial/gitlab.py +332 -0
- regscale/integrations/commercial/grype.py +165 -0
- regscale/integrations/commercial/ibm.py +90 -0
- regscale/integrations/commercial/import_all/__init__.py +0 -0
- regscale/integrations/commercial/import_all/import_all_cmd.py +467 -0
- regscale/integrations/commercial/import_all/scan_file_fingerprints.json +27 -0
- regscale/integrations/commercial/jira.py +1046 -0
- regscale/integrations/commercial/mappings/__init__.py +0 -0
- regscale/integrations/commercial/mappings/csf_controls.json +713 -0
- regscale/integrations/commercial/mappings/nist_800_53_r5_controls.json +1516 -0
- regscale/integrations/commercial/nessus/__init__.py +0 -0
- regscale/integrations/commercial/nessus/nessus_utils.py +429 -0
- regscale/integrations/commercial/nessus/scanner.py +416 -0
- regscale/integrations/commercial/nexpose.py +90 -0
- regscale/integrations/commercial/okta.py +798 -0
- regscale/integrations/commercial/opentext/__init__.py +0 -0
- regscale/integrations/commercial/opentext/click.py +99 -0
- regscale/integrations/commercial/opentext/scanner.py +143 -0
- regscale/integrations/commercial/prisma.py +91 -0
- regscale/integrations/commercial/qualys.py +1462 -0
- regscale/integrations/commercial/salesforce.py +980 -0
- regscale/integrations/commercial/sap/__init__.py +0 -0
- regscale/integrations/commercial/sap/click.py +31 -0
- regscale/integrations/commercial/sap/sysdig/__init__.py +0 -0
- regscale/integrations/commercial/sap/sysdig/click.py +57 -0
- regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +190 -0
- regscale/integrations/commercial/sap/tenable/__init__.py +0 -0
- regscale/integrations/commercial/sap/tenable/click.py +49 -0
- regscale/integrations/commercial/sap/tenable/scanner.py +196 -0
- regscale/integrations/commercial/servicenow.py +1756 -0
- regscale/integrations/commercial/sicura/__init__.py +0 -0
- regscale/integrations/commercial/sicura/api.py +855 -0
- regscale/integrations/commercial/sicura/commands.py +73 -0
- regscale/integrations/commercial/sicura/scanner.py +481 -0
- regscale/integrations/commercial/sicura/variables.py +16 -0
- regscale/integrations/commercial/snyk.py +90 -0
- regscale/integrations/commercial/sonarcloud.py +260 -0
- regscale/integrations/commercial/sqlserver.py +369 -0
- regscale/integrations/commercial/stig_mapper_integration/__init__.py +0 -0
- regscale/integrations/commercial/stig_mapper_integration/click_commands.py +38 -0
- regscale/integrations/commercial/stig_mapper_integration/mapping_engine.py +353 -0
- regscale/integrations/commercial/stigv2/__init__.py +0 -0
- regscale/integrations/commercial/stigv2/ckl_parser.py +349 -0
- regscale/integrations/commercial/stigv2/click_commands.py +95 -0
- regscale/integrations/commercial/stigv2/stig_integration.py +202 -0
- regscale/integrations/commercial/synqly/__init__.py +0 -0
- regscale/integrations/commercial/synqly/assets.py +46 -0
- regscale/integrations/commercial/synqly/ticketing.py +132 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +223 -0
- regscale/integrations/commercial/synqly_jira.py +840 -0
- regscale/integrations/commercial/tenablev2/__init__.py +0 -0
- regscale/integrations/commercial/tenablev2/authenticate.py +31 -0
- regscale/integrations/commercial/tenablev2/click.py +1584 -0
- regscale/integrations/commercial/tenablev2/scanner.py +504 -0
- regscale/integrations/commercial/tenablev2/stig_parsers.py +140 -0
- regscale/integrations/commercial/tenablev2/utils.py +78 -0
- regscale/integrations/commercial/tenablev2/variables.py +17 -0
- regscale/integrations/commercial/trivy.py +162 -0
- regscale/integrations/commercial/veracode.py +96 -0
- regscale/integrations/commercial/wizv2/WizDataMixin.py +97 -0
- regscale/integrations/commercial/wizv2/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/click.py +429 -0
- regscale/integrations/commercial/wizv2/constants.py +1001 -0
- regscale/integrations/commercial/wizv2/issue.py +361 -0
- regscale/integrations/commercial/wizv2/models.py +112 -0
- regscale/integrations/commercial/wizv2/parsers.py +339 -0
- regscale/integrations/commercial/wizv2/sbom.py +115 -0
- regscale/integrations/commercial/wizv2/scanner.py +416 -0
- regscale/integrations/commercial/wizv2/utils.py +796 -0
- regscale/integrations/commercial/wizv2/variables.py +39 -0
- regscale/integrations/commercial/wizv2/wiz_auth.py +159 -0
- regscale/integrations/commercial/xray.py +91 -0
- regscale/integrations/integration/__init__.py +2 -0
- regscale/integrations/integration/integration.py +26 -0
- regscale/integrations/integration/inventory.py +17 -0
- regscale/integrations/integration/issue.py +100 -0
- regscale/integrations/integration_override.py +149 -0
- regscale/integrations/public/__init__.py +103 -0
- regscale/integrations/public/cisa.py +641 -0
- regscale/integrations/public/criticality_updater.py +70 -0
- regscale/integrations/public/emass.py +411 -0
- regscale/integrations/public/emass_slcm_import.py +697 -0
- regscale/integrations/public/fedramp/__init__.py +0 -0
- regscale/integrations/public/fedramp/appendix_parser.py +548 -0
- regscale/integrations/public/fedramp/click.py +479 -0
- regscale/integrations/public/fedramp/components.py +714 -0
- regscale/integrations/public/fedramp/docx_parser.py +259 -0
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +1124 -0
- regscale/integrations/public/fedramp/fedramp_common.py +3181 -0
- regscale/integrations/public/fedramp/fedramp_docx.py +388 -0
- regscale/integrations/public/fedramp/fedramp_five.py +2343 -0
- regscale/integrations/public/fedramp/fedramp_traversal.py +138 -0
- regscale/integrations/public/fedramp/import_fedramp_r4_ssp.py +279 -0
- regscale/integrations/public/fedramp/import_workbook.py +495 -0
- regscale/integrations/public/fedramp/inventory_items.py +244 -0
- regscale/integrations/public/fedramp/mappings/__init__.py +0 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +7388 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r5_params.json +8636 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +9605 -0
- regscale/integrations/public/fedramp/mappings/system_roles.py +34 -0
- regscale/integrations/public/fedramp/mappings/user.py +175 -0
- regscale/integrations/public/fedramp/mappings/values.py +141 -0
- regscale/integrations/public/fedramp/markdown_parser.py +150 -0
- regscale/integrations/public/fedramp/metadata.py +689 -0
- regscale/integrations/public/fedramp/models/__init__.py +59 -0
- regscale/integrations/public/fedramp/models/leveraged_auth_new.py +168 -0
- regscale/integrations/public/fedramp/models/poam_importer.py +522 -0
- regscale/integrations/public/fedramp/parts_mapper.py +107 -0
- regscale/integrations/public/fedramp/poam/__init__.py +0 -0
- regscale/integrations/public/fedramp/poam/scanner.py +851 -0
- regscale/integrations/public/fedramp/properties.py +201 -0
- regscale/integrations/public/fedramp/reporting.py +84 -0
- regscale/integrations/public/fedramp/resources.py +496 -0
- regscale/integrations/public/fedramp/rosetta.py +110 -0
- regscale/integrations/public/fedramp/ssp_logger.py +87 -0
- regscale/integrations/public/fedramp/system_characteristics.py +922 -0
- regscale/integrations/public/fedramp/system_control_implementations.py +582 -0
- regscale/integrations/public/fedramp/system_implementation.py +190 -0
- regscale/integrations/public/fedramp/xml_utils.py +87 -0
- regscale/integrations/public/nist_catalog.py +275 -0
- regscale/integrations/public/oscal.py +1946 -0
- regscale/integrations/public/otx.py +169 -0
- regscale/integrations/scanner_integration.py +2692 -0
- regscale/integrations/variables.py +25 -0
- regscale/models/__init__.py +7 -0
- regscale/models/app_models/__init__.py +5 -0
- regscale/models/app_models/catalog_compare.py +213 -0
- regscale/models/app_models/click.py +252 -0
- regscale/models/app_models/datetime_encoder.py +21 -0
- regscale/models/app_models/import_validater.py +321 -0
- regscale/models/app_models/mapping.py +260 -0
- regscale/models/app_models/pipeline.py +37 -0
- regscale/models/click_models.py +413 -0
- regscale/models/config.py +154 -0
- regscale/models/email_style.css +67 -0
- regscale/models/hierarchy.py +8 -0
- regscale/models/inspect_models.py +79 -0
- regscale/models/integration_models/__init__.py +0 -0
- regscale/models/integration_models/amazon_models/__init__.py +0 -0
- regscale/models/integration_models/amazon_models/inspector.py +262 -0
- regscale/models/integration_models/amazon_models/inspector_scan.py +206 -0
- regscale/models/integration_models/aqua.py +247 -0
- regscale/models/integration_models/azure_alerts.py +255 -0
- regscale/models/integration_models/base64.py +23 -0
- regscale/models/integration_models/burp.py +433 -0
- regscale/models/integration_models/burp_models.py +128 -0
- regscale/models/integration_models/cisa_kev_data.json +19333 -0
- regscale/models/integration_models/defender_data.py +93 -0
- regscale/models/integration_models/defenderimport.py +143 -0
- regscale/models/integration_models/drf.py +443 -0
- regscale/models/integration_models/ecr_models/__init__.py +0 -0
- regscale/models/integration_models/ecr_models/data.py +69 -0
- regscale/models/integration_models/ecr_models/ecr.py +239 -0
- regscale/models/integration_models/flat_file_importer.py +1079 -0
- regscale/models/integration_models/grype_import.py +247 -0
- regscale/models/integration_models/ibm.py +126 -0
- regscale/models/integration_models/implementation_results.py +85 -0
- regscale/models/integration_models/nexpose.py +140 -0
- regscale/models/integration_models/prisma.py +202 -0
- regscale/models/integration_models/qualys.py +720 -0
- regscale/models/integration_models/qualys_scanner.py +160 -0
- regscale/models/integration_models/sbom/__init__.py +0 -0
- regscale/models/integration_models/sbom/cyclone_dx.py +139 -0
- regscale/models/integration_models/send_reminders.py +620 -0
- regscale/models/integration_models/snyk.py +155 -0
- regscale/models/integration_models/synqly_models/__init__.py +0 -0
- regscale/models/integration_models/synqly_models/capabilities.json +1 -0
- regscale/models/integration_models/synqly_models/connector_types.py +22 -0
- regscale/models/integration_models/synqly_models/connectors/__init__.py +7 -0
- regscale/models/integration_models/synqly_models/connectors/assets.py +97 -0
- regscale/models/integration_models/synqly_models/connectors/ticketing.py +583 -0
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +169 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +331 -0
- regscale/models/integration_models/synqly_models/param.py +72 -0
- regscale/models/integration_models/synqly_models/synqly_model.py +733 -0
- regscale/models/integration_models/synqly_models/tenants.py +39 -0
- regscale/models/integration_models/tenable_models/__init__.py +0 -0
- regscale/models/integration_models/tenable_models/integration.py +187 -0
- regscale/models/integration_models/tenable_models/models.py +513 -0
- regscale/models/integration_models/trivy_import.py +231 -0
- regscale/models/integration_models/veracode.py +217 -0
- regscale/models/integration_models/xray.py +135 -0
- regscale/models/locking.py +100 -0
- regscale/models/platform.py +110 -0
- regscale/models/regscale_models/__init__.py +67 -0
- regscale/models/regscale_models/assessment.py +570 -0
- regscale/models/regscale_models/assessment_plan.py +52 -0
- regscale/models/regscale_models/asset.py +567 -0
- regscale/models/regscale_models/asset_mapping.py +190 -0
- regscale/models/regscale_models/case.py +42 -0
- regscale/models/regscale_models/catalog.py +261 -0
- regscale/models/regscale_models/cci.py +46 -0
- regscale/models/regscale_models/change.py +167 -0
- regscale/models/regscale_models/checklist.py +372 -0
- regscale/models/regscale_models/comment.py +49 -0
- regscale/models/regscale_models/compliance_settings.py +112 -0
- regscale/models/regscale_models/component.py +412 -0
- regscale/models/regscale_models/component_mapping.py +65 -0
- regscale/models/regscale_models/control.py +38 -0
- regscale/models/regscale_models/control_implementation.py +1128 -0
- regscale/models/regscale_models/control_objective.py +261 -0
- regscale/models/regscale_models/control_parameter.py +100 -0
- regscale/models/regscale_models/control_test.py +34 -0
- regscale/models/regscale_models/control_test_plan.py +75 -0
- regscale/models/regscale_models/control_test_result.py +52 -0
- regscale/models/regscale_models/custom_field.py +245 -0
- regscale/models/regscale_models/data.py +109 -0
- regscale/models/regscale_models/data_center.py +40 -0
- regscale/models/regscale_models/deviation.py +203 -0
- regscale/models/regscale_models/email.py +97 -0
- regscale/models/regscale_models/evidence.py +47 -0
- regscale/models/regscale_models/evidence_mapping.py +40 -0
- regscale/models/regscale_models/facility.py +59 -0
- regscale/models/regscale_models/file.py +382 -0
- regscale/models/regscale_models/filetag.py +37 -0
- regscale/models/regscale_models/form_field_value.py +94 -0
- regscale/models/regscale_models/group.py +169 -0
- regscale/models/regscale_models/implementation_objective.py +335 -0
- regscale/models/regscale_models/implementation_option.py +275 -0
- regscale/models/regscale_models/implementation_role.py +33 -0
- regscale/models/regscale_models/incident.py +177 -0
- regscale/models/regscale_models/interconnection.py +43 -0
- regscale/models/regscale_models/issue.py +1176 -0
- regscale/models/regscale_models/leveraged_authorization.py +125 -0
- regscale/models/regscale_models/line_of_inquiry.py +52 -0
- regscale/models/regscale_models/link.py +205 -0
- regscale/models/regscale_models/meta_data.py +64 -0
- regscale/models/regscale_models/mixins/__init__.py +0 -0
- regscale/models/regscale_models/mixins/parent_cache.py +124 -0
- regscale/models/regscale_models/module.py +224 -0
- regscale/models/regscale_models/modules.py +191 -0
- regscale/models/regscale_models/objective.py +14 -0
- regscale/models/regscale_models/parameter.py +87 -0
- regscale/models/regscale_models/ports_protocol.py +81 -0
- regscale/models/regscale_models/privacy.py +89 -0
- regscale/models/regscale_models/profile.py +50 -0
- regscale/models/regscale_models/profile_link.py +68 -0
- regscale/models/regscale_models/profile_mapping.py +124 -0
- regscale/models/regscale_models/project.py +63 -0
- regscale/models/regscale_models/property.py +278 -0
- regscale/models/regscale_models/question.py +85 -0
- regscale/models/regscale_models/questionnaire.py +87 -0
- regscale/models/regscale_models/questionnaire_instance.py +177 -0
- regscale/models/regscale_models/rbac.py +132 -0
- regscale/models/regscale_models/reference.py +86 -0
- regscale/models/regscale_models/regscale_model.py +1643 -0
- regscale/models/regscale_models/requirement.py +29 -0
- regscale/models/regscale_models/risk.py +274 -0
- regscale/models/regscale_models/sbom.py +54 -0
- regscale/models/regscale_models/scan_history.py +436 -0
- regscale/models/regscale_models/search.py +53 -0
- regscale/models/regscale_models/security_control.py +132 -0
- regscale/models/regscale_models/security_plan.py +204 -0
- regscale/models/regscale_models/software_inventory.py +159 -0
- regscale/models/regscale_models/stake_holder.py +64 -0
- regscale/models/regscale_models/stig.py +647 -0
- regscale/models/regscale_models/supply_chain.py +152 -0
- regscale/models/regscale_models/system_role.py +188 -0
- regscale/models/regscale_models/system_role_external_assignment.py +40 -0
- regscale/models/regscale_models/tag.py +37 -0
- regscale/models/regscale_models/tag_mapping.py +19 -0
- regscale/models/regscale_models/task.py +133 -0
- regscale/models/regscale_models/threat.py +196 -0
- regscale/models/regscale_models/user.py +175 -0
- regscale/models/regscale_models/user_group.py +55 -0
- regscale/models/regscale_models/vulnerability.py +242 -0
- regscale/models/regscale_models/vulnerability_mapping.py +162 -0
- regscale/models/regscale_models/workflow.py +55 -0
- regscale/models/regscale_models/workflow_action.py +34 -0
- regscale/models/regscale_models/workflow_instance.py +269 -0
- regscale/models/regscale_models/workflow_instance_step.py +114 -0
- regscale/models/regscale_models/workflow_template.py +58 -0
- regscale/models/regscale_models/workflow_template_step.py +45 -0
- regscale/regscale.py +815 -0
- regscale/utils/__init__.py +7 -0
- regscale/utils/b64conversion.py +14 -0
- regscale/utils/click_utils.py +118 -0
- regscale/utils/decorators.py +48 -0
- regscale/utils/dict_utils.py +59 -0
- regscale/utils/files.py +79 -0
- regscale/utils/fxns.py +30 -0
- regscale/utils/graphql_client.py +113 -0
- regscale/utils/lists.py +16 -0
- regscale/utils/numbers.py +12 -0
- regscale/utils/shell.py +148 -0
- regscale/utils/string.py +121 -0
- regscale/utils/synqly_utils.py +165 -0
- regscale/utils/threading/__init__.py +8 -0
- regscale/utils/threading/threadhandler.py +131 -0
- regscale/utils/threading/threadsafe_counter.py +47 -0
- regscale/utils/threading/threadsafe_dict.py +242 -0
- regscale/utils/threading/threadsafe_list.py +83 -0
- regscale/utils/version.py +104 -0
- regscale/validation/__init__.py +0 -0
- regscale/validation/address.py +37 -0
- regscale/validation/record.py +48 -0
- regscale/visualization/__init__.py +5 -0
- regscale/visualization/click.py +34 -0
- regscale_cli-6.16.0.0.dist-info/LICENSE +21 -0
- regscale_cli-6.16.0.0.dist-info/METADATA +659 -0
- regscale_cli-6.16.0.0.dist-info/RECORD +481 -0
- regscale_cli-6.16.0.0.dist-info/WHEEL +5 -0
- regscale_cli-6.16.0.0.dist-info/entry_points.txt +6 -0
- regscale_cli-6.16.0.0.dist-info/top_level.txt +2 -0
- tests/fixtures/__init__.py +2 -0
- tests/fixtures/api.py +87 -0
- tests/fixtures/models.py +91 -0
- tests/fixtures/test_fixture.py +144 -0
- tests/mocks/__init__.py +0 -0
- tests/mocks/objects.py +3 -0
- tests/mocks/response.py +32 -0
- tests/mocks/xml.py +13 -0
- tests/regscale/__init__.py +0 -0
- tests/regscale/core/__init__.py +0 -0
- tests/regscale/core/test_api.py +232 -0
- tests/regscale/core/test_app.py +406 -0
- tests/regscale/core/test_login.py +37 -0
- tests/regscale/core/test_logz.py +66 -0
- tests/regscale/core/test_sbom_generator.py +87 -0
- tests/regscale/core/test_validation_utils.py +163 -0
- tests/regscale/core/test_version.py +78 -0
- tests/regscale/models/__init__.py +0 -0
- tests/regscale/models/test_asset.py +71 -0
- tests/regscale/models/test_config.py +26 -0
- tests/regscale/models/test_control_implementation.py +27 -0
- tests/regscale/models/test_import.py +97 -0
- tests/regscale/models/test_issue.py +36 -0
- tests/regscale/models/test_mapping.py +52 -0
- tests/regscale/models/test_platform.py +31 -0
- tests/regscale/models/test_regscale_model.py +346 -0
- tests/regscale/models/test_report.py +32 -0
- tests/regscale/models/test_tenable_integrations.py +118 -0
- tests/regscale/models/test_user_model.py +121 -0
- tests/regscale/test_about.py +19 -0
- tests/regscale/test_authorization.py +65 -0
|
@@ -0,0 +1,2692 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Scanner Integration Class"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
import dataclasses
|
|
8
|
+
import enum
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from typing import Any, Dict, Generic, Iterator, List, Optional, Set, TypeVar, Union
|
|
18
|
+
|
|
19
|
+
from rich.progress import Progress, TaskID
|
|
20
|
+
|
|
21
|
+
from regscale.core.app.application import Application
|
|
22
|
+
from regscale.core.app.utils.api_handler import APIHandler
|
|
23
|
+
from regscale.core.app.utils.app_utils import create_progress_object, get_current_datetime
|
|
24
|
+
from regscale.core.app.utils.catalog_utils.common import objective_to_control_dot
|
|
25
|
+
from regscale.core.utils.date import date_obj, date_str, datetime_str, days_from_today, get_day_increment
|
|
26
|
+
from regscale.integrations.commercial.durosuite.process_devices import scan_durosuite_devices
|
|
27
|
+
from regscale.integrations.commercial.durosuite.variables import DuroSuiteVariables
|
|
28
|
+
from regscale.integrations.commercial.stig_mapper_integration.mapping_engine import StigMappingEngine as STIGMapper
|
|
29
|
+
from regscale.integrations.public.cisa import pull_cisa_kev
|
|
30
|
+
from regscale.integrations.variables import ScannerVariables
|
|
31
|
+
from regscale.models import DateTimeEncoder, OpenIssueDict, regscale_models
|
|
32
|
+
from regscale.utils.threading import ThreadSafeDict, ThreadSafeList
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
K = TypeVar("K") # Key type
|
|
37
|
+
V = TypeVar("V") # Value type
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_thread_workers_max() -> int:
|
|
41
|
+
"""
|
|
42
|
+
Get the maximum number of thread workers
|
|
43
|
+
|
|
44
|
+
:return: The maximum number of thread workers
|
|
45
|
+
:rtype: int
|
|
46
|
+
"""
|
|
47
|
+
return ScannerVariables.threadMaxWorkers
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def issue_due_date(
|
|
51
|
+
severity: regscale_models.IssueSeverity,
|
|
52
|
+
created_date: str,
|
|
53
|
+
critical: int = ScannerVariables.issueDueDates.get("critical", 30),
|
|
54
|
+
high: int = ScannerVariables.issueDueDates.get("high", 60),
|
|
55
|
+
moderate: int = ScannerVariables.issueDueDates.get("moderate", 120),
|
|
56
|
+
low: int = ScannerVariables.issueDueDates.get("low", 364),
|
|
57
|
+
title: str = "",
|
|
58
|
+
config: Optional[Dict[str, Dict]] = None,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Calculate the due date for an issue based on its severity and creation date.
|
|
62
|
+
|
|
63
|
+
:param regscale_models.IssueSeverity severity: The severity of the issue.
|
|
64
|
+
:param str created_date: The creation date of the issue.
|
|
65
|
+
:param int critical: Days until due for high severity issues. Default is 30.
|
|
66
|
+
:param int high: Days until due for high severity issues. Default is 60.
|
|
67
|
+
:param int moderate: Days until due for moderate severity issues. Default is 210.
|
|
68
|
+
:param int low: Days until due for low severity issues. Default is 364.
|
|
69
|
+
:param str title: The title of the Integration.
|
|
70
|
+
:param Dict[str, Dict] config: Configuration options for the due date calculation.
|
|
71
|
+
:return: The due date for the issue.
|
|
72
|
+
:rtype: str
|
|
73
|
+
"""
|
|
74
|
+
if config is None:
|
|
75
|
+
config = {}
|
|
76
|
+
|
|
77
|
+
due_date_map = {
|
|
78
|
+
regscale_models.IssueSeverity.Critical: critical,
|
|
79
|
+
regscale_models.IssueSeverity.High: high,
|
|
80
|
+
regscale_models.IssueSeverity.Moderate: moderate,
|
|
81
|
+
regscale_models.IssueSeverity.Low: low,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if title and config:
|
|
85
|
+
# if title in a config key, use that key
|
|
86
|
+
issues_dict = config.get("issues", {})
|
|
87
|
+
matching_key = next((key.lower() for key in issues_dict if title.lower() in key.lower()), None)
|
|
88
|
+
if matching_key:
|
|
89
|
+
title_config = issues_dict.get(matching_key, {})
|
|
90
|
+
due_date_map = {
|
|
91
|
+
regscale_models.IssueSeverity.Critical: title_config.get("critical", critical),
|
|
92
|
+
regscale_models.IssueSeverity.High: title_config.get("high", high),
|
|
93
|
+
regscale_models.IssueSeverity.Moderate: title_config.get("moderate", moderate),
|
|
94
|
+
regscale_models.IssueSeverity.Low: title_config.get("low", low),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
days = due_date_map.get(severity, low)
|
|
98
|
+
return date_str(get_day_increment(start=created_date, days=days))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ManagedDefaultDict(Generic[K, V]):
|
|
102
|
+
"""
|
|
103
|
+
A thread-safe default dictionary that uses a multiprocessing Manager.
|
|
104
|
+
|
|
105
|
+
:param default_factory: A callable that produces default values for missing keys
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, default_factory):
|
|
109
|
+
self.store: ThreadSafeDict[Any, Any] = ThreadSafeDict() # type: ignore[type-arg]
|
|
110
|
+
self.default_factory = default_factory
|
|
111
|
+
|
|
112
|
+
def __getitem__(self, key: Any) -> Any:
|
|
113
|
+
"""
|
|
114
|
+
Get the item from the store
|
|
115
|
+
|
|
116
|
+
:param Any key: Key to get the item from the store
|
|
117
|
+
:return: Value from the store
|
|
118
|
+
:rtype: Any
|
|
119
|
+
"""
|
|
120
|
+
if key not in self.store:
|
|
121
|
+
self.store[key] = self.default_factory()
|
|
122
|
+
return self.store[key]
|
|
123
|
+
|
|
124
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Set the item in the store
|
|
127
|
+
|
|
128
|
+
:param Any key: Key to set the item in the store
|
|
129
|
+
:param Any value: Value to set in the store
|
|
130
|
+
:rtype: None
|
|
131
|
+
"""
|
|
132
|
+
self.store[key] = value
|
|
133
|
+
|
|
134
|
+
def __contains__(self, key: Any) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Check if the key is in the store
|
|
137
|
+
|
|
138
|
+
:param Any key: Key to check in the store
|
|
139
|
+
:return: Whether the key is in the store
|
|
140
|
+
:rtype: bool
|
|
141
|
+
"""
|
|
142
|
+
return key in self.store
|
|
143
|
+
|
|
144
|
+
def __len__(self) -> int:
|
|
145
|
+
"""
|
|
146
|
+
Get the length of the store
|
|
147
|
+
|
|
148
|
+
:return: Number of items in the store
|
|
149
|
+
:rtype: int
|
|
150
|
+
"""
|
|
151
|
+
return len(self.store)
|
|
152
|
+
|
|
153
|
+
def get(self, key: Any, default: Optional[Any] = None) -> Optional[Any]:
|
|
154
|
+
"""
|
|
155
|
+
Get the value from the store
|
|
156
|
+
|
|
157
|
+
:param Any key: Key to get the value from the store
|
|
158
|
+
:param Optional[Any] default: Default value to return if the key is not in the store, defaults to None
|
|
159
|
+
:return: The value from the store, or the default value
|
|
160
|
+
:rtype: Optional[Any]
|
|
161
|
+
"""
|
|
162
|
+
if key not in self.store:
|
|
163
|
+
return default
|
|
164
|
+
return self.store[key]
|
|
165
|
+
|
|
166
|
+
def items(self) -> Any:
|
|
167
|
+
"""
|
|
168
|
+
Get the items from the store
|
|
169
|
+
|
|
170
|
+
:return: Items from the store
|
|
171
|
+
:rtype: Any
|
|
172
|
+
"""
|
|
173
|
+
return self.store.items()
|
|
174
|
+
|
|
175
|
+
def keys(self) -> Any:
|
|
176
|
+
"""
|
|
177
|
+
Get the keys from the store
|
|
178
|
+
|
|
179
|
+
:return: Keys from the store
|
|
180
|
+
:rtype: Any
|
|
181
|
+
"""
|
|
182
|
+
return self.store.keys()
|
|
183
|
+
|
|
184
|
+
def values(self) -> Any:
|
|
185
|
+
"""
|
|
186
|
+
Get the values from the store
|
|
187
|
+
|
|
188
|
+
:return: Values in the store
|
|
189
|
+
:rtype: Any
|
|
190
|
+
"""
|
|
191
|
+
return self.store.values()
|
|
192
|
+
|
|
193
|
+
def update(self, *args, **kwargs) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Update the store
|
|
196
|
+
|
|
197
|
+
:rtype: None
|
|
198
|
+
"""
|
|
199
|
+
self.store.update(*args, **kwargs)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclasses.dataclass
|
|
203
|
+
class IntegrationAsset:
|
|
204
|
+
"""
|
|
205
|
+
Dataclass for integration assets.
|
|
206
|
+
|
|
207
|
+
Represents an asset to be integrated, including its metadata and associated components.
|
|
208
|
+
If a component does not exist, it will be created based on the names provided in ``component_names``.
|
|
209
|
+
|
|
210
|
+
:param str name: The name of the asset.
|
|
211
|
+
:param str identifier: A unique identifier for the asset.
|
|
212
|
+
:param str asset_type: The type of the asset.
|
|
213
|
+
:param str asset_category: The category of the asset.
|
|
214
|
+
:param str component_type: The type of the component, defaults to ``ComponentType.Hardware``.
|
|
215
|
+
:param Optional[int] parent_id: The ID of the parent asset, defaults to None.
|
|
216
|
+
:param Optional[str] parent_module: The module of the parent asset, defaults to None.
|
|
217
|
+
:param str status: The status of the asset, defaults to "Active (On Network)".
|
|
218
|
+
:param str date_last_updated: The last update date of the asset, defaults to the current datetime.
|
|
219
|
+
:param Optional[str] asset_owner_id: The ID of the asset owner, defaults to None.
|
|
220
|
+
:param Optional[str] mac_address: The MAC address of the asset, defaults to None.
|
|
221
|
+
:param Optional[str] fqdn: The Fully Qualified Domain Name of the asset, defaults to None.
|
|
222
|
+
:param Optional[str] ip_address: The IP address of the asset, defaults to None.
|
|
223
|
+
:param List[str] component_names: A list of strings that represent the names of the components associated with the
|
|
224
|
+
asset, components will be created if they do not exist.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
name: str
|
|
228
|
+
identifier: str
|
|
229
|
+
asset_type: str
|
|
230
|
+
asset_category: str
|
|
231
|
+
component_type: str = regscale_models.ComponentType.Hardware
|
|
232
|
+
description: str = ""
|
|
233
|
+
parent_id: Optional[int] = None
|
|
234
|
+
parent_module: Optional[str] = None
|
|
235
|
+
status: regscale_models.AssetStatus = regscale_models.AssetStatus.Active
|
|
236
|
+
date_last_updated: str = dataclasses.field(default_factory=get_current_datetime)
|
|
237
|
+
asset_owner_id: Optional[str] = None
|
|
238
|
+
mac_address: Optional[str] = None
|
|
239
|
+
fqdn: Optional[str] = None
|
|
240
|
+
ip_address: Optional[str] = None
|
|
241
|
+
component_names: List[str] = dataclasses.field(default_factory=list)
|
|
242
|
+
is_virtual: bool = True
|
|
243
|
+
|
|
244
|
+
# Additional fields from Wiz integration
|
|
245
|
+
external_id: Optional[str] = None
|
|
246
|
+
management_type: Optional[str] = None
|
|
247
|
+
software_vendor: Optional[str] = None
|
|
248
|
+
software_version: Optional[str] = None
|
|
249
|
+
software_name: Optional[str] = None
|
|
250
|
+
location: Optional[str] = None
|
|
251
|
+
notes: Optional[str] = None
|
|
252
|
+
model: Optional[str] = None
|
|
253
|
+
manufacturer: Optional[str] = None
|
|
254
|
+
other_tracking_number: Optional[str] = None
|
|
255
|
+
serial_number: Optional[str] = None
|
|
256
|
+
asset_tag_number: Optional[str] = None
|
|
257
|
+
is_public_facing: Optional[bool] = None
|
|
258
|
+
azure_identifier: Optional[str] = None
|
|
259
|
+
disk_storage: Optional[int] = None
|
|
260
|
+
cpu: Optional[int] = None
|
|
261
|
+
ram: Optional[int] = None
|
|
262
|
+
operating_system: Optional[regscale_models.AssetOperatingSystem] = None
|
|
263
|
+
os_version: Optional[str] = None
|
|
264
|
+
end_of_life_date: Optional[str] = None
|
|
265
|
+
vlan_id: Optional[str] = None
|
|
266
|
+
uri: Optional[str] = None
|
|
267
|
+
aws_identifier: Optional[str] = None
|
|
268
|
+
google_identifier: Optional[str] = None
|
|
269
|
+
other_cloud_identifier: Optional[str] = None
|
|
270
|
+
patch_level: Optional[str] = None
|
|
271
|
+
cpe: Optional[str] = None
|
|
272
|
+
|
|
273
|
+
source_data: Optional[Dict[str, Any]] = None
|
|
274
|
+
url: Optional[str] = None
|
|
275
|
+
ports_and_protocols: List[Dict[str, Any]] = dataclasses.field(default_factory=list)
|
|
276
|
+
software_inventory: List[Dict[str, Any]] = dataclasses.field(default_factory=list)
|
|
277
|
+
|
|
278
|
+
def __post_init__(self):
|
|
279
|
+
if self.ip_address in ["", "0.0.0.0"]:
|
|
280
|
+
self.ip_address = None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclasses.dataclass
|
|
284
|
+
class IntegrationFinding:
|
|
285
|
+
"""
|
|
286
|
+
Dataclass for integration findings.
|
|
287
|
+
|
|
288
|
+
:param list[str] control_labels: A list of control labels associated with the finding.
|
|
289
|
+
:param str title: The title of the finding.
|
|
290
|
+
:param str category: The category of the finding.
|
|
291
|
+
:param regscale_models.IssueSeverity severity: The severity of the finding, based on regscale_models.IssueSeverity.
|
|
292
|
+
:param str description: A description of the finding.
|
|
293
|
+
:param regscale_models.ControlTestResultStatus status: The status of the finding, based on
|
|
294
|
+
regscale_models.ControlTestResultStatus.
|
|
295
|
+
:param str priority: The priority of the finding, defaults to "Medium".
|
|
296
|
+
:param str issue_type: The type of issue, defaults to "Risk".
|
|
297
|
+
:param str issue_title: The title of the issue, defaults to an empty string.
|
|
298
|
+
:param str date_created: The creation date of the finding, defaults to the current datetime.
|
|
299
|
+
:param str due_date: The due date of the finding, defaults to 60 days from the current datetime.
|
|
300
|
+
:param str date_last_updated: The last update date of the finding, defaults to the current datetime.
|
|
301
|
+
:param str external_id: An external identifier for the finding, defaults to an empty string.
|
|
302
|
+
:param str gaps: A description of any gaps identified, defaults to an empty string.
|
|
303
|
+
:param str observations: Observations related to the finding, defaults to an empty string.
|
|
304
|
+
:param str evidence: Evidence supporting the finding, defaults to an empty string.
|
|
305
|
+
:param str identified_risk: The risk identified by the finding, defaults to an empty string.
|
|
306
|
+
:param str impact: The impact of the finding, defaults to an empty string.
|
|
307
|
+
:param str recommendation_for_mitigation: Recommendations for mitigating the finding, defaults to an empty string.
|
|
308
|
+
:param str asset_identifier: The identifier of the asset associated with the finding, defaults to an empty string.
|
|
309
|
+
:param Optional[str] cci_ref: The Common Configuration Enumeration reference for the finding, defaults to None.
|
|
310
|
+
:param str rule_id: The rule ID of the finding, defaults to an empty string.
|
|
311
|
+
:param str rule_version: The version of the rule associated with the finding, defaults to an empty string.
|
|
312
|
+
:param str results: The results of the finding, defaults to an empty string.
|
|
313
|
+
:param Optional[str] comments: Additional comments related to the finding, defaults to None.
|
|
314
|
+
:param Optional[str] source_report: The source report of the finding, defaults to None.
|
|
315
|
+
:param Optional[str] point_of_contact: The point of contact for the finding, used to create property defaults to None.
|
|
316
|
+
:param Optional[str] milestone_changes: Milestone Changes for the finding, defaults to None.
|
|
317
|
+
:param Optional[str] adjusted_risk_rating: The adjusted risk rating of the finding, defaults to None.
|
|
318
|
+
:param Optional[str] risk_adjustment: The risk adjustment of the finding, (Should be Yes, No, Pending), defaults to No.
|
|
319
|
+
:param Optional[str] operational_requirements: The operational requirements of the finding, defaults to None.
|
|
320
|
+
:param Optional[str] deviation_rationale: The rationale for any deviations from the finding, defaults to None.
|
|
321
|
+
:param str baseline: The baseline of the finding, defaults to an empty string.
|
|
322
|
+
:param str poam_comments: Comments related to the Plan of Action and Milestones (POAM) for the finding, defaults to
|
|
323
|
+
:param Optional[int] vulnerability_id: The ID of the vulnerability associated with the finding, defaults to None.
|
|
324
|
+
an empty string.
|
|
325
|
+
:param Optional[str] basis_for_adjustment: The basis for adjusting the finding, defaults to None.
|
|
326
|
+
:param Optional[str] vulnerability_number: STIG vulnerability number
|
|
327
|
+
:param Optional[str] vulnerability_type: The type of vulnerability, defaults to None.
|
|
328
|
+
:param Optional[str] plugin_id: The ID of the plugin associated with the finding, defaults to None.
|
|
329
|
+
:param Optional[str] plugin_name: The name of the plugin associated with the finding, defaults to None.
|
|
330
|
+
:param Optional[str] dns: The DNS name associated with the finding, defaults to None.
|
|
331
|
+
:param int severity_int: The severity integer of the finding, defaults to 0.
|
|
332
|
+
:param Optional[str] cve: The CVE of the finding, defaults to None.
|
|
333
|
+
:param Optional[float] cvss_v3_score: The CVSS v3 score of the finding, defaults to None.
|
|
334
|
+
:param Optional[float] cvss_v2_score: The CVSS v2 score of the finding, defaults to None.
|
|
335
|
+
:param Optional[str] cvss_score: The CVSS score of the finding, defaults to None.
|
|
336
|
+
:param Optional[str] cvss_v3_base_score: The CVSS v3 base score of the finding, defaults to None.
|
|
337
|
+
:param Optional[str] ip_address: The IP address associated with the finding, defaults to None.
|
|
338
|
+
:param Optional[str] first_seen: The first seen date of the finding, defaults to the current datetime.
|
|
339
|
+
:param Optional[str] last_seen: The last seen date of the finding, defaults to the current datetime.
|
|
340
|
+
:param Optional[str] oval_def: The OVAL definition of the finding, defaults to None.
|
|
341
|
+
:param Optional[str] scan_date: The scan date of the finding, defaults to the current datetime.
|
|
342
|
+
:param Optional[str] rule_id_full: The full rule ID of the finding, defaults to an empty string.
|
|
343
|
+
:param Optional[str] group_id: The group ID of the finding, defaults to an empty string.
|
|
344
|
+
:param Optional[str] vulnerable_asset: The vulnerable asset of the finding, defaults to None.
|
|
345
|
+
:param Optional[str] remediation: The remediation of the finding, defaults to None.
|
|
346
|
+
:param Optional[str] source_rule_id: The source rule ID of the finding, defaults to None.
|
|
347
|
+
:param Optional[str] poam_id: The POAM ID of the finding, defaults to None.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
control_labels: List[str]
|
|
351
|
+
title: str
|
|
352
|
+
category: str
|
|
353
|
+
plugin_name: str
|
|
354
|
+
severity: regscale_models.IssueSeverity
|
|
355
|
+
description: str
|
|
356
|
+
status: Union[regscale_models.ControlTestResultStatus, regscale_models.ChecklistStatus, regscale_models.IssueStatus]
|
|
357
|
+
priority: str = "Medium"
|
|
358
|
+
|
|
359
|
+
# Vulns
|
|
360
|
+
first_seen: str = dataclasses.field(default_factory=get_current_datetime)
|
|
361
|
+
last_seen: str = dataclasses.field(default_factory=get_current_datetime)
|
|
362
|
+
cve: Optional[str] = None
|
|
363
|
+
cvss_v3_score: Optional[float] = None
|
|
364
|
+
cvss_v2_score: Optional[float] = None
|
|
365
|
+
ip_address: Optional[str] = None
|
|
366
|
+
plugin_id: Optional[str] = None
|
|
367
|
+
dns: Optional[str] = None
|
|
368
|
+
severity_int: int = 0
|
|
369
|
+
security_check: Optional[str] = None
|
|
370
|
+
|
|
371
|
+
# Issues
|
|
372
|
+
issue_title: str = ""
|
|
373
|
+
issue_type: str = "Risk"
|
|
374
|
+
date_created: str = dataclasses.field(default_factory=get_current_datetime)
|
|
375
|
+
date_last_updated: str = dataclasses.field(default_factory=get_current_datetime)
|
|
376
|
+
due_date: str = dataclasses.field(default_factory=lambda: date_str(days_from_today(60)))
|
|
377
|
+
external_id: str = ""
|
|
378
|
+
gaps: str = ""
|
|
379
|
+
observations: str = ""
|
|
380
|
+
evidence: str = ""
|
|
381
|
+
identified_risk: str = ""
|
|
382
|
+
impact: str = ""
|
|
383
|
+
recommendation_for_mitigation: str = ""
|
|
384
|
+
asset_identifier: str = ""
|
|
385
|
+
comments: Optional[str] = None
|
|
386
|
+
source_report: Optional[str] = None
|
|
387
|
+
point_of_contact: Optional[str] = None
|
|
388
|
+
milestone_changes: Optional[str] = None
|
|
389
|
+
planned_milestone_changes: Optional[str] = None
|
|
390
|
+
adjusted_risk_rating: Optional[str] = None
|
|
391
|
+
risk_adjustment: str = "No"
|
|
392
|
+
operational_requirements: Optional[str] = None
|
|
393
|
+
deviation_rationale: Optional[str] = None
|
|
394
|
+
|
|
395
|
+
poam_comments: Optional[str] = None
|
|
396
|
+
vulnerability_id: Optional[int] = None
|
|
397
|
+
_control_implementation_ids: List[int] = dataclasses.field(default_factory=list)
|
|
398
|
+
|
|
399
|
+
# Stig
|
|
400
|
+
checklist_status: regscale_models.ChecklistStatus = dataclasses.field(
|
|
401
|
+
default=regscale_models.ChecklistStatus.NOT_REVIEWED
|
|
402
|
+
)
|
|
403
|
+
cci_ref: Optional[str] = None
|
|
404
|
+
rule_id: str = ""
|
|
405
|
+
rule_version: str = ""
|
|
406
|
+
results: str = ""
|
|
407
|
+
baseline: str = ""
|
|
408
|
+
vulnerability_number: str = ""
|
|
409
|
+
oval_def: str = ""
|
|
410
|
+
scan_date: str = ""
|
|
411
|
+
rule_id_full: str = ""
|
|
412
|
+
group_id: str = ""
|
|
413
|
+
|
|
414
|
+
# Wiz
|
|
415
|
+
vulnerable_asset: Optional[str] = None
|
|
416
|
+
remediation: Optional[str] = None
|
|
417
|
+
cvss_score: Optional[float] = None
|
|
418
|
+
cvss_v3_base_score: Optional[float] = None
|
|
419
|
+
source_rule_id: Optional[str] = None
|
|
420
|
+
vulnerability_type: Optional[str] = None
|
|
421
|
+
|
|
422
|
+
# CoalFre POAM
|
|
423
|
+
basis_for_adjustment: Optional[str] = None
|
|
424
|
+
poam_id: Optional[str] = None
|
|
425
|
+
|
|
426
|
+
# Additional fields from Wiz integration
|
|
427
|
+
vpr_score: Optional[float] = None
|
|
428
|
+
|
|
429
|
+
def __post_init__(self):
|
|
430
|
+
if self.plugin_name is None:
|
|
431
|
+
self.plugin_name = self.cve or self.title
|
|
432
|
+
if self.plugin_id is None:
|
|
433
|
+
self.plugin_id = self.plugin_name
|
|
434
|
+
|
|
435
|
+
def get_issue_status(self) -> regscale_models.IssueStatus:
|
|
436
|
+
return (
|
|
437
|
+
regscale_models.IssueStatus.Closed
|
|
438
|
+
if (
|
|
439
|
+
self.status == regscale_models.ControlTestResultStatus.PASS
|
|
440
|
+
or self.status == regscale_models.IssueStatus.Closed
|
|
441
|
+
)
|
|
442
|
+
else regscale_models.IssueStatus.Open
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def __eq__(self, other: Any) -> bool:
|
|
446
|
+
"""
|
|
447
|
+
Check if the finding is equal to another finding
|
|
448
|
+
|
|
449
|
+
:param Any other: The other finding to compare
|
|
450
|
+
:return: Whether the findings are equal
|
|
451
|
+
:rtype: bool
|
|
452
|
+
"""
|
|
453
|
+
if not isinstance(other, IntegrationFinding):
|
|
454
|
+
return NotImplemented
|
|
455
|
+
return (self.title, self.category, self.external_id) == (other.title, other.category, other.external_id)
|
|
456
|
+
|
|
457
|
+
def __hash__(self) -> int:
|
|
458
|
+
"""
|
|
459
|
+
Get the hash of the finding
|
|
460
|
+
|
|
461
|
+
:return: Hash of the finding
|
|
462
|
+
:rtype: int
|
|
463
|
+
"""
|
|
464
|
+
return hash((self.title, self.category, self.external_id))
|
|
465
|
+
|
|
466
|
+
def is_valid(self) -> bool:
|
|
467
|
+
"""
|
|
468
|
+
Determines if the finding is valid based on the presence of `date_last_updated` and `risk_adjustment`.
|
|
469
|
+
|
|
470
|
+
:return: True if the finding is valid, False otherwise.
|
|
471
|
+
:rtype: bool
|
|
472
|
+
"""
|
|
473
|
+
# Check if these fields are not empty or None
|
|
474
|
+
if not self.date_last_updated:
|
|
475
|
+
logger.warning("Finding %s is missing date_last_updated, skipping..", self.poam_id)
|
|
476
|
+
return False
|
|
477
|
+
|
|
478
|
+
if not self.risk_adjustment:
|
|
479
|
+
logger.warning("Finding %s is missing risk_adjustment, skipping..", self.poam_id)
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
# Additional validation logic can be added here if needed
|
|
483
|
+
# For example, ensure risk_adjustment is one of the allowed values ("Yes", "No", "Pending")
|
|
484
|
+
allowed_risk_adjustments = {"Yes", "No", "Pending"}
|
|
485
|
+
if self.risk_adjustment not in allowed_risk_adjustments:
|
|
486
|
+
logger.warning("Finding %s has a disallowed risk adjustment, skipping..", self.poam_id)
|
|
487
|
+
return False
|
|
488
|
+
|
|
489
|
+
return True
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class ScannerIntegrationType(str, enum.Enum):
|
|
493
|
+
"""
|
|
494
|
+
Enumeration for scanner integration types.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
CHECKLIST = "checklist"
|
|
498
|
+
CONTROL_TEST = "control_test"
|
|
499
|
+
VULNERABILITY = "vulnerability"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class FindingStatus(str, enum.Enum):
|
|
503
|
+
OPEN = regscale_models.IssueStatus.Open
|
|
504
|
+
CLOSED = regscale_models.IssueStatus.Closed
|
|
505
|
+
FAIL = regscale_models.IssueStatus.Open
|
|
506
|
+
PASS = regscale_models.IssueStatus.Closed
|
|
507
|
+
NOT_APPLICABLE = regscale_models.IssueStatus.Closed
|
|
508
|
+
NOT_REVIEWED = regscale_models.IssueStatus.Open
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class ScannerIntegration(ABC):
|
|
512
|
+
"""
|
|
513
|
+
Abstract class for scanner integrations.
|
|
514
|
+
|
|
515
|
+
:param int plan_id: The ID of the security plan
|
|
516
|
+
:param int tenant_id: The ID of the tenant, defaults to 1
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
stig_mapper = None
|
|
520
|
+
# Basic configuration options
|
|
521
|
+
options_map_assets_to_components: bool = False
|
|
522
|
+
type: ScannerIntegrationType = ScannerIntegrationType.CONTROL_TEST
|
|
523
|
+
title: str = "Scanner Integration"
|
|
524
|
+
asset_identifier_field: str = "otherTrackingNumber"
|
|
525
|
+
issue_identifier_field: str = ""
|
|
526
|
+
_max_poam_id: Optional[int] = None # Value holder for get_max_poam_id
|
|
527
|
+
|
|
528
|
+
# Progress trackers
|
|
529
|
+
asset_progress: Progress
|
|
530
|
+
finding_progress: Progress
|
|
531
|
+
|
|
532
|
+
# Processing counts
|
|
533
|
+
num_assets_to_process: Optional[int] = None
|
|
534
|
+
num_findings_to_process: Optional[int] = None
|
|
535
|
+
|
|
536
|
+
# Lock registry
|
|
537
|
+
_lock_registry: ThreadSafeDict = ThreadSafeDict()
|
|
538
|
+
_global_lock = threading.Lock() # Class-level lock
|
|
539
|
+
_kev_data = ThreadSafeDict() # Class-level lock
|
|
540
|
+
|
|
541
|
+
# Error handling
|
|
542
|
+
errors: List[str] = []
|
|
543
|
+
|
|
544
|
+
# Mapping dictionaries
|
|
545
|
+
finding_status_map: dict[Any, regscale_models.IssueStatus] = {}
|
|
546
|
+
checklist_status_map: dict[Any, regscale_models.ChecklistStatus] = {}
|
|
547
|
+
finding_severity_map: dict[Any, regscale_models.IssueSeverity] = {}
|
|
548
|
+
issue_to_vulnerability_map: dict[regscale_models.IssueSeverity, regscale_models.VulnerabilitySeverity] = {
|
|
549
|
+
regscale_models.IssueSeverity.Low: regscale_models.VulnerabilitySeverity.Low,
|
|
550
|
+
regscale_models.IssueSeverity.Moderate: regscale_models.VulnerabilitySeverity.Medium,
|
|
551
|
+
regscale_models.IssueSeverity.High: regscale_models.VulnerabilitySeverity.High,
|
|
552
|
+
regscale_models.IssueSeverity.Critical: regscale_models.VulnerabilitySeverity.Critical,
|
|
553
|
+
}
|
|
554
|
+
asset_map: dict[str, regscale_models.Asset] = {}
|
|
555
|
+
# cci_to_control_map: dict[str, set[int]] = {}
|
|
556
|
+
control_implementation_id_map: dict[str, int] = {}
|
|
557
|
+
control_map: dict[int, str] = {}
|
|
558
|
+
control_id_to_implementation_map: dict[int, int] = {}
|
|
559
|
+
|
|
560
|
+
# Existing issues map
|
|
561
|
+
existing_issue_ids_by_implementation_map: dict[int, List[OpenIssueDict]] = defaultdict(list)
|
|
562
|
+
|
|
563
|
+
# Scan Date
|
|
564
|
+
scan_date: str = ""
|
|
565
|
+
enable_finding_date_update = False
|
|
566
|
+
|
|
567
|
+
# Close Outdated Findings
|
|
568
|
+
close_outdated_findings = True
|
|
569
|
+
|
|
570
|
+
def __init__(self, plan_id: int, tenant_id: int = 1, **kwargs):
|
|
571
|
+
"""
|
|
572
|
+
Initialize the ScannerIntegration.
|
|
573
|
+
|
|
574
|
+
:param int plan_id: The ID of the security plan
|
|
575
|
+
:param int tenant_id: The ID of the tenant, defaults to 1
|
|
576
|
+
:param kwargs: Additional keyword arguments
|
|
577
|
+
"""
|
|
578
|
+
self.app = Application()
|
|
579
|
+
self.alerted_assets: Set[str] = set()
|
|
580
|
+
self.regscale_version: str = APIHandler().regscale_version # noqa
|
|
581
|
+
logger.info(f"RegScale Version: {self.regscale_version}")
|
|
582
|
+
self.plan_id: int = plan_id
|
|
583
|
+
self.tenant_id: int = tenant_id
|
|
584
|
+
self.components: ThreadSafeList[Any] = ThreadSafeList()
|
|
585
|
+
self.asset_map_by_identifier: ThreadSafeDict[str, regscale_models.Asset] = ThreadSafeDict()
|
|
586
|
+
self.software_to_create: ThreadSafeList[regscale_models.SoftwareInventory] = ThreadSafeList()
|
|
587
|
+
self.software_to_update: ThreadSafeList[regscale_models.SoftwareInventory] = ThreadSafeList()
|
|
588
|
+
self.data_to_create: ThreadSafeList[regscale_models.Data] = ThreadSafeList()
|
|
589
|
+
self.data_to_update: ThreadSafeList[regscale_models.Data] = ThreadSafeList()
|
|
590
|
+
self.link_to_create: ThreadSafeList[regscale_models.Link] = ThreadSafeList()
|
|
591
|
+
self.link_to_update: ThreadSafeList[regscale_models.Link] = ThreadSafeList()
|
|
592
|
+
|
|
593
|
+
self.existing_issues_map: ThreadSafeDict[int, List[regscale_models.Issue]] = ThreadSafeDict()
|
|
594
|
+
self.components_by_title: ThreadSafeDict[str, regscale_models.Component] = ThreadSafeDict()
|
|
595
|
+
self.control_tests_map: ManagedDefaultDict[int, regscale_models.ControlTest] = ManagedDefaultDict(list)
|
|
596
|
+
|
|
597
|
+
self.implementation_objective_map: ThreadSafeDict[str, int] = ThreadSafeDict()
|
|
598
|
+
self.implementation_option_map: ThreadSafeDict[str, int] = ThreadSafeDict()
|
|
599
|
+
self.control_implementation_map: ThreadSafeDict[int, regscale_models.ControlImplementation] = ThreadSafeDict()
|
|
600
|
+
|
|
601
|
+
self.control_implementation_id_map = regscale_models.ControlImplementation.get_control_label_map_by_plan(
|
|
602
|
+
plan_id=plan_id
|
|
603
|
+
)
|
|
604
|
+
self.control_map = {v: k for k, v in self.control_implementation_id_map.items()}
|
|
605
|
+
self.existing_issue_ids_by_implementation_map = regscale_models.Issue.get_open_issues_ids_by_implementation_id(
|
|
606
|
+
plan_id=plan_id
|
|
607
|
+
) # GraphQL Call
|
|
608
|
+
self.control_id_to_implementation_map = regscale_models.ControlImplementation.get_control_id_map_by_plan(
|
|
609
|
+
plan_id=plan_id
|
|
610
|
+
)
|
|
611
|
+
self.cci_to_control_map: ThreadSafeDict[str, set[int]] = ThreadSafeDict()
|
|
612
|
+
self._no_ccis: bool = False
|
|
613
|
+
self.cci_to_control_map_lock: threading.Lock = threading.Lock()
|
|
614
|
+
|
|
615
|
+
self.assessment_map: ThreadSafeDict[int, regscale_models.Assessment] = ThreadSafeDict()
|
|
616
|
+
self.assessor_id: str = self.get_assessor_id()
|
|
617
|
+
self.asset_progress: Progress = create_progress_object()
|
|
618
|
+
self.finding_progress: Progress = create_progress_object()
|
|
619
|
+
self.stig_mapper = self.load_stig_mapper()
|
|
620
|
+
kev_data = pull_cisa_kev()
|
|
621
|
+
thread_safe_kev_data = ThreadSafeDict()
|
|
622
|
+
thread_safe_kev_data.update(kev_data)
|
|
623
|
+
self._kev_data = thread_safe_kev_data
|
|
624
|
+
|
|
625
|
+
@classmethod
|
|
626
|
+
def _get_lock(cls, key: str) -> threading.RLock:
|
|
627
|
+
"""
|
|
628
|
+
Get or create a lock associated with a key.
|
|
629
|
+
|
|
630
|
+
:param str key: The cache key
|
|
631
|
+
:return: A reentrant lock
|
|
632
|
+
:rtype: RLock
|
|
633
|
+
"""
|
|
634
|
+
lock = cls._lock_registry.get(key)
|
|
635
|
+
if lock is None:
|
|
636
|
+
with cls._global_lock: # Use a class-level lock to ensure thread safety
|
|
637
|
+
lock = cls._lock_registry.get(key)
|
|
638
|
+
if lock is None:
|
|
639
|
+
lock = threading.RLock()
|
|
640
|
+
cls._lock_registry[key] = lock
|
|
641
|
+
return lock
|
|
642
|
+
|
|
643
|
+
@staticmethod
|
|
644
|
+
def load_stig_mapper() -> Optional[STIGMapper]:
|
|
645
|
+
"""
|
|
646
|
+
Load the STIG Mapper file
|
|
647
|
+
|
|
648
|
+
:return: None
|
|
649
|
+
"""
|
|
650
|
+
from os import path
|
|
651
|
+
|
|
652
|
+
stig_mapper_file = ScannerVariables.stigMapperFile
|
|
653
|
+
if not path.exists(stig_mapper_file):
|
|
654
|
+
return None
|
|
655
|
+
try:
|
|
656
|
+
stig_mapper = STIGMapper(json_file=stig_mapper_file)
|
|
657
|
+
return stig_mapper
|
|
658
|
+
except Exception as e:
|
|
659
|
+
logger.debug(f"Warning Unable to loading STIG Mapper file: {e}")
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
@staticmethod
|
|
663
|
+
def get_assessor_id() -> str:
|
|
664
|
+
"""
|
|
665
|
+
Gets the ID of the assessor
|
|
666
|
+
|
|
667
|
+
:return: The ID of the assessor
|
|
668
|
+
:rtype: str
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
return regscale_models.Issue.get_user_id()
|
|
672
|
+
|
|
673
|
+
def get_cci_to_control_map(self) -> ThreadSafeDict[str, set[int]] | dict:
|
|
674
|
+
"""
|
|
675
|
+
Gets the CCI to control map
|
|
676
|
+
|
|
677
|
+
:return: The CCI to control map
|
|
678
|
+
:rtype: ThreadSafeDict[str, set[int]] | dict
|
|
679
|
+
"""
|
|
680
|
+
if self._no_ccis:
|
|
681
|
+
return self.cci_to_control_map
|
|
682
|
+
with self.cci_to_control_map_lock:
|
|
683
|
+
if any(self.cci_to_control_map):
|
|
684
|
+
return self.cci_to_control_map
|
|
685
|
+
logger.info("Getting CCI to control map...")
|
|
686
|
+
self.cci_to_control_map = regscale_models.map_ccis_to_control_ids(parent_id=self.plan_id) # type: ignore
|
|
687
|
+
if not any(self.cci_to_control_map):
|
|
688
|
+
self._no_ccis = True
|
|
689
|
+
return self.cci_to_control_map
|
|
690
|
+
|
|
691
|
+
def get_control_to_cci_map(self) -> dict[int, set[str]]:
|
|
692
|
+
"""
|
|
693
|
+
Gets the security control id to CCI map
|
|
694
|
+
|
|
695
|
+
:return: The security control id to CCI map
|
|
696
|
+
:rtype: dict[int, set[str]]
|
|
697
|
+
"""
|
|
698
|
+
control_id_to_cci_map = defaultdict(set)
|
|
699
|
+
for cci, control_ids in self.get_cci_to_control_map().items():
|
|
700
|
+
for control_id in control_ids:
|
|
701
|
+
control_id_to_cci_map[control_id].add(cci)
|
|
702
|
+
return control_id_to_cci_map
|
|
703
|
+
|
|
704
|
+
def get_control_implementation_id_for_cci(self, cci: Optional[str]) -> Optional[int]:
|
|
705
|
+
"""
|
|
706
|
+
Gets the control implementation ID for a CCI
|
|
707
|
+
|
|
708
|
+
:param Optional[str] cci: The CCI
|
|
709
|
+
:return: The control ID
|
|
710
|
+
:rtype: Optional[int]
|
|
711
|
+
"""
|
|
712
|
+
if not cci:
|
|
713
|
+
return None
|
|
714
|
+
|
|
715
|
+
cci_to_control_map = self.get_cci_to_control_map()
|
|
716
|
+
if cci not in cci_to_control_map:
|
|
717
|
+
cci = "CCI-000366"
|
|
718
|
+
|
|
719
|
+
if control_ids := cci_to_control_map.get(cci, set()):
|
|
720
|
+
for control_id in control_ids:
|
|
721
|
+
return self.control_id_to_implementation_map.get(control_id)
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
def get_asset_map(self) -> dict[str, regscale_models.Asset]:
|
|
725
|
+
"""
|
|
726
|
+
Retrieves a mapping of asset identifiers to their corresponding Asset objects. This method supports two modes
|
|
727
|
+
of operation based on the `options_map_assets_to_components` flag. If the flag is set, it fetches the asset
|
|
728
|
+
map using a specified key field from the assets associated with the given plan ID. Otherwise, it constructs
|
|
729
|
+
the map by fetching all assets under the specified plan and using the asset identifier field as the key.
|
|
730
|
+
|
|
731
|
+
:return: A dictionary mapping asset identifiers to Asset objects.
|
|
732
|
+
:rtype: dict[str, regscale_models.Asset]
|
|
733
|
+
"""
|
|
734
|
+
if self.options_map_assets_to_components:
|
|
735
|
+
# Fetches the asset map directly using a specified key field.
|
|
736
|
+
return regscale_models.Asset.get_map(plan_id=self.plan_id, key_field=self.asset_identifier_field)
|
|
737
|
+
else:
|
|
738
|
+
# Constructs the asset map by fetching all assets under the plan and using the asset identifier field as
|
|
739
|
+
# the key.
|
|
740
|
+
return { # type: ignore
|
|
741
|
+
getattr(x, self.asset_identifier_field): x
|
|
742
|
+
for x in regscale_models.Asset.get_all_by_parent(
|
|
743
|
+
parent_id=self.plan_id,
|
|
744
|
+
parent_module=regscale_models.SecurityPlan.get_module_string(),
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
@abstractmethod
|
|
749
|
+
def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
|
|
750
|
+
"""
|
|
751
|
+
Fetches findings from the integration
|
|
752
|
+
|
|
753
|
+
:return: A list of findings
|
|
754
|
+
:rtype: List[IntegrationFinding]
|
|
755
|
+
"""
|
|
756
|
+
|
|
757
|
+
@abstractmethod
|
|
758
|
+
def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
|
|
759
|
+
"""
|
|
760
|
+
Fetches assets from the integration
|
|
761
|
+
|
|
762
|
+
:return: An iterator of assets
|
|
763
|
+
:rtype: Iterator[IntegrationAsset]
|
|
764
|
+
"""
|
|
765
|
+
|
|
766
|
+
def get_finding_status(self, status: Optional[str]) -> regscale_models.IssueStatus:
|
|
767
|
+
"""
|
|
768
|
+
Gets the RegScale issue status based on the integration finding status
|
|
769
|
+
|
|
770
|
+
:param Optional[str] status: The status of the finding
|
|
771
|
+
:return: The RegScale issue status
|
|
772
|
+
:rtype: regscale_models.IssueStatus
|
|
773
|
+
"""
|
|
774
|
+
return self.finding_status_map.get(status, regscale_models.IssueStatus.Open)
|
|
775
|
+
|
|
776
|
+
def get_checklist_status(self, status: Optional[str]) -> regscale_models.ChecklistStatus:
|
|
777
|
+
"""
|
|
778
|
+
Gets the RegScale checklist status based on the integration finding status
|
|
779
|
+
|
|
780
|
+
:param Optional[str] status: The status of the finding
|
|
781
|
+
:return: The RegScale checklist status
|
|
782
|
+
:rtype: regscale_models.ChecklistStatus
|
|
783
|
+
"""
|
|
784
|
+
return self.checklist_status_map.get(status, regscale_models.ChecklistStatus.NOT_REVIEWED)
|
|
785
|
+
|
|
786
|
+
def get_finding_severity(self, severity: Optional[str]) -> regscale_models.IssueSeverity:
|
|
787
|
+
"""
|
|
788
|
+
Gets the RegScale issue severity based on the integration finding severity
|
|
789
|
+
|
|
790
|
+
:param Optional[str] severity: The severity of the finding
|
|
791
|
+
:return: The RegScale issue severity
|
|
792
|
+
:rtype: regscale_models.IssueSeverity
|
|
793
|
+
"""
|
|
794
|
+
return self.finding_severity_map.get(severity, regscale_models.IssueSeverity.NotAssigned)
|
|
795
|
+
|
|
796
|
+
def get_finding_identifier(self, finding: IntegrationFinding) -> str:
|
|
797
|
+
"""
|
|
798
|
+
Gets the finding identifier for the finding
|
|
799
|
+
|
|
800
|
+
:param IntegrationFinding finding: The finding
|
|
801
|
+
:return: The finding identifier
|
|
802
|
+
:rtype: str
|
|
803
|
+
"""
|
|
804
|
+
prefix = f"{self.plan_id}:"
|
|
805
|
+
if ScannerVariables.tenableGroupByPlugin and finding.plugin_id:
|
|
806
|
+
return f"{prefix}{finding.plugin_id}"
|
|
807
|
+
prefix += finding.cve or finding.plugin_id or finding.rule_id or self.hash_string(finding.external_id).__str__()
|
|
808
|
+
if ScannerVariables.issueCreation.lower() == "perasset":
|
|
809
|
+
return f"{prefix}:{finding.asset_identifier}"
|
|
810
|
+
return prefix
|
|
811
|
+
|
|
812
|
+
def get_or_create_assessment(self, control_implementation_id: int) -> regscale_models.Assessment:
|
|
813
|
+
"""
|
|
814
|
+
Gets or creates a RegScale assessment
|
|
815
|
+
|
|
816
|
+
:param int control_implementation_id: The ID of the control implementation
|
|
817
|
+
:return: The assessment
|
|
818
|
+
:rtype: regscale_models.Assessment
|
|
819
|
+
"""
|
|
820
|
+
logger.info("Getting or create assessment for control implementation %d", control_implementation_id)
|
|
821
|
+
assessment: Optional[regscale_models.Assessment] = self.assessment_map.get(control_implementation_id)
|
|
822
|
+
if assessment:
|
|
823
|
+
logger.debug(
|
|
824
|
+
"Found cached assessment %s for control implementation %s", assessment.id, control_implementation_id
|
|
825
|
+
)
|
|
826
|
+
else:
|
|
827
|
+
logger.debug("Assessment not found for control implementation %d", control_implementation_id)
|
|
828
|
+
assessment = regscale_models.Assessment(
|
|
829
|
+
plannedStart=get_current_datetime(),
|
|
830
|
+
plannedFinish=get_current_datetime(),
|
|
831
|
+
status=regscale_models.AssessmentStatus.COMPLETE.value,
|
|
832
|
+
assessmentResult=regscale_models.AssessmentResultsStatus.FAIL.value,
|
|
833
|
+
actualFinish=get_current_datetime(),
|
|
834
|
+
leadAssessorId=self.assessor_id,
|
|
835
|
+
parentId=control_implementation_id,
|
|
836
|
+
parentModule=regscale_models.ControlImplementation.get_module_string(),
|
|
837
|
+
title=f"{self.title} Assessment",
|
|
838
|
+
assessmentType=regscale_models.AssessmentType.QA_SURVEILLANCE.value,
|
|
839
|
+
).create()
|
|
840
|
+
self.assessment_map[control_implementation_id] = assessment
|
|
841
|
+
return assessment
|
|
842
|
+
|
|
843
|
+
def get_components(self) -> ThreadSafeList[regscale_models.Component]:
|
|
844
|
+
"""
|
|
845
|
+
Get all components from the integration
|
|
846
|
+
|
|
847
|
+
:return: A list of components
|
|
848
|
+
:rtype: ThreadSafeList[regscale_models.Component]
|
|
849
|
+
"""
|
|
850
|
+
if any(self.components):
|
|
851
|
+
return self.components
|
|
852
|
+
components: List[regscale_models.Component] = regscale_models.Component.get_all_by_parent(
|
|
853
|
+
parent_id=self.plan_id,
|
|
854
|
+
parent_module=regscale_models.SecurityPlan.get_module_string(),
|
|
855
|
+
)
|
|
856
|
+
self.components = ThreadSafeList(components)
|
|
857
|
+
return self.components
|
|
858
|
+
|
|
859
|
+
def get_component_by_title(self) -> dict:
|
|
860
|
+
"""
|
|
861
|
+
Get all components from the integration
|
|
862
|
+
|
|
863
|
+
:return: A dictionary of components
|
|
864
|
+
:rtype: dict
|
|
865
|
+
"""
|
|
866
|
+
return {component.title: component for component in self.get_components()}
|
|
867
|
+
|
|
868
|
+
# Asset Methods
|
|
869
|
+
def set_asset_defaults(self, asset: IntegrationAsset) -> IntegrationAsset:
|
|
870
|
+
"""
|
|
871
|
+
Set default values for the asset (Thread Safe)
|
|
872
|
+
|
|
873
|
+
:param IntegrationAsset asset: The integration asset
|
|
874
|
+
:return: The asset with which defaults should be set
|
|
875
|
+
:rtype: IntegrationAsset
|
|
876
|
+
"""
|
|
877
|
+
if not asset.asset_owner_id:
|
|
878
|
+
asset.asset_owner_id = self.get_assessor_id()
|
|
879
|
+
if not asset.status:
|
|
880
|
+
asset.status = regscale_models.AssetStatus.Active
|
|
881
|
+
return asset
|
|
882
|
+
|
|
883
|
+
def process_asset(
|
|
884
|
+
self,
|
|
885
|
+
asset: IntegrationAsset,
|
|
886
|
+
loading_assets: TaskID,
|
|
887
|
+
) -> None:
|
|
888
|
+
"""
|
|
889
|
+
Safely processes a single asset in a concurrent environment. This method ensures thread safety
|
|
890
|
+
by utilizing a threading lock. It assigns default values to the asset if necessary, maps the asset
|
|
891
|
+
to components if specified, and updates the progress of asset loading.
|
|
892
|
+
(Thread Safe)
|
|
893
|
+
|
|
894
|
+
:param IntegrationAsset asset: The integration asset to be processed.
|
|
895
|
+
:param TaskID loading_assets: The identifier for the task tracking the progress of asset loading.
|
|
896
|
+
:rtype: None
|
|
897
|
+
"""
|
|
898
|
+
|
|
899
|
+
# Assign default values to the asset if they are not already set.
|
|
900
|
+
asset = self.set_asset_defaults(asset)
|
|
901
|
+
|
|
902
|
+
# If mapping assets to components is enabled and the asset has associated component names,
|
|
903
|
+
# attempt to update or create each asset under its respective component.
|
|
904
|
+
if any(asset.component_names):
|
|
905
|
+
for component_name in asset.component_names:
|
|
906
|
+
self.update_or_create_asset(asset, component_name)
|
|
907
|
+
else:
|
|
908
|
+
# If no component mapping is required, add the asset directly to the security plan without a component.
|
|
909
|
+
self.update_or_create_asset(asset, None)
|
|
910
|
+
|
|
911
|
+
if self.num_assets_to_process and self.asset_progress.tasks[loading_assets].total != float(
|
|
912
|
+
self.num_assets_to_process
|
|
913
|
+
):
|
|
914
|
+
self.asset_progress.update(
|
|
915
|
+
loading_assets,
|
|
916
|
+
total=self.num_assets_to_process,
|
|
917
|
+
description=f"[#f8b737]Creating and updating {self.num_assets_to_process} assets from {self.title}.",
|
|
918
|
+
)
|
|
919
|
+
self.asset_progress.advance(loading_assets, 1)
|
|
920
|
+
|
|
921
|
+
def update_or_create_asset(
|
|
922
|
+
self,
|
|
923
|
+
asset: IntegrationAsset,
|
|
924
|
+
component_name: Optional[str] = None,
|
|
925
|
+
) -> None:
|
|
926
|
+
"""
|
|
927
|
+
Update or create an asset in RegScale.
|
|
928
|
+
|
|
929
|
+
This method either updates an existing asset or creates a new one within a thread-safe manner. It handles
|
|
930
|
+
the asset's association with a component, creating the component if it does not exist.
|
|
931
|
+
(Thread Safe)
|
|
932
|
+
|
|
933
|
+
:param IntegrationAsset asset: The asset to be updated or created.
|
|
934
|
+
:param Optional[str] component_name: The name of the component to associate the asset with. If None, the asset
|
|
935
|
+
is added directly to the security plan without a component association.
|
|
936
|
+
"""
|
|
937
|
+
# Continue with normal asset creation/update
|
|
938
|
+
if not asset.identifier:
|
|
939
|
+
logger.warning("Asset has no identifier, skipping")
|
|
940
|
+
return
|
|
941
|
+
|
|
942
|
+
component = None
|
|
943
|
+
if component_name:
|
|
944
|
+
logger.debug("Searching for component: %s...", component_name)
|
|
945
|
+
component = self.components_by_title.get(component_name)
|
|
946
|
+
if not component:
|
|
947
|
+
logger.debug("No existing component found with name %s, proceeding to create it...", component_name)
|
|
948
|
+
component = regscale_models.Component(
|
|
949
|
+
title=component_name,
|
|
950
|
+
componentType=asset.component_type,
|
|
951
|
+
securityPlansId=self.plan_id,
|
|
952
|
+
description=component_name,
|
|
953
|
+
componentOwnerId=self.get_assessor_id(),
|
|
954
|
+
).get_or_create()
|
|
955
|
+
self.components.append(component)
|
|
956
|
+
if component.securityPlansId:
|
|
957
|
+
component_mapping = regscale_models.ComponentMapping(
|
|
958
|
+
componentId=component.id,
|
|
959
|
+
securityPlanId=self.plan_id,
|
|
960
|
+
)
|
|
961
|
+
component_mapping.get_or_create()
|
|
962
|
+
self.components_by_title[component_name] = component
|
|
963
|
+
|
|
964
|
+
created, existing_or_new_asset = self.create_new_asset(asset, component=None)
|
|
965
|
+
|
|
966
|
+
# If the asset is associated with a component, create a mapping between them.
|
|
967
|
+
if existing_or_new_asset and component:
|
|
968
|
+
_was_created, _asset_mapping = regscale_models.AssetMapping(
|
|
969
|
+
assetId=existing_or_new_asset.id,
|
|
970
|
+
componentId=component.id,
|
|
971
|
+
).get_or_create_with_status()
|
|
972
|
+
|
|
973
|
+
if created and DuroSuiteVariables.duroSuiteEnabled:
|
|
974
|
+
# Check if this is a DuroSuite compatible asset
|
|
975
|
+
scan_durosuite_devices(asset=asset, plan_id=self.plan_id, progress=self.asset_progress)
|
|
976
|
+
|
|
977
|
+
def create_new_asset(
|
|
978
|
+
self, asset: IntegrationAsset, component: Optional[regscale_models.Component]
|
|
979
|
+
) -> tuple[bool, Optional[regscale_models.Asset]]:
|
|
980
|
+
"""
|
|
981
|
+
Creates a new asset in the system based on the provided integration asset details.
|
|
982
|
+
Associates the asset with a component or directly with the security plan.
|
|
983
|
+
|
|
984
|
+
:param IntegrationAsset asset: The integration asset from which the new asset will be created.
|
|
985
|
+
:param Optional[regscale_models.Component] component: The component to link the asset to, or None.
|
|
986
|
+
:return: Tuple of (was_created, newly created asset instance).
|
|
987
|
+
:rtype: tuple[bool, Optional[regscale_models.Asset]]
|
|
988
|
+
"""
|
|
989
|
+
# Ensure the asset has a name
|
|
990
|
+
if not asset.name:
|
|
991
|
+
logger.warning(
|
|
992
|
+
"Asset name is required for asset creation. Skipping asset creation of asset_type: %s", asset.asset_type
|
|
993
|
+
)
|
|
994
|
+
return False, None
|
|
995
|
+
|
|
996
|
+
new_asset = regscale_models.Asset(
|
|
997
|
+
name=asset.name,
|
|
998
|
+
description=asset.description,
|
|
999
|
+
bVirtual=asset.is_virtual,
|
|
1000
|
+
otherTrackingNumber=asset.other_tracking_number or asset.identifier,
|
|
1001
|
+
assetOwnerId=asset.asset_owner_id or "Unknown",
|
|
1002
|
+
parentId=component.id if component else self.plan_id,
|
|
1003
|
+
parentModule=(
|
|
1004
|
+
regscale_models.Component.get_module_string()
|
|
1005
|
+
if component
|
|
1006
|
+
else regscale_models.SecurityPlan.get_module_string()
|
|
1007
|
+
),
|
|
1008
|
+
assetType=asset.asset_type,
|
|
1009
|
+
dateLastUpdated=asset.date_last_updated or get_current_datetime(),
|
|
1010
|
+
status=asset.status,
|
|
1011
|
+
assetCategory=asset.asset_category,
|
|
1012
|
+
managementType=asset.management_type,
|
|
1013
|
+
notes=asset.notes,
|
|
1014
|
+
model=asset.model,
|
|
1015
|
+
manufacturer=asset.manufacturer,
|
|
1016
|
+
serialNumber=asset.serial_number,
|
|
1017
|
+
assetTagNumber=asset.asset_tag_number,
|
|
1018
|
+
bPublicFacing=asset.is_public_facing,
|
|
1019
|
+
azureIdentifier=asset.azure_identifier,
|
|
1020
|
+
location=asset.location,
|
|
1021
|
+
ipAddress=asset.ip_address,
|
|
1022
|
+
fqdn=asset.fqdn,
|
|
1023
|
+
macAddress=asset.mac_address,
|
|
1024
|
+
diskStorage=asset.disk_storage,
|
|
1025
|
+
cpu=asset.cpu,
|
|
1026
|
+
ram=asset.ram or 0,
|
|
1027
|
+
operatingSystem=asset.operating_system,
|
|
1028
|
+
osVersion=asset.os_version,
|
|
1029
|
+
endOfLifeDate=asset.end_of_life_date,
|
|
1030
|
+
vlanId=asset.vlan_id,
|
|
1031
|
+
uri=asset.uri,
|
|
1032
|
+
awsIdentifier=asset.aws_identifier,
|
|
1033
|
+
googleIdentifier=asset.google_identifier,
|
|
1034
|
+
otherCloudIdentifier=asset.other_cloud_identifier,
|
|
1035
|
+
patchLevel=asset.patch_level,
|
|
1036
|
+
cpe=asset.cpe,
|
|
1037
|
+
softwareVersion=asset.software_version,
|
|
1038
|
+
softwareName=asset.software_name,
|
|
1039
|
+
softwareVendor=asset.software_vendor,
|
|
1040
|
+
)
|
|
1041
|
+
if self.asset_identifier_field:
|
|
1042
|
+
setattr(new_asset, self.asset_identifier_field, asset.identifier)
|
|
1043
|
+
|
|
1044
|
+
created, new_asset = new_asset.create_or_update_with_status(bulk_update=True)
|
|
1045
|
+
# add to asset_map_by_identifier
|
|
1046
|
+
self.asset_map_by_identifier[asset.identifier] = new_asset
|
|
1047
|
+
logger.debug("Created new asset with identifier %s", asset.identifier)
|
|
1048
|
+
|
|
1049
|
+
self.handle_software_inventory(new_asset, asset.software_inventory, created)
|
|
1050
|
+
self.create_asset_data_and_link(new_asset, asset)
|
|
1051
|
+
self.create_or_update_ports_protocol(new_asset, asset)
|
|
1052
|
+
if self.stig_mapper:
|
|
1053
|
+
self.stig_mapper.map_associated_stigs_to_asset(asset=new_asset, ssp_id=self.plan_id)
|
|
1054
|
+
return created, new_asset
|
|
1055
|
+
|
|
1056
|
+
def handle_software_inventory(
|
|
1057
|
+
self, new_asset: regscale_models.Asset, software_inventory: List[Dict[str, Any]], created: bool
|
|
1058
|
+
) -> None:
|
|
1059
|
+
"""
|
|
1060
|
+
Handles the software inventory for the asset.
|
|
1061
|
+
|
|
1062
|
+
:param regscale_models.Asset new_asset: The newly created asset
|
|
1063
|
+
:param List[Dict[str, Any]] software_inventory: List of software inventory items
|
|
1064
|
+
:param bool created: Flag indicating if the asset was newly created
|
|
1065
|
+
:rtype: None
|
|
1066
|
+
"""
|
|
1067
|
+
if not software_inventory:
|
|
1068
|
+
return
|
|
1069
|
+
|
|
1070
|
+
existing_software: list[regscale_models.SoftwareInventory] = (
|
|
1071
|
+
[]
|
|
1072
|
+
if created
|
|
1073
|
+
else regscale_models.SoftwareInventory.get_all_by_parent(
|
|
1074
|
+
parent_id=new_asset.id,
|
|
1075
|
+
parent_module=None,
|
|
1076
|
+
)
|
|
1077
|
+
)
|
|
1078
|
+
existing_software_dict = {(s.name, s.version): s for s in existing_software}
|
|
1079
|
+
software_in_scan = set()
|
|
1080
|
+
|
|
1081
|
+
for software in software_inventory:
|
|
1082
|
+
software_name = software.get("name")
|
|
1083
|
+
if not software_name:
|
|
1084
|
+
logger.error("Software name is required for software inventory")
|
|
1085
|
+
continue
|
|
1086
|
+
|
|
1087
|
+
software_version = software.get("version")
|
|
1088
|
+
software_in_scan.add((software_name, software_version))
|
|
1089
|
+
|
|
1090
|
+
if (software_name, software_version) not in existing_software_dict:
|
|
1091
|
+
self.software_to_create.append(
|
|
1092
|
+
regscale_models.SoftwareInventory(
|
|
1093
|
+
name=software_name,
|
|
1094
|
+
parentHardwareAssetId=new_asset.id,
|
|
1095
|
+
version=software_version,
|
|
1096
|
+
# references=software.get("references", []),
|
|
1097
|
+
)
|
|
1098
|
+
)
|
|
1099
|
+
else:
|
|
1100
|
+
self.software_to_update.append(existing_software_dict[(software_name, software_version)])
|
|
1101
|
+
|
|
1102
|
+
# Remove software that is no longer in the scan
|
|
1103
|
+
for software_key, software_obj in existing_software_dict.items():
|
|
1104
|
+
if software_key not in software_in_scan:
|
|
1105
|
+
software_obj.delete()
|
|
1106
|
+
|
|
1107
|
+
def create_asset_data_and_link(self, asset: regscale_models.Asset, integration_asset: IntegrationAsset) -> None:
|
|
1108
|
+
"""
|
|
1109
|
+
Creates Data and Link objects for the given asset.
|
|
1110
|
+
|
|
1111
|
+
:param regscale_models.Asset asset: The asset to create Data and Link for
|
|
1112
|
+
:param IntegrationAsset integration_asset: The integration asset containing source data and URL
|
|
1113
|
+
:rtype: None
|
|
1114
|
+
"""
|
|
1115
|
+
if integration_asset.source_data:
|
|
1116
|
+
# Optimization, create an api that gets the data by plan and parent module
|
|
1117
|
+
regscale_models.Data(
|
|
1118
|
+
parentId=asset.id,
|
|
1119
|
+
parentModule=asset.get_module_string(),
|
|
1120
|
+
dataSource=self.title,
|
|
1121
|
+
dataType=regscale_models.DataDataType.JSON.value,
|
|
1122
|
+
rawData=json.dumps(integration_asset.source_data, indent=2, cls=DateTimeEncoder),
|
|
1123
|
+
lastUpdatedById=integration_asset.asset_owner_id or "Unknown",
|
|
1124
|
+
createdById=integration_asset.asset_owner_id or "Unknown",
|
|
1125
|
+
).create_or_update(bulk_create=True, bulk_update=True)
|
|
1126
|
+
if integration_asset.url:
|
|
1127
|
+
link = regscale_models.Link(
|
|
1128
|
+
parentID=asset.id,
|
|
1129
|
+
parentModule=asset.get_module_string(),
|
|
1130
|
+
url=integration_asset.url,
|
|
1131
|
+
title="Asset Provider URL",
|
|
1132
|
+
)
|
|
1133
|
+
if link.find_by_unique():
|
|
1134
|
+
self.link_to_update.append(link)
|
|
1135
|
+
else:
|
|
1136
|
+
self.link_to_create.append(link)
|
|
1137
|
+
|
|
1138
|
+
@staticmethod
|
|
1139
|
+
def create_or_update_ports_protocol(asset: regscale_models.Asset, integration_asset: IntegrationAsset) -> None:
|
|
1140
|
+
"""
|
|
1141
|
+
Creates or updates PortsProtocol objects for the given asset.
|
|
1142
|
+
|
|
1143
|
+
:param regscale_models.Asset asset: The asset to create or update PortsProtocol for
|
|
1144
|
+
:param IntegrationAsset integration_asset: The integration asset containing ports and protocols information
|
|
1145
|
+
:rtype: None
|
|
1146
|
+
"""
|
|
1147
|
+
if integration_asset.ports_and_protocols:
|
|
1148
|
+
for port_protocol in integration_asset.ports_and_protocols:
|
|
1149
|
+
if not port_protocol.get("start_port") or not port_protocol.get("end_port"):
|
|
1150
|
+
logger.error("Invalid port protocol data: %s", port_protocol)
|
|
1151
|
+
continue
|
|
1152
|
+
regscale_models.PortsProtocol(
|
|
1153
|
+
parentId=asset.id,
|
|
1154
|
+
parentModule=asset.get_module_string(),
|
|
1155
|
+
startPort=port_protocol.get("start_port", 0),
|
|
1156
|
+
endPort=port_protocol.get("end_port", 0),
|
|
1157
|
+
service=port_protocol.get("service", asset.name),
|
|
1158
|
+
protocol=port_protocol.get("protocol"),
|
|
1159
|
+
purpose=port_protocol.get("purpose", f"Grant access to {asset.name}"),
|
|
1160
|
+
usedBy=asset.name,
|
|
1161
|
+
).create_or_update()
|
|
1162
|
+
|
|
1163
|
+
def update_regscale_assets(self, assets: Iterator[IntegrationAsset]) -> int:
|
|
1164
|
+
"""
|
|
1165
|
+
Updates RegScale assets based on the integration assets
|
|
1166
|
+
|
|
1167
|
+
:param Iterator[IntegrationAsset] assets: The integration assets
|
|
1168
|
+
:return: The number of assets processed
|
|
1169
|
+
:rtype: int
|
|
1170
|
+
"""
|
|
1171
|
+
logger.info("Updating RegScale assets...")
|
|
1172
|
+
loading_assets = self._setup_progress_bar()
|
|
1173
|
+
logger.info("Pre-populating cache")
|
|
1174
|
+
regscale_models.AssetMapping.populate_cache_by_plan(self.plan_id)
|
|
1175
|
+
regscale_models.ComponentMapping.populate_cache_by_plan(self.plan_id)
|
|
1176
|
+
|
|
1177
|
+
if self.options_map_assets_to_components:
|
|
1178
|
+
thread_safe_dict: ThreadSafeDict[str, regscale_models.Component] = ThreadSafeDict()
|
|
1179
|
+
thread_safe_dict.update(self.get_component_by_title())
|
|
1180
|
+
self.components_by_title = thread_safe_dict
|
|
1181
|
+
|
|
1182
|
+
assets_processed = self._process_assets(assets, loading_assets)
|
|
1183
|
+
|
|
1184
|
+
self._perform_batch_operations(self.asset_progress)
|
|
1185
|
+
|
|
1186
|
+
return assets_processed
|
|
1187
|
+
|
|
1188
|
+
def _setup_progress_bar(self) -> TaskID:
|
|
1189
|
+
"""
|
|
1190
|
+
Sets up the progress bar for asset processing.
|
|
1191
|
+
|
|
1192
|
+
:return: The task ID for the progress bar
|
|
1193
|
+
:rtype: TaskID
|
|
1194
|
+
"""
|
|
1195
|
+
asset_count = self.num_assets_to_process or None
|
|
1196
|
+
return self.asset_progress.add_task(
|
|
1197
|
+
f"[#f8b737]Creating and updating{f' {asset_count}' if asset_count else ''} asset(s) from {self.title}.",
|
|
1198
|
+
total=asset_count,
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
def _process_assets(self, assets: Iterator[IntegrationAsset], loading_assets: TaskID) -> int:
|
|
1202
|
+
"""
|
|
1203
|
+
Processes the assets using single or multi-threaded approach based on THREAD_MAX_WORKERS.
|
|
1204
|
+
|
|
1205
|
+
:param Iterator[IntegrationAsset] assets: The assets to process
|
|
1206
|
+
:param TaskID loading_assets: The task ID for the progress bar
|
|
1207
|
+
:return: The number of assets processed
|
|
1208
|
+
:rtype: int
|
|
1209
|
+
"""
|
|
1210
|
+
assets_processed = 0
|
|
1211
|
+
# prime cache
|
|
1212
|
+
regscale_models.Asset.get_all_by_parent(
|
|
1213
|
+
parent_id=self.plan_id, parent_module=regscale_models.SecurityPlan.get_module_string()
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
process_func = lambda my_asset: self._process_single_asset(my_asset, loading_assets) # noqa: E731
|
|
1217
|
+
|
|
1218
|
+
if get_thread_workers_max() == 1:
|
|
1219
|
+
for asset in assets:
|
|
1220
|
+
if process_func(asset):
|
|
1221
|
+
assets_processed = self._update_processed_count(assets_processed)
|
|
1222
|
+
else:
|
|
1223
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=get_thread_workers_max()) as executor:
|
|
1224
|
+
future_to_asset = {executor.submit(process_func, asset): asset for asset in assets}
|
|
1225
|
+
for future in concurrent.futures.as_completed(future_to_asset):
|
|
1226
|
+
if future.result():
|
|
1227
|
+
assets_processed = self._update_processed_count(assets_processed)
|
|
1228
|
+
|
|
1229
|
+
return assets_processed
|
|
1230
|
+
|
|
1231
|
+
def _process_single_asset(self, asset: IntegrationAsset, loading_assets: TaskID) -> bool:
|
|
1232
|
+
"""
|
|
1233
|
+
Processes a single asset and handles any exceptions.
|
|
1234
|
+
|
|
1235
|
+
:param IntegrationAsset asset: The asset to process
|
|
1236
|
+
:param TaskID loading_assets: The task ID for the progress bar
|
|
1237
|
+
:return: True if the asset was processed successfully, False otherwise
|
|
1238
|
+
:rtype: bool
|
|
1239
|
+
"""
|
|
1240
|
+
try:
|
|
1241
|
+
self.process_asset(asset, loading_assets)
|
|
1242
|
+
return True
|
|
1243
|
+
except Exception as exc:
|
|
1244
|
+
self.log_error("An error occurred when processing asset %s: %s", asset.name, exc)
|
|
1245
|
+
return False
|
|
1246
|
+
|
|
1247
|
+
@staticmethod
|
|
1248
|
+
def _update_processed_count(assets_processed: int) -> int:
|
|
1249
|
+
"""
|
|
1250
|
+
Updates and logs the count of processed assets.
|
|
1251
|
+
|
|
1252
|
+
:param int assets_processed: The current count of processed assets
|
|
1253
|
+
:return: The updated count of processed assets
|
|
1254
|
+
:rtype: int
|
|
1255
|
+
"""
|
|
1256
|
+
assets_processed += 1
|
|
1257
|
+
if assets_processed % 100 == 0:
|
|
1258
|
+
logger.info("Processed %d assets.", assets_processed)
|
|
1259
|
+
return assets_processed
|
|
1260
|
+
|
|
1261
|
+
def _perform_batch_operations(self, progress: Progress) -> None:
|
|
1262
|
+
"""
|
|
1263
|
+
Performs batch operations for assets, software inventory, and data.
|
|
1264
|
+
|
|
1265
|
+
:rtype: None
|
|
1266
|
+
"""
|
|
1267
|
+
logger.info("Bulk saving assets...")
|
|
1268
|
+
regscale_models.Asset.bulk_save(progress_context=progress)
|
|
1269
|
+
regscale_models.Issue.bulk_save(progress_context=progress)
|
|
1270
|
+
regscale_models.Property.bulk_save(progress_context=progress)
|
|
1271
|
+
|
|
1272
|
+
if self.software_to_create:
|
|
1273
|
+
regscale_models.SoftwareInventory.batch_create(items=self.software_to_create, progress_context=progress)
|
|
1274
|
+
if self.software_to_update:
|
|
1275
|
+
regscale_models.SoftwareInventory.batch_update(items=self.software_to_update, progress_context=progress)
|
|
1276
|
+
regscale_models.Data.bulk_save(progress_context=progress)
|
|
1277
|
+
|
|
1278
|
+
@staticmethod
|
|
1279
|
+
def get_issue_title(finding: IntegrationFinding) -> str:
|
|
1280
|
+
"""
|
|
1281
|
+
Gets the issue title based on the POAM Title Type variable.
|
|
1282
|
+
|
|
1283
|
+
:param IntegrationFinding finding: The finding data
|
|
1284
|
+
:return: The issue title
|
|
1285
|
+
:rtype: str
|
|
1286
|
+
"""
|
|
1287
|
+
issue_title = finding.title or ""
|
|
1288
|
+
if ScannerVariables.poamTitleType.lower() == "pluginid" or not issue_title:
|
|
1289
|
+
issue_title = (
|
|
1290
|
+
f"{finding.plugin_id or finding.cve or finding.rule_id}: {finding.plugin_name or finding.description}"
|
|
1291
|
+
)
|
|
1292
|
+
return issue_title[:450]
|
|
1293
|
+
|
|
1294
|
+
# Finding Methods
|
|
1295
|
+
def create_or_update_issue_from_finding(
|
|
1296
|
+
self,
|
|
1297
|
+
title: str,
|
|
1298
|
+
finding: IntegrationFinding,
|
|
1299
|
+
) -> regscale_models.Issue:
|
|
1300
|
+
"""
|
|
1301
|
+
Creates or updates a RegScale issue from a finding
|
|
1302
|
+
|
|
1303
|
+
:param str title: The title of the issue
|
|
1304
|
+
:param IntegrationFinding finding: The finding data
|
|
1305
|
+
:return: The created or updated RegScale issue
|
|
1306
|
+
:rtype: regscale_models.Issue
|
|
1307
|
+
"""
|
|
1308
|
+
issue_status = finding.get_issue_status()
|
|
1309
|
+
finding_id = self.get_finding_identifier(finding)
|
|
1310
|
+
finding_id_lock = self._get_lock(finding_id)
|
|
1311
|
+
|
|
1312
|
+
with finding_id_lock:
|
|
1313
|
+
if ScannerVariables.issueCreation.lower() != "perasset":
|
|
1314
|
+
# Check if we should consolidate open issues based on integrationFindingId
|
|
1315
|
+
if issue_status == regscale_models.IssueStatus.Open:
|
|
1316
|
+
existing_issues = regscale_models.Issue.find_by_integration_finding_id(finding_id)
|
|
1317
|
+
# Find an open issue to update
|
|
1318
|
+
issue = next(
|
|
1319
|
+
(issue for issue in existing_issues if issue.status != regscale_models.IssueStatus.Closed), None
|
|
1320
|
+
)
|
|
1321
|
+
if issue:
|
|
1322
|
+
return self._create_or_update_issue(finding, issue_status, title, issue)
|
|
1323
|
+
|
|
1324
|
+
# Check if we should consolidate closed issues based on integrationFindingId and issueDueDates
|
|
1325
|
+
elif issue_status == regscale_models.IssueStatus.Closed:
|
|
1326
|
+
existing_issues = regscale_models.Issue.find_by_integration_finding_id(finding_id)
|
|
1327
|
+
# Find a closed issue with matching due date to consolidate with
|
|
1328
|
+
matching_closed_issue = next(
|
|
1329
|
+
(
|
|
1330
|
+
issue
|
|
1331
|
+
for issue in existing_issues
|
|
1332
|
+
if issue.status == regscale_models.IssueStatus.Closed
|
|
1333
|
+
and date_str(issue.dueDate) == date_str(finding.due_date)
|
|
1334
|
+
),
|
|
1335
|
+
None,
|
|
1336
|
+
)
|
|
1337
|
+
if matching_closed_issue:
|
|
1338
|
+
return self._create_or_update_issue(finding, issue_status, title, matching_closed_issue)
|
|
1339
|
+
|
|
1340
|
+
return self._create_or_update_issue(finding, issue_status, title)
|
|
1341
|
+
|
|
1342
|
+
def _create_or_update_issue(
|
|
1343
|
+
self,
|
|
1344
|
+
finding: IntegrationFinding,
|
|
1345
|
+
issue_status: regscale_models.IssueStatus,
|
|
1346
|
+
title: str,
|
|
1347
|
+
existing_issue: Optional[regscale_models.Issue] = None,
|
|
1348
|
+
) -> regscale_models.Issue:
|
|
1349
|
+
"""
|
|
1350
|
+
Creates or updates a RegScale issue
|
|
1351
|
+
|
|
1352
|
+
:param IntegrationFinding finding: The finding data
|
|
1353
|
+
:param str issue_status: The status of the issue
|
|
1354
|
+
:param str title: The title of the issue
|
|
1355
|
+
:param Optional[regscale_models.Issue] existing_issue: Existing issue to update, if any
|
|
1356
|
+
:return: The created or updated RegScale issue
|
|
1357
|
+
:rtype: regscale_models.Issue
|
|
1358
|
+
"""
|
|
1359
|
+
# Prepare issue data
|
|
1360
|
+
issue_title = self.get_issue_title(finding) or title
|
|
1361
|
+
description = finding.description or ""
|
|
1362
|
+
remediation_description = finding.recommendation_for_mitigation or finding.remediation or ""
|
|
1363
|
+
is_poam = self.is_poam(finding)
|
|
1364
|
+
|
|
1365
|
+
if existing_issue:
|
|
1366
|
+
logger.debug(
|
|
1367
|
+
"Updating existing issue %s with assetIdentifier %s", existing_issue.id, finding.asset_identifier
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
# If we have an existing issue, update its fields instead of creating a new one
|
|
1371
|
+
issue = existing_issue or regscale_models.Issue()
|
|
1372
|
+
|
|
1373
|
+
# Get consolidated asset identifier
|
|
1374
|
+
asset_identifier = self.get_consolidated_asset_identifier(finding, existing_issue)
|
|
1375
|
+
|
|
1376
|
+
# Update all fields
|
|
1377
|
+
issue.parentId = self.plan_id
|
|
1378
|
+
issue.parentModule = regscale_models.SecurityPlan.get_module_string()
|
|
1379
|
+
issue.vulnerabilityId = finding.vulnerability_id
|
|
1380
|
+
issue.title = issue_title
|
|
1381
|
+
issue.dateCreated = finding.date_created
|
|
1382
|
+
issue.status = issue_status
|
|
1383
|
+
issue.dateCompleted = (
|
|
1384
|
+
self.get_date_completed(finding, issue_status)
|
|
1385
|
+
if issue_status == regscale_models.IssueStatus.Closed
|
|
1386
|
+
else None
|
|
1387
|
+
)
|
|
1388
|
+
issue.severityLevel = finding.severity
|
|
1389
|
+
issue.issueOwnerId = self.assessor_id
|
|
1390
|
+
issue.securityPlanId = self.plan_id
|
|
1391
|
+
issue.identification = "Vulnerability Assessment"
|
|
1392
|
+
issue.dateFirstDetected = finding.date_created
|
|
1393
|
+
issue.dueDate = finding.due_date
|
|
1394
|
+
issue.description = description
|
|
1395
|
+
issue.sourceReport = finding.source_report or self.title
|
|
1396
|
+
issue.recommendedActions = finding.recommendation_for_mitigation
|
|
1397
|
+
issue.assetIdentifier = asset_identifier
|
|
1398
|
+
issue.securityChecks = finding.security_check or finding.external_id
|
|
1399
|
+
issue.remediationDescription = remediation_description
|
|
1400
|
+
issue.integrationFindingId = self.get_finding_identifier(finding)
|
|
1401
|
+
issue.poamComments = finding.poam_comments
|
|
1402
|
+
issue.cve = finding.cve
|
|
1403
|
+
control_id = self.get_control_implementation_id_for_cci(finding.cci_ref) if finding.cci_ref else None
|
|
1404
|
+
issue.controlId = control_id # TODO REMOVE
|
|
1405
|
+
# Add the control implementation ids and the cci ref if it exists
|
|
1406
|
+
# Get control implementation ID for CCI if it exists
|
|
1407
|
+
# Only add CCI control ID if it exists
|
|
1408
|
+
cci_control_ids = [control_id] if control_id is not None else []
|
|
1409
|
+
|
|
1410
|
+
issue.controlImplementationIds = list(set(finding._control_implementation_ids + cci_control_ids)) # noqa
|
|
1411
|
+
issue.isPoam = is_poam
|
|
1412
|
+
issue.basisForAdjustment = (
|
|
1413
|
+
finding.basis_for_adjustment if finding.basis_for_adjustment else f"{self.title} import"
|
|
1414
|
+
)
|
|
1415
|
+
issue.pluginId = finding.plugin_id
|
|
1416
|
+
issue.originalRiskRating = regscale_models.Issue.assign_risk_rating(finding.severity)
|
|
1417
|
+
# Current: changes
|
|
1418
|
+
# Planned: planned changes
|
|
1419
|
+
issue.changes = "<p>Current: {}</p><p>Planned: {}</p>".format(
|
|
1420
|
+
finding.milestone_changes, finding.planned_milestone_changes
|
|
1421
|
+
)
|
|
1422
|
+
issue.adjustedRiskRating = finding.adjusted_risk_rating
|
|
1423
|
+
issue.riskAdjustment = finding.risk_adjustment
|
|
1424
|
+
issue.operationalRequirement = finding.operational_requirements
|
|
1425
|
+
issue.deviationRationale = finding.deviation_rationale
|
|
1426
|
+
|
|
1427
|
+
if finding.cve:
|
|
1428
|
+
issue = self.lookup_kev_and_upate_issue(cve=finding.cve, issue=issue, cisa_kevs=self._kev_data)
|
|
1429
|
+
|
|
1430
|
+
if existing_issue:
|
|
1431
|
+
logger.debug("Saving existing issue %s with assetIdentifier: %s", issue.id, issue.assetIdentifier)
|
|
1432
|
+
issue.save(bulk=True)
|
|
1433
|
+
else:
|
|
1434
|
+
issue = issue.create_or_update(
|
|
1435
|
+
bulk_update=True, defaults={"otherIdentifier": self._get_other_identifier(finding, is_poam)}
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
if poc := finding.point_of_contact:
|
|
1439
|
+
_ = regscale_models.Property(
|
|
1440
|
+
key="POC",
|
|
1441
|
+
value=poc,
|
|
1442
|
+
parentId=issue.id,
|
|
1443
|
+
parentModule="issues",
|
|
1444
|
+
).create_or_update(bulk_create=True, bulk_update=True)
|
|
1445
|
+
|
|
1446
|
+
return issue
|
|
1447
|
+
|
|
1448
|
+
@staticmethod
|
|
1449
|
+
def get_consolidated_asset_identifier(
|
|
1450
|
+
finding: IntegrationFinding,
|
|
1451
|
+
existing_issue: Optional[regscale_models.Issue] = None,
|
|
1452
|
+
) -> str:
|
|
1453
|
+
"""
|
|
1454
|
+
Gets the consolidated asset identifier, combining the finding's asset identifier
|
|
1455
|
+
with any existing asset identifiers from the issue.
|
|
1456
|
+
|
|
1457
|
+
:param IntegrationFinding finding: The finding data
|
|
1458
|
+
:param Optional[regscale_models.Issue] existing_issue: The existing issue to consolidate with, if any
|
|
1459
|
+
:return: The consolidated asset identifier
|
|
1460
|
+
:rtype: str
|
|
1461
|
+
"""
|
|
1462
|
+
delimiter = "\n"
|
|
1463
|
+
if not existing_issue or ScannerVariables.issueCreation.lower() == "perasset":
|
|
1464
|
+
return finding.asset_identifier
|
|
1465
|
+
|
|
1466
|
+
# Get existing asset identifiers
|
|
1467
|
+
existing_asset_identifiers = set((existing_issue.assetIdentifier or "").split(delimiter))
|
|
1468
|
+
if finding.asset_identifier not in existing_asset_identifiers:
|
|
1469
|
+
existing_asset_identifiers.add(finding.asset_identifier)
|
|
1470
|
+
|
|
1471
|
+
return delimiter.join(existing_asset_identifiers)
|
|
1472
|
+
|
|
1473
|
+
def _get_other_identifier(self, finding: IntegrationFinding, is_poam: bool) -> Optional[str]:
|
|
1474
|
+
"""
|
|
1475
|
+
Gets the other identifier for an issue
|
|
1476
|
+
|
|
1477
|
+
:param IntegrationFinding finding: The finding data
|
|
1478
|
+
:param bool is_poam: Whether this is a POAM issue
|
|
1479
|
+
:return: The other identifier if applicable
|
|
1480
|
+
:rtype: Optional[str]
|
|
1481
|
+
"""
|
|
1482
|
+
# If existing POAM ID is greater than the cached max, update the cached max
|
|
1483
|
+
if finding.poam_id:
|
|
1484
|
+
if (poam_id := self.parse_poam_id(finding.poam_id)) and poam_id > (self._max_poam_id or 0):
|
|
1485
|
+
self._max_poam_id = poam_id
|
|
1486
|
+
return finding.poam_id
|
|
1487
|
+
|
|
1488
|
+
# Only called if isPoam is True and creating a new issue
|
|
1489
|
+
if is_poam and ScannerVariables.incrementPoamIdentifier:
|
|
1490
|
+
return f"V-{self.get_next_poam_id():04d}"
|
|
1491
|
+
return None
|
|
1492
|
+
|
|
1493
|
+
@staticmethod
|
|
1494
|
+
def lookup_kev_and_upate_issue(
|
|
1495
|
+
cve: str, issue: regscale_models.Issue, cisa_kevs: Optional[ThreadSafeDict[str, Any]] = None
|
|
1496
|
+
) -> regscale_models.Issue:
|
|
1497
|
+
"""
|
|
1498
|
+
Determine if the cve is part of the published CISA KEV list
|
|
1499
|
+
|
|
1500
|
+
:param str cve: The CVE to lookup in CISAs KEV list
|
|
1501
|
+
:param regscale_models.Issue issue: The issue to update kevList field and dueDate if found in KEV List
|
|
1502
|
+
:param Optional[ThreadSafeDict[str, Any]] cisa_kevs: The CISA KEV data to search the findings
|
|
1503
|
+
:return: The updated issue
|
|
1504
|
+
:rtype: regscale_models.Issue
|
|
1505
|
+
"""
|
|
1506
|
+
from datetime import datetime
|
|
1507
|
+
|
|
1508
|
+
from regscale.core.app.utils.app_utils import convert_datetime_to_regscale_string
|
|
1509
|
+
|
|
1510
|
+
issue.kevList = "No"
|
|
1511
|
+
|
|
1512
|
+
if cisa_kevs:
|
|
1513
|
+
kev_data = next(
|
|
1514
|
+
(
|
|
1515
|
+
entry
|
|
1516
|
+
for entry in cisa_kevs.get("vulnerabilities", [])
|
|
1517
|
+
if entry.get("cveID", "").lower() == cve.lower()
|
|
1518
|
+
),
|
|
1519
|
+
None,
|
|
1520
|
+
)
|
|
1521
|
+
if kev_data:
|
|
1522
|
+
issue.dueDate = convert_datetime_to_regscale_string(datetime.strptime(kev_data["dueDate"], "%Y-%m-%d"))
|
|
1523
|
+
issue.kevList = "Yes"
|
|
1524
|
+
|
|
1525
|
+
return issue
|
|
1526
|
+
|
|
1527
|
+
@staticmethod
|
|
1528
|
+
def group_by_plugin(existing_issue: regscale_models.Issue, finding: IntegrationFinding) -> regscale_models.Issue:
|
|
1529
|
+
"""
|
|
1530
|
+
Merges the CVEs for the issue if the group by plugin is enabled
|
|
1531
|
+
|
|
1532
|
+
:param regscale_models.Issue regscale_models.Issue existing_issue: The existing issue
|
|
1533
|
+
:param IntegrationFinding finding: The finding data
|
|
1534
|
+
:return: The existing issue
|
|
1535
|
+
:rtype: regscale_models.Issue
|
|
1536
|
+
"""
|
|
1537
|
+
if ScannerVariables.tenableGroupByPlugin and finding.cve:
|
|
1538
|
+
# consolidate cve, but only for this switch
|
|
1539
|
+
existing_cves = (existing_issue.cve or "").split(",")
|
|
1540
|
+
existing_issue.cve = ",".join(set(existing_cves + [finding.cve]))
|
|
1541
|
+
return existing_issue
|
|
1542
|
+
|
|
1543
|
+
@staticmethod
|
|
1544
|
+
def is_poam(finding: IntegrationFinding) -> bool:
|
|
1545
|
+
"""
|
|
1546
|
+
Determines if an issue should be considered a Plan of Action and Milestones (POAM).
|
|
1547
|
+
|
|
1548
|
+
:param IntegrationFinding finding: The finding to check
|
|
1549
|
+
:return: True if the issue should be a POAM, False otherwise
|
|
1550
|
+
:rtype: bool
|
|
1551
|
+
"""
|
|
1552
|
+
if ScannerVariables.vulnerabilityCreation.lower() == "poamcreation":
|
|
1553
|
+
return True
|
|
1554
|
+
if finding.due_date < get_current_datetime():
|
|
1555
|
+
return True
|
|
1556
|
+
return False
|
|
1557
|
+
|
|
1558
|
+
def handle_failing_finding(
|
|
1559
|
+
self,
|
|
1560
|
+
issue_title: str,
|
|
1561
|
+
finding: IntegrationFinding,
|
|
1562
|
+
) -> None:
|
|
1563
|
+
"""
|
|
1564
|
+
Handles findings that have failed by creating or updating an issue.
|
|
1565
|
+
|
|
1566
|
+
:param str issue_title: The title of the issue
|
|
1567
|
+
:param IntegrationFinding finding: The finding data that has failed
|
|
1568
|
+
:rtype: None
|
|
1569
|
+
"""
|
|
1570
|
+
logger.debug("Creating issue for failing finding %s", finding.external_id)
|
|
1571
|
+
found_issue = self.create_or_update_issue_from_finding(
|
|
1572
|
+
title=issue_title,
|
|
1573
|
+
finding=finding,
|
|
1574
|
+
)
|
|
1575
|
+
# Update the control implementation status to NOT_IMPLEMENTED since we have a failing finding
|
|
1576
|
+
if found_issue.controlImplementationIds:
|
|
1577
|
+
for control_id in found_issue.controlImplementationIds:
|
|
1578
|
+
self.update_control_implementation_status_after_close(control_id)
|
|
1579
|
+
|
|
1580
|
+
def handle_failing_checklist(
|
|
1581
|
+
self,
|
|
1582
|
+
finding: IntegrationFinding,
|
|
1583
|
+
plan_id: int,
|
|
1584
|
+
) -> None:
|
|
1585
|
+
"""
|
|
1586
|
+
Handles failing checklists by creating or updating implementation options and objectives.
|
|
1587
|
+
|
|
1588
|
+
:param IntegrationFinding finding: The finding data
|
|
1589
|
+
:param int plan_id: The ID of the security plan
|
|
1590
|
+
:rtype: None
|
|
1591
|
+
"""
|
|
1592
|
+
if finding.cci_ref:
|
|
1593
|
+
failing_objectives = regscale_models.ControlObjective.fetch_control_objectives_by_other_id(
|
|
1594
|
+
parent_id=plan_id, other_id_contains=finding.cci_ref
|
|
1595
|
+
)
|
|
1596
|
+
failing_objectives += regscale_models.ControlObjective.fetch_control_objectives_by_name(
|
|
1597
|
+
parent_id=plan_id, name_contains=finding.cci_ref
|
|
1598
|
+
)
|
|
1599
|
+
for failing_objective in failing_objectives:
|
|
1600
|
+
if failing_objective.name.lower().startswith("cci-"):
|
|
1601
|
+
implementation_id = self.get_control_implementation_id_for_cci(failing_objective.name)
|
|
1602
|
+
else:
|
|
1603
|
+
control_label = objective_to_control_dot(failing_objective.name)
|
|
1604
|
+
if control_label not in self.control_implementation_id_map:
|
|
1605
|
+
logger.warning("Control %s not found for %s", control_label, control_label)
|
|
1606
|
+
continue
|
|
1607
|
+
implementation_id = self.control_implementation_id_map[control_label]
|
|
1608
|
+
|
|
1609
|
+
failing_option = regscale_models.ImplementationOption(
|
|
1610
|
+
name="Failed STIG",
|
|
1611
|
+
description="Failed STIG Security Checks",
|
|
1612
|
+
acceptability=regscale_models.ImplementationStatus.NOT_IMPLEMENTED,
|
|
1613
|
+
objectiveId=failing_objective.id,
|
|
1614
|
+
securityControlId=failing_objective.securityControlId,
|
|
1615
|
+
responsibility="Customer",
|
|
1616
|
+
).create_or_update()
|
|
1617
|
+
|
|
1618
|
+
_ = regscale_models.ImplementationObjective(
|
|
1619
|
+
securityControlId=failing_objective.securityControlId,
|
|
1620
|
+
implementationId=implementation_id,
|
|
1621
|
+
objectiveId=failing_objective.id,
|
|
1622
|
+
optionId=failing_option.id,
|
|
1623
|
+
status=regscale_models.ImplementationStatus.NOT_IMPLEMENTED,
|
|
1624
|
+
statement=failing_objective.description,
|
|
1625
|
+
responsibility="Customer",
|
|
1626
|
+
).create_or_update()
|
|
1627
|
+
|
|
1628
|
+
# Create assessment and control test result
|
|
1629
|
+
assessment = self.get_or_create_assessment(implementation_id)
|
|
1630
|
+
if implementation_id:
|
|
1631
|
+
control_test = self.create_or_get_control_test(finding, implementation_id)
|
|
1632
|
+
self.create_control_test_result(
|
|
1633
|
+
finding, control_test, assessment, regscale_models.ControlTestResultStatus.FAIL
|
|
1634
|
+
)
|
|
1635
|
+
|
|
1636
|
+
def handle_passing_checklist(
|
|
1637
|
+
self,
|
|
1638
|
+
finding: IntegrationFinding,
|
|
1639
|
+
plan_id: int,
|
|
1640
|
+
) -> None:
|
|
1641
|
+
"""
|
|
1642
|
+
Handles passing checklists by creating or updating implementation options and objectives.
|
|
1643
|
+
|
|
1644
|
+
:param IntegrationFinding finding: The finding data
|
|
1645
|
+
:param int plan_id: The ID of the security plan
|
|
1646
|
+
:rtype: None
|
|
1647
|
+
"""
|
|
1648
|
+
if finding.cci_ref:
|
|
1649
|
+
passing_objectives = regscale_models.ControlObjective.fetch_control_objectives_by_other_id(
|
|
1650
|
+
parent_id=plan_id, other_id_contains=finding.cci_ref
|
|
1651
|
+
)
|
|
1652
|
+
passing_objectives += regscale_models.ControlObjective.fetch_control_objectives_by_name(
|
|
1653
|
+
parent_id=plan_id, name_contains=finding.cci_ref
|
|
1654
|
+
)
|
|
1655
|
+
for passing_objective in passing_objectives:
|
|
1656
|
+
if passing_objective.name.lower().startswith("cci-"):
|
|
1657
|
+
implementation_id = self.get_control_implementation_id_for_cci(passing_objective.name)
|
|
1658
|
+
else:
|
|
1659
|
+
control_label = objective_to_control_dot(passing_objective.name)
|
|
1660
|
+
if control_label not in self.control_implementation_id_map:
|
|
1661
|
+
logger.warning("Control %s not found for %s", control_label, control_label)
|
|
1662
|
+
continue
|
|
1663
|
+
implementation_id = self.control_implementation_id_map[control_label]
|
|
1664
|
+
|
|
1665
|
+
# Skip if we couldn't determine the implementation ID
|
|
1666
|
+
if implementation_id is None:
|
|
1667
|
+
logger.warning("Could not determine implementation ID for objective %s", passing_objective.name)
|
|
1668
|
+
continue
|
|
1669
|
+
|
|
1670
|
+
passing_option = regscale_models.ImplementationOption(
|
|
1671
|
+
name="Passed STIG",
|
|
1672
|
+
description="Passed STIG Security Checks",
|
|
1673
|
+
acceptability=regscale_models.ImplementationStatus.FULLY_IMPLEMENTED,
|
|
1674
|
+
objectiveId=passing_objective.id,
|
|
1675
|
+
securityControlId=passing_objective.securityControlId,
|
|
1676
|
+
responsibility="Customer",
|
|
1677
|
+
).create_or_update()
|
|
1678
|
+
|
|
1679
|
+
_ = regscale_models.ImplementationObjective(
|
|
1680
|
+
securityControlId=passing_objective.securityControlId,
|
|
1681
|
+
implementationId=implementation_id,
|
|
1682
|
+
objectiveId=passing_objective.id,
|
|
1683
|
+
optionId=passing_option.id,
|
|
1684
|
+
status=regscale_models.ImplementationStatus.FULLY_IMPLEMENTED,
|
|
1685
|
+
statement=passing_objective.description,
|
|
1686
|
+
responsibility="Customer",
|
|
1687
|
+
).create_or_update()
|
|
1688
|
+
|
|
1689
|
+
# Create assessment and control test result
|
|
1690
|
+
assessment = self.get_or_create_assessment(implementation_id)
|
|
1691
|
+
control_test = self.create_or_get_control_test(finding, implementation_id)
|
|
1692
|
+
self.create_control_test_result(
|
|
1693
|
+
finding, control_test, assessment, regscale_models.ControlTestResultStatus.PASS
|
|
1694
|
+
)
|
|
1695
|
+
|
|
1696
|
+
@staticmethod
|
|
1697
|
+
def create_or_get_control_test(
|
|
1698
|
+
finding: IntegrationFinding, control_implementation_id: int
|
|
1699
|
+
) -> regscale_models.ControlTest:
|
|
1700
|
+
"""
|
|
1701
|
+
Create or get an existing control test.
|
|
1702
|
+
|
|
1703
|
+
:param IntegrationFinding finding: The finding associated with the control test
|
|
1704
|
+
:param int control_implementation_id: The ID of the control implementation
|
|
1705
|
+
:return: The created or existing control test
|
|
1706
|
+
:rtype: regscale_models.ControlTest
|
|
1707
|
+
"""
|
|
1708
|
+
return regscale_models.ControlTest(
|
|
1709
|
+
uuid=finding.external_id,
|
|
1710
|
+
parentControlId=control_implementation_id,
|
|
1711
|
+
testCriteria=finding.cci_ref or finding.description,
|
|
1712
|
+
).get_or_create()
|
|
1713
|
+
|
|
1714
|
+
def get_asset_by_identifier(self, identifier: str) -> Optional[regscale_models.Asset]:
|
|
1715
|
+
"""
|
|
1716
|
+
Gets an asset by its identifier
|
|
1717
|
+
|
|
1718
|
+
:param str identifier: The identifier of the asset
|
|
1719
|
+
:return: The asset
|
|
1720
|
+
:rtype: Optional[regscale_models.Asset]
|
|
1721
|
+
"""
|
|
1722
|
+
asset = self.asset_map_by_identifier.get(identifier)
|
|
1723
|
+
if not asset and identifier not in self.alerted_assets:
|
|
1724
|
+
self.alerted_assets.add(identifier)
|
|
1725
|
+
self.log_error("1. Asset not found for identifier %s", identifier)
|
|
1726
|
+
return asset
|
|
1727
|
+
|
|
1728
|
+
def process_checklist(self, finding: IntegrationFinding) -> int:
|
|
1729
|
+
"""
|
|
1730
|
+
Processes a single checklist item based on the provided finding.
|
|
1731
|
+
|
|
1732
|
+
This method checks if the asset related to the finding exists, updates or creates a checklist item,
|
|
1733
|
+
and handles the finding based on its status (pass/fail).
|
|
1734
|
+
|
|
1735
|
+
:param IntegrationFinding finding: The finding to process
|
|
1736
|
+
:return: 1 if the checklist was processed, 0 if not
|
|
1737
|
+
:rtype: int
|
|
1738
|
+
"""
|
|
1739
|
+
logger.debug("Processing checklist %s", finding.external_id)
|
|
1740
|
+
if not (asset := self.get_asset_by_identifier(finding.asset_identifier)):
|
|
1741
|
+
logger.error("2. Asset not found for identifier %s", finding.asset_identifier)
|
|
1742
|
+
return 0
|
|
1743
|
+
|
|
1744
|
+
tool = regscale_models.ChecklistTool.STIGs
|
|
1745
|
+
if finding.vulnerability_type == "Vulnerability Scan":
|
|
1746
|
+
tool = regscale_models.ChecklistTool.VulnerabilityScanner
|
|
1747
|
+
|
|
1748
|
+
if not finding.cci_ref:
|
|
1749
|
+
finding.cci_ref = "CCI-000366"
|
|
1750
|
+
|
|
1751
|
+
logger.debug("Create or update checklist for %s", finding.external_id)
|
|
1752
|
+
regscale_models.Checklist(
|
|
1753
|
+
status=finding.checklist_status,
|
|
1754
|
+
assetId=asset.id,
|
|
1755
|
+
tool=tool,
|
|
1756
|
+
baseline=finding.baseline,
|
|
1757
|
+
vulnerabilityId=finding.vulnerability_number,
|
|
1758
|
+
results=finding.results,
|
|
1759
|
+
check=finding.title,
|
|
1760
|
+
cci=finding.cci_ref,
|
|
1761
|
+
ruleId=finding.rule_id,
|
|
1762
|
+
version=finding.rule_version,
|
|
1763
|
+
comments=finding.comments,
|
|
1764
|
+
datePerformed=finding.date_created,
|
|
1765
|
+
).create_or_update()
|
|
1766
|
+
|
|
1767
|
+
# For both passing and failing findings, let the vulnerability mapping handle the closure
|
|
1768
|
+
if finding.status != regscale_models.ChecklistStatus.PASS:
|
|
1769
|
+
logger.debug("Handling failing checklist for %s", finding.external_id)
|
|
1770
|
+
if self.type == ScannerIntegrationType.CHECKLIST:
|
|
1771
|
+
self.handle_failing_checklist(finding=finding, plan_id=self.plan_id)
|
|
1772
|
+
self.handle_failing_finding(
|
|
1773
|
+
issue_title=finding.issue_title or finding.title,
|
|
1774
|
+
finding=finding,
|
|
1775
|
+
)
|
|
1776
|
+
return 1
|
|
1777
|
+
|
|
1778
|
+
def handle_control_finding(self, finding: IntegrationFinding) -> None:
|
|
1779
|
+
"""
|
|
1780
|
+
Handle a control finding, either passing or failing.
|
|
1781
|
+
|
|
1782
|
+
:param IntegrationFinding finding: The finding to handle
|
|
1783
|
+
:rtype: None
|
|
1784
|
+
"""
|
|
1785
|
+
if finding.status == regscale_models.ControlTestResultStatus.PASS:
|
|
1786
|
+
# For passing findings, we'll let the normal vulnerability mapping closure handle it
|
|
1787
|
+
pass
|
|
1788
|
+
else:
|
|
1789
|
+
self.handle_failing_finding(
|
|
1790
|
+
issue_title="Finding %s failed",
|
|
1791
|
+
finding=finding,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
def update_regscale_findings(self, findings: Iterator[IntegrationFinding]) -> int:
|
|
1795
|
+
"""
|
|
1796
|
+
Updates RegScale findings, checklists, and vulnerabilities in a single pass.
|
|
1797
|
+
|
|
1798
|
+
:param Iterator[IntegrationFinding] findings: The integration findings
|
|
1799
|
+
:return: The number of findings processed
|
|
1800
|
+
:rtype: int
|
|
1801
|
+
"""
|
|
1802
|
+
logger.info("Updating RegScale findings...")
|
|
1803
|
+
scan_history = self.create_scan_history()
|
|
1804
|
+
current_vulnerabilities: Dict[int, Set[int]] = defaultdict(set)
|
|
1805
|
+
processed_findings_count = 0
|
|
1806
|
+
findings_to_process = self.num_findings_to_process
|
|
1807
|
+
loading_findings = self.finding_progress.add_task(
|
|
1808
|
+
f"[#f8b737]Processing {f'{findings_to_process} ' if findings_to_process else ''}finding(s) from {self.title}",
|
|
1809
|
+
total=self.num_findings_to_process if self.num_findings_to_process else None,
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
# Locks for thread-safe operations
|
|
1813
|
+
count_lock = threading.RLock()
|
|
1814
|
+
|
|
1815
|
+
def process_finding_with_progress(finding_to_process: IntegrationFinding) -> None:
|
|
1816
|
+
"""
|
|
1817
|
+
Process a single finding and update progress.
|
|
1818
|
+
|
|
1819
|
+
:param IntegrationFinding finding_to_process: The finding to process
|
|
1820
|
+
:rtype: None
|
|
1821
|
+
"""
|
|
1822
|
+
nonlocal processed_findings_count
|
|
1823
|
+
try:
|
|
1824
|
+
self.process_finding(finding_to_process, scan_history, current_vulnerabilities)
|
|
1825
|
+
with count_lock:
|
|
1826
|
+
processed_findings_count += 1
|
|
1827
|
+
if findings_to_process and self.finding_progress.tasks[loading_findings].total != float(
|
|
1828
|
+
findings_to_process
|
|
1829
|
+
):
|
|
1830
|
+
self.finding_progress.update(
|
|
1831
|
+
loading_findings,
|
|
1832
|
+
total=findings_to_process,
|
|
1833
|
+
description=f"[#f8b737]Processing {findings_to_process} findings from {self.title}.",
|
|
1834
|
+
)
|
|
1835
|
+
self.finding_progress.advance(loading_findings, 1)
|
|
1836
|
+
except Exception as exc:
|
|
1837
|
+
self.log_error(
|
|
1838
|
+
"An error occurred when processing finding %s: %s",
|
|
1839
|
+
finding_to_process.external_id,
|
|
1840
|
+
exc,
|
|
1841
|
+
)
|
|
1842
|
+
|
|
1843
|
+
if get_thread_workers_max() == 1:
|
|
1844
|
+
for finding in findings:
|
|
1845
|
+
process_finding_with_progress(finding)
|
|
1846
|
+
else:
|
|
1847
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=get_thread_workers_max()) as executor:
|
|
1848
|
+
list(executor.map(process_finding_with_progress, findings))
|
|
1849
|
+
|
|
1850
|
+
# Close outdated issues
|
|
1851
|
+
scan_history.save()
|
|
1852
|
+
regscale_models.Issue.bulk_save(progress_context=self.finding_progress)
|
|
1853
|
+
self.close_outdated_issues(current_vulnerabilities)
|
|
1854
|
+
|
|
1855
|
+
return processed_findings_count
|
|
1856
|
+
|
|
1857
|
+
@staticmethod
|
|
1858
|
+
def parse_poam_id(poam_identifier: str) -> Optional[int]:
|
|
1859
|
+
"""
|
|
1860
|
+
Parses a POAM identifier string to extract the numeric ID.
|
|
1861
|
+
|
|
1862
|
+
:param str poam_identifier: The POAM identifier string (e.g. "V-1234")
|
|
1863
|
+
:return: The numeric ID portion, or None if invalid format
|
|
1864
|
+
:rtype: Optional[int]
|
|
1865
|
+
"""
|
|
1866
|
+
if not poam_identifier or not poam_identifier.startswith("V-"):
|
|
1867
|
+
return None
|
|
1868
|
+
try:
|
|
1869
|
+
return int("".join(c for c in poam_identifier.split("-")[1] if c.isdigit()))
|
|
1870
|
+
except (IndexError, ValueError):
|
|
1871
|
+
return None
|
|
1872
|
+
|
|
1873
|
+
def get_next_poam_id(self) -> int:
|
|
1874
|
+
"""
|
|
1875
|
+
Retrieves the Next POAM ID for the current security plan in a thread-safe manner.
|
|
1876
|
+
|
|
1877
|
+
:return: The Next POAM ID
|
|
1878
|
+
:rtype: int
|
|
1879
|
+
"""
|
|
1880
|
+
# Use the class's _get_lock method to get a thread-safe lock
|
|
1881
|
+
with self._get_lock("poam_id"):
|
|
1882
|
+
# If we haven't cached the max ID yet
|
|
1883
|
+
if not isinstance(self._max_poam_id, int):
|
|
1884
|
+
logger.info("Fetching max POAM ID...")
|
|
1885
|
+
# Get all existing POAM IDs and find the maximum
|
|
1886
|
+
issues: List[regscale_models.Issue] = regscale_models.Issue.get_all_by_parent(
|
|
1887
|
+
parent_id=self.plan_id,
|
|
1888
|
+
parent_module=regscale_models.SecurityPlan.get_module_string(),
|
|
1889
|
+
)
|
|
1890
|
+
self._max_poam_id = max(
|
|
1891
|
+
(
|
|
1892
|
+
parsed_id
|
|
1893
|
+
for issue in issues
|
|
1894
|
+
if issue.otherIdentifier
|
|
1895
|
+
and (parsed_id := self.parse_poam_id(issue.otherIdentifier)) is not None
|
|
1896
|
+
),
|
|
1897
|
+
default=0,
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
# Increment the cached max ID and store it
|
|
1901
|
+
self._max_poam_id = (self._max_poam_id or 0) + 1
|
|
1902
|
+
return self._max_poam_id
|
|
1903
|
+
|
|
1904
|
+
def create_scan_history(self) -> regscale_models.ScanHistory:
|
|
1905
|
+
"""
|
|
1906
|
+
Creates a new ScanHistory object for the current scan.
|
|
1907
|
+
|
|
1908
|
+
:return: A newly created ScanHistory object
|
|
1909
|
+
:rtype: regscale_models.ScanHistory
|
|
1910
|
+
"""
|
|
1911
|
+
scan_history = regscale_models.ScanHistory(
|
|
1912
|
+
parentId=self.plan_id,
|
|
1913
|
+
parentModule=regscale_models.SecurityPlan.get_module_string(),
|
|
1914
|
+
scanningTool=self.title,
|
|
1915
|
+
scanDate=get_current_datetime(),
|
|
1916
|
+
createdById=self.assessor_id,
|
|
1917
|
+
tenantsId=self.tenant_id,
|
|
1918
|
+
vLow=0,
|
|
1919
|
+
vMedium=0,
|
|
1920
|
+
vHigh=0,
|
|
1921
|
+
vCritical=0,
|
|
1922
|
+
).create()
|
|
1923
|
+
|
|
1924
|
+
count = 0
|
|
1925
|
+
regscale_models.ScanHistory.delete_object_cache(scan_history)
|
|
1926
|
+
while not regscale_models.ScanHistory.get_object(object_id=scan_history.id) or count > 10:
|
|
1927
|
+
logger.info("Waiting for ScanHistory to be created...")
|
|
1928
|
+
time.sleep(1)
|
|
1929
|
+
count += 1
|
|
1930
|
+
regscale_models.ScanHistory.delete_object_cache(scan_history)
|
|
1931
|
+
return scan_history
|
|
1932
|
+
|
|
1933
|
+
def process_finding(
|
|
1934
|
+
self,
|
|
1935
|
+
finding: IntegrationFinding,
|
|
1936
|
+
scan_history: regscale_models.ScanHistory,
|
|
1937
|
+
current_vulnerabilities: Dict[int, Set[int]],
|
|
1938
|
+
) -> None:
|
|
1939
|
+
"""
|
|
1940
|
+
Process a single finding, handling both checklist and vulnerability cases.
|
|
1941
|
+
|
|
1942
|
+
:param IntegrationFinding finding: The finding to process
|
|
1943
|
+
:param regscale_models.ScanHistory scan_history: The current scan history
|
|
1944
|
+
:param Dict[int, Set[int]] current_vulnerabilities: Dictionary of current vulnerabilities
|
|
1945
|
+
:rtype: None
|
|
1946
|
+
"""
|
|
1947
|
+
# Update finding dates if scan date is set
|
|
1948
|
+
finding = self.update_integration_finding_dates(
|
|
1949
|
+
finding=finding,
|
|
1950
|
+
existing_issues_dict={}, # We'll handle issue lookup in create_or_update_issue_from_finding
|
|
1951
|
+
scan_history=scan_history,
|
|
1952
|
+
)
|
|
1953
|
+
|
|
1954
|
+
# Process checklist if applicable
|
|
1955
|
+
if self.type == ScannerIntegrationType.CHECKLIST:
|
|
1956
|
+
if not (asset := self.get_asset_by_identifier(finding.asset_identifier)):
|
|
1957
|
+
logger.error("2. Asset not found for identifier %s", finding.asset_identifier)
|
|
1958
|
+
return
|
|
1959
|
+
|
|
1960
|
+
tool = regscale_models.ChecklistTool.STIGs
|
|
1961
|
+
if finding.vulnerability_type == "Vulnerability Scan":
|
|
1962
|
+
tool = regscale_models.ChecklistTool.VulnerabilityScanner
|
|
1963
|
+
|
|
1964
|
+
if not finding.cci_ref:
|
|
1965
|
+
finding.cci_ref = "CCI-000366"
|
|
1966
|
+
|
|
1967
|
+
# Convert checklist status to string
|
|
1968
|
+
checklist_status_str = str(finding.checklist_status.value)
|
|
1969
|
+
|
|
1970
|
+
logger.debug("Create or update checklist for %s", finding.external_id)
|
|
1971
|
+
regscale_models.Checklist(
|
|
1972
|
+
status=checklist_status_str,
|
|
1973
|
+
assetId=asset.id,
|
|
1974
|
+
tool=tool,
|
|
1975
|
+
baseline=finding.baseline,
|
|
1976
|
+
vulnerabilityId=finding.vulnerability_number,
|
|
1977
|
+
results=finding.results,
|
|
1978
|
+
check=finding.title,
|
|
1979
|
+
cci=finding.cci_ref,
|
|
1980
|
+
ruleId=finding.rule_id,
|
|
1981
|
+
version=finding.rule_version,
|
|
1982
|
+
comments=finding.comments,
|
|
1983
|
+
datePerformed=finding.date_created,
|
|
1984
|
+
).create_or_update()
|
|
1985
|
+
|
|
1986
|
+
# For failing findings, handle control implementation updates
|
|
1987
|
+
if finding.status != regscale_models.IssueStatus.Closed:
|
|
1988
|
+
logger.debug("Handling failing checklist for %s", finding.external_id)
|
|
1989
|
+
if self.type == ScannerIntegrationType.CHECKLIST:
|
|
1990
|
+
self.handle_failing_checklist(finding=finding, plan_id=self.plan_id)
|
|
1991
|
+
else:
|
|
1992
|
+
logger.debug("Handling passing checklist for %s", finding.external_id)
|
|
1993
|
+
self.handle_passing_checklist(finding=finding, plan_id=self.plan_id)
|
|
1994
|
+
|
|
1995
|
+
# Process vulnerability if applicable
|
|
1996
|
+
if finding.status != regscale_models.IssueStatus.Closed:
|
|
1997
|
+
if asset := self.get_asset_by_identifier(finding.asset_identifier):
|
|
1998
|
+
if vulnerability_id := self.handle_vulnerability(finding, asset, scan_history):
|
|
1999
|
+
current_vulnerabilities[asset.id].add(vulnerability_id)
|
|
2000
|
+
|
|
2001
|
+
# Handle failing finding (creates/updates issues) for both checklist and vulnerability cases
|
|
2002
|
+
if finding.status != regscale_models.IssueStatus.Closed:
|
|
2003
|
+
self.handle_failing_finding(
|
|
2004
|
+
issue_title=finding.issue_title or finding.title,
|
|
2005
|
+
finding=finding,
|
|
2006
|
+
)
|
|
2007
|
+
|
|
2008
|
+
# Update scan history severity counts
|
|
2009
|
+
self.set_severity_count_for_scan(finding.severity, scan_history)
|
|
2010
|
+
|
|
2011
|
+
def create_vulnerability_from_finding(
|
|
2012
|
+
self, finding: IntegrationFinding, asset: regscale_models.Asset, scan_history: regscale_models.ScanHistory
|
|
2013
|
+
) -> regscale_models.Vulnerability:
|
|
2014
|
+
"""
|
|
2015
|
+
Creates a vulnerability from an integration finding.
|
|
2016
|
+
|
|
2017
|
+
:param IntegrationFinding finding: The integration finding
|
|
2018
|
+
:param regscale_models.Asset asset: The associated asset
|
|
2019
|
+
:param regscale_models.ScanHistory scan_history: The scan history
|
|
2020
|
+
:return: The created vulnerability
|
|
2021
|
+
:rtype: regscale_models.Vulnerability
|
|
2022
|
+
"""
|
|
2023
|
+
vulnerability = regscale_models.Vulnerability(
|
|
2024
|
+
title=finding.title,
|
|
2025
|
+
cve=finding.cve,
|
|
2026
|
+
vprScore=(
|
|
2027
|
+
finding.vpr_score if hasattr(finding, "vprScore") else None
|
|
2028
|
+
), # If this is the VPR score, otherwise use a different field
|
|
2029
|
+
cvsSv3BaseScore=finding.cvss_v3_base_score or finding.cvss_v3_score or finding.cvss_score,
|
|
2030
|
+
scanId=scan_history.id,
|
|
2031
|
+
severity=self.issue_to_vulnerability_map.get(finding.severity, regscale_models.VulnerabilitySeverity.Low),
|
|
2032
|
+
description=finding.description,
|
|
2033
|
+
dateLastUpdated=finding.date_last_updated,
|
|
2034
|
+
parentId=self.plan_id,
|
|
2035
|
+
parentModule=regscale_models.SecurityPlan.get_module_string(),
|
|
2036
|
+
dns=asset.fqdn or "unknown",
|
|
2037
|
+
status=regscale_models.VulnerabilityStatus.Open,
|
|
2038
|
+
ipAddress=finding.ip_address or asset.ipAddress or "",
|
|
2039
|
+
firstSeen=finding.first_seen,
|
|
2040
|
+
lastSeen=finding.last_seen,
|
|
2041
|
+
plugInName=finding.cve or finding.plugin_name, # Use CVE if available, otherwise use plugin name
|
|
2042
|
+
# plugInId=finding.plugin_id, # Vulnerability.pluginId is an int, but it is a string on Issue
|
|
2043
|
+
exploitAvailable=None, # Set this if you have information about exploit availability
|
|
2044
|
+
plugInText=finding.observations, # or finding.evidence, whichever is more appropriate
|
|
2045
|
+
port=finding.port if hasattr(finding, "port") else None,
|
|
2046
|
+
protocol=finding.protocol if hasattr(finding, "protocol") else None,
|
|
2047
|
+
operatingSystem=asset.operating_system if hasattr(asset, "operating_system") else None,
|
|
2048
|
+
)
|
|
2049
|
+
|
|
2050
|
+
vulnerability = vulnerability.create_or_update()
|
|
2051
|
+
if re.match(r"^\d+\.\d+(\.\d+){0,2}$", self.regscale_version) or self.regscale_version >= "5.64.0":
|
|
2052
|
+
regscale_models.VulnerabilityMapping(
|
|
2053
|
+
vulnerabilityId=vulnerability.id,
|
|
2054
|
+
assetId=asset.id,
|
|
2055
|
+
scanId=scan_history.id,
|
|
2056
|
+
securityPlansId=self.plan_id,
|
|
2057
|
+
createdById=self.assessor_id,
|
|
2058
|
+
tenantsId=self.tenant_id,
|
|
2059
|
+
isPublic=True,
|
|
2060
|
+
dateCreated=get_current_datetime(),
|
|
2061
|
+
firstSeen=finding.first_seen,
|
|
2062
|
+
lastSeen=finding.last_seen,
|
|
2063
|
+
status=finding.status,
|
|
2064
|
+
).create_unique()
|
|
2065
|
+
return vulnerability
|
|
2066
|
+
|
|
2067
|
+
def handle_vulnerability(
|
|
2068
|
+
self,
|
|
2069
|
+
finding: IntegrationFinding,
|
|
2070
|
+
asset: Optional[regscale_models.Asset],
|
|
2071
|
+
scan_history: regscale_models.ScanHistory,
|
|
2072
|
+
) -> Optional[int]:
|
|
2073
|
+
"""
|
|
2074
|
+
Handles the vulnerabilities for a finding.
|
|
2075
|
+
|
|
2076
|
+
:param IntegrationFinding finding: The integration finding
|
|
2077
|
+
:param Optional[regscale_models.Asset] asset: The associated asset
|
|
2078
|
+
:param regscale_models.ScanHistory scan_history: The scan history
|
|
2079
|
+
:rtype: Optional[int]
|
|
2080
|
+
:return: The vulnerability ID
|
|
2081
|
+
"""
|
|
2082
|
+
if not (finding.plugin_name or finding.cve):
|
|
2083
|
+
logger.warning("No Plugin Name or CVE found for finding %s", finding.title)
|
|
2084
|
+
return None
|
|
2085
|
+
|
|
2086
|
+
if not asset:
|
|
2087
|
+
logger.warning("VulnerabilityMapping Error: Asset not found for identifier %s", finding.asset_identifier)
|
|
2088
|
+
return None
|
|
2089
|
+
|
|
2090
|
+
vulnerability = self.create_vulnerability_from_finding(finding, asset, scan_history)
|
|
2091
|
+
finding.vulnerability_id = vulnerability.id
|
|
2092
|
+
|
|
2093
|
+
# Handle associated issue
|
|
2094
|
+
self.create_or_update_issue_from_finding(
|
|
2095
|
+
title=finding.title,
|
|
2096
|
+
finding=finding,
|
|
2097
|
+
)
|
|
2098
|
+
|
|
2099
|
+
return vulnerability.id
|
|
2100
|
+
|
|
2101
|
+
def _filter_vulns_open_by_other_tools(
|
|
2102
|
+
self, all_vulns: list[regscale_models.Vulnerability]
|
|
2103
|
+
) -> list[regscale_models.Vulnerability]:
|
|
2104
|
+
"""
|
|
2105
|
+
Fetch vulnerabilities that are open and not associated with other tools.
|
|
2106
|
+
:param list[regscale_models.Vulnerability] all_vulns: List of all vulnerabilities to check the scanningTool
|
|
2107
|
+
:return: List of matching vulnerabilities
|
|
2108
|
+
:rtype: list[regscale_models.Vulnerability]
|
|
2109
|
+
"""
|
|
2110
|
+
vuln_list = []
|
|
2111
|
+
for vuln in all_vulns:
|
|
2112
|
+
other_tool = False
|
|
2113
|
+
open_vuln_mappings = regscale_models.VulnerabilityMapping.find_by_vulnerability(vuln.id, status="Open")
|
|
2114
|
+
for vuln_mapping in open_vuln_mappings:
|
|
2115
|
+
if vuln_mapping.scanId is not None:
|
|
2116
|
+
scan_history = regscale_models.ScanHistory.get_object(vuln_mapping.scanId)
|
|
2117
|
+
if scan_history and scan_history.scanningTool != self.title:
|
|
2118
|
+
other_tool = True
|
|
2119
|
+
break
|
|
2120
|
+
if not other_tool:
|
|
2121
|
+
vuln_list.append(vuln)
|
|
2122
|
+
return vuln_list
|
|
2123
|
+
|
|
2124
|
+
def close_outdated_vulnerabilities(self, current_vulnerabilities: Dict[int, Set[int]]) -> None:
|
|
2125
|
+
"""
|
|
2126
|
+
Closes vulnerabilities that are not in the current set of vulnerability IDs for each asset.
|
|
2127
|
+
|
|
2128
|
+
:param Dict[int, Set[int]] current_vulnerabilities: Dictionary of asset IDs to lists of current vulnerability IDs
|
|
2129
|
+
:rtype: None
|
|
2130
|
+
"""
|
|
2131
|
+
# Get all current vulnerability IDs
|
|
2132
|
+
current_vuln_ids = {vuln_id for vuln_ids in current_vulnerabilities.values() for vuln_id in vuln_ids}
|
|
2133
|
+
|
|
2134
|
+
# Get all vulnerabilities for this security plan
|
|
2135
|
+
all_vulnerabilities: list[regscale_models.Vulnerability] = regscale_models.Vulnerability.get_all_by_parent(
|
|
2136
|
+
parent_id=self.plan_id, parent_module=regscale_models.SecurityPlan.get_module_string()
|
|
2137
|
+
)
|
|
2138
|
+
|
|
2139
|
+
# Pre-filter vulnerabilities that are not in current set
|
|
2140
|
+
outdated_vulns = [v for v in all_vulnerabilities if v.id not in current_vuln_ids]
|
|
2141
|
+
|
|
2142
|
+
# Filter by tool
|
|
2143
|
+
tool_vulns = self._filter_vulns_open_by_other_tools(all_vulns=outdated_vulns)
|
|
2144
|
+
|
|
2145
|
+
closed_count = 0
|
|
2146
|
+
for vuln in tool_vulns:
|
|
2147
|
+
if vuln.status != regscale_models.VulnerabilityStatus.Closed:
|
|
2148
|
+
self.close_mappings_list(vuln) # Close matching mappings
|
|
2149
|
+
vuln.status = regscale_models.VulnerabilityStatus.Closed
|
|
2150
|
+
vuln.dateClosed = get_current_datetime()
|
|
2151
|
+
vuln.save()
|
|
2152
|
+
closed_count += 1
|
|
2153
|
+
logger.info("Closed vulnerability %d", vuln.id)
|
|
2154
|
+
|
|
2155
|
+
logger.info("Closed %d outdated vulnerabilities.", closed_count)
|
|
2156
|
+
|
|
2157
|
+
@classmethod
|
|
2158
|
+
def close_mappings_list(cls, vuln: regscale_models.Vulnerability) -> None:
|
|
2159
|
+
"""
|
|
2160
|
+
Close all mappings for a vulnerability.
|
|
2161
|
+
|
|
2162
|
+
:param regscale_models.Vulnerability vuln: The vulnerability to close mappings for
|
|
2163
|
+
:rtype: None
|
|
2164
|
+
"""
|
|
2165
|
+
mappings: List[regscale_models.VulnerabilityMapping] = [
|
|
2166
|
+
mapping
|
|
2167
|
+
for mapping in regscale_models.VulnerabilityMapping.find_by_vulnerability(
|
|
2168
|
+
vuln.id, status=regscale_models.IssueStatus.Open
|
|
2169
|
+
)
|
|
2170
|
+
if mapping is not None
|
|
2171
|
+
]
|
|
2172
|
+
for mapping in mappings:
|
|
2173
|
+
# Don't close for other tools
|
|
2174
|
+
if mapping.scanId:
|
|
2175
|
+
scan = regscale_models.ScanHistory.get_object(mapping.scanId)
|
|
2176
|
+
if scan and scan.scanningTool != cls.title:
|
|
2177
|
+
continue
|
|
2178
|
+
|
|
2179
|
+
# This one uses IssueStatus
|
|
2180
|
+
mapping.status = regscale_models.IssueStatus.Closed
|
|
2181
|
+
mapping.dateClosed = get_current_datetime()
|
|
2182
|
+
mapping.save()
|
|
2183
|
+
|
|
2184
|
+
def close_outdated_issues(self, current_vulnerabilities: Dict[int, Set[int]]) -> int:
|
|
2185
|
+
"""
|
|
2186
|
+
Closes issues that are not associated with current vulnerabilities for each asset.
|
|
2187
|
+
After closing issues, updates the status of affected control implementations.
|
|
2188
|
+
|
|
2189
|
+
:param Dict[int, Set[int]] current_vulnerabilities: Dictionary mapping asset IDs to sets of current vulnerability IDs
|
|
2190
|
+
:return: Number of issues closed
|
|
2191
|
+
:rtype: int
|
|
2192
|
+
"""
|
|
2193
|
+
if not self.close_outdated_findings:
|
|
2194
|
+
# This should normally be set to True, but on POAM import, we do not want to automatically close issues,
|
|
2195
|
+
# unless the sheet specifies to do so
|
|
2196
|
+
logger.info("Skipping closing outdated issues.")
|
|
2197
|
+
return 0
|
|
2198
|
+
|
|
2199
|
+
closed_count = 0
|
|
2200
|
+
affected_control_ids = set()
|
|
2201
|
+
|
|
2202
|
+
# Get all open issues for this security plan
|
|
2203
|
+
open_issues = regscale_models.Issue.fetch_issues_by_ssp(
|
|
2204
|
+
None, ssp_id=self.plan_id, status=regscale_models.IssueStatus.Open.value
|
|
2205
|
+
)
|
|
2206
|
+
|
|
2207
|
+
# Create a progress bar
|
|
2208
|
+
task_id = self.finding_progress.add_task("[cyan]Closing outdated issues...", total=len(open_issues))
|
|
2209
|
+
|
|
2210
|
+
for issue in open_issues:
|
|
2211
|
+
if self.should_close_issue(issue, current_vulnerabilities):
|
|
2212
|
+
issue.status = regscale_models.IssueStatus.Closed
|
|
2213
|
+
issue.dateCompleted = get_current_datetime()
|
|
2214
|
+
changes_text = f"{get_current_datetime('%b %d, %Y')} - Closed by {self.title} for having no current vulnerabilities."
|
|
2215
|
+
if issue.changes:
|
|
2216
|
+
issue.changes += f"\n{changes_text}"
|
|
2217
|
+
else:
|
|
2218
|
+
issue.changes = changes_text
|
|
2219
|
+
issue.save()
|
|
2220
|
+
closed_count += 1
|
|
2221
|
+
|
|
2222
|
+
# Track affected control implementations
|
|
2223
|
+
if issue.controlImplementationIds:
|
|
2224
|
+
affected_control_ids.update(issue.controlImplementationIds)
|
|
2225
|
+
|
|
2226
|
+
# Update the progress bar
|
|
2227
|
+
self.finding_progress.update(task_id, advance=1)
|
|
2228
|
+
|
|
2229
|
+
# Update status of affected control implementations
|
|
2230
|
+
for control_id in affected_control_ids:
|
|
2231
|
+
self.update_control_implementation_status_after_close(control_id)
|
|
2232
|
+
|
|
2233
|
+
logger.info("Closed %d outdated issues.", closed_count)
|
|
2234
|
+
return closed_count
|
|
2235
|
+
|
|
2236
|
+
def update_control_implementation_status_after_close(self, control_id: int) -> None:
|
|
2237
|
+
"""
|
|
2238
|
+
Updates the status of a control implementation after closing issues.
|
|
2239
|
+
Sets to FULLY_IMPLEMENTED if no open issues remain, NOT_IMPLEMENTED if any issues are open.
|
|
2240
|
+
|
|
2241
|
+
:param int control_id: The ID of the control implementation to update
|
|
2242
|
+
:rtype: None
|
|
2243
|
+
"""
|
|
2244
|
+
# Get the control implementation
|
|
2245
|
+
control_implementation = self.control_implementation_map.get(
|
|
2246
|
+
control_id
|
|
2247
|
+
) or regscale_models.ControlImplementation.get_object(object_id=control_id)
|
|
2248
|
+
|
|
2249
|
+
if not control_implementation:
|
|
2250
|
+
logger.warning("Control implementation %d not found", control_id)
|
|
2251
|
+
return
|
|
2252
|
+
|
|
2253
|
+
# Check if there are any open issues for this control implementation
|
|
2254
|
+
open_issues = self.existing_issue_ids_by_implementation_map.get(control_id, [])
|
|
2255
|
+
|
|
2256
|
+
# Set status based on presence of open issues
|
|
2257
|
+
new_status = (
|
|
2258
|
+
regscale_models.ImplementationStatus.FULLY_IMPLEMENTED
|
|
2259
|
+
if not open_issues
|
|
2260
|
+
else regscale_models.ImplementationStatus.NOT_IMPLEMENTED
|
|
2261
|
+
)
|
|
2262
|
+
|
|
2263
|
+
if control_implementation.status != new_status:
|
|
2264
|
+
control_implementation.status = new_status
|
|
2265
|
+
self.control_implementation_map[control_id] = control_implementation.save()
|
|
2266
|
+
logger.info("Updated control implementation %d status to %s", control_id, new_status)
|
|
2267
|
+
|
|
2268
|
+
def should_close_issue(self, issue: regscale_models.Issue, current_vulnerabilities: Dict[int, Set[int]]) -> bool:
|
|
2269
|
+
"""
|
|
2270
|
+
Determines if an issue should be closed based on current vulnerabilities.
|
|
2271
|
+
An issue should be closed if it has no more active vulnerability mappings for any assets.
|
|
2272
|
+
|
|
2273
|
+
:param regscale_models.Issue issue: The issue to check
|
|
2274
|
+
:param Dict[int, Set[int]] current_vulnerabilities: Dictionary of current vulnerabilities
|
|
2275
|
+
:return: True if the issue should be closed, False otherwise
|
|
2276
|
+
:rtype: bool
|
|
2277
|
+
"""
|
|
2278
|
+
# Do not close issues from other tools
|
|
2279
|
+
if issue.sourceReport != self.title:
|
|
2280
|
+
return False
|
|
2281
|
+
|
|
2282
|
+
# If the issue has a vulnerability ID, check if it's still current for any asset
|
|
2283
|
+
if issue.vulnerabilityId:
|
|
2284
|
+
# Get vulnerability mappings for this issue
|
|
2285
|
+
vuln_mappings = regscale_models.VulnerabilityMapping.find_by_issue(
|
|
2286
|
+
issue.id, status=regscale_models.IssueStatus.Open
|
|
2287
|
+
)
|
|
2288
|
+
|
|
2289
|
+
# Check if the issue's vulnerability is still current for any asset
|
|
2290
|
+
# If it is, we shouldn't close the issue
|
|
2291
|
+
if any(
|
|
2292
|
+
mapping.assetId in current_vulnerabilities
|
|
2293
|
+
and issue.vulnerabilityId in current_vulnerabilities[mapping.assetId]
|
|
2294
|
+
for mapping in vuln_mappings
|
|
2295
|
+
):
|
|
2296
|
+
return False
|
|
2297
|
+
|
|
2298
|
+
# If we've checked all conditions and found no current vulnerabilities, we should close it
|
|
2299
|
+
return True
|
|
2300
|
+
|
|
2301
|
+
@staticmethod
|
|
2302
|
+
def set_severity_count_for_scan(severity: str, scan_history: regscale_models.ScanHistory) -> None:
|
|
2303
|
+
"""
|
|
2304
|
+
Increments the count of the severity
|
|
2305
|
+
:param str severity: Severity of the vulnerability
|
|
2306
|
+
:param regscale_models.ScanHistory scan_history: Scan history object
|
|
2307
|
+
:rtype: None
|
|
2308
|
+
"""
|
|
2309
|
+
if severity == regscale_models.IssueSeverity.Low:
|
|
2310
|
+
scan_history.vLow += 1
|
|
2311
|
+
elif severity == regscale_models.IssueSeverity.Moderate:
|
|
2312
|
+
scan_history.vMedium += 1
|
|
2313
|
+
elif severity == regscale_models.IssueSeverity.High:
|
|
2314
|
+
scan_history.vHigh += 1
|
|
2315
|
+
elif severity == regscale_models.IssueSeverity.Critical:
|
|
2316
|
+
scan_history.vCritical += 1
|
|
2317
|
+
|
|
2318
|
+
@classmethod
|
|
2319
|
+
def cci_assessment(cls, plan_id: int) -> None:
|
|
2320
|
+
"""
|
|
2321
|
+
Creates or updates CCI assessments in RegScale
|
|
2322
|
+
|
|
2323
|
+
:param int plan_id: The ID of the security plan
|
|
2324
|
+
:rtype: None
|
|
2325
|
+
"""
|
|
2326
|
+
instance = cls(plan_id=plan_id)
|
|
2327
|
+
for control_id, ccis in instance.get_control_to_cci_map().items():
|
|
2328
|
+
if not (implementation_id := instance.control_id_to_implementation_map.get(control_id)):
|
|
2329
|
+
logger.error("Control Implementation for %d not found in RegScale", control_id)
|
|
2330
|
+
continue
|
|
2331
|
+
assessment = instance.get_or_create_assessment(implementation_id)
|
|
2332
|
+
assessment_result = regscale_models.AssessmentResultsStatus.PASS
|
|
2333
|
+
open_issues: List[OpenIssueDict] = instance.existing_issue_ids_by_implementation_map.get(
|
|
2334
|
+
implementation_id, []
|
|
2335
|
+
)
|
|
2336
|
+
ccis.add("CCI-000366")
|
|
2337
|
+
for cci in sorted(ccis):
|
|
2338
|
+
logger.debug("Creating assessment for CCI %s for implementation %d", cci, implementation_id)
|
|
2339
|
+
result = regscale_models.ControlTestResultStatus.PASS
|
|
2340
|
+
for issue in open_issues:
|
|
2341
|
+
if cci.lower() in issue.get("integrationFindingId", "").lower():
|
|
2342
|
+
result = regscale_models.ControlTestResultStatus.FAIL
|
|
2343
|
+
assessment_result = regscale_models.AssessmentResultsStatus.FAIL
|
|
2344
|
+
break
|
|
2345
|
+
|
|
2346
|
+
control_test_key = f"{implementation_id}-{cci}"
|
|
2347
|
+
control_test = instance.control_tests_map.get(
|
|
2348
|
+
control_test_key,
|
|
2349
|
+
regscale_models.ControlTest(
|
|
2350
|
+
parentControlId=implementation_id,
|
|
2351
|
+
testCriteria=cci,
|
|
2352
|
+
).get_or_create(),
|
|
2353
|
+
)
|
|
2354
|
+
regscale_models.ControlTestResult(
|
|
2355
|
+
parentTestId=control_test.id if control_test else None,
|
|
2356
|
+
parentAssessmentId=assessment.id,
|
|
2357
|
+
result=result,
|
|
2358
|
+
dateAssessed=get_current_datetime(),
|
|
2359
|
+
assessedById=instance.assessor_id,
|
|
2360
|
+
).create()
|
|
2361
|
+
assessment.assessmentResult = assessment_result
|
|
2362
|
+
assessment.save()
|
|
2363
|
+
|
|
2364
|
+
@classmethod
|
|
2365
|
+
def sync_findings(cls, plan_id: int, **kwargs) -> int:
|
|
2366
|
+
"""
|
|
2367
|
+
Synchronizes findings from the integration to RegScale.
|
|
2368
|
+
|
|
2369
|
+
:param int plan_id: The ID of the RegScale SSP
|
|
2370
|
+
:return: The number of findings processed
|
|
2371
|
+
:rtype: int
|
|
2372
|
+
"""
|
|
2373
|
+
logger.info("Syncing %s findings...", cls.title)
|
|
2374
|
+
instance = cls(plan_id=plan_id)
|
|
2375
|
+
instance.set_keys(**kwargs)
|
|
2376
|
+
# If a progress object was passed, use it instead of creating a new one
|
|
2377
|
+
instance.finding_progress = kwargs.pop("progress") if "progress" in kwargs else create_progress_object()
|
|
2378
|
+
instance.enable_finding_date_update = kwargs.get("enable_finding_date_update", False)
|
|
2379
|
+
if finding_count := kwargs.get("finding_count"):
|
|
2380
|
+
instance.num_findings_to_process = finding_count
|
|
2381
|
+
kwargs["plan_id"] = plan_id
|
|
2382
|
+
|
|
2383
|
+
with instance.finding_progress:
|
|
2384
|
+
findings = instance.fetch_findings(**kwargs)
|
|
2385
|
+
# Update the asset map with the latest assets
|
|
2386
|
+
logger.info("Getting asset map...")
|
|
2387
|
+
instance.asset_map_by_identifier.update(instance.get_asset_map())
|
|
2388
|
+
findings_processed = instance.update_regscale_findings(findings=findings)
|
|
2389
|
+
|
|
2390
|
+
if instance.errors:
|
|
2391
|
+
logger.error("Summary of errors encountered:")
|
|
2392
|
+
for error in instance.errors:
|
|
2393
|
+
logger.error(error)
|
|
2394
|
+
else:
|
|
2395
|
+
logger.info("All findings have been processed successfully.")
|
|
2396
|
+
|
|
2397
|
+
logger.info("Processed %d findings.", findings_processed)
|
|
2398
|
+
return findings_processed
|
|
2399
|
+
|
|
2400
|
+
@classmethod
|
|
2401
|
+
def sync_assets(cls, plan_id: int, **kwargs) -> int:
|
|
2402
|
+
"""
|
|
2403
|
+
Synchronizes assets from the integration to RegScale.
|
|
2404
|
+
|
|
2405
|
+
:param int plan_id: The ID of the RegScale SSP
|
|
2406
|
+
:return: The number of assets processed
|
|
2407
|
+
:rtype: int
|
|
2408
|
+
"""
|
|
2409
|
+
logger.info("Syncing %s assets...", cls.title)
|
|
2410
|
+
instance = cls(plan_id=plan_id, **kwargs)
|
|
2411
|
+
instance.set_keys(**kwargs)
|
|
2412
|
+
instance.asset_progress = kwargs.pop("progress") if "progress" in kwargs else create_progress_object()
|
|
2413
|
+
if asset_count := kwargs.get("asset_count"):
|
|
2414
|
+
instance.num_assets_to_process = asset_count
|
|
2415
|
+
|
|
2416
|
+
with instance.asset_progress:
|
|
2417
|
+
assets = instance.fetch_assets(**kwargs)
|
|
2418
|
+
assets_processed = instance.update_regscale_assets(assets=assets)
|
|
2419
|
+
|
|
2420
|
+
if instance.errors:
|
|
2421
|
+
logger.error("Summary of errors encountered:")
|
|
2422
|
+
for error in instance.errors:
|
|
2423
|
+
logger.error(error)
|
|
2424
|
+
else:
|
|
2425
|
+
logger.info("All assets have been processed successfully.")
|
|
2426
|
+
|
|
2427
|
+
APIHandler().log_api_summary()
|
|
2428
|
+
logger.info("%d assets processed.", assets_processed)
|
|
2429
|
+
return assets_processed
|
|
2430
|
+
|
|
2431
|
+
@classmethod
|
|
2432
|
+
def set_keys(cls, **kwargs):
|
|
2433
|
+
"""
|
|
2434
|
+
Set the attributes for an integration
|
|
2435
|
+
:rtype: None
|
|
2436
|
+
"""
|
|
2437
|
+
for key, value in kwargs.items():
|
|
2438
|
+
if hasattr(cls, key):
|
|
2439
|
+
setattr(cls, key, value)
|
|
2440
|
+
else:
|
|
2441
|
+
logger.debug("Unable to set the %s attribute", key)
|
|
2442
|
+
|
|
2443
|
+
def log_error(self, msg: str, *args) -> None:
|
|
2444
|
+
"""
|
|
2445
|
+
Logs an error message
|
|
2446
|
+
|
|
2447
|
+
:param str msg: The error message
|
|
2448
|
+
:rtype: None
|
|
2449
|
+
"""
|
|
2450
|
+
logger.error(msg, *args, exc_info=True)
|
|
2451
|
+
self.errors.append(msg % args)
|
|
2452
|
+
|
|
2453
|
+
def update_integration_finding_dates(
|
|
2454
|
+
self,
|
|
2455
|
+
finding: IntegrationFinding,
|
|
2456
|
+
existing_issues_dict: Dict[str, regscale_models.Issue],
|
|
2457
|
+
scan_history: regscale_models.ScanHistory,
|
|
2458
|
+
) -> IntegrationFinding:
|
|
2459
|
+
"""
|
|
2460
|
+
Update the dates of the integration finding based on the scan date and whether the finding is new or existing.
|
|
2461
|
+
|
|
2462
|
+
:param IntegrationFinding finding: The integration finding
|
|
2463
|
+
:param Dict[str, regscale_models.Issue] existing_issues_dict: Dictionary of existing issues
|
|
2464
|
+
:param regscale_models.ScanHistory scan_history: List of existing scan history objects
|
|
2465
|
+
:return: The updated integration finding or the original finding if the scan date is not set
|
|
2466
|
+
:rtype: IntegrationFinding
|
|
2467
|
+
"""
|
|
2468
|
+
if self.scan_date and self.enable_finding_date_update:
|
|
2469
|
+
issue = self.get_issue(existing_issues_dict, finding)
|
|
2470
|
+
vulnerabilities = (
|
|
2471
|
+
self.get_vulnerabilities(issue=issue, status=regscale_models.IssueStatus.Open) if issue else []
|
|
2472
|
+
)
|
|
2473
|
+
existing_vuln = self.get_existing_vuln(vulnerabilities, finding)
|
|
2474
|
+
finding = self.update_finding_dates(finding, existing_vuln, issue)
|
|
2475
|
+
self.update_scan(scan_history=scan_history)
|
|
2476
|
+
|
|
2477
|
+
return finding
|
|
2478
|
+
|
|
2479
|
+
def get_issue(
|
|
2480
|
+
self, existing_issues_dict: Dict[str, regscale_models.Issue], finding: IntegrationFinding
|
|
2481
|
+
) -> Optional[regscale_models.Issue]:
|
|
2482
|
+
"""
|
|
2483
|
+
Get the existing issue for the integration finding
|
|
2484
|
+
|
|
2485
|
+
:param Dict[str, regscale_models.Issue] existing_issues_dict: Dictionary of existing issues
|
|
2486
|
+
:param IntegrationFinding finding: The integration finding
|
|
2487
|
+
:return: The existing issue
|
|
2488
|
+
:rtype: Optional[regscale_models.Issue]
|
|
2489
|
+
"""
|
|
2490
|
+
return existing_issues_dict.get(f"{self.get_finding_identifier(finding)}_{finding.asset_identifier}")
|
|
2491
|
+
|
|
2492
|
+
@staticmethod
|
|
2493
|
+
def get_vulnerabilities(issue: regscale_models.Issue, status: str) -> List[regscale_models.VulnerabilityMapping]:
|
|
2494
|
+
"""
|
|
2495
|
+
Get the vulnerabilities for the issue
|
|
2496
|
+
|
|
2497
|
+
:param regscale_models.Issue issue: The issue
|
|
2498
|
+
:param str status: The status of the vulnerability
|
|
2499
|
+
:return: The list of vulnerabilities
|
|
2500
|
+
:rtype: List[regscale_models.VulnerabilityMapping]
|
|
2501
|
+
"""
|
|
2502
|
+
return regscale_models.VulnerabilityMapping.find_by_issue(issue.id, status=status) if issue else []
|
|
2503
|
+
|
|
2504
|
+
def get_existing_vuln(
|
|
2505
|
+
self, vulnerabilities: List[regscale_models.VulnerabilityMapping], finding: IntegrationFinding
|
|
2506
|
+
) -> Optional[regscale_models.VulnerabilityMapping]:
|
|
2507
|
+
"""
|
|
2508
|
+
Get the existing vulnerability for the integration finding
|
|
2509
|
+
|
|
2510
|
+
:param List[regscale_models.VulnerabilityMapping] vulnerabilities: The list of existing vulnerabilities
|
|
2511
|
+
:param IntegrationFinding finding: The integration finding
|
|
2512
|
+
:return: The existing vulnerability
|
|
2513
|
+
:rtype: Optional[regscale_models.VulnerabilityMapping]
|
|
2514
|
+
"""
|
|
2515
|
+
existing_vuln = min(vulnerabilities, key=lambda vuln: vuln.firstSeen) if vulnerabilities else None
|
|
2516
|
+
scan_date = date_obj(self.scan_date)
|
|
2517
|
+
first_seen = date_obj(finding.first_seen)
|
|
2518
|
+
if existing_vuln and scan_date and first_seen and scan_date < first_seen:
|
|
2519
|
+
finding.first_seen = self.scan_date
|
|
2520
|
+
return existing_vuln
|
|
2521
|
+
|
|
2522
|
+
def update_finding_dates(
|
|
2523
|
+
self,
|
|
2524
|
+
finding: IntegrationFinding,
|
|
2525
|
+
existing_vuln: Optional[regscale_models.VulnerabilityMapping],
|
|
2526
|
+
issue: Optional[regscale_models.Issue],
|
|
2527
|
+
) -> IntegrationFinding:
|
|
2528
|
+
"""
|
|
2529
|
+
Update the dates of the integration finding based on the scan date and whether the finding is new or existing.
|
|
2530
|
+
|
|
2531
|
+
:param IntegrationFinding finding: The integration finding
|
|
2532
|
+
:param Optional[regscale_models.VulnerabilityMapping] existing_vuln: The existing vulnerability mapping
|
|
2533
|
+
:param Optional[regscale_models.Issue] issue: The existing issue
|
|
2534
|
+
:return: The updated integration finding
|
|
2535
|
+
:rtype: IntegrationFinding
|
|
2536
|
+
"""
|
|
2537
|
+
if not finding.due_date:
|
|
2538
|
+
if not existing_vuln:
|
|
2539
|
+
finding.first_seen = self.scan_date
|
|
2540
|
+
finding.date_created = self.scan_date
|
|
2541
|
+
# From @mlongworth:
|
|
2542
|
+
# It also appears that the suspense date (due date for remediation) is set based upon the import date rather
|
|
2543
|
+
# than the scan date. This calculation needs to be based upon scan date e.g. scan date of 2/5/2024 severity
|
|
2544
|
+
# High, should set the due date for remediation in the POA&M to 4/5/2024.
|
|
2545
|
+
finding.due_date = issue_due_date(
|
|
2546
|
+
severity=finding.severity,
|
|
2547
|
+
created_date=finding.date_created or self.scan_date,
|
|
2548
|
+
title=self.title,
|
|
2549
|
+
config=self.app.config,
|
|
2550
|
+
)
|
|
2551
|
+
else:
|
|
2552
|
+
finding.first_seen = existing_vuln.firstSeen if existing_vuln else finding.first_seen
|
|
2553
|
+
if issue:
|
|
2554
|
+
finding.date_created = issue.dateFirstDetected or finding.date_created
|
|
2555
|
+
scan_date = date_obj(self.scan_date)
|
|
2556
|
+
first_seen = date_obj(finding.first_seen)
|
|
2557
|
+
if scan_date and first_seen and scan_date >= first_seen:
|
|
2558
|
+
finding.last_seen = self.scan_date
|
|
2559
|
+
return finding
|
|
2560
|
+
|
|
2561
|
+
def update_scan(self, scan_history: regscale_models.ScanHistory) -> None:
|
|
2562
|
+
"""
|
|
2563
|
+
Update the scan history object for the current security plan
|
|
2564
|
+
|
|
2565
|
+
:param regscale_models.ScanHistory scan_history: The list of existing scan history objects
|
|
2566
|
+
:return: None
|
|
2567
|
+
:rtype: None
|
|
2568
|
+
"""
|
|
2569
|
+
scan_history.scanDate = datetime_str(self.scan_date)
|
|
2570
|
+
scan_history.save()
|
|
2571
|
+
|
|
2572
|
+
@staticmethod
|
|
2573
|
+
def get_date_completed(finding: IntegrationFinding, issue_status: regscale_models.IssueStatus) -> Optional[str]:
|
|
2574
|
+
"""
|
|
2575
|
+
Returns the date when the issue was completed based on the issue status.
|
|
2576
|
+
|
|
2577
|
+
:param IntegrationFinding finding: The finding data
|
|
2578
|
+
:param regscale_models.IssueStatus issue_status: The status of the issue
|
|
2579
|
+
:return: The date when the issue was completed if the issue status is Closed, else None
|
|
2580
|
+
:rtype: Optional[str]
|
|
2581
|
+
"""
|
|
2582
|
+
return finding.date_last_updated if issue_status == regscale_models.IssueStatus.Closed else None
|
|
2583
|
+
|
|
2584
|
+
@staticmethod
|
|
2585
|
+
def hash_string(input_string: str) -> str:
|
|
2586
|
+
"""
|
|
2587
|
+
Hashes a string using SHA-256
|
|
2588
|
+
|
|
2589
|
+
:param str input_string: The string to hash
|
|
2590
|
+
:return: The hashed string
|
|
2591
|
+
:rtype: str
|
|
2592
|
+
"""
|
|
2593
|
+
return hashlib.sha256(input_string.encode()).hexdigest()
|
|
2594
|
+
|
|
2595
|
+
def update_control_implementation_status(
|
|
2596
|
+
self,
|
|
2597
|
+
issue: regscale_models.Issue,
|
|
2598
|
+
open_issue_ids: List[int],
|
|
2599
|
+
status: regscale_models.ImplementationStatus,
|
|
2600
|
+
) -> None:
|
|
2601
|
+
"""
|
|
2602
|
+
Updates the control implementation status based on the open issues.
|
|
2603
|
+
|
|
2604
|
+
:param regscale_models.Issue issue: The issue being closed
|
|
2605
|
+
:param List[int] open_issue_ids: List of open issue IDs
|
|
2606
|
+
:param regscale_models.ImplementationStatus status: The status to set
|
|
2607
|
+
:rtype: None
|
|
2608
|
+
"""
|
|
2609
|
+
# Method is deprecated - using update_control_implementation_status_after_close instead
|
|
2610
|
+
pass
|
|
2611
|
+
|
|
2612
|
+
def update_regscale_checklists(self, findings: List[IntegrationFinding]) -> int:
|
|
2613
|
+
"""
|
|
2614
|
+
Process checklists from IntegrationFindings, optionally using multiple threads.
|
|
2615
|
+
|
|
2616
|
+
:param List[IntegrationFinding] findings: The findings to process
|
|
2617
|
+
:return: The number of checklists processed
|
|
2618
|
+
:rtype: int
|
|
2619
|
+
"""
|
|
2620
|
+
logger.info("Updating RegScale checklists...")
|
|
2621
|
+
loading_findings = self.finding_progress.add_task(
|
|
2622
|
+
f"[#f8b737]Creating and updating checklists from {self.title}."
|
|
2623
|
+
)
|
|
2624
|
+
checklists_processed = 0
|
|
2625
|
+
|
|
2626
|
+
def process_finding(finding_to_process: IntegrationFinding) -> None:
|
|
2627
|
+
"""
|
|
2628
|
+
Process a single finding and update the progress bar.
|
|
2629
|
+
|
|
2630
|
+
:param IntegrationFinding finding_to_process: The finding to process
|
|
2631
|
+
:rtype: None
|
|
2632
|
+
"""
|
|
2633
|
+
nonlocal checklists_processed
|
|
2634
|
+
try:
|
|
2635
|
+
self.process_checklist(finding_to_process)
|
|
2636
|
+
if self.num_findings_to_process and self.finding_progress.tasks[loading_findings].total != float(
|
|
2637
|
+
self.num_findings_to_process
|
|
2638
|
+
):
|
|
2639
|
+
self.finding_progress.update(
|
|
2640
|
+
loading_findings,
|
|
2641
|
+
total=self.num_findings_to_process,
|
|
2642
|
+
description=f"[#f8b737]Creating and updating {self.num_findings_to_process} checklists from {self.title}.",
|
|
2643
|
+
)
|
|
2644
|
+
self.finding_progress.advance(loading_findings, 1)
|
|
2645
|
+
checklists_processed += 1
|
|
2646
|
+
except Exception as exc:
|
|
2647
|
+
self.log_error(
|
|
2648
|
+
"An error occurred when processing asset %s for finding %s: %s",
|
|
2649
|
+
finding.asset_identifier,
|
|
2650
|
+
finding_to_process.external_id,
|
|
2651
|
+
exc,
|
|
2652
|
+
)
|
|
2653
|
+
|
|
2654
|
+
if get_thread_workers_max() == 1:
|
|
2655
|
+
for finding in findings:
|
|
2656
|
+
process_finding(finding)
|
|
2657
|
+
else:
|
|
2658
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=get_thread_workers_max()) as executor:
|
|
2659
|
+
list(executor.map(process_finding, findings))
|
|
2660
|
+
|
|
2661
|
+
return checklists_processed
|
|
2662
|
+
|
|
2663
|
+
def create_control_test_result(
|
|
2664
|
+
self,
|
|
2665
|
+
finding: IntegrationFinding,
|
|
2666
|
+
control_test: regscale_models.ControlTest,
|
|
2667
|
+
assessment: regscale_models.Assessment,
|
|
2668
|
+
result: regscale_models.ControlTestResultStatus,
|
|
2669
|
+
) -> None:
|
|
2670
|
+
"""
|
|
2671
|
+
Create a control test result.
|
|
2672
|
+
|
|
2673
|
+
:param IntegrationFinding finding: The finding associated with the test result
|
|
2674
|
+
:param regscale_models.ControlTest control_test: The control test
|
|
2675
|
+
:param regscale_models.Assessment assessment: The assessment
|
|
2676
|
+
:param regscale_models.ControlTestResultStatus result: The result of the test
|
|
2677
|
+
:rtype: None
|
|
2678
|
+
"""
|
|
2679
|
+
regscale_models.ControlTestResult(
|
|
2680
|
+
parentTestId=control_test.id,
|
|
2681
|
+
parentAssessmentId=assessment.id,
|
|
2682
|
+
uuid=finding.external_id,
|
|
2683
|
+
result=result,
|
|
2684
|
+
dateAssessed=finding.date_created,
|
|
2685
|
+
assessedById=self.assessor_id,
|
|
2686
|
+
gaps=finding.gaps,
|
|
2687
|
+
observations=finding.observations,
|
|
2688
|
+
evidence=finding.evidence,
|
|
2689
|
+
identifiedRisk=finding.identified_risk,
|
|
2690
|
+
impact=finding.impact,
|
|
2691
|
+
recommendationForMitigation=finding.recommendation_for_mitigation,
|
|
2692
|
+
).create()
|