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,855 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import enum
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Optional, Dict, Any, Type, TypeVar, List, Union
|
|
11
|
+
from urllib.parse import urljoin, urlencode
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
from pydantic import BaseModel, Field, RootModel
|
|
15
|
+
from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError
|
|
16
|
+
|
|
17
|
+
from regscale.integrations.commercial.sicura.variables import SicuraVariables
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("regscale")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SicuraProfile(str, enum.Enum):
|
|
23
|
+
"""Enum for Sicura scan profiles."""
|
|
24
|
+
|
|
25
|
+
I_MISSION_CRITICAL_CLASSIFIED = "I - Mission Critical Classified"
|
|
26
|
+
I_MISSION_CRITICAL_PUBLIC = "I - Mission Critical Public"
|
|
27
|
+
I_MISSION_CRITICAL_SENSITIVE = "I - Mission Critical Sensitive"
|
|
28
|
+
II_HIGH_IMPORTANCE_CLASSIFIED = "II - High Importance Classified"
|
|
29
|
+
II_HIGH_IMPORTANCE_PUBLIC = "II - High Importance Public"
|
|
30
|
+
II_HIGH_IMPORTANCE_SENSITIVE = "II - High Importance Sensitive"
|
|
31
|
+
III_ADMINISTRATIVE_CLASSIFIED = "III - Administrative Classified"
|
|
32
|
+
III_ADMINISTRATIVE_PUBLIC = "III - Administrative Public"
|
|
33
|
+
III_ADMINISTRATIVE_SENSITIVE = "III - Administrative Sensitive"
|
|
34
|
+
LEVEL_1_SERVER = "Level 1 - Server"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SicuraModel(BaseModel):
|
|
38
|
+
"""Base model for Sicura API responses."""
|
|
39
|
+
|
|
40
|
+
class Config:
|
|
41
|
+
populate_by_name = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Device(SicuraModel):
|
|
45
|
+
"""Model for Sicura device information."""
|
|
46
|
+
|
|
47
|
+
id: int = Field(None, alias="id")
|
|
48
|
+
name: str = Field(..., alias="name")
|
|
49
|
+
fqdn: str = Field(..., alias="fqdn")
|
|
50
|
+
ip_address: Optional[str] = None
|
|
51
|
+
platforms: str = ""
|
|
52
|
+
scannable_profiles: Optional[Dict[str, Dict[str, Any]]] = Field(default_factory=dict)
|
|
53
|
+
most_recent_scan: Optional[str] = None
|
|
54
|
+
type: Optional[str] = None
|
|
55
|
+
last_updated_time: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
class Config:
|
|
58
|
+
populate_by_name = True
|
|
59
|
+
extra = "allow"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PendingDevice(SicuraModel):
|
|
63
|
+
"""Model for a pending device waiting to be accepted."""
|
|
64
|
+
|
|
65
|
+
id: int
|
|
66
|
+
fqdn: str
|
|
67
|
+
signature: str
|
|
68
|
+
platform: str
|
|
69
|
+
platform_title: str
|
|
70
|
+
last_update: str
|
|
71
|
+
ip_address: str
|
|
72
|
+
rejected: bool
|
|
73
|
+
|
|
74
|
+
class Config:
|
|
75
|
+
populate_by_name = True
|
|
76
|
+
extra = "allow"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def retry_with_backoff(retries=3, backoff_in_seconds=1):
|
|
80
|
+
"""
|
|
81
|
+
Decorator for retrying a function with exponential backoff.
|
|
82
|
+
|
|
83
|
+
:param int retries: Number of retries
|
|
84
|
+
:param int backoff_in_seconds: Initial backoff time in seconds
|
|
85
|
+
:return: Decorated function
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def decorator(func):
|
|
89
|
+
@wraps(func)
|
|
90
|
+
def wrapper(*args, **kwargs):
|
|
91
|
+
x = 0
|
|
92
|
+
while True:
|
|
93
|
+
try:
|
|
94
|
+
return func(*args, **kwargs)
|
|
95
|
+
except (Timeout, RequestsConnectionError) as e:
|
|
96
|
+
if x == retries:
|
|
97
|
+
raise e
|
|
98
|
+
sleep = backoff_in_seconds * 2**x + random.uniform(0, 1)
|
|
99
|
+
time.sleep(sleep)
|
|
100
|
+
x += 1
|
|
101
|
+
|
|
102
|
+
return wrapper
|
|
103
|
+
|
|
104
|
+
return decorator
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AuthResponse(SicuraModel):
|
|
108
|
+
"""Model for authentication response."""
|
|
109
|
+
|
|
110
|
+
token: str
|
|
111
|
+
expires_at: Optional[str] = None
|
|
112
|
+
|
|
113
|
+
class Config:
|
|
114
|
+
populate_by_name = True
|
|
115
|
+
extra = "allow"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ScanJob(SicuraModel):
|
|
119
|
+
"""Model for a Sicura scan job."""
|
|
120
|
+
|
|
121
|
+
id: int = Field(..., alias="id")
|
|
122
|
+
node_id: int = Field(..., alias="node_id")
|
|
123
|
+
timestamp: str = Field(..., alias="timestamp")
|
|
124
|
+
name: str = Field(..., alias="name")
|
|
125
|
+
status_id: int = Field(..., alias="status_id")
|
|
126
|
+
attributes: Dict[str, Any] = Field(default_factory=dict, alias="attributes")
|
|
127
|
+
task_id: int = Field(..., alias="task_id")
|
|
128
|
+
|
|
129
|
+
class Config:
|
|
130
|
+
populate_by_name = True
|
|
131
|
+
extra = "allow"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ScanControl(RootModel):
|
|
135
|
+
"""Model for scan control data."""
|
|
136
|
+
|
|
137
|
+
# Control fields are dynamic, allowing any key with boolean values
|
|
138
|
+
root: Dict[str, bool] = Field(default_factory=dict)
|
|
139
|
+
|
|
140
|
+
# RootModel doesn't support the extra option in Config
|
|
141
|
+
# class Config:
|
|
142
|
+
# populate_by_name = True
|
|
143
|
+
# extra = "allow"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ScanResult(SicuraModel):
|
|
147
|
+
"""Model for an individual scan result."""
|
|
148
|
+
|
|
149
|
+
title: str
|
|
150
|
+
ce_name: str
|
|
151
|
+
result: str # 'pass' or 'fail'
|
|
152
|
+
description: str
|
|
153
|
+
controls: Dict[str, bool]
|
|
154
|
+
state: str
|
|
155
|
+
state_reason: List[str] = Field(default_factory=list)
|
|
156
|
+
|
|
157
|
+
class Config:
|
|
158
|
+
populate_by_name = True
|
|
159
|
+
extra = "allow"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ScanSummary(SicuraModel):
|
|
163
|
+
"""Model for scan summary statistics."""
|
|
164
|
+
|
|
165
|
+
total: int
|
|
166
|
+
pass_count: int = Field(alias="pass")
|
|
167
|
+
fail: int
|
|
168
|
+
pass_percentage: float
|
|
169
|
+
|
|
170
|
+
class Config:
|
|
171
|
+
populate_by_name = True
|
|
172
|
+
extra = "allow"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ScanReport(SicuraModel):
|
|
176
|
+
"""Model for a complete scan report."""
|
|
177
|
+
|
|
178
|
+
device_id: int
|
|
179
|
+
fqdn: str
|
|
180
|
+
ip_address: Optional[str] = None
|
|
181
|
+
scans: List[ScanResult]
|
|
182
|
+
summary: ScanSummary
|
|
183
|
+
|
|
184
|
+
class Config:
|
|
185
|
+
populate_by_name = True
|
|
186
|
+
extra = "allow"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
SicuraModelType = TypeVar("SicuraModelType", bound=SicuraModel)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SicuraAPI:
|
|
193
|
+
"""Methods to interact with Sicura API"""
|
|
194
|
+
|
|
195
|
+
# Status ID mapping
|
|
196
|
+
JOB_STATUS = {1: "QUEUED", 2: "RUNNING", 3: "COMPLETE", 4: "ERROR", 5: "CANCELLED", 6: "TIMEOUT"}
|
|
197
|
+
|
|
198
|
+
# Filter parameter constants
|
|
199
|
+
FILTER_FQDN = "filter[fqdn]"
|
|
200
|
+
FILTER_IP_ADDRESS = "filter[ip_address]"
|
|
201
|
+
FILTER_TYPE = "filter[type]"
|
|
202
|
+
FILTER_REJECTED = "filter[rejected]"
|
|
203
|
+
FILTER_TASK_ID = "filter[task_id]"
|
|
204
|
+
|
|
205
|
+
def __init__(self):
|
|
206
|
+
"""
|
|
207
|
+
Initialize Sicura API client.
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
self.base_url = SicuraVariables.sicuraURL.rstrip("/")
|
|
211
|
+
self.session = requests.Session()
|
|
212
|
+
self.csrf_token = None
|
|
213
|
+
|
|
214
|
+
@retry_with_backoff(retries=3, backoff_in_seconds=1)
|
|
215
|
+
def _make_request(
|
|
216
|
+
self,
|
|
217
|
+
method: str,
|
|
218
|
+
endpoint: str,
|
|
219
|
+
data: Optional[Dict[str, Any]] = None,
|
|
220
|
+
params: Optional[Dict[str, Any]] = None,
|
|
221
|
+
files: Optional[Dict[str, Any]] = None,
|
|
222
|
+
) -> Optional[Union[dict, str]]:
|
|
223
|
+
"""
|
|
224
|
+
Make a request to the Sicura API.
|
|
225
|
+
|
|
226
|
+
:param str method: HTTP method
|
|
227
|
+
:param str endpoint: API endpoint
|
|
228
|
+
:param Optional[Dict[str, Any]] data: Request data
|
|
229
|
+
:param Optional[Dict[str, Any]] params: Query parameters
|
|
230
|
+
:param Optional[Dict[str, Any]] files: Files to upload
|
|
231
|
+
:return: Response data or None if request failed
|
|
232
|
+
:rtype: Optional[Union[dict, str]]
|
|
233
|
+
"""
|
|
234
|
+
url = urljoin(self.base_url, endpoint)
|
|
235
|
+
logger.debug(f"Making request with params: {url}?{urlencode(params) if params else ''}")
|
|
236
|
+
logger.debug(f"Current session cookies: {json.dumps(dict(self.session.cookies), indent=2)}")
|
|
237
|
+
|
|
238
|
+
if data:
|
|
239
|
+
self.session.headers["Content-Type"] = "application/json"
|
|
240
|
+
if not endpoint.endswith("/auth/token"):
|
|
241
|
+
if not self.csrf_token:
|
|
242
|
+
self.csrf_token = self.get_csrf_token()
|
|
243
|
+
if self.csrf_token:
|
|
244
|
+
self.session.headers["X-CSRF-TOKEN"] = str(self.csrf_token)
|
|
245
|
+
self.session.headers["auth-token-signature"] = SicuraVariables.sicuraToken
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
# Use the session object to maintain cookies between requests
|
|
249
|
+
logger.debug(f"Current session headers: {dict(self.session.headers)}")
|
|
250
|
+
response = self.session.request(
|
|
251
|
+
method,
|
|
252
|
+
url,
|
|
253
|
+
headers=self.session.headers,
|
|
254
|
+
json=data,
|
|
255
|
+
params=params,
|
|
256
|
+
verify=True,
|
|
257
|
+
timeout=60,
|
|
258
|
+
files=files,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
logger.debug(f"Response cookies: {dict(response.cookies)}")
|
|
262
|
+
|
|
263
|
+
if response.status_code == 403:
|
|
264
|
+
logger.error("Authentication failed")
|
|
265
|
+
raise requests.exceptions.HTTPError("Authentication failed", response=response)
|
|
266
|
+
|
|
267
|
+
if response.status_code == 404:
|
|
268
|
+
logger.error(f"Resource not found: {url}")
|
|
269
|
+
try:
|
|
270
|
+
return response.json()
|
|
271
|
+
except requests.exceptions.JSONDecodeError:
|
|
272
|
+
response.raise_for_status()
|
|
273
|
+
else:
|
|
274
|
+
response.raise_for_status()
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
logger.debug(f"Response JSON: {json.dumps(response.json(), indent=2)}")
|
|
278
|
+
return response.json()
|
|
279
|
+
except requests.exceptions.JSONDecodeError:
|
|
280
|
+
logger.debug(f"Response Text: {response.text}")
|
|
281
|
+
return response.text
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"Method: {method}, Endpoint: {endpoint}, Data: {data}, Params: {params}, Files: {files}")
|
|
285
|
+
logger.error(f"Request failed: {e}", exc_info=True)
|
|
286
|
+
raise
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def handle_response(response: Optional[Dict[str, Any]], model: Type[SicuraModelType]) -> Optional[SicuraModelType]:
|
|
290
|
+
"""
|
|
291
|
+
Handle API response and convert to appropriate Sicura model.
|
|
292
|
+
|
|
293
|
+
:param Optional[Dict[str, Any]] response: API response
|
|
294
|
+
:param Type[SicuraModelType] model: Sicura model to validate response against
|
|
295
|
+
:return: Validated Sicura model instance or None if validation fails
|
|
296
|
+
:rtype: Optional[SicuraModelType]
|
|
297
|
+
"""
|
|
298
|
+
if response is None:
|
|
299
|
+
return None
|
|
300
|
+
logger.debug(f"Handling Response: {response}")
|
|
301
|
+
try:
|
|
302
|
+
return model.model_validate(response)
|
|
303
|
+
except ValueError as e:
|
|
304
|
+
logging.error(f"Error validating response: {e}", exc_info=True)
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def get_csrf_token(self) -> Optional[AuthResponse]:
|
|
308
|
+
"""
|
|
309
|
+
Get authentication token from Sicura API.
|
|
310
|
+
|
|
311
|
+
:return: Authentication response
|
|
312
|
+
:rtype: Optional[AuthResponse]
|
|
313
|
+
:raises requests.exceptions.RequestException: If the request fails
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
response = self._make_request("GET", "/auth/token")
|
|
317
|
+
self.csrf_token = response
|
|
318
|
+
return response
|
|
319
|
+
except requests.exceptions.RequestException as e:
|
|
320
|
+
logger.error(f"Error getting authentication token: {e}", exc_info=True)
|
|
321
|
+
raise
|
|
322
|
+
|
|
323
|
+
class Device(SicuraModel):
|
|
324
|
+
"""Model for Sicura device information."""
|
|
325
|
+
|
|
326
|
+
id: int = Field(None, alias="id")
|
|
327
|
+
name: str = Field(..., alias="name")
|
|
328
|
+
fqdn: Optional[str] = None
|
|
329
|
+
ip_address: Optional[str] = None
|
|
330
|
+
platforms: str = ""
|
|
331
|
+
scannable_profiles: Optional[Dict[str, Dict[str, Any]]] = Field(default_factory=dict)
|
|
332
|
+
most_recent_scan: Optional[str] = None
|
|
333
|
+
|
|
334
|
+
class Config:
|
|
335
|
+
populate_by_name = True
|
|
336
|
+
extra = "allow"
|
|
337
|
+
|
|
338
|
+
def get_devices(self, ip_address: Optional[str] = None, fqdn: Optional[str] = None) -> List[SicuraAPI.Device]:
|
|
339
|
+
"""
|
|
340
|
+
Get devices from Sicura API.
|
|
341
|
+
|
|
342
|
+
:param Optional[str] ip_address: IP address to filter devices by
|
|
343
|
+
:param Optional[str] fqdn: FQDN to filter devices by
|
|
344
|
+
:return: List of devices
|
|
345
|
+
:rtype: List[SicuraAPI.Device]
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
response = self._make_request(
|
|
349
|
+
"GET",
|
|
350
|
+
"/backend/api/jaeger/v1/nodes",
|
|
351
|
+
params={
|
|
352
|
+
"verbose": "true",
|
|
353
|
+
"attributes": "platforms,scannable_profiles,most_recent_scan",
|
|
354
|
+
self.FILTER_FQDN: fqdn,
|
|
355
|
+
self.FILTER_IP_ADDRESS: ip_address,
|
|
356
|
+
self.FILTER_TYPE: "endpoint",
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Handle 404 or empty response
|
|
361
|
+
if not response or (isinstance(response, dict) and "detail" in response):
|
|
362
|
+
logger.debug(f"No devices found: {response}")
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
# Convert response to list if it's a single device
|
|
366
|
+
if isinstance(response, dict):
|
|
367
|
+
response = [response]
|
|
368
|
+
|
|
369
|
+
return [
|
|
370
|
+
item
|
|
371
|
+
for item in (self.handle_response(item, SicuraAPI.Device) for item in response if item is not None)
|
|
372
|
+
if item is not None
|
|
373
|
+
]
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"Failed to get devices: {e}", exc_info=True)
|
|
376
|
+
return []
|
|
377
|
+
|
|
378
|
+
def create_scan_task(
|
|
379
|
+
self,
|
|
380
|
+
device_id: int,
|
|
381
|
+
platform: str,
|
|
382
|
+
profile: SicuraProfile,
|
|
383
|
+
task_name: Optional[str] = None,
|
|
384
|
+
scheduled_time: Optional[datetime.datetime] = None,
|
|
385
|
+
) -> Optional[str]:
|
|
386
|
+
"""
|
|
387
|
+
Create a scanning task for a specific device.
|
|
388
|
+
|
|
389
|
+
:param int device_id: ID of the device to scan
|
|
390
|
+
:param str platform: Platform name (e.g., 'Red Hat Enterprise Linux 9')
|
|
391
|
+
:param SicuraProfile profile: Scan profile name (e.g., 'I - Mission Critical Classified')
|
|
392
|
+
:param Optional[str] task_name: Name for the scan task (default: auto-generated)
|
|
393
|
+
:param Optional[datetime.datetime] scheduled_time: When to run the scan (default: now)
|
|
394
|
+
:return: Task ID if successful, None otherwise
|
|
395
|
+
:rtype: Optional[str]
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
# Generate default task name if not provided
|
|
399
|
+
if not task_name:
|
|
400
|
+
task_name = f"Scan {platform} - {profile} ({datetime.datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
|
401
|
+
|
|
402
|
+
# Default to current time if not specified
|
|
403
|
+
if not scheduled_time:
|
|
404
|
+
scheduled_time = datetime.datetime.now()
|
|
405
|
+
|
|
406
|
+
# Format timestamp for API
|
|
407
|
+
timestamp = scheduled_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
408
|
+
|
|
409
|
+
payload = {
|
|
410
|
+
"name": task_name,
|
|
411
|
+
"type": "Scanning",
|
|
412
|
+
"scope": device_id,
|
|
413
|
+
"cron": "",
|
|
414
|
+
"timestamp": timestamp,
|
|
415
|
+
"repeating": False,
|
|
416
|
+
"scanAttributes": {"platform": platform, "profile": profile},
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
result = self._make_request("POST", "/backend/api/jaeger/v1/tasks/", data=payload)
|
|
420
|
+
|
|
421
|
+
if result:
|
|
422
|
+
logger.info(f"Successfully created scan task with ID: {result}")
|
|
423
|
+
return result
|
|
424
|
+
else:
|
|
425
|
+
logger.error(f"Failed to create scan task. Response: {result}")
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Error creating scan task: {e}", exc_info=True)
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
def get_task_status(self, task_id: Union[int, str]) -> Optional[Dict[str, Any]]:
|
|
433
|
+
"""
|
|
434
|
+
Get the status of a task by its ID.
|
|
435
|
+
|
|
436
|
+
:param Union[int, str] task_id: ID of the task to check
|
|
437
|
+
:return: Dictionary with job information including status, or None if not found
|
|
438
|
+
:rtype: Optional[Dict[str, Any]]
|
|
439
|
+
"""
|
|
440
|
+
try:
|
|
441
|
+
response = self._make_request(
|
|
442
|
+
"GET", "/backend/api/jaeger/v1/jobs", params={"verbose": "true", self.FILTER_TASK_ID: task_id}
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Handle 404 or empty response
|
|
446
|
+
if not response or (isinstance(response, list) and not response):
|
|
447
|
+
logger.debug(f"No jobs found for task ID {task_id}")
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
# Convert response to list if it's a single job
|
|
451
|
+
if isinstance(response, dict):
|
|
452
|
+
response = [response]
|
|
453
|
+
|
|
454
|
+
# Process the jobs
|
|
455
|
+
jobs = []
|
|
456
|
+
for job_data in response:
|
|
457
|
+
job = self.handle_response(job_data, ScanJob)
|
|
458
|
+
if job:
|
|
459
|
+
# Add human-readable status
|
|
460
|
+
job_dict = job.model_dump()
|
|
461
|
+
job_dict["status"] = self.JOB_STATUS.get(job.status_id, "UNKNOWN")
|
|
462
|
+
jobs.append(job_dict)
|
|
463
|
+
|
|
464
|
+
if not jobs:
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
return {"task_id": task_id, "jobs": jobs, "latest_status": jobs[-1]["status"] if jobs else "UNKNOWN"}
|
|
468
|
+
|
|
469
|
+
except Exception as e:
|
|
470
|
+
logger.error(f"Error getting task status: {e}", exc_info=True)
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
def get_scan_results(
|
|
474
|
+
self,
|
|
475
|
+
fqdn: str,
|
|
476
|
+
platform: Optional[str] = None,
|
|
477
|
+
profile: SicuraProfile = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
|
|
478
|
+
) -> Optional[ScanReport]:
|
|
479
|
+
"""
|
|
480
|
+
Get scan results for a specific device.
|
|
481
|
+
|
|
482
|
+
:param str fqdn: Fully qualified domain name of the device
|
|
483
|
+
:param Optional[str] platform: Platform name to filter results (e.g., 'Red Hat Enterprise Linux 9')
|
|
484
|
+
:param SicuraProfile profile: Profile name to filter results (e.g., 'I - Mission Critical Classified')
|
|
485
|
+
:return: Scan report containing device info and scan results, or None if not found
|
|
486
|
+
:rtype: Optional[ScanReport]
|
|
487
|
+
"""
|
|
488
|
+
try:
|
|
489
|
+
params = {"verbose": "true", "attributes": "scans", self.FILTER_FQDN: fqdn}
|
|
490
|
+
|
|
491
|
+
if platform:
|
|
492
|
+
params["platform"] = platform
|
|
493
|
+
|
|
494
|
+
if profile:
|
|
495
|
+
params["profile"] = profile
|
|
496
|
+
|
|
497
|
+
response = self._make_request("GET", "/backend/api/jaeger/v1/nodes", params=params)
|
|
498
|
+
|
|
499
|
+
# Handle 404 or empty response
|
|
500
|
+
if not response or (isinstance(response, list) and not response):
|
|
501
|
+
logger.debug(f"No scan results found for FQDN {fqdn}")
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
# If we got a single device (dict), convert to a list
|
|
505
|
+
if isinstance(response, dict):
|
|
506
|
+
response = [response]
|
|
507
|
+
|
|
508
|
+
# Process only the first device that matches
|
|
509
|
+
if response and isinstance(response[0], dict):
|
|
510
|
+
device = response[0]
|
|
511
|
+
|
|
512
|
+
# Check if scans are present
|
|
513
|
+
if not device.get("scans"):
|
|
514
|
+
logger.debug(f"No scan results found in device data for {fqdn}")
|
|
515
|
+
return None # Return None if no scans available
|
|
516
|
+
|
|
517
|
+
# Calculate summary stats
|
|
518
|
+
pass_count = sum(1 for scan in device.get("scans", []) if scan.get("result") == "pass")
|
|
519
|
+
fail_count = sum(1 for scan in device.get("scans", []) if scan.get("result") == "fail")
|
|
520
|
+
total_count = len(device.get("scans", []))
|
|
521
|
+
|
|
522
|
+
# Create the raw result data
|
|
523
|
+
result_data = {
|
|
524
|
+
"device_id": device.get("id"),
|
|
525
|
+
"fqdn": device.get("fqdn"),
|
|
526
|
+
"ip_address": device.get("ip_address"),
|
|
527
|
+
"scans": device.get("scans", []),
|
|
528
|
+
"summary": {
|
|
529
|
+
"total": total_count,
|
|
530
|
+
"pass": pass_count,
|
|
531
|
+
"fail": fail_count,
|
|
532
|
+
"pass_percentage": (pass_count / total_count * 100) if total_count > 0 else 0,
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# Convert to ScanReport model
|
|
537
|
+
try:
|
|
538
|
+
return ScanReport.model_validate(result_data)
|
|
539
|
+
except Exception as e:
|
|
540
|
+
logger.error(f"Error creating scan report model: {e}, result_data: {result_data}")
|
|
541
|
+
|
|
542
|
+
return None
|
|
543
|
+
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.error(f"Error getting scan results: {e}", exc_info=True)
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
def wait_for_scan_results(
|
|
549
|
+
self,
|
|
550
|
+
task_id: Union[int, str],
|
|
551
|
+
fqdn: str,
|
|
552
|
+
platform: Optional[str] = None,
|
|
553
|
+
profile: SicuraProfile = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
|
|
554
|
+
max_wait_time: int = 600,
|
|
555
|
+
poll_interval: int = 10,
|
|
556
|
+
) -> Optional[Union[ScanReport, Dict[str, Any]]]:
|
|
557
|
+
"""
|
|
558
|
+
Wait for a scan task to complete and then return the scan results.
|
|
559
|
+
|
|
560
|
+
:param Union[int, str] task_id: ID of the scan task to monitor
|
|
561
|
+
:param str fqdn: Fully qualified domain name of the device
|
|
562
|
+
:param Optional[str] platform: Platform name to filter results
|
|
563
|
+
:param SicuraProfile profile: Profile name to filter results
|
|
564
|
+
:param int max_wait_time: Maximum time to wait in seconds (default: 10 minutes)
|
|
565
|
+
:param int poll_interval: Time between status checks in seconds (default: 10 seconds)
|
|
566
|
+
:return: Scan results once the task is complete, or None if timeout or error
|
|
567
|
+
:rtype: Optional[Union[ScanReport, Dict[str, Any]]]
|
|
568
|
+
"""
|
|
569
|
+
start_time = time.time()
|
|
570
|
+
elapsed_time = 0.0
|
|
571
|
+
|
|
572
|
+
logger.info(f"Waiting for scan task {task_id} to complete...")
|
|
573
|
+
|
|
574
|
+
# Poll until we get a non-QUEUED status or hit timeout
|
|
575
|
+
while elapsed_time < max_wait_time:
|
|
576
|
+
task_status = self.get_task_status(task_id)
|
|
577
|
+
|
|
578
|
+
if not task_status:
|
|
579
|
+
logger.warning(f"Could not retrieve status for task {task_id}")
|
|
580
|
+
time.sleep(poll_interval)
|
|
581
|
+
elapsed_time = time.time() - start_time
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
latest_status = task_status.get("latest_status")
|
|
585
|
+
logger.info(f"Current task status: {latest_status} (elapsed: {elapsed_time:.1f}s)")
|
|
586
|
+
|
|
587
|
+
# If we have a status and it's not QUEUED, we can proceed
|
|
588
|
+
if latest_status and latest_status != "QUEUED":
|
|
589
|
+
# If we've reached a terminal state
|
|
590
|
+
if latest_status in ["COMPLETE", "ERROR", "CANCELLED", "TIMEOUT"]:
|
|
591
|
+
if latest_status == "COMPLETE":
|
|
592
|
+
logger.info(f"Scan task {task_id} completed successfully, fetching results...")
|
|
593
|
+
# Wait a moment for results to be processed
|
|
594
|
+
time.sleep(2)
|
|
595
|
+
return self.get_scan_results(fqdn, platform, profile)
|
|
596
|
+
else:
|
|
597
|
+
logger.error(f"Scan task {task_id} ended with status {latest_status}")
|
|
598
|
+
return None
|
|
599
|
+
|
|
600
|
+
# Wait before polling again
|
|
601
|
+
time.sleep(poll_interval)
|
|
602
|
+
elapsed_time = time.time() - start_time
|
|
603
|
+
|
|
604
|
+
logger.error(f"Timed out waiting for scan task {task_id} to complete after {max_wait_time} seconds")
|
|
605
|
+
return None
|
|
606
|
+
|
|
607
|
+
def get_pending_devices(
|
|
608
|
+
self, fqdn: Optional[str] = None, ip_address: Optional[str] = None, rejected: bool = False
|
|
609
|
+
) -> List[PendingDevice]:
|
|
610
|
+
"""
|
|
611
|
+
Get pending devices waiting to be accepted.
|
|
612
|
+
|
|
613
|
+
:param Optional[str] fqdn: FQDN to filter devices by
|
|
614
|
+
:param Optional[str] ip_address: IP address to filter devices by
|
|
615
|
+
:param bool rejected: Whether to include rejected devices (default: False)
|
|
616
|
+
:return: List of pending devices
|
|
617
|
+
:rtype: List[PendingDevice]
|
|
618
|
+
"""
|
|
619
|
+
try:
|
|
620
|
+
params = {
|
|
621
|
+
"verbose": "true",
|
|
622
|
+
"attributes": "id,fqdn,signature,platform,platform_title,last_update,ip_address,rejected",
|
|
623
|
+
self.FILTER_REJECTED: str(rejected).lower(),
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if fqdn:
|
|
627
|
+
params[self.FILTER_FQDN] = fqdn
|
|
628
|
+
|
|
629
|
+
if ip_address:
|
|
630
|
+
params[self.FILTER_IP_ADDRESS] = ip_address
|
|
631
|
+
|
|
632
|
+
response = self._make_request("GET", "/backend/api/jaeger/v1/node_templates/", params=params)
|
|
633
|
+
|
|
634
|
+
# Handle 404 or empty response
|
|
635
|
+
if not response or (isinstance(response, dict) and "detail" in response):
|
|
636
|
+
logger.debug(f"No pending devices found: {response}")
|
|
637
|
+
return []
|
|
638
|
+
|
|
639
|
+
# Convert response to list if it's a single device
|
|
640
|
+
if isinstance(response, dict):
|
|
641
|
+
response = [response]
|
|
642
|
+
|
|
643
|
+
return [
|
|
644
|
+
item
|
|
645
|
+
for item in (self.handle_response(item, PendingDevice) for item in response if item is not None)
|
|
646
|
+
if item is not None
|
|
647
|
+
]
|
|
648
|
+
except Exception as e:
|
|
649
|
+
logger.error(f"Failed to get pending devices: {e}", exc_info=True)
|
|
650
|
+
return []
|
|
651
|
+
|
|
652
|
+
def accept_pending_device(self, device_id: int) -> bool:
|
|
653
|
+
"""
|
|
654
|
+
Accept a pending device.
|
|
655
|
+
|
|
656
|
+
:param int device_id: ID of the pending device to accept
|
|
657
|
+
:return: True if successful, False otherwise
|
|
658
|
+
:rtype: bool
|
|
659
|
+
"""
|
|
660
|
+
try:
|
|
661
|
+
# Use PUT method with the correct endpoint and payload
|
|
662
|
+
result = self._make_request(
|
|
663
|
+
"PUT",
|
|
664
|
+
f"/backend/api/jaeger/v1/node_templates/{device_id}",
|
|
665
|
+
params={"verbose": "true", "include_controls": "true", "action": "promote"},
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if result:
|
|
669
|
+
logger.info(f"Successfully accepted pending device with ID: {device_id}")
|
|
670
|
+
return True
|
|
671
|
+
else:
|
|
672
|
+
logger.error(f"Failed to accept pending device. Response: {result}")
|
|
673
|
+
return False
|
|
674
|
+
|
|
675
|
+
except Exception as e:
|
|
676
|
+
logger.error(f"Error accepting pending device: {e}", exc_info=True)
|
|
677
|
+
return False
|
|
678
|
+
|
|
679
|
+
def reject_pending_device(self, device_id: int) -> bool:
|
|
680
|
+
"""
|
|
681
|
+
Reject a pending device.
|
|
682
|
+
|
|
683
|
+
:param int device_id: ID of the pending device to reject
|
|
684
|
+
:return: True if successful, False otherwise
|
|
685
|
+
:rtype: bool
|
|
686
|
+
"""
|
|
687
|
+
try:
|
|
688
|
+
# Use PUT method with the correct endpoint and payload
|
|
689
|
+
result = self._make_request(
|
|
690
|
+
"PUT",
|
|
691
|
+
f"/backend/api/jaeger/v1/node_templates/{device_id}",
|
|
692
|
+
params={"verbose": "true", "include_controls": "true", "action": "reject"},
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
if result:
|
|
696
|
+
logger.info(f"Successfully rejected pending device with ID: {device_id}")
|
|
697
|
+
return True
|
|
698
|
+
else:
|
|
699
|
+
logger.error(f"Failed to reject pending device. Response: {result}")
|
|
700
|
+
return False
|
|
701
|
+
|
|
702
|
+
except Exception as e:
|
|
703
|
+
logger.error(f"Error rejecting pending device: {e}", exc_info=True)
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
def delete_device(self, device_id: int) -> bool:
|
|
707
|
+
"""
|
|
708
|
+
Delete a device from Sicura.
|
|
709
|
+
|
|
710
|
+
:param int device_id: ID of the device to delete
|
|
711
|
+
:return: True if successful, False otherwise
|
|
712
|
+
:rtype: bool
|
|
713
|
+
"""
|
|
714
|
+
try:
|
|
715
|
+
result = self._make_request("DELETE", f"/backend/api/jaeger/v1/nodes/{device_id}")
|
|
716
|
+
|
|
717
|
+
# For DELETE operations, an empty result typically indicates success
|
|
718
|
+
if result is None or result == "" or (isinstance(result, dict) and not result):
|
|
719
|
+
logger.info(f"Successfully deleted device with ID: {device_id}")
|
|
720
|
+
return True
|
|
721
|
+
else:
|
|
722
|
+
logger.error(f"Failed to delete device. Response: {result}")
|
|
723
|
+
return False
|
|
724
|
+
|
|
725
|
+
except Exception as e:
|
|
726
|
+
logger.error(f"Error deleting device: {e}", exc_info=True)
|
|
727
|
+
return False
|
|
728
|
+
|
|
729
|
+
def create_enforcement_task(
|
|
730
|
+
self,
|
|
731
|
+
device_id: int,
|
|
732
|
+
profile: Optional[SicuraProfile] = None,
|
|
733
|
+
ce_names: Optional[List[str]] = None,
|
|
734
|
+
task_name: Optional[str] = None,
|
|
735
|
+
scheduled_time: Optional[datetime.datetime] = None,
|
|
736
|
+
no_op: bool = False,
|
|
737
|
+
) -> Optional[str]:
|
|
738
|
+
"""
|
|
739
|
+
Create an enforcement task for a specific device based on scan results or provided ce_names.
|
|
740
|
+
|
|
741
|
+
:param int device_id: ID of the device to enforce
|
|
742
|
+
:param Optional[SicuraProfile] profile: Profile to use for fetching scan results if ce_names not provided
|
|
743
|
+
:param Optional[List[str]] ce_names: List of CE names to enforce. If None, will fetch from scan results
|
|
744
|
+
:param Optional[str] task_name: Name for the enforcement task (default: auto-generated)
|
|
745
|
+
:param Optional[datetime.datetime] scheduled_time: When to run the task (default: now)
|
|
746
|
+
:param bool no_op: If True, will create a "dry run" enforcement that doesn't make changes
|
|
747
|
+
:return: Task ID if successful, None otherwise
|
|
748
|
+
:rtype: Optional[str]
|
|
749
|
+
"""
|
|
750
|
+
try:
|
|
751
|
+
# Get CE names if not provided
|
|
752
|
+
if ce_names is None:
|
|
753
|
+
ce_names = self._get_failed_ce_names(device_id, profile)
|
|
754
|
+
if not ce_names:
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
# Create and submit task
|
|
758
|
+
return self._submit_enforcement_task(device_id, profile, ce_names, task_name, scheduled_time, no_op)
|
|
759
|
+
|
|
760
|
+
except Exception as e:
|
|
761
|
+
logger.error(f"Error creating enforcement task: {e}", exc_info=True)
|
|
762
|
+
return None
|
|
763
|
+
|
|
764
|
+
def _get_failed_ce_names(self, device_id: int, profile: Optional[SicuraProfile]) -> Optional[List[str]]:
|
|
765
|
+
"""
|
|
766
|
+
Get the CE names of failed checks for a device.
|
|
767
|
+
|
|
768
|
+
:param int device_id: ID of the device to get CE names for
|
|
769
|
+
:param Optional[SicuraProfile] profile: Profile to use for fetching scan results
|
|
770
|
+
:return: List of CE names for failed checks, or None if not found
|
|
771
|
+
:rtype: Optional[List[str]]
|
|
772
|
+
"""
|
|
773
|
+
if profile is None:
|
|
774
|
+
logger.error("Either ce_names or profile must be provided")
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
# Get device info to get FQDN
|
|
778
|
+
devices = self.get_devices(fqdn=None, ip_address=None)
|
|
779
|
+
target_device = next((device for device in devices if device.id == device_id), None)
|
|
780
|
+
|
|
781
|
+
if not target_device or not target_device.fqdn:
|
|
782
|
+
logger.error(f"Could not find device with ID {device_id} or device has no FQDN")
|
|
783
|
+
return None
|
|
784
|
+
|
|
785
|
+
# Get scan results for the device
|
|
786
|
+
scan_results = self.get_scan_results(fqdn=target_device.fqdn, platform=target_device.platforms, profile=profile)
|
|
787
|
+
|
|
788
|
+
if not scan_results:
|
|
789
|
+
logger.error(f"No scan results found for device {device_id} with profile {profile}")
|
|
790
|
+
return None
|
|
791
|
+
|
|
792
|
+
# Extract ce_names from failed checks
|
|
793
|
+
ce_names = [
|
|
794
|
+
scan.ce_name
|
|
795
|
+
for scan in scan_results.scans
|
|
796
|
+
if scan.result == "fail" and hasattr(scan, "ce_name") and scan.ce_name
|
|
797
|
+
]
|
|
798
|
+
|
|
799
|
+
if not ce_names:
|
|
800
|
+
logger.warning(f"No failed checks found for device {device_id} with profile {profile}")
|
|
801
|
+
return None
|
|
802
|
+
|
|
803
|
+
return ce_names
|
|
804
|
+
|
|
805
|
+
def _submit_enforcement_task(
|
|
806
|
+
self,
|
|
807
|
+
device_id: int,
|
|
808
|
+
profile: Optional[SicuraProfile],
|
|
809
|
+
ce_names: List[str],
|
|
810
|
+
task_name: Optional[str] = None,
|
|
811
|
+
scheduled_time: Optional[datetime.datetime] = None,
|
|
812
|
+
no_op: bool = False,
|
|
813
|
+
) -> Optional[str]:
|
|
814
|
+
"""
|
|
815
|
+
Submit an enforcement task to the API.
|
|
816
|
+
|
|
817
|
+
:param int device_id: ID of the device to enforce
|
|
818
|
+
:param Optional[SicuraProfile] profile: Profile to use for the task name
|
|
819
|
+
:param List[str] ce_names: List of CE names to enforce
|
|
820
|
+
:param Optional[str] task_name: Name for the enforcement task
|
|
821
|
+
:param Optional[datetime.datetime] scheduled_time: When to run the task
|
|
822
|
+
:param bool no_op: If True, will create a "dry run" enforcement
|
|
823
|
+
:return: Task ID if successful, None otherwise
|
|
824
|
+
:rtype: Optional[str]
|
|
825
|
+
"""
|
|
826
|
+
# Generate default task name if not provided
|
|
827
|
+
if not task_name:
|
|
828
|
+
task_name = f"Enforce {profile} ({datetime.datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
|
829
|
+
|
|
830
|
+
# Default to current time if not specified
|
|
831
|
+
if not scheduled_time:
|
|
832
|
+
scheduled_time = datetime.datetime.now()
|
|
833
|
+
|
|
834
|
+
# Format timestamp for API
|
|
835
|
+
timestamp = scheduled_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
836
|
+
|
|
837
|
+
payload = {
|
|
838
|
+
"name": task_name,
|
|
839
|
+
"type": "Enforcement",
|
|
840
|
+
"scope": device_id,
|
|
841
|
+
"cron": "",
|
|
842
|
+
"timestamp": timestamp,
|
|
843
|
+
"repeating": False,
|
|
844
|
+
"enforcementAttributes": {"platform": profile, "ce_names": ce_names, "no_op": no_op},
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
logger.info(f"Creating enforcement task for device {device_id} with {len(ce_names)} CE names")
|
|
848
|
+
result = self._make_request("POST", "/backend/api/jaeger/v1/tasks/", data=payload)
|
|
849
|
+
|
|
850
|
+
if result:
|
|
851
|
+
logger.info(f"Successfully created enforcement task with ID: {result}")
|
|
852
|
+
return result
|
|
853
|
+
else:
|
|
854
|
+
logger.error(f"Failed to create enforcement task. Response: {result}")
|
|
855
|
+
return None
|