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,1511 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""RegScale Microsoft Defender recommendations and alerts integration"""
|
|
4
|
+
# standard python imports
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from json import JSONDecodeError
|
|
8
|
+
from os import PathLike
|
|
9
|
+
from typing import Literal, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import requests
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.progress import Progress
|
|
16
|
+
|
|
17
|
+
from regscale.core.app.api import Api
|
|
18
|
+
from regscale.core.app.internal.login import is_valid
|
|
19
|
+
from regscale.core.app.logz import create_logger
|
|
20
|
+
from regscale.core.app.utils.app_utils import (
|
|
21
|
+
check_license,
|
|
22
|
+
create_progress_object,
|
|
23
|
+
error_and_exit,
|
|
24
|
+
flatten_dict,
|
|
25
|
+
get_current_datetime,
|
|
26
|
+
reformat_str_date,
|
|
27
|
+
uncamel_case,
|
|
28
|
+
save_data_to,
|
|
29
|
+
)
|
|
30
|
+
from regscale.models import regscale_id, regscale_module, regscale_ssp_id, Asset, Component, File, Issue
|
|
31
|
+
from regscale.models.integration_models.defender_data import DefenderData
|
|
32
|
+
from regscale.models.integration_models.flat_file_importer import FlatFileImporter
|
|
33
|
+
from regscale.utils.string import generate_html_table_from_dict
|
|
34
|
+
|
|
35
|
+
LOGIN_ERROR = "Login Invalid RegScale Credentials, please login for a new token."
|
|
36
|
+
console = Console()
|
|
37
|
+
job_progress = create_progress_object()
|
|
38
|
+
logger = create_logger()
|
|
39
|
+
unique_recs = []
|
|
40
|
+
issues_to_create = []
|
|
41
|
+
closed = []
|
|
42
|
+
updated = []
|
|
43
|
+
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
44
|
+
IDENTIFICATION_TYPE = "Vulnerability Assessment"
|
|
45
|
+
CLOUD_RECS = "Microsoft Defender for Cloud Recommendation"
|
|
46
|
+
APP_JSON = "application/json"
|
|
47
|
+
AFD_ENDPOINTS = "microsoft.cdn/profiles/afdendpoints"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
######################################################################################################
|
|
51
|
+
#
|
|
52
|
+
# Adding application to Microsoft Defender API:
|
|
53
|
+
# https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/exposed-apis-create-app-webapp
|
|
54
|
+
# Microsoft Defender 365 APIs Docs:
|
|
55
|
+
# https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/exposed-apis-list?view=o365-worldwide
|
|
56
|
+
# Microsoft Defender for Cloud Alerts API Docs:
|
|
57
|
+
# https://learn.microsoft.com/en-us/rest/api/defenderforcloud/alerts?view=rest-defenderforcloud-2022-01-01
|
|
58
|
+
# Microsoft Defender for Cloud Recommendations API Docs:
|
|
59
|
+
# https://learn.microsoft.com/en-us/rest/api/defenderforcloud/assessments/list?view=rest-defenderforcloud-2020-01-01
|
|
60
|
+
# Microsoft Defender for Cloud Resources API Docs:
|
|
61
|
+
# https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph/resources/resources
|
|
62
|
+
#
|
|
63
|
+
######################################################################################################
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.group()
|
|
67
|
+
def defender():
|
|
68
|
+
"""Create RegScale issues for each Microsoft Defender 365 Recommendation"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@defender.command(name="authenticate")
|
|
72
|
+
@click.option(
|
|
73
|
+
"--system",
|
|
74
|
+
type=click.Choice(["cloud", "365"], case_sensitive=False),
|
|
75
|
+
help="Pull recommendations from Microsoft Defender 365 or Microsoft Defender for Cloud.",
|
|
76
|
+
prompt="Please choose a system",
|
|
77
|
+
required=True,
|
|
78
|
+
)
|
|
79
|
+
def authenticate_in_defender(system: Literal["cloud", "365"]):
|
|
80
|
+
"""Obtains an access token using the credentials provided in init.yaml."""
|
|
81
|
+
authenticate(system=system)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@defender.command(name="sync_365_alerts")
|
|
85
|
+
@regscale_id(required=False, default=None, prompt=False)
|
|
86
|
+
@regscale_module(required=False, default=None, prompt=False)
|
|
87
|
+
def sync_365_alerts(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
|
|
88
|
+
"""
|
|
89
|
+
Get Microsoft Defender 365 alerts and create RegScale
|
|
90
|
+
issues with the information from Microsoft Defender 365.
|
|
91
|
+
"""
|
|
92
|
+
sync_defender_and_regscale(
|
|
93
|
+
parent_id=regscale_id, parent_module=regscale_module, system="365", defender_object="alerts"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@defender.command(name="sync_365_recommendations")
|
|
98
|
+
@regscale_id(required=False, default=None, prompt=False)
|
|
99
|
+
@regscale_module(required=False, default=None, prompt=False)
|
|
100
|
+
def sync_365_recommendations(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
|
|
101
|
+
"""
|
|
102
|
+
Get Microsoft Defender 365 recommendations and create RegScale
|
|
103
|
+
issues with the information from Microsoft Defender 365.
|
|
104
|
+
"""
|
|
105
|
+
sync_defender_and_regscale(
|
|
106
|
+
parent_id=regscale_id, parent_module=regscale_module, system="365", defender_object="recommendations"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@defender.command(name="sync_cloud_resources")
|
|
111
|
+
@regscale_ssp_id()
|
|
112
|
+
def sync_cloud_resources(regscale_ssp_id: int):
|
|
113
|
+
"""
|
|
114
|
+
Get Microsoft Defender for Cloud resources and create RegScale assets with the information from Microsoft
|
|
115
|
+
Defender for Cloud.
|
|
116
|
+
"""
|
|
117
|
+
sync_resources(ssp_id=regscale_ssp_id)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@defender.command(name="export_resources")
|
|
121
|
+
@regscale_id()
|
|
122
|
+
@regscale_module()
|
|
123
|
+
@click.option(
|
|
124
|
+
"--query_name",
|
|
125
|
+
"-q",
|
|
126
|
+
"-n",
|
|
127
|
+
type=click.STRING,
|
|
128
|
+
help="The name of the saved query to export from Microsoft Defender for Cloud resource graph queries.",
|
|
129
|
+
prompt="Enter the name of the query to export",
|
|
130
|
+
default=None,
|
|
131
|
+
)
|
|
132
|
+
@click.option(
|
|
133
|
+
"--no_upload",
|
|
134
|
+
"-n",
|
|
135
|
+
is_flag=True,
|
|
136
|
+
help="Flag to skip uploading the exported .csv file to RegScale.",
|
|
137
|
+
default=False,
|
|
138
|
+
)
|
|
139
|
+
@click.option(
|
|
140
|
+
"--all_queries",
|
|
141
|
+
"-a",
|
|
142
|
+
is_flag=True,
|
|
143
|
+
help="Export all saved queries from Microsoft Defender for Cloud resource graph queries.",
|
|
144
|
+
)
|
|
145
|
+
def export_resources_to_csv(
|
|
146
|
+
regscale_id: int, regscale_module: str, query_name: str, no_upload: bool, all_queries: bool
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
Export data from Microsoft Defender for Cloud queries and save them to a .csv file.
|
|
150
|
+
"""
|
|
151
|
+
export_resources(
|
|
152
|
+
parent_id=regscale_id,
|
|
153
|
+
parent_module=regscale_module,
|
|
154
|
+
query_name=query_name,
|
|
155
|
+
no_upload=no_upload,
|
|
156
|
+
all_queries=all_queries,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@defender.command(name="sync_cloud_alerts")
|
|
161
|
+
@regscale_id(required=False, default=None, prompt=False)
|
|
162
|
+
@regscale_module(required=False, default=None, prompt=False)
|
|
163
|
+
def sync_cloud_alerts(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
|
|
164
|
+
"""
|
|
165
|
+
Get Microsoft Defender for Cloud alerts and create RegScale
|
|
166
|
+
issues with the information from Microsoft Defender for Cloud.
|
|
167
|
+
"""
|
|
168
|
+
sync_defender_and_regscale(
|
|
169
|
+
parent_id=regscale_id, parent_module=regscale_module, system="cloud", defender_object="alerts"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@defender.command(name="sync_cloud_recommendations")
|
|
174
|
+
@regscale_id(required=False, default=None, prompt=False)
|
|
175
|
+
@regscale_module(required=False, default=None, prompt=False)
|
|
176
|
+
def sync_cloud_recommendations(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
|
|
177
|
+
"""
|
|
178
|
+
Get Microsoft Defender for Cloud recommendations and create RegScale
|
|
179
|
+
issues with the information from Microsoft Defender for Cloud.
|
|
180
|
+
"""
|
|
181
|
+
sync_defender_and_regscale(
|
|
182
|
+
parent_id=regscale_id, parent_module=regscale_module, system="cloud", defender_object="recommendations"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@defender.command(name="import_alerts")
|
|
187
|
+
@FlatFileImporter.common_scanner_options(
|
|
188
|
+
message="File path to the folder containing Defender .csv files to process to RegScale.",
|
|
189
|
+
prompt="File path to Defender files",
|
|
190
|
+
import_name="defender",
|
|
191
|
+
)
|
|
192
|
+
def import_alerts(
|
|
193
|
+
folder_path: PathLike[str],
|
|
194
|
+
regscale_ssp_id: int,
|
|
195
|
+
scan_date: datetime,
|
|
196
|
+
mappings_path: Path,
|
|
197
|
+
disable_mapping: bool,
|
|
198
|
+
s3_bucket: str,
|
|
199
|
+
s3_prefix: str,
|
|
200
|
+
aws_profile: str,
|
|
201
|
+
upload_file: bool,
|
|
202
|
+
):
|
|
203
|
+
"""
|
|
204
|
+
Import Microsoft Defender alerts from a CSV file
|
|
205
|
+
"""
|
|
206
|
+
import_defender_alerts(
|
|
207
|
+
folder_path,
|
|
208
|
+
regscale_ssp_id,
|
|
209
|
+
scan_date,
|
|
210
|
+
mappings_path,
|
|
211
|
+
disable_mapping,
|
|
212
|
+
s3_bucket,
|
|
213
|
+
s3_prefix,
|
|
214
|
+
aws_profile,
|
|
215
|
+
upload_file,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def import_defender_alerts(
|
|
220
|
+
folder_path: PathLike[str],
|
|
221
|
+
regscale_ssp_id: int,
|
|
222
|
+
scan_date: datetime,
|
|
223
|
+
mappings_path: Path,
|
|
224
|
+
disable_mapping: bool,
|
|
225
|
+
s3_bucket: str,
|
|
226
|
+
s3_prefix: str,
|
|
227
|
+
aws_profile: str,
|
|
228
|
+
upload_file: Optional[bool] = True,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Import Microsoft Defender alerts from a CSV file
|
|
232
|
+
|
|
233
|
+
:param PathLike[str] folder_path: File path to the folder containing Defender .csv files to process to RegScale
|
|
234
|
+
:param int regscale_ssp_id: The RegScale SSP ID
|
|
235
|
+
:param datetime scan_date: The date of the scan
|
|
236
|
+
:param Path mappings_path: The path to the mappings file
|
|
237
|
+
:param bool disable_mapping: Whether to disable custom mappings
|
|
238
|
+
:param str s3_bucket: The S3 bucket to download the files from
|
|
239
|
+
:param str s3_prefix: The S3 prefix to download the files from
|
|
240
|
+
:param str aws_profile: The AWS profile to use for S3 access
|
|
241
|
+
:param Optional[bool] upload_file: Whether to upload the file to RegScale after processing, defaults to True
|
|
242
|
+
:rtype: None
|
|
243
|
+
"""
|
|
244
|
+
from regscale.models.integration_models.defenderimport import DefenderImport
|
|
245
|
+
|
|
246
|
+
FlatFileImporter.import_files(
|
|
247
|
+
import_type=DefenderImport,
|
|
248
|
+
import_name="Defender",
|
|
249
|
+
file_types=".csv",
|
|
250
|
+
folder_path=folder_path,
|
|
251
|
+
regscale_ssp_id=regscale_ssp_id,
|
|
252
|
+
scan_date=scan_date,
|
|
253
|
+
mappings_path=mappings_path,
|
|
254
|
+
disable_mapping=disable_mapping,
|
|
255
|
+
s3_bucket=s3_bucket,
|
|
256
|
+
s3_prefix=s3_prefix,
|
|
257
|
+
aws_profile=aws_profile,
|
|
258
|
+
upload_file=upload_file,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def authenticate(system: Literal["cloud", "365"]) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Obtains an access token using the credentials provided in init.yaml
|
|
265
|
+
|
|
266
|
+
:param Literal["cloud", "365"] system: The system to authenticate for, either Defender 365 or Defender for Cloud
|
|
267
|
+
:rtype: None
|
|
268
|
+
"""
|
|
269
|
+
app = check_license()
|
|
270
|
+
api = Api()
|
|
271
|
+
if system == "365":
|
|
272
|
+
url = "https://api.securitycenter.microsoft.com/api/alerts"
|
|
273
|
+
elif system == "cloud":
|
|
274
|
+
url = (
|
|
275
|
+
f'https://management.azure.com/subscriptions/{app.config["azureCloudSubscriptionId"]}/'
|
|
276
|
+
+ "providers/Microsoft.Security/alerts?api-version=2022-01-01"
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
error_and_exit("Please enter 365 or cloud for the system.")
|
|
280
|
+
check_token(api=api, system=system, url=url)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def sync_defender_and_regscale(
|
|
284
|
+
parent_id: Optional[int] = None,
|
|
285
|
+
parent_module: Optional[str] = None,
|
|
286
|
+
system: Literal["365", "cloud"] = "365",
|
|
287
|
+
defender_object: Literal["alerts", "recommendations"] = "recommendations",
|
|
288
|
+
) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Sync Microsoft Defender data with RegScale
|
|
291
|
+
|
|
292
|
+
:param Optional[int] parent_id: The RegScale ID to sync the alerts to, defaults to None
|
|
293
|
+
:param Optional[str] parent_module: The RegScale module to sync the alerts to, defaults to None
|
|
294
|
+
:param Literal["365", "cloud"] system: The system to sync the alerts from, defaults to "365"
|
|
295
|
+
:param Literal["alerts", "recommendations"] defender_object: The type of data to sync, defaults to "recommendations"
|
|
296
|
+
:rtype: None
|
|
297
|
+
"""
|
|
298
|
+
app = check_license()
|
|
299
|
+
api = Api()
|
|
300
|
+
# check if RegScale token is valid:
|
|
301
|
+
if not is_valid(app=app):
|
|
302
|
+
error_and_exit(LOGIN_ERROR)
|
|
303
|
+
mapping_key = f"{system}_{defender_object}"
|
|
304
|
+
url_mapping = {
|
|
305
|
+
"365_alerts": "https://api.securitycenter.microsoft.com/api/alerts",
|
|
306
|
+
"365_recommendations": "https://api.securitycenter.microsoft.com/api/recommendations",
|
|
307
|
+
"cloud_alerts": f'https://management.azure.com/subscriptions/{app.config["azureCloudSubscriptionId"]}/'
|
|
308
|
+
+ "providers/Microsoft.Security/alerts?api-version=2022-01-01",
|
|
309
|
+
"cloud_recommendations": f"https://management.azure.com/subscriptions/{app.config['azureCloudSubscriptionId']}/"
|
|
310
|
+
+ "providers/Microsoft.Security/assessments?api-version=2020-01-01&$expand=metadata",
|
|
311
|
+
}
|
|
312
|
+
url = url_mapping[mapping_key]
|
|
313
|
+
defender_key = "id" if system == "365" else "name"
|
|
314
|
+
mapping_func = {
|
|
315
|
+
"365_alerts": map_365_alert_to_issue,
|
|
316
|
+
"365_recommendations": map_365_recommendation_to_issue,
|
|
317
|
+
"cloud_alerts": map_cloud_alert_to_issue,
|
|
318
|
+
"cloud_recommendations": map_cloud_recommendation_to_issue,
|
|
319
|
+
}
|
|
320
|
+
# check the azure token, get a new one if needed
|
|
321
|
+
token = check_token(api=api, system=system, url=url)
|
|
322
|
+
|
|
323
|
+
# set headers for the data
|
|
324
|
+
headers = {"Content-Type": APP_JSON, "Authorization": token}
|
|
325
|
+
logging_object = f"{defender_object[:-1]}(s)"
|
|
326
|
+
logging_system = "365" if system == "365" else "for Cloud"
|
|
327
|
+
logger.info(f"Retrieving Microsoft Defender {system.title()} {logging_object}...")
|
|
328
|
+
if defender_objects := get_items_from_azure(
|
|
329
|
+
api=api,
|
|
330
|
+
headers=headers,
|
|
331
|
+
url=url,
|
|
332
|
+
):
|
|
333
|
+
defender_data = [
|
|
334
|
+
DefenderData(id=data[defender_key], data=data, system=system, object=defender_object)
|
|
335
|
+
for data in defender_objects
|
|
336
|
+
]
|
|
337
|
+
integration_field = defender_data[0].integration_field
|
|
338
|
+
logger.info(f"Found {len(defender_data)} Microsoft Defender {logging_system} {logging_object}.")
|
|
339
|
+
else:
|
|
340
|
+
defender_data = []
|
|
341
|
+
integration_field = DefenderData.get_integration_field(system=system, object=defender_object)
|
|
342
|
+
logger.info(f"No Microsoft Defender {logging_system} {defender_object} found.")
|
|
343
|
+
|
|
344
|
+
# get all issues from RegScale where the defenderId field is populated
|
|
345
|
+
# if regscale_id and regscale_module aren't provided
|
|
346
|
+
if parent_id and parent_module:
|
|
347
|
+
app.logger.info(f"Retrieving issues from RegScale for {parent_module} #{parent_id}...")
|
|
348
|
+
issues = Issue.get_all_by_parent(parent_id=parent_id, parent_module=parent_module)
|
|
349
|
+
# sort the issues that have the integration field populated
|
|
350
|
+
issues = [issue for issue in issues if getattr(issue, integration_field, None)]
|
|
351
|
+
elif mapping_key == "cloud_recommendations":
|
|
352
|
+
app.logger.warning(f"Retrieving all issues with {integration_field} populated in RegScale...")
|
|
353
|
+
issues = Issue.get_all_by_manual_detection_source(value=CLOUD_RECS)
|
|
354
|
+
else:
|
|
355
|
+
app.logger.warning(f"Retrieving all issues with {integration_field} populated in RegScale...")
|
|
356
|
+
issues = Issue.get_all_by_integration_field(field=integration_field)
|
|
357
|
+
logger.info(f"Retrieved {len(issues)} issue(s) from RegScale.")
|
|
358
|
+
|
|
359
|
+
regscale_issues = [
|
|
360
|
+
DefenderData(
|
|
361
|
+
id=getattr(issue, integration_field, ""), data=issue.model_dump(), system=system, object=defender_object
|
|
362
|
+
)
|
|
363
|
+
for issue in issues
|
|
364
|
+
]
|
|
365
|
+
new_issues = []
|
|
366
|
+
# create progress bars for each threaded task
|
|
367
|
+
with job_progress:
|
|
368
|
+
# see if there are any issues with defender id populated
|
|
369
|
+
if regscale_issues:
|
|
370
|
+
logger.info(f"{len(regscale_issues)} RegScale issue(s) will be analyzed.")
|
|
371
|
+
# create progress bar and analyze the RegScale issues
|
|
372
|
+
analyze_regscale_issues = job_progress.add_task(
|
|
373
|
+
f"[#f8b737]Analyzing {len(regscale_issues)} RegScale issue(s)...", total=len(regscale_issues)
|
|
374
|
+
)
|
|
375
|
+
# evaluate open issues in RegScale
|
|
376
|
+
app.thread_manager.submit_tasks_from_list(
|
|
377
|
+
evaluate_open_issues,
|
|
378
|
+
regscale_issues,
|
|
379
|
+
(
|
|
380
|
+
api,
|
|
381
|
+
defender_data,
|
|
382
|
+
analyze_regscale_issues,
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
_ = app.thread_manager.execute_and_verify()
|
|
386
|
+
else:
|
|
387
|
+
logger.info("No issues from RegScale need to be analyzed.")
|
|
388
|
+
# compare defender 365 recommendations and RegScale issues
|
|
389
|
+
# while removing duplicates, updating existing RegScale Issues,
|
|
390
|
+
# and adding new unique recommendations to unique_recs global variable
|
|
391
|
+
if defender_data:
|
|
392
|
+
logger.info(
|
|
393
|
+
f"Comparing {len(defender_data)} Microsoft Defender {logging_system} {logging_object} "
|
|
394
|
+
f"and {len(regscale_issues)} RegScale issue(s).",
|
|
395
|
+
)
|
|
396
|
+
compare_task = job_progress.add_task(
|
|
397
|
+
f"[#ef5d23]Comparing {len(defender_data)} Microsoft Defender {logging_system} {logging_object} and "
|
|
398
|
+
+ f"{len(regscale_issues)} RegScale issue(s)...",
|
|
399
|
+
total=len(defender_data),
|
|
400
|
+
)
|
|
401
|
+
app.thread_manager.submit_tasks_from_list(
|
|
402
|
+
compare_defender_and_regscale,
|
|
403
|
+
defender_data,
|
|
404
|
+
(
|
|
405
|
+
api,
|
|
406
|
+
regscale_issues,
|
|
407
|
+
defender_key,
|
|
408
|
+
compare_task,
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
_ = app.thread_manager.execute_and_verify()
|
|
412
|
+
# start threads and progress bar for # of issues that need to be created
|
|
413
|
+
if len(unique_recs) > 0:
|
|
414
|
+
logger.info("Prepping %s issue(s) for creation in RegScale.", len(unique_recs))
|
|
415
|
+
create_issues = job_progress.add_task(
|
|
416
|
+
f"[#21a5bb]Prepping {len(unique_recs)} issue(s) for creation in RegScale...",
|
|
417
|
+
total=len(unique_recs),
|
|
418
|
+
)
|
|
419
|
+
app.thread_manager.submit_tasks_from_list(
|
|
420
|
+
prep_issues_for_creation,
|
|
421
|
+
unique_recs,
|
|
422
|
+
(
|
|
423
|
+
mapping_func[mapping_key],
|
|
424
|
+
api.config,
|
|
425
|
+
defender_key,
|
|
426
|
+
parent_id,
|
|
427
|
+
parent_module,
|
|
428
|
+
create_issues,
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
_ = app.thread_manager.execute_and_verify()
|
|
432
|
+
logger.info(
|
|
433
|
+
"%s/%s issue(s) ready for creation in RegScale.",
|
|
434
|
+
len(issues_to_create),
|
|
435
|
+
len(unique_recs),
|
|
436
|
+
)
|
|
437
|
+
new_issues = Issue.batch_create(issues_to_create, progress_context=job_progress)
|
|
438
|
+
logger.info(f"Created {len(new_issues)} issue(s) in RegScale.")
|
|
439
|
+
# check if issues needed to be created, updated or closed and print the appropriate message
|
|
440
|
+
if (len(unique_recs) + len(updated) + len(closed)) == 0:
|
|
441
|
+
logger.info("[green]No changes required for existing RegScale issue(s)!")
|
|
442
|
+
else:
|
|
443
|
+
logger.info(
|
|
444
|
+
f"{len(new_issues)} issue(s) created, {len(updated)} issue(s)"
|
|
445
|
+
+ f" updated and {len(closed)} issue(s) were closed in RegScale."
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def check_token(api: Api, system: Literal["cloud", "365"], url: Optional[str] = None) -> str:
|
|
450
|
+
"""
|
|
451
|
+
Function to check if current Azure token from init.yaml is valid, if not replace it
|
|
452
|
+
|
|
453
|
+
:param Api api: API object
|
|
454
|
+
:param Literal["cloud", "365"] system: Which system to check JWT for, either Defender 365 or Defender for Cloud
|
|
455
|
+
:param str url: The URL to use for authentication, defaults to None
|
|
456
|
+
:return: returns JWT for Microsoft 365 Defender or Microsoft Defender for Cloud depending on system provided
|
|
457
|
+
:rtype: str
|
|
458
|
+
"""
|
|
459
|
+
# set up variables for the provided system
|
|
460
|
+
if system == "cloud":
|
|
461
|
+
key = "azureCloudAccessToken"
|
|
462
|
+
elif system.lower() == "365":
|
|
463
|
+
key = "azure365AccessToken"
|
|
464
|
+
else:
|
|
465
|
+
error_and_exit(
|
|
466
|
+
f"{system.title()} is not supported, only Microsoft 365 Defender and Microsoft Defender for Cloud."
|
|
467
|
+
)
|
|
468
|
+
current_token = api.config[key]
|
|
469
|
+
# check the token if it isn't blank
|
|
470
|
+
if current_token and url:
|
|
471
|
+
# set the headers
|
|
472
|
+
header = {"Content-Type": APP_JSON, "Authorization": current_token}
|
|
473
|
+
# test current token by getting recommendations
|
|
474
|
+
token_pass = api.get(url=url, headers=header)
|
|
475
|
+
# check the status code
|
|
476
|
+
if getattr(token_pass, "status_code", 0) == 200:
|
|
477
|
+
# token still valid, return it
|
|
478
|
+
token = api.config[key]
|
|
479
|
+
logger.info(
|
|
480
|
+
"Current token for %s is still valid and will be used for future requests.",
|
|
481
|
+
system.title(),
|
|
482
|
+
)
|
|
483
|
+
elif getattr(token_pass, "status_code", 0) == 403:
|
|
484
|
+
# token doesn't have permissions, notify user and exit
|
|
485
|
+
error_and_exit(
|
|
486
|
+
"Incorrect permissions set for application. Cannot retrieve recommendations.\n"
|
|
487
|
+
+ f"{token_pass.status_code}: {token_pass.reason}\n{token_pass.text}"
|
|
488
|
+
)
|
|
489
|
+
else:
|
|
490
|
+
# token is no longer valid, get a new one
|
|
491
|
+
token = get_token(api=api, system=system)
|
|
492
|
+
# token is empty, get a new token
|
|
493
|
+
else:
|
|
494
|
+
token = get_token(api=api, system=system)
|
|
495
|
+
return token
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def get_token(api: Api, system: Literal["cloud", "365"]) -> str:
|
|
499
|
+
"""
|
|
500
|
+
Function to get a token from Microsoft Azure and saves it to init.yaml
|
|
501
|
+
|
|
502
|
+
:param Api api: API object
|
|
503
|
+
:param Literal[str] system: Which platform to authenticate for Microsoft Defender, cloud or 365
|
|
504
|
+
:return: JWT from Azure
|
|
505
|
+
:rtype: str
|
|
506
|
+
"""
|
|
507
|
+
# set the url and body for request
|
|
508
|
+
if system == "365":
|
|
509
|
+
url = f'https://login.windows.net/{api.config["azure365TenantId"]}/oauth2/token'
|
|
510
|
+
client_id = api.config["azure365ClientId"]
|
|
511
|
+
client_secret = api.config["azure365Secret"]
|
|
512
|
+
resource = "https://api.securitycenter.windows.com"
|
|
513
|
+
key = "azure365AccessToken"
|
|
514
|
+
elif system == "cloud":
|
|
515
|
+
url = f'https://login.microsoftonline.com/{api.config["azureCloudTenantId"]}/oauth2/token'
|
|
516
|
+
client_id = api.config["azureCloudClientId"]
|
|
517
|
+
client_secret = api.config["azureCloudSecret"]
|
|
518
|
+
resource = "https://management.azure.com"
|
|
519
|
+
key = "azureCloudAccessToken"
|
|
520
|
+
else:
|
|
521
|
+
error_and_exit(
|
|
522
|
+
f"{system.title()} is not supported, only Microsoft `365` Defender and Microsoft Defender for `Cloud`."
|
|
523
|
+
)
|
|
524
|
+
data = {
|
|
525
|
+
"resource": resource,
|
|
526
|
+
"client_id": client_id,
|
|
527
|
+
"client_secret": client_secret,
|
|
528
|
+
"grant_type": "client_credentials",
|
|
529
|
+
}
|
|
530
|
+
# get the data
|
|
531
|
+
response = api.post(
|
|
532
|
+
url=url,
|
|
533
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
534
|
+
data=data,
|
|
535
|
+
)
|
|
536
|
+
try:
|
|
537
|
+
return parse_and_save_token(response, api, key, system)
|
|
538
|
+
except KeyError as ex:
|
|
539
|
+
# notify user we weren't able to get a token and exit
|
|
540
|
+
error_and_exit(f"Didn't receive token from Azure.\n{ex}\n{response.text}")
|
|
541
|
+
except JSONDecodeError as ex:
|
|
542
|
+
# notify user we weren't able to get a token and exit
|
|
543
|
+
error_and_exit(f"Unable to authenticate with Azure.\n{ex}\n{response.text}")
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def parse_and_save_token(response: requests.Response, api: Api, key: str, system: str) -> str:
|
|
547
|
+
"""
|
|
548
|
+
Function to parse the token from the response and save it to init.yaml
|
|
549
|
+
|
|
550
|
+
:param requests.Response response: Response from API call
|
|
551
|
+
:param Api api: API object
|
|
552
|
+
:param str key: Key to use for init.yaml token update
|
|
553
|
+
:param str system: Which system to check JWT for, either Defender 365 or Defender for Cloud
|
|
554
|
+
:return: JWT from Azure for the provided system
|
|
555
|
+
:rtype: str
|
|
556
|
+
"""
|
|
557
|
+
# try to read the response and parse the token
|
|
558
|
+
res = response.json()
|
|
559
|
+
token = res["access_token"]
|
|
560
|
+
|
|
561
|
+
# add the token to init.yaml
|
|
562
|
+
api.config[key] = f"Bearer {token}"
|
|
563
|
+
|
|
564
|
+
# write the changes back to file
|
|
565
|
+
api.app.save_config(api.config) # type: ignore
|
|
566
|
+
|
|
567
|
+
# notify the user we were successful
|
|
568
|
+
logger.info(f"Azure {system.title()} Login Successful! Init.yaml file was updated with the new access token.")
|
|
569
|
+
# return the token string
|
|
570
|
+
return api.config[key]
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def get_items_from_azure(api: Api, headers: dict, url: str) -> list:
|
|
574
|
+
"""
|
|
575
|
+
Function to get data from Microsoft Defender returns the data as a list while handling pagination
|
|
576
|
+
|
|
577
|
+
:param Api api: API object
|
|
578
|
+
:param dict headers: Headers used for API call
|
|
579
|
+
:param str url: URL to use for the API call
|
|
580
|
+
:return: list of recommendations
|
|
581
|
+
:rtype: list
|
|
582
|
+
"""
|
|
583
|
+
# get the data via api call
|
|
584
|
+
response = api.get(url=url, headers=headers)
|
|
585
|
+
if response.status_code != 200:
|
|
586
|
+
error_and_exit(
|
|
587
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
|
|
588
|
+
+ f"\n{response.text}",
|
|
589
|
+
)
|
|
590
|
+
# try to read the response
|
|
591
|
+
try:
|
|
592
|
+
response_data = response.json()
|
|
593
|
+
# try to get the values from the api response
|
|
594
|
+
defender_data = response_data["value"]
|
|
595
|
+
except JSONDecodeError:
|
|
596
|
+
# notify user if there was a json decode error from API response and exit
|
|
597
|
+
error_and_exit("JSON Decode error")
|
|
598
|
+
except KeyError:
|
|
599
|
+
# notify user there was no data from API response and exit
|
|
600
|
+
error_and_exit(
|
|
601
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.text}"
|
|
602
|
+
)
|
|
603
|
+
# check if pagination is required to fetch all data from Microsoft Defender
|
|
604
|
+
if next_link := response_data.get("nextLink"):
|
|
605
|
+
# get the rest of the data
|
|
606
|
+
defender_data.extend(get_items_from_azure(api=api, headers=headers, url=next_link))
|
|
607
|
+
# return the defender recommendations
|
|
608
|
+
return defender_data
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def get_due_date(score: Union[str, int, None], config: dict, key: str) -> str:
|
|
612
|
+
"""
|
|
613
|
+
Function to return due date based on the severity score of
|
|
614
|
+
the Microsoft Defender recommendation; the values are in the init.yaml
|
|
615
|
+
and if not, use the industry standards
|
|
616
|
+
|
|
617
|
+
:param Union[str, int, None] score: Severity score from Microsoft Defender
|
|
618
|
+
:param dict config: Application config
|
|
619
|
+
:param str key: The key to use for init.yaml
|
|
620
|
+
:return: Due date for the issue
|
|
621
|
+
:rtype: str
|
|
622
|
+
"""
|
|
623
|
+
# check severity score and assign it to the appropriate due date
|
|
624
|
+
# using the init.yaml specified days
|
|
625
|
+
today = datetime.now().strftime("%m/%d/%y")
|
|
626
|
+
|
|
627
|
+
if not score:
|
|
628
|
+
score = 0
|
|
629
|
+
|
|
630
|
+
# check if the score is a string, if so convert it to an int & determine due date
|
|
631
|
+
if isinstance(score, str):
|
|
632
|
+
if score.lower() == "low":
|
|
633
|
+
score = 3
|
|
634
|
+
elif score.lower() == "medium":
|
|
635
|
+
score = 5
|
|
636
|
+
elif score.lower() == "high":
|
|
637
|
+
score = 9
|
|
638
|
+
else:
|
|
639
|
+
score = 0
|
|
640
|
+
if score >= 7:
|
|
641
|
+
days = config["issues"][key]["high"]
|
|
642
|
+
elif 4 <= score < 7:
|
|
643
|
+
days = config["issues"][key]["moderate"]
|
|
644
|
+
else:
|
|
645
|
+
days = config["issues"][key]["low"]
|
|
646
|
+
due_date = datetime.strptime(today, "%m/%d/%y") + timedelta(days=days)
|
|
647
|
+
return due_date.strftime(DATE_FORMAT)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def format_description(defender_data: dict, tenant_id: str) -> str:
|
|
651
|
+
"""
|
|
652
|
+
Function to format the provided dictionary into an HTML table
|
|
653
|
+
|
|
654
|
+
:param dict defender_data: Microsoft Defender data as a dictionary
|
|
655
|
+
:param str tenant_id: The Microsoft Defender tenant ID
|
|
656
|
+
:return: HTML table as a string
|
|
657
|
+
:rtype: str
|
|
658
|
+
"""
|
|
659
|
+
url = get_defender_url(defender_data, tenant_id)
|
|
660
|
+
defender_data = flatten_dict(data=defender_data)
|
|
661
|
+
payload = create_payload(defender_data) # type: ignore
|
|
662
|
+
description = create_html_table(payload, url)
|
|
663
|
+
return description
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def get_defender_url(rec: dict, tenant_id: str) -> str:
|
|
667
|
+
"""
|
|
668
|
+
Function to get the URL for the Microsoft Defender data
|
|
669
|
+
|
|
670
|
+
:param dict rec: Microsoft Defender data as a dictionary
|
|
671
|
+
:param str tenant_id: The Microsoft Defender tenant ID
|
|
672
|
+
:return: URL as a string
|
|
673
|
+
:rtype: str
|
|
674
|
+
"""
|
|
675
|
+
try:
|
|
676
|
+
url = rec["properties"]["alertUri"]
|
|
677
|
+
except KeyError:
|
|
678
|
+
url = f"https://security.microsoft.com/security-recommendations?tid={tenant_id}"
|
|
679
|
+
return f'<a href="{url}">{url}</a>'
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def create_payload(rec: dict) -> dict:
|
|
683
|
+
"""
|
|
684
|
+
Function to create a payload for the Microsoft Defender data
|
|
685
|
+
|
|
686
|
+
:param dict rec: Microsoft Defender data as a dictionary
|
|
687
|
+
:return: Payload as a dictionary
|
|
688
|
+
:rtype: dict
|
|
689
|
+
"""
|
|
690
|
+
payload = {}
|
|
691
|
+
skip_keys = ["associatedthreats", "alerturi", "investigation steps"]
|
|
692
|
+
for key, value in rec.items():
|
|
693
|
+
key = key.replace("propertiesExtendedProperties", "").replace("properties", "")
|
|
694
|
+
if isinstance(value, list) and len(value) > 0 and key.lower() not in skip_keys:
|
|
695
|
+
payload[uncamel_case(key)] = process_list_value(value)
|
|
696
|
+
elif key.lower() not in skip_keys and "entities" not in key.lower():
|
|
697
|
+
if not isinstance(value, list):
|
|
698
|
+
payload[uncamel_case(key)] = value
|
|
699
|
+
return payload
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def process_list_value(value: list) -> str:
|
|
703
|
+
"""
|
|
704
|
+
Function to process the list value for the Microsoft Defender data
|
|
705
|
+
|
|
706
|
+
:param list value: List of values
|
|
707
|
+
:return: Processed list value as a string
|
|
708
|
+
:rtype: str
|
|
709
|
+
"""
|
|
710
|
+
if isinstance(value[0], dict):
|
|
711
|
+
return "".join(f"</br>{k}: {v}" for item in value for k, v in item.items())
|
|
712
|
+
elif isinstance(value[0], list):
|
|
713
|
+
return "".join("</br>".join(item) for item in value)
|
|
714
|
+
else:
|
|
715
|
+
return "</br>".join(value)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def create_html_table(payload: dict, url: str) -> str:
|
|
719
|
+
"""
|
|
720
|
+
Function to create an HTML table for the Microsoft Defender data
|
|
721
|
+
|
|
722
|
+
:param dict payload: Payload for the Microsoft Defender data
|
|
723
|
+
:param str url: URL for the Microsoft Defender data
|
|
724
|
+
:return: HTML table as a string
|
|
725
|
+
:rtype: str
|
|
726
|
+
"""
|
|
727
|
+
description = '<table style="border: 1px solid;">'
|
|
728
|
+
for key, value in payload.items():
|
|
729
|
+
if value:
|
|
730
|
+
if "time" in key.lower():
|
|
731
|
+
value = reformat_str_date(value, dt_format="%b %d, %Y")
|
|
732
|
+
description += (
|
|
733
|
+
f'<tr><td style="border: 1px solid;"><b>{key}</b></td>'
|
|
734
|
+
f'<td style="border: 1px solid;">{value}</td></tr>'
|
|
735
|
+
)
|
|
736
|
+
description += (
|
|
737
|
+
'<tr><td style="border: 1px solid;"><b>View in Defender</b></td>'
|
|
738
|
+
f'<td style="border: 1px solid;">{url}</td></tr>'
|
|
739
|
+
)
|
|
740
|
+
description += "</table>"
|
|
741
|
+
return description
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def compare_defender_and_regscale(def_data: DefenderData, args: Tuple) -> None:
|
|
745
|
+
"""
|
|
746
|
+
Function to check for duplicates between issues in RegScale
|
|
747
|
+
and recommendations/alerts from Microsoft Defender while using threads
|
|
748
|
+
|
|
749
|
+
:param DefenderData def_data: Microsoft Defender data
|
|
750
|
+
:param Tuple args: Tuple of args to use during the process
|
|
751
|
+
:rtype: None
|
|
752
|
+
"""
|
|
753
|
+
# set local variables with the args that were passed
|
|
754
|
+
api, issues, defender_key, task = args
|
|
755
|
+
|
|
756
|
+
# see if recommendation has been analyzed already
|
|
757
|
+
if not def_data.analyzed:
|
|
758
|
+
# change analyzed flag
|
|
759
|
+
def_data.analyzed = True
|
|
760
|
+
|
|
761
|
+
# set duplication flag to false
|
|
762
|
+
dupe_check = False
|
|
763
|
+
|
|
764
|
+
# iterate through the RegScale issues with defenderId populated
|
|
765
|
+
for issue in issues:
|
|
766
|
+
# check if the RegScale key == Windows Defender ID
|
|
767
|
+
if issue.data.get(issue.integration_field) == def_data.data[defender_key]:
|
|
768
|
+
# change the duplication flag to True
|
|
769
|
+
dupe_check = True
|
|
770
|
+
# check if the RegScale issue is closed or cancelled
|
|
771
|
+
if issue.data["status"].lower() in ["closed", "cancelled"]:
|
|
772
|
+
# reopen RegScale issue because Microsoft Defender has
|
|
773
|
+
# recommended it again
|
|
774
|
+
change_issue_status(
|
|
775
|
+
api=api,
|
|
776
|
+
status=api.config["issues"][issue.init_key]["status"],
|
|
777
|
+
issue=issue.data,
|
|
778
|
+
rec=def_data,
|
|
779
|
+
rec_type=issue.init_key,
|
|
780
|
+
)
|
|
781
|
+
# check if the recommendation is a duplicate
|
|
782
|
+
if dupe_check is False:
|
|
783
|
+
# append unique recommendation to global unique_reqs
|
|
784
|
+
unique_recs.append(def_data)
|
|
785
|
+
job_progress.update(task, advance=1)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def evaluate_open_issues(issue: DefenderData, args: Tuple) -> None:
|
|
789
|
+
"""
|
|
790
|
+
function to check for Open RegScale issues against Microsoft
|
|
791
|
+
Defender recommendations and will close the issues that are
|
|
792
|
+
no longer recommended by Microsoft Defender while using threads
|
|
793
|
+
|
|
794
|
+
:param DefenderData issue: Microsoft Defender data
|
|
795
|
+
:param Tuple args: Tuple of args to use during the process
|
|
796
|
+
:rtype: None
|
|
797
|
+
"""
|
|
798
|
+
# set up local variables from the passed args
|
|
799
|
+
api, defender_data, task = args
|
|
800
|
+
|
|
801
|
+
defender_data_dict = {defender_data.id: defender_data for defender_data in defender_data if defender_data.id}
|
|
802
|
+
|
|
803
|
+
# check if the issue has already been analyzed
|
|
804
|
+
if not issue.analyzed:
|
|
805
|
+
# set analyzed to true
|
|
806
|
+
issue.analyzed = True
|
|
807
|
+
|
|
808
|
+
# check if the RegScale defenderId was recommended by Microsoft Defender
|
|
809
|
+
if issue.data.get(issue.integration_field) not in defender_data_dict and issue.data["status"] not in [
|
|
810
|
+
"Closed",
|
|
811
|
+
"Cancelled",
|
|
812
|
+
]:
|
|
813
|
+
# the RegScale issue is no longer being recommended and the issue
|
|
814
|
+
# status is not closed or cancelled, we need to close the issue
|
|
815
|
+
change_issue_status(
|
|
816
|
+
api=api,
|
|
817
|
+
status="Closed",
|
|
818
|
+
issue=issue.data,
|
|
819
|
+
rec=defender_data_dict.get(issue.data.get(issue.integration_field)),
|
|
820
|
+
rec_type=issue.init_key,
|
|
821
|
+
)
|
|
822
|
+
job_progress.update(task, advance=1)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def change_issue_status(
|
|
826
|
+
api: Api,
|
|
827
|
+
status: str,
|
|
828
|
+
issue: dict,
|
|
829
|
+
rec: Optional[DefenderData] = None,
|
|
830
|
+
rec_type: str = None,
|
|
831
|
+
) -> None:
|
|
832
|
+
"""
|
|
833
|
+
Function to change a RegScale issue to the provided status
|
|
834
|
+
|
|
835
|
+
:param Api api: API object
|
|
836
|
+
:param str status: Status to change the provided issue to
|
|
837
|
+
:param dict issue: RegScale issue
|
|
838
|
+
:param dict rec: Microsoft Defender recommendation, defaults to None
|
|
839
|
+
:param str rec_type: The platform of Microsoft Defender (cloud or 365), defaults to None
|
|
840
|
+
:rtype: None
|
|
841
|
+
"""
|
|
842
|
+
# update issue last updated time, set user to current user and change status
|
|
843
|
+
# to the status that was passed
|
|
844
|
+
issue["lastUpdatedById"] = api.config["userId"]
|
|
845
|
+
issue["dateLastUpdated"] = get_current_datetime(DATE_FORMAT)
|
|
846
|
+
issue["status"] = status
|
|
847
|
+
|
|
848
|
+
if not rec:
|
|
849
|
+
return
|
|
850
|
+
rec = rec.data
|
|
851
|
+
|
|
852
|
+
# check if rec dictionary was passed, if not create it
|
|
853
|
+
if rec_type == "defender365":
|
|
854
|
+
issue["title"] = rec["recommendationName"]
|
|
855
|
+
issue["description"] = format_description(defender_data=rec, tenant_id=api.config["azure365TenantId"])
|
|
856
|
+
issue["severityLevel"] = Issue.assign_severity(rec["severityScore"])
|
|
857
|
+
issue["issueOwnerId"] = api.config["userId"]
|
|
858
|
+
issue["dueDate"] = get_due_date(score=rec["severityScore"], config=api.config, key="defender365")
|
|
859
|
+
elif rec_type == "defenderCloud":
|
|
860
|
+
issue["title"] = (f'{rec["properties"]["productName"]} Alert - {rec["properties"]["compromisedEntity"]}',)
|
|
861
|
+
issue["description"] = format_description(defender_data=rec, tenant_id=api.config["azureCloudTenantId"])
|
|
862
|
+
issue["severityLevel"] = (Issue.assign_severity(rec["properties"]["severity"]),)
|
|
863
|
+
issue["issueOwnerId"] = api.config["userId"]
|
|
864
|
+
issue["dueDate"] = get_due_date(
|
|
865
|
+
score=rec["properties"]["severity"],
|
|
866
|
+
config=api.config,
|
|
867
|
+
key="defenderCloud",
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# if we are closing the issue, update the date completed
|
|
871
|
+
if status.lower() == "closed":
|
|
872
|
+
if rec_type == "defender365":
|
|
873
|
+
message = "via Microsoft 365 Defender"
|
|
874
|
+
elif rec_type == "defenderCloud":
|
|
875
|
+
message = "via Microsoft Defender for Cloud"
|
|
876
|
+
else:
|
|
877
|
+
message = "via Microsoft Defender"
|
|
878
|
+
issue["dateCompleted"] = get_current_datetime(DATE_FORMAT)
|
|
879
|
+
issue["description"] += f'<p>No longer reported {message} as of {get_current_datetime("%b %d,%Y")}</p>'
|
|
880
|
+
closed.append(issue)
|
|
881
|
+
else:
|
|
882
|
+
issue["dateCompleted"] = ""
|
|
883
|
+
updated.append(issue)
|
|
884
|
+
|
|
885
|
+
# use the api to change the status of the given issue
|
|
886
|
+
Issue(**issue).save()
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def prep_issues_for_creation(def_data: DefenderData, args: Tuple) -> None:
|
|
890
|
+
"""
|
|
891
|
+
Function to utilize threading and create an issues in RegScale for the assigned thread
|
|
892
|
+
|
|
893
|
+
:param DefenderData def_data: Microsoft Defender data to create an issue for
|
|
894
|
+
:param Tuple args: Tuple of args to use during the process
|
|
895
|
+
:rtype: None
|
|
896
|
+
"""
|
|
897
|
+
# set up local variables from args passed
|
|
898
|
+
mapping_func, config, defender_key, parent_id, parent_module, task = args
|
|
899
|
+
|
|
900
|
+
# set the recommendation for the thread for later use in the function
|
|
901
|
+
description = format_description(defender_data=def_data.data, tenant_id=config["azure365TenantId"])
|
|
902
|
+
|
|
903
|
+
# check if the recommendation was already created as a RegScale issue
|
|
904
|
+
if not def_data.created:
|
|
905
|
+
# set created flag to true
|
|
906
|
+
def_data.created = True
|
|
907
|
+
|
|
908
|
+
# set up the data payload for RegScale API
|
|
909
|
+
issue = mapping_func(data=def_data, config=config, description=description)
|
|
910
|
+
issue.__setattr__(def_data.integration_field, def_data.data[defender_key])
|
|
911
|
+
if parent_id and parent_module:
|
|
912
|
+
issue.parentId = parent_id
|
|
913
|
+
issue.parentModule = parent_module
|
|
914
|
+
issues_to_create.append(issue)
|
|
915
|
+
job_progress.update(task, advance=1)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def map_365_alert_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
|
|
919
|
+
"""
|
|
920
|
+
Function to map a Microsoft 365 Defender alert to a RegScale issue
|
|
921
|
+
|
|
922
|
+
:param DefenderData data: Microsoft Defender recommendation
|
|
923
|
+
:param dict config: Application config
|
|
924
|
+
:param str description: Description of the alert
|
|
925
|
+
:return: RegScale issue object
|
|
926
|
+
:rtype: Issue
|
|
927
|
+
"""
|
|
928
|
+
return Issue(
|
|
929
|
+
title=f'{data.data["title"]}',
|
|
930
|
+
description=description,
|
|
931
|
+
severityLevel=Issue.assign_severity(data.data["severity"]),
|
|
932
|
+
dueDate=get_due_date(score=data.data["severity"], config=config, key=data.init_key),
|
|
933
|
+
identification=IDENTIFICATION_TYPE,
|
|
934
|
+
assetIdentifier=f'Machine ID:{data.data["machineId"]}\n'
|
|
935
|
+
f'DNS Name({data.data.get("computerDnsName", "No DNS Name found")})',
|
|
936
|
+
status=config["issues"][data.init_key]["status"],
|
|
937
|
+
sourceReport="Microsoft Defender 365 Alert",
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def map_365_recommendation_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
|
|
942
|
+
"""
|
|
943
|
+
Function to map a Microsoft 365 Defender recommendation to a RegScale issue
|
|
944
|
+
|
|
945
|
+
:param DefenderData data: Microsoft Defender recommendation
|
|
946
|
+
:param dict config: Application config
|
|
947
|
+
:param str description: Description of the recommendation
|
|
948
|
+
:return: RegScale issue object
|
|
949
|
+
:rtype: Issue
|
|
950
|
+
"""
|
|
951
|
+
severity = data.data["severityScore"]
|
|
952
|
+
return Issue(
|
|
953
|
+
title=f'{data.data["recommendationName"]}',
|
|
954
|
+
description=description,
|
|
955
|
+
severityLevel=Issue.assign_severity(severity),
|
|
956
|
+
dueDate=get_due_date(score=severity, config=config, key=data.init_key),
|
|
957
|
+
identification=IDENTIFICATION_TYPE,
|
|
958
|
+
status=config["issues"][data.init_key]["status"],
|
|
959
|
+
vendorName=data.data["vendor"],
|
|
960
|
+
sourceReport="Microsoft Defender 365 Recommendation",
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def map_cloud_alert_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
|
|
965
|
+
"""
|
|
966
|
+
Function to map a Microsoft Defender for Cloud alert to a RegScale issue
|
|
967
|
+
|
|
968
|
+
:param DefenderData data: Microsoft Defender for Cloud alert
|
|
969
|
+
:param dict config: Application config
|
|
970
|
+
:param str description: Description of the alert
|
|
971
|
+
:return: RegScale issue object
|
|
972
|
+
:rtype: Issue
|
|
973
|
+
"""
|
|
974
|
+
severity = data.data["properties"]["severity"]
|
|
975
|
+
return Issue(
|
|
976
|
+
title=f'{data.data["properties"]["productName"]} Alert - {data.data["properties"]["compromisedEntity"]}',
|
|
977
|
+
description=description,
|
|
978
|
+
severityLevel=Issue.assign_severity(severity),
|
|
979
|
+
dueDate=get_due_date(
|
|
980
|
+
score=severity,
|
|
981
|
+
config=config,
|
|
982
|
+
key=data.init_key,
|
|
983
|
+
),
|
|
984
|
+
assetIdentifier="\n".join(
|
|
985
|
+
resource["azureResourceId"]
|
|
986
|
+
for resource in data.data["properties"].get("resourceIdentifiers", [])
|
|
987
|
+
if "azureResourceId" in resource
|
|
988
|
+
),
|
|
989
|
+
recommendedActions="\n".join(data.data["properties"].get("remediationSteps", [])),
|
|
990
|
+
identification=IDENTIFICATION_TYPE,
|
|
991
|
+
status=config["issues"]["defenderCloud"]["status"],
|
|
992
|
+
vendorName=data.data["properties"]["vendorName"],
|
|
993
|
+
sourceReport="Microsoft Defender for Cloud Alert",
|
|
994
|
+
otherIdentifier=data.data["id"],
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def map_cloud_recommendation_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
|
|
999
|
+
"""
|
|
1000
|
+
Function to map a Microsoft Defender for Cloud alert to a RegScale issue
|
|
1001
|
+
|
|
1002
|
+
:param DefenderData data: Microsoft Defender for Cloud alert
|
|
1003
|
+
:param dict config: Application config
|
|
1004
|
+
:param str description: Description of the alert
|
|
1005
|
+
:return: RegScale issue object
|
|
1006
|
+
:rtype: Issue
|
|
1007
|
+
"""
|
|
1008
|
+
metadata = data.data["properties"].get("metadata", {})
|
|
1009
|
+
severity = metadata.get("severity")
|
|
1010
|
+
resource_details = data.data["properties"].get("resourceDetails", {})
|
|
1011
|
+
res_parts = [
|
|
1012
|
+
resource_details.get("ResourceProvider"),
|
|
1013
|
+
resource_details.get("ResourceType"),
|
|
1014
|
+
resource_details.get("ResourceName"),
|
|
1015
|
+
]
|
|
1016
|
+
res_parts = filter(None, res_parts)
|
|
1017
|
+
title = f"{metadata.get('displayName')}{' on ' if res_parts else ''}{'/'.join(res_parts)}"
|
|
1018
|
+
return Issue(
|
|
1019
|
+
title=title,
|
|
1020
|
+
description=description,
|
|
1021
|
+
severityLevel=Issue.assign_severity(severity),
|
|
1022
|
+
dueDate=get_due_date(
|
|
1023
|
+
score=severity,
|
|
1024
|
+
config=config,
|
|
1025
|
+
key=data.init_key,
|
|
1026
|
+
),
|
|
1027
|
+
identification=IDENTIFICATION_TYPE,
|
|
1028
|
+
status=config["issues"]["defenderCloud"]["status"],
|
|
1029
|
+
recommendedActions=metadata.get("remediationDescription"),
|
|
1030
|
+
assetIdentifier=resource_details.get("Id"),
|
|
1031
|
+
sourceReport=CLOUD_RECS,
|
|
1032
|
+
manualDetectionId=data.id,
|
|
1033
|
+
manualDetectionSource=CLOUD_RECS,
|
|
1034
|
+
otherIdentifier=data.data["id"],
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def fetch_resources_from_azure(
|
|
1039
|
+
api: Api, headers: dict, query: Optional[str] = None, skip_token: Optional[str] = None, record_count: int = 0
|
|
1040
|
+
) -> list[dict]:
|
|
1041
|
+
"""
|
|
1042
|
+
Function to fetch Microsoft Defender resources from Azure
|
|
1043
|
+
|
|
1044
|
+
:param Api api: API object
|
|
1045
|
+
:param dict headers: Headers used for API call
|
|
1046
|
+
:param Optional[str] query: Query to use for the API call, if none provided,
|
|
1047
|
+
:param Optional[str] skip_token: Token to skip results, used during pagination, defaults to None
|
|
1048
|
+
:param int record_count: Number of records fetched, defaults to 0, used for logging during pagination
|
|
1049
|
+
:return: list of Microsoft Defender resources
|
|
1050
|
+
:rtype: list[dict]
|
|
1051
|
+
"""
|
|
1052
|
+
url = "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01"
|
|
1053
|
+
if query:
|
|
1054
|
+
payload = {"query": query}
|
|
1055
|
+
else:
|
|
1056
|
+
payload = {
|
|
1057
|
+
"query": query or "resources",
|
|
1058
|
+
"subscriptions": [api.config["azureCloudSubscriptionId"]],
|
|
1059
|
+
}
|
|
1060
|
+
if skip_token:
|
|
1061
|
+
payload["options"] = {"$skipToken": skip_token}
|
|
1062
|
+
api.logger.info("Retrieving more Microsoft Defender resources from Azure...")
|
|
1063
|
+
else:
|
|
1064
|
+
api.logger.info("Retrieving Microsoft Defender resources from Azure...")
|
|
1065
|
+
response = api.post(url=url, headers=headers, json=payload)
|
|
1066
|
+
if response.status_code != 200:
|
|
1067
|
+
error_and_exit(
|
|
1068
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
|
|
1069
|
+
+ f"\n{response.text}",
|
|
1070
|
+
)
|
|
1071
|
+
try:
|
|
1072
|
+
response_data = response.json()
|
|
1073
|
+
total_records = response_data.get("totalRecords", 0)
|
|
1074
|
+
count = response_data.get("count", 0)
|
|
1075
|
+
api.logger.info(f"Received {count + record_count}/{total_records} items from Microsoft Defender.")
|
|
1076
|
+
# try to get the values from the api response
|
|
1077
|
+
defender_data = response_data["data"]
|
|
1078
|
+
except JSONDecodeError:
|
|
1079
|
+
# notify user if there was a json decode error from API response and exit
|
|
1080
|
+
error_and_exit("JSON Decode error")
|
|
1081
|
+
except KeyError:
|
|
1082
|
+
# notify user there was no data from API response and exit
|
|
1083
|
+
error_and_exit(
|
|
1084
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.reason}\n"
|
|
1085
|
+
+ f"{response.text}"
|
|
1086
|
+
)
|
|
1087
|
+
# check if pagination is required to fetch all data from Microsoft Defender
|
|
1088
|
+
skip_token = response_data.get("$skipToken")
|
|
1089
|
+
if response.status_code == 200 and skip_token:
|
|
1090
|
+
# get the rest of the data
|
|
1091
|
+
defender_data.extend(
|
|
1092
|
+
fetch_resources_from_azure(api=api, headers=headers, query=query, skip_token=skip_token, record_count=count)
|
|
1093
|
+
)
|
|
1094
|
+
# return the defender recommendations
|
|
1095
|
+
return defender_data
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def map_asset(data: dict, existing_assets: dict[str, Asset]) -> Asset:
|
|
1099
|
+
"""
|
|
1100
|
+
Function to map data to an Asset object
|
|
1101
|
+
|
|
1102
|
+
:param dict data: Data from Microsoft Defender
|
|
1103
|
+
:param dict[str, Asset] existing_assets: Existing assets from RegScale
|
|
1104
|
+
:return: Asset object
|
|
1105
|
+
:rtype: Asset
|
|
1106
|
+
"""
|
|
1107
|
+
asset_id = data.get("id")
|
|
1108
|
+
properties = data.get("properties", {})
|
|
1109
|
+
resource_type = data.get("type", "").lower()
|
|
1110
|
+
try:
|
|
1111
|
+
ip_mapping = {
|
|
1112
|
+
"microsoft.network/networksecuritygroups": properties.get("securityRules", [{}])[0]
|
|
1113
|
+
.get("properties", {})
|
|
1114
|
+
.get("destinationAddressPrefix"),
|
|
1115
|
+
"microsoft.network/virtualnetworks": properties.get("addressSpace", {}).get("addressPrefixes"),
|
|
1116
|
+
"microsoft.app/managedenvironments": properties.get("staticIp"),
|
|
1117
|
+
"microsoft.network/networkinterfaces": properties.get("ipConfigurations", [{}])[0]
|
|
1118
|
+
.get("properties", {})
|
|
1119
|
+
.get("privateIPAddress"),
|
|
1120
|
+
}
|
|
1121
|
+
except IndexError:
|
|
1122
|
+
ip_mapping = {}
|
|
1123
|
+
try:
|
|
1124
|
+
fqdn_mapping = {
|
|
1125
|
+
"microsoft.keyvault/vaults": properties.get("vaultUri"),
|
|
1126
|
+
"microsoft.storage/storageaccounts": properties.get("primaryEndpoints", {}).get("blob"),
|
|
1127
|
+
"microsoft.appconfiguration/configurationstores": properties.get("endpoint"),
|
|
1128
|
+
"microsoft.dbforpostgresql/flexibleservers": properties.get("fullyQualifiedDomainName"),
|
|
1129
|
+
AFD_ENDPOINTS: properties.get("hostName"),
|
|
1130
|
+
"microsoft.containerregistry/registries": properties.get("loginServer"),
|
|
1131
|
+
"microsoft.app/containerapps": properties.get("configuration", {}).get("ingress", {}).get("fqdn"),
|
|
1132
|
+
"microsoft.network/privatednszones": data.get("name"),
|
|
1133
|
+
"microsoft.cognitiveservices/accounts": properties.get("endpoint"),
|
|
1134
|
+
}
|
|
1135
|
+
except IndexError:
|
|
1136
|
+
fqdn_mapping = {}
|
|
1137
|
+
# pylint: disable=line-too-long
|
|
1138
|
+
function_mapping = {
|
|
1139
|
+
"microsoft.network/privateendpoints": "Private endpoint that links the private link and the nic together",
|
|
1140
|
+
"microsoft.network/networkinterfaces": "Network Interface that connects to everything internal to the resource group",
|
|
1141
|
+
"microsoft.network/privatednszones": "Dns zone that will connect to the private endpoint and network interfaces",
|
|
1142
|
+
"microsoft.network/privatednszones/virtualnetworklinks": "Link for the Private DNS zone back to the vnet",
|
|
1143
|
+
"microsoft.app/containerapps": "Application runner that houses the running Docker Container",
|
|
1144
|
+
"microsoft.network/publicipaddresses": "Public ip address used for load balancing the container apps",
|
|
1145
|
+
"microsoft.storage/storageaccounts": "Storage blob to house unstructured files uploaded to the platform",
|
|
1146
|
+
"microsoft.network/networksecuritygroups": "Network protection for internal communications and load balancing",
|
|
1147
|
+
"microsoft.network/networkwatchers/flowlogs": "Logs that determine the flow of traffic",
|
|
1148
|
+
"microsoft.sql/servers/databases": "Database that houses application logs",
|
|
1149
|
+
"microsoft.network/virtualnetworks": "Network Interface that determines what the valid IP range is for all internal resources",
|
|
1150
|
+
"microsoft.portal/dashboards": "Dashboard that shows the status of the application and traffic",
|
|
1151
|
+
"microsoft.dataprotection/backupvaults": "Azure Blob Storage Account backup location",
|
|
1152
|
+
"microsoft.keyvault/vaults": "To securely store API keys, passwords, certificates, or cryptographic keys",
|
|
1153
|
+
"microsoft.managedidentity/userassignedidentities": "Identity that connects all internal resources in the resource group",
|
|
1154
|
+
"microsoft.app/managedenvironments": "Application environment to connect to the vnet",
|
|
1155
|
+
"microsoft.sql/servers": "Server that will house the database for the application logs",
|
|
1156
|
+
"microsoft.sql/servers/encryptionprotector": "Server encryption",
|
|
1157
|
+
"microsoft.appconfiguration/configurationstores": "Configure, store, and retrieve parameters and settings. Store configuration for all system components in the environment",
|
|
1158
|
+
"microsoft.insights/metricalerts": "Alerts that trigger when exceptions hit above 100",
|
|
1159
|
+
"microsoft.insights/webtests": "Test to ensure the integrity of the app and alert when availability drops",
|
|
1160
|
+
"microsoft.insights/components": "Insights and mapping for the data flow through the platform container application",
|
|
1161
|
+
"microsoft.dbforpostgresql/flexibleservers": "Application Database for OpenAI and Automation containers",
|
|
1162
|
+
"microsoft.network/loadbalancers": "Load Balancer that handles the load traffic for the containerapp",
|
|
1163
|
+
"microsoft.insights/activitylogalerts": "Alert rule to send an email to the Action Group when the trigger event happens",
|
|
1164
|
+
"microsoft.operationalinsights/workspaces": "Collection of Logs contained in a workspace",
|
|
1165
|
+
"microsoft.insights/actiongroups": "Action Group to send Emails to when alerts trigger",
|
|
1166
|
+
"microsoft.network/networkwatchers": "Monitor on the network to look for any suspecious activity",
|
|
1167
|
+
"microsoft.app/managedenvironments/certificates": "Tls cert for the application environment",
|
|
1168
|
+
"microsoft.authorization/roledefinitions": "Custom role definition",
|
|
1169
|
+
"microsoft.alertsmanagement/actionrules": "Alert Processing Rule to show when to trigger",
|
|
1170
|
+
"microsoft.network/frontdoorwebapplicationfirewallpolicies": "Waf protection policy that connects to the firewall and frontdoor",
|
|
1171
|
+
"microsoft.cdn/profiles": "Monitoring and controlling inbound and outbound traffic to the environment. Functions as a Web Application Firewall (WAF) and performs Network Address Translation (NAT) connecting public networks to a series of private tenant Virtual Networks (VNets)",
|
|
1172
|
+
"microsoft.resourcegraph/queries": "Query to return all resources in the SaaS subscription in the resource graph",
|
|
1173
|
+
"microsoft.network/firewallpolicies": "Firewall policy that connects to frontdoor and handles our traffic coming into the system",
|
|
1174
|
+
AFD_ENDPOINTS: "Endpoint that all of the routes attach to",
|
|
1175
|
+
"microsoft.containerregistry/registries": "House the Docker container image for ContainerApp pull",
|
|
1176
|
+
"microsoft.operationalinsights/querypacks": "Log analytics query that loads default queries for running",
|
|
1177
|
+
"microsoft.alertsmanagement/smartdetectoralertrules": "Failure Anomalies notifies you of an unusual rise in the rate of failed HTTP requests or dependency calls.",
|
|
1178
|
+
}
|
|
1179
|
+
# pylint: enable=line-too-long
|
|
1180
|
+
from regscale.models.regscale_models import AssetType, AssetCategory, AssetStatus
|
|
1181
|
+
|
|
1182
|
+
if asset_id in existing_assets:
|
|
1183
|
+
return existing_assets[asset_id]
|
|
1184
|
+
mapped_asset = Asset(
|
|
1185
|
+
extra_data={"type": f'{data.get("type")}'},
|
|
1186
|
+
id=0,
|
|
1187
|
+
description=generate_html_table_from_dict(data),
|
|
1188
|
+
status=AssetStatus.Active.value,
|
|
1189
|
+
name=data.get("name", asset_id),
|
|
1190
|
+
assetType=AssetType.Other,
|
|
1191
|
+
assetCategory=AssetCategory.Software,
|
|
1192
|
+
otherTrackingNumber=asset_id,
|
|
1193
|
+
softwareFunction=function_mapping.get(resource_type, properties.get("description")),
|
|
1194
|
+
ipAddress=str(ip_mapping.get(resource_type, properties.get("ipAddress"))),
|
|
1195
|
+
bPublicFacing=resource_type in ["microsoft.cdn/profiles", AFD_ENDPOINTS],
|
|
1196
|
+
bAuthenticatedScan=resource_type
|
|
1197
|
+
not in [
|
|
1198
|
+
"microsoft.alertsmanagement/actionrules",
|
|
1199
|
+
"microsoft.alertsmanagement/smartdetectoralertrules",
|
|
1200
|
+
],
|
|
1201
|
+
bVirtual=True,
|
|
1202
|
+
baselineConfiguration="Azure Hardening Guide",
|
|
1203
|
+
)
|
|
1204
|
+
if fqdn := fqdn_mapping.get(resource_type, properties.get("dnsSettings", {}).get("fqdn")):
|
|
1205
|
+
mapped_asset.fqdn = fqdn
|
|
1206
|
+
mapped_asset.description += f"<p>FQDN: {fqdn}</p>"
|
|
1207
|
+
return mapped_asset
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def map_assets(data: list[dict], existing_assets: list[Asset], progress: Progress) -> list[Asset]:
|
|
1211
|
+
"""
|
|
1212
|
+
Function to map data to an Asset object using threads
|
|
1213
|
+
|
|
1214
|
+
:param list[dict] data: Data from Microsoft Defender Resource APi
|
|
1215
|
+
:param list[Asset] existing_assets: List of existing assets, used to prevent duplicates
|
|
1216
|
+
:param Progress progress: Progress object to track progress
|
|
1217
|
+
:return: List of Asset objects
|
|
1218
|
+
:rtype: list[Asset]
|
|
1219
|
+
"""
|
|
1220
|
+
existing_assets = {asset.otherTrackingNumber: asset for asset in existing_assets}
|
|
1221
|
+
from regscale.integrations.variables import ScannerVariables
|
|
1222
|
+
|
|
1223
|
+
with ThreadPoolExecutor(max_workers=ScannerVariables.threadMaxWorkers) as executor:
|
|
1224
|
+
futures = [executor.submit(map_asset, asset, existing_assets) for asset in data]
|
|
1225
|
+
mapping_assets = progress.add_task(
|
|
1226
|
+
f"[#f8b737]Mapping Microsoft Defender {len(data)} resource(s) to RegScale assets...", total=len(data)
|
|
1227
|
+
)
|
|
1228
|
+
assets = []
|
|
1229
|
+
for future in as_completed(futures):
|
|
1230
|
+
if result := future.result():
|
|
1231
|
+
assets.append(result)
|
|
1232
|
+
progress.update(mapping_assets, advance=1)
|
|
1233
|
+
logger.info(f"Mapped {len(assets)}/{len(data)} Microsoft Defender resource(s) to RegScale asset(s).")
|
|
1234
|
+
return assets
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def sync_resources(ssp_id: int):
|
|
1238
|
+
"""
|
|
1239
|
+
Function to sync Microsoft Defender resources with RegScale assets
|
|
1240
|
+
|
|
1241
|
+
:param int ssp_id: The RegScale SSP ID to sync resources to
|
|
1242
|
+
:rtype: None
|
|
1243
|
+
"""
|
|
1244
|
+
app = check_license()
|
|
1245
|
+
api = Api()
|
|
1246
|
+
# check if RegScale token is valid:
|
|
1247
|
+
if not is_valid(app=app):
|
|
1248
|
+
error_and_exit(LOGIN_ERROR)
|
|
1249
|
+
token = check_token(api=api, system="cloud")
|
|
1250
|
+
headers = {"Content-Type": APP_JSON, "Authorization": token}
|
|
1251
|
+
cloud_resources = fetch_resources_from_azure(api=api, headers=headers)
|
|
1252
|
+
app.logger.info(f"Retrieving assets from RegScale for security plan #{ssp_id}...")
|
|
1253
|
+
if assets := Asset.get_map(plan_id=ssp_id):
|
|
1254
|
+
assets = list(assets.values())
|
|
1255
|
+
with create_progress_object() as progress:
|
|
1256
|
+
logger.info(f"Retrieved {len(assets)} asset(s) from RegScale.")
|
|
1257
|
+
cloud_assets = map_assets(data=cloud_resources, existing_assets=assets, progress=progress)
|
|
1258
|
+
azure_comps = {asset.extra_data.get("type") for asset in cloud_assets if asset.extra_data.get("type")}
|
|
1259
|
+
api.logger.info("Fetching components from RegScale...")
|
|
1260
|
+
if existing_components := Component.get_map(plan_id=ssp_id):
|
|
1261
|
+
logger.info(f"Retrieved {len(existing_components)} component(s) from RegScale.")
|
|
1262
|
+
existing_components = list(existing_components.values())
|
|
1263
|
+
comp_mapping = {
|
|
1264
|
+
component.title: component for component in existing_components if component.title in azure_comps
|
|
1265
|
+
}
|
|
1266
|
+
logger.info(
|
|
1267
|
+
f"Found {len(comp_mapping)}/{len(azure_comps)} component(s) required for importing "
|
|
1268
|
+
"Microsoft Defender resources as asset(s) in RegScale."
|
|
1269
|
+
)
|
|
1270
|
+
else:
|
|
1271
|
+
existing_components = []
|
|
1272
|
+
comp_mapping = {}
|
|
1273
|
+
if missing_comps_mapping := map_missing_components(
|
|
1274
|
+
components=azure_comps,
|
|
1275
|
+
existing_components=existing_components,
|
|
1276
|
+
ssp_id=ssp_id,
|
|
1277
|
+
progress=progress,
|
|
1278
|
+
):
|
|
1279
|
+
new_components = create_objects_with_threads(
|
|
1280
|
+
"components", list(missing_comps_mapping.values()), progress=progress
|
|
1281
|
+
)
|
|
1282
|
+
missing_comps_mapping = {component.description: component for component in new_components}
|
|
1283
|
+
comp_mapping.update(missing_comps_mapping)
|
|
1284
|
+
if assets_to_create := map_assets_to_components(
|
|
1285
|
+
assets=[asset for asset in cloud_assets if asset.id == 0],
|
|
1286
|
+
component_mapping=comp_mapping,
|
|
1287
|
+
ssp_id=ssp_id,
|
|
1288
|
+
progress=progress,
|
|
1289
|
+
):
|
|
1290
|
+
new_assets = create_objects_with_threads("assets", assets_to_create, progress=progress)
|
|
1291
|
+
logger.info(f"Created {len(new_assets)}/{len(cloud_assets)} asset(s) in RegScale.")
|
|
1292
|
+
else:
|
|
1293
|
+
logger.info(f"[green]All {len(cloud_assets)} Microsoft Defender resource(s) already exist in RegScale.")
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def map_assets_to_components(
|
|
1297
|
+
assets: list[Asset], component_mapping: dict[str, Component], ssp_id: int, progress: Progress
|
|
1298
|
+
) -> list[Asset]:
|
|
1299
|
+
"""
|
|
1300
|
+
Function to map assets to components
|
|
1301
|
+
|
|
1302
|
+
:param list[Asset] assets: List of assets to map
|
|
1303
|
+
:param dict[str, Component] component_mapping: Dictionary of component titles and their corresponding component
|
|
1304
|
+
:param int ssp_id: The RegScale SSP ID to add the assets to, used if no component is found to map to
|
|
1305
|
+
:param Progress progress: Progress object to track progress
|
|
1306
|
+
:return: List of assets with updated parentIds and parentModules
|
|
1307
|
+
:rtype: list[Asset]
|
|
1308
|
+
"""
|
|
1309
|
+
updated_assets = []
|
|
1310
|
+
if assets:
|
|
1311
|
+
mapping_assets = progress.add_task(
|
|
1312
|
+
f"[#f8b737]Mapping {len(assets)} asset(s) to RegScale components...", total=len(assets)
|
|
1313
|
+
)
|
|
1314
|
+
for asset in assets:
|
|
1315
|
+
if asset_type := asset.extra_data.get("type"):
|
|
1316
|
+
if component := component_mapping.get(asset_type):
|
|
1317
|
+
asset.extra_data["componentId"] = component.id
|
|
1318
|
+
asset.parentId = ssp_id
|
|
1319
|
+
asset.parentModule = "securityplans"
|
|
1320
|
+
updated_assets.append(asset)
|
|
1321
|
+
progress.update(mapping_assets, advance=1)
|
|
1322
|
+
logger.info(f"Updated parentIds and parentModules for {len(assets)} asset(s).")
|
|
1323
|
+
return updated_assets
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def map_missing_components(components: set, existing_components: list, ssp_id: int, progress: Progress) -> dict:
|
|
1327
|
+
"""
|
|
1328
|
+
Function to create missing components in RegScale
|
|
1329
|
+
|
|
1330
|
+
:param set components: Set of expected components to create
|
|
1331
|
+
:param list existing_components: List of existing components
|
|
1332
|
+
:param int ssp_id: The RegScale SSP ID to add the components to
|
|
1333
|
+
:param Progress progress: Progress object to track progress
|
|
1334
|
+
:return: Dictionary of component titles and their corresponding component objects
|
|
1335
|
+
:rtype: dict
|
|
1336
|
+
"""
|
|
1337
|
+
from regscale.models.regscale_models import ComponentType, ComponentStatus
|
|
1338
|
+
|
|
1339
|
+
missing_components = components - {component.title for component in existing_components}
|
|
1340
|
+
component_mapping = {}
|
|
1341
|
+
if missing_components:
|
|
1342
|
+
mapping_components = progress.add_task(
|
|
1343
|
+
f"[#ef5d23]Mapping {len(missing_components)} missing component(s)...", total=len(missing_components)
|
|
1344
|
+
)
|
|
1345
|
+
for component in missing_components:
|
|
1346
|
+
component_obj = Component(
|
|
1347
|
+
id=0,
|
|
1348
|
+
title=component,
|
|
1349
|
+
description=component,
|
|
1350
|
+
componentType=ComponentType.Software.value,
|
|
1351
|
+
status=ComponentStatus.Active.value,
|
|
1352
|
+
securityPlansId=ssp_id,
|
|
1353
|
+
)
|
|
1354
|
+
component_mapping[component] = component_obj
|
|
1355
|
+
progress.update(mapping_components, advance=1)
|
|
1356
|
+
logger.info(f"Mapped {len(component_mapping)}/{len(missing_components)} missing component(s).")
|
|
1357
|
+
return component_mapping
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
def create_objects_with_threads(object_name: str, objects: list, progress: Progress) -> list:
|
|
1361
|
+
"""
|
|
1362
|
+
Create a list of objects in RegScale using threads
|
|
1363
|
+
|
|
1364
|
+
:param str object_name: Type of object to create
|
|
1365
|
+
:param list objects: A list of objects to create
|
|
1366
|
+
:param Progress progress: Progress object to track progress
|
|
1367
|
+
:rtype: List of created objects
|
|
1368
|
+
:rtype: list
|
|
1369
|
+
"""
|
|
1370
|
+
from regscale.integrations.variables import ScannerVariables
|
|
1371
|
+
|
|
1372
|
+
created_objects = []
|
|
1373
|
+
created_mappings = []
|
|
1374
|
+
failed_count = 0
|
|
1375
|
+
asset_component_ids = {
|
|
1376
|
+
obj.otherTrackingNumber: obj.extra_data["componentId"]
|
|
1377
|
+
for obj in objects
|
|
1378
|
+
if isinstance(obj, Asset) and obj.extra_data.get("componentId")
|
|
1379
|
+
}
|
|
1380
|
+
with ThreadPoolExecutor(max_workers=ScannerVariables.threadMaxWorkers) as executor:
|
|
1381
|
+
futures = [executor.submit(obj.create) for obj in objects]
|
|
1382
|
+
create_task = progress.add_task(f"[#21a5bb]Creating {len(objects)} {object_name}...", total=len(objects))
|
|
1383
|
+
for future in as_completed(futures):
|
|
1384
|
+
try:
|
|
1385
|
+
if future.result():
|
|
1386
|
+
res = future.result()
|
|
1387
|
+
if isinstance(res, Asset) and res.otherTrackingNumber in asset_component_ids:
|
|
1388
|
+
from regscale.models.regscale_models import AssetMapping
|
|
1389
|
+
|
|
1390
|
+
new_mapping = AssetMapping(
|
|
1391
|
+
assetId=res.id, componentId=asset_component_ids[res.otherTrackingNumber]
|
|
1392
|
+
).create()
|
|
1393
|
+
created_mappings.append(new_mapping)
|
|
1394
|
+
elif isinstance(res, Component):
|
|
1395
|
+
from regscale.models.regscale_models import ComponentMapping
|
|
1396
|
+
|
|
1397
|
+
new_mapping = ComponentMapping(securityPlanId=res.securityPlansId, componentId=res.id).create()
|
|
1398
|
+
created_mappings.append(new_mapping)
|
|
1399
|
+
created_objects.append(res)
|
|
1400
|
+
else:
|
|
1401
|
+
failed_count += 1
|
|
1402
|
+
except Exception as e:
|
|
1403
|
+
logger.error(f"Failed to create {object_name[:-1]}: {e}")
|
|
1404
|
+
failed_count += 1
|
|
1405
|
+
progress.update(create_task, advance=1)
|
|
1406
|
+
logger.info(
|
|
1407
|
+
f"Created {len(created_objects)}/{len(objects)} {object_name}, {len(created_mappings)} mappings, and failed "
|
|
1408
|
+
f"to create {failed_count} {object_name}."
|
|
1409
|
+
)
|
|
1410
|
+
return created_objects
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
def export_resources(parent_id: int, parent_module: str, query_name: str, no_upload: bool, all_queries: bool) -> None:
|
|
1414
|
+
"""
|
|
1415
|
+
Export data from Microsoft Defender for Cloud queries and save them to a .csv file
|
|
1416
|
+
|
|
1417
|
+
:param int parent_id: The RegScale ID to save the data to
|
|
1418
|
+
:param str parent_module: The RegScale module to save the data to
|
|
1419
|
+
:param str query_name: The name of the query to export from Microsoft Defender for Cloud resource graph queries
|
|
1420
|
+
:param bool no_upload: Flag to skip uploading the exported .csv file to RegScale
|
|
1421
|
+
:param bool all_queries: If True, export all saved queries from Microsoft Defender for Cloud resource graph queries
|
|
1422
|
+
:rtype: None
|
|
1423
|
+
"""
|
|
1424
|
+
app = check_license()
|
|
1425
|
+
api = Api()
|
|
1426
|
+
# check if RegScale token is valid:
|
|
1427
|
+
if not is_valid(app=app):
|
|
1428
|
+
error_and_exit(LOGIN_ERROR)
|
|
1429
|
+
token = check_token(api=api, system="cloud")
|
|
1430
|
+
headers = {"Content-Type": APP_JSON, "Authorization": token}
|
|
1431
|
+
url = f"https://management.azure.com/subscriptions/{api.config['azureCloudSubscriptionId']}/providers/Microsoft.ResourceGraph/queries?api-version=2024-04-01"
|
|
1432
|
+
response = api.get(url=url, headers=headers)
|
|
1433
|
+
if response.raise_for_status():
|
|
1434
|
+
response.raise_for_status()
|
|
1435
|
+
cloud_queries = response.json().get("value", [])
|
|
1436
|
+
if all_queries:
|
|
1437
|
+
for query in cloud_queries:
|
|
1438
|
+
fetch_save_and_upload_query(
|
|
1439
|
+
api=api,
|
|
1440
|
+
headers=headers,
|
|
1441
|
+
query=query,
|
|
1442
|
+
parent_id=parent_id,
|
|
1443
|
+
parent_module=parent_module,
|
|
1444
|
+
no_upload=no_upload,
|
|
1445
|
+
)
|
|
1446
|
+
else:
|
|
1447
|
+
query = prompt_user_for_query_selection(queries=cloud_queries, query_name=query_name)
|
|
1448
|
+
fetch_save_and_upload_query(
|
|
1449
|
+
api=api, headers=headers, query=query, parent_id=parent_id, parent_module=parent_module, no_upload=no_upload
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def prompt_user_for_query_selection(queries: list, query_name: Optional[str] = None) -> dict:
|
|
1454
|
+
"""
|
|
1455
|
+
Function to prompt the user to select a query from a list of queries
|
|
1456
|
+
|
|
1457
|
+
:param list queries: The list of queries to select from
|
|
1458
|
+
:param str query_name: The name of the query to select, defaults to None
|
|
1459
|
+
:return: The selected query
|
|
1460
|
+
:rtype: dict
|
|
1461
|
+
"""
|
|
1462
|
+
if query_name and any(q for q in queries if q["name"].lower() == query_name.lower()):
|
|
1463
|
+
return next(q for q in queries if q["name"].lower() == query_name.lower())
|
|
1464
|
+
query = click.prompt("Select a query", type=click.Choice([query["name"] for query in queries]), show_choices=True)
|
|
1465
|
+
return next(q for q in queries if q["name"].lower() == query.lower())
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def fetch_save_and_upload_query(
|
|
1469
|
+
api: Api, headers: dict, query: dict, parent_id: int, parent_module: str, no_upload: bool
|
|
1470
|
+
) -> None:
|
|
1471
|
+
"""
|
|
1472
|
+
Function to fetch Microsoft Defender queries from Azure and save them to a .xlsx file
|
|
1473
|
+
|
|
1474
|
+
:param Api api: The API object, used to call Microsoft Defender
|
|
1475
|
+
:param dict headers: The headers to use for the request
|
|
1476
|
+
:param dict query: The query object to parse and run
|
|
1477
|
+
:param int parent_id: The RegScale ID to upload the results to
|
|
1478
|
+
:param str parent_module: The RegScale module to upload the results to
|
|
1479
|
+
:param bool no_upload: Flag to skip uploading the exported .csv file to RegScale
|
|
1480
|
+
:rtype: None
|
|
1481
|
+
"""
|
|
1482
|
+
api.logger.info(f"Exporting data from Microsoft Defender for Cloud query: {query['name']}...")
|
|
1483
|
+
data = fetch_and_run_query(api=api, headers=headers, query=query)
|
|
1484
|
+
todays_date = get_current_datetime(dt_format="%Y%m%d")
|
|
1485
|
+
file_path = Path(f"./artifacts/{query['name']}_{todays_date}.csv")
|
|
1486
|
+
save_data_to(file=file_path, data=data, transpose_data=False)
|
|
1487
|
+
if not no_upload and File.upload_file_to_regscale(
|
|
1488
|
+
file_name=file_path,
|
|
1489
|
+
parent_id=parent_id,
|
|
1490
|
+
parent_module=parent_module,
|
|
1491
|
+
api=api,
|
|
1492
|
+
):
|
|
1493
|
+
api.logger.info(f"Successfully uploaded {file_path.name} to {parent_module} #{parent_id} in RegScale.")
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def fetch_and_run_query(api: Api, headers: dict, query: dict) -> list[dict]:
|
|
1497
|
+
"""
|
|
1498
|
+
Function to fetch Microsoft Defender queries from Azure and run them
|
|
1499
|
+
|
|
1500
|
+
:param Api api: The API object, used to call Microsoft Defender
|
|
1501
|
+
:param dict headers: The headers to use for the request
|
|
1502
|
+
:param dict query: The query object to parse and run
|
|
1503
|
+
:return: list of Microsoft Defender resources by using the query
|
|
1504
|
+
:rtype: list[dict]
|
|
1505
|
+
"""
|
|
1506
|
+
url = f"https://management.azure.com/subscriptions/{query['subscriptionId']}/resourceGroups/{query['resourceGroup']}/providers/Microsoft.ResourceGraph/queries/{query['name']}?api-version=2024-04-01"
|
|
1507
|
+
response = api.get(url=url, headers=headers)
|
|
1508
|
+
if response.raise_for_status():
|
|
1509
|
+
response.raise_for_status()
|
|
1510
|
+
query = response.json().get("properties", {}).get("query")
|
|
1511
|
+
return fetch_resources_from_azure(api=api, headers=headers, query=query)
|