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,851 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""FedRAMP Scanner Integration"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from typing import Iterator, List, Optional
|
|
8
|
+
|
|
9
|
+
from openpyxl import load_workbook # type: ignore
|
|
10
|
+
from openpyxl.utils import column_index_from_string # type: ignore
|
|
11
|
+
from openpyxl.utils.exceptions import InvalidFileException # type: ignore
|
|
12
|
+
from openpyxl.workbook import Workbook # type: ignore
|
|
13
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
14
|
+
|
|
15
|
+
from regscale.core.app.utils.app_utils import error_and_exit, get_current_datetime
|
|
16
|
+
from regscale.core.utils.date import date_str
|
|
17
|
+
from regscale.integrations.scanner_integration import (
|
|
18
|
+
IntegrationAsset,
|
|
19
|
+
IntegrationFinding,
|
|
20
|
+
ScannerIntegration,
|
|
21
|
+
issue_due_date,
|
|
22
|
+
)
|
|
23
|
+
from regscale.models import ImportValidater, IssueSeverity, Mapping, regscale_models
|
|
24
|
+
from regscale.validation.address import validate_ip_address, validate_mac_address
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("regscale")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FedrampPoamIntegration(ScannerIntegration):
|
|
30
|
+
"""Integration class for FedRAMP POAM scanning."""
|
|
31
|
+
|
|
32
|
+
# Keys set in the `set_keys` method of `ScannerIntegration`
|
|
33
|
+
title = "FedRAMP"
|
|
34
|
+
file_path: str = ""
|
|
35
|
+
poam_sheets: List[str] = []
|
|
36
|
+
validators: dict = {}
|
|
37
|
+
workbook: Optional[Workbook] = None
|
|
38
|
+
|
|
39
|
+
asset_identifier_field = "otherTrackingNumber"
|
|
40
|
+
finding_severity_map = {
|
|
41
|
+
"Low": regscale_models.IssueSeverity.Low,
|
|
42
|
+
"Moderate": regscale_models.IssueSeverity.Moderate,
|
|
43
|
+
"High": regscale_models.IssueSeverity.High,
|
|
44
|
+
"Critical": regscale_models.IssueSeverity.Critical,
|
|
45
|
+
}
|
|
46
|
+
poam_id_header = "POAM ID"
|
|
47
|
+
blank_records: int = 0
|
|
48
|
+
blank_threshold: int = 3
|
|
49
|
+
error_records: int = 0
|
|
50
|
+
skipped_records: int = 0
|
|
51
|
+
processed_assets: set[str] = set() # Track processed assets across all methods
|
|
52
|
+
|
|
53
|
+
# TODO: Pair this down to only usable data
|
|
54
|
+
fedramp_poam_columns = [
|
|
55
|
+
"POAM ID",
|
|
56
|
+
"Weakness Name",
|
|
57
|
+
"Weakness Description",
|
|
58
|
+
"Weakness Detector Source",
|
|
59
|
+
"Weakness Source Identifier",
|
|
60
|
+
"Asset Identifier",
|
|
61
|
+
"Point of Contact",
|
|
62
|
+
"Resources Required",
|
|
63
|
+
"Overall Remediation Plan",
|
|
64
|
+
"Original Detection Date",
|
|
65
|
+
"Scheduled Completion Date",
|
|
66
|
+
"Planned Milestones",
|
|
67
|
+
"Milestone Changes",
|
|
68
|
+
"Status Date",
|
|
69
|
+
# "Vendor Dependency",
|
|
70
|
+
# "Last Vendor Check-in Date",
|
|
71
|
+
# "Vendor Dependent Product Name",
|
|
72
|
+
"Original Risk Rating",
|
|
73
|
+
"Adjusted Risk Rating",
|
|
74
|
+
"Risk Adjustment",
|
|
75
|
+
"False Positive",
|
|
76
|
+
"Operational Requirement",
|
|
77
|
+
"Deviation Rationale",
|
|
78
|
+
# "Supporting Documents",
|
|
79
|
+
"Comments",
|
|
80
|
+
# "Auto-Approve",
|
|
81
|
+
# "Binding Operational Directive 22-01 tracking",
|
|
82
|
+
# "Binding Operational Directive 22-01 Due Date",
|
|
83
|
+
# "CVE",
|
|
84
|
+
# "Service Name",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
def __init__(self, plan_id: int, **kwargs: dict):
|
|
88
|
+
super().__init__(plan_id=plan_id)
|
|
89
|
+
try:
|
|
90
|
+
# Use read_only mode for memory efficiency, purposefully use kwarg index to force KeyError
|
|
91
|
+
if "file_path" in kwargs:
|
|
92
|
+
self.file_path = kwargs["file_path"]
|
|
93
|
+
if not self.file_path:
|
|
94
|
+
raise ValueError("File path is required")
|
|
95
|
+
self.workbook = self.workbook or load_workbook(filename=self.file_path, data_only=True, read_only=True)
|
|
96
|
+
self.poam_sheets = kwargs.get("poam_sheets") or [
|
|
97
|
+
sheet for sheet in self.workbook.sheetnames if re.search("POA&M Items", sheet)
|
|
98
|
+
]
|
|
99
|
+
except (FileNotFoundError, InvalidFileException, KeyError) as e:
|
|
100
|
+
logger.error(f"Failed to load workbook: {e}")
|
|
101
|
+
return
|
|
102
|
+
# Validate Here
|
|
103
|
+
if not self.validators and isinstance(self.poam_sheets, list):
|
|
104
|
+
for sheet in self.poam_sheets:
|
|
105
|
+
ws = self.workbook[sheet]
|
|
106
|
+
mapping_path = "./mappings/fedramp_poam/" + sheet
|
|
107
|
+
validator = ImportValidater(
|
|
108
|
+
file_path=self.file_path,
|
|
109
|
+
disable_mapping=True,
|
|
110
|
+
required_headers=self.fedramp_poam_columns,
|
|
111
|
+
worksheet_name=sheet,
|
|
112
|
+
mapping_file_path=mapping_path,
|
|
113
|
+
prompt=True,
|
|
114
|
+
skip_rows=self.find_header_row(ws),
|
|
115
|
+
ignore_unnamed=True,
|
|
116
|
+
)
|
|
117
|
+
self.validators[sheet] = validator
|
|
118
|
+
self.processed_assets = set() # Reset processed assets on init
|
|
119
|
+
|
|
120
|
+
def __enter__(self):
|
|
121
|
+
"""Context manager entry."""
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
125
|
+
"""Context manager exit - cleanup resources."""
|
|
126
|
+
if self.workbook:
|
|
127
|
+
self.workbook.close()
|
|
128
|
+
|
|
129
|
+
def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
|
|
130
|
+
"""
|
|
131
|
+
Fetches findings from FedRAMP POAM files.
|
|
132
|
+
|
|
133
|
+
:raises ValueError: If file path is not set
|
|
134
|
+
:yield: Iterator of validated integration findings
|
|
135
|
+
"""
|
|
136
|
+
if not self.file_path:
|
|
137
|
+
raise ValueError("File path is required")
|
|
138
|
+
|
|
139
|
+
findings = []
|
|
140
|
+
try:
|
|
141
|
+
for sheet in self.poam_sheets:
|
|
142
|
+
validator = self.validators.get(sheet)
|
|
143
|
+
if not validator:
|
|
144
|
+
logger.warning(f"No validator found for sheet: {sheet}")
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
sheet_kwargs = {**kwargs, "sheet": sheet}
|
|
148
|
+
sheet_findings = self._process_sheet(**sheet_kwargs)
|
|
149
|
+
findings.extend(sheet_findings)
|
|
150
|
+
|
|
151
|
+
self.num_findings_to_process = len(findings)
|
|
152
|
+
return iter(findings)
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Error fetching findings from POAM file: {str(e)}")
|
|
156
|
+
return iter(findings)
|
|
157
|
+
|
|
158
|
+
def _process_sheet(self, **kwargs: dict) -> List[IntegrationFinding]:
|
|
159
|
+
"""
|
|
160
|
+
Process a single sheet from the POAM workbook.
|
|
161
|
+
|
|
162
|
+
:param str sheet: The sheet name
|
|
163
|
+
:param **kwargs: Arbitrary keyword arguments
|
|
164
|
+
:return: List of IntegrationFinding objects
|
|
165
|
+
:rtype: List[IntegrationFinding]
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
finding_lst = []
|
|
169
|
+
if not self.workbook:
|
|
170
|
+
return finding_lst
|
|
171
|
+
sheet = kwargs.get("sheet")
|
|
172
|
+
previous_status_date: str = None
|
|
173
|
+
resolve_status = kwargs.get("resolve_empty_status_date", "CURRENT_DATE")
|
|
174
|
+
ws = self.workbook[sheet]
|
|
175
|
+
validator = self.validators.get(sheet)
|
|
176
|
+
category = ws["C3"].value or "Low"
|
|
177
|
+
if not ws["C3"].value:
|
|
178
|
+
logger.warning(f"Category is required in cell C3. Defaulting to Low for sheet {sheet}.")
|
|
179
|
+
|
|
180
|
+
status = self.determine_status(sheet)
|
|
181
|
+
if status is None:
|
|
182
|
+
logger.warning(f"Unable to determine POA&M status for sheet {sheet}. Skipping import.")
|
|
183
|
+
return finding_lst
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
start_row = self.find_start_row(validator.data.values)
|
|
187
|
+
except IndexError:
|
|
188
|
+
return finding_lst
|
|
189
|
+
|
|
190
|
+
if start_row is None:
|
|
191
|
+
logger.warning(f"No POAM entries found in sheet {sheet}. Skipping.")
|
|
192
|
+
return finding_lst
|
|
193
|
+
|
|
194
|
+
logger.info("Processing sheet: %s for findings, rows: %i", sheet, len(validator.data))
|
|
195
|
+
for index, row in enumerate(validator.data.values):
|
|
196
|
+
try:
|
|
197
|
+
if index < start_row:
|
|
198
|
+
continue
|
|
199
|
+
if not validator and validator.mapping:
|
|
200
|
+
logger.error("Validator mapping or validator mapping is None")
|
|
201
|
+
break
|
|
202
|
+
val_mapping = validator.mapping # convert tuple to dict
|
|
203
|
+
data = dict(zip(val_mapping.mapping, row))
|
|
204
|
+
|
|
205
|
+
if not isinstance(data, dict):
|
|
206
|
+
logger.error("data must be a dictionary")
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
if parsed_category := self.determine_category(data, validator):
|
|
210
|
+
category = parsed_category
|
|
211
|
+
# Category must be in IssueSeverity
|
|
212
|
+
if category not in [IssueSeverity.Low.name, IssueSeverity.Moderate.name, IssueSeverity.High.name]:
|
|
213
|
+
logger.warning(f"Invalid Original Risk Rating: {category} in sheet {sheet}. Skipping.")
|
|
214
|
+
continue
|
|
215
|
+
logger.debug(f"Processing row {index} in sheet {sheet.strip()} for findings")
|
|
216
|
+
logger.debug(f"Status: {status}, Category: {category}")
|
|
217
|
+
if not status:
|
|
218
|
+
logger.warning(f"Status is required in sheet {sheet}. Skipping.")
|
|
219
|
+
continue
|
|
220
|
+
if not category:
|
|
221
|
+
logger.warning(f"Category is required in sheet {sheet}. Skipping.")
|
|
222
|
+
continue
|
|
223
|
+
findings = self.parse_finding(
|
|
224
|
+
data=data,
|
|
225
|
+
previous_status_date=previous_status_date,
|
|
226
|
+
status=status,
|
|
227
|
+
category=category,
|
|
228
|
+
index=index,
|
|
229
|
+
sheet=sheet,
|
|
230
|
+
validator=validator,
|
|
231
|
+
resolve_status=resolve_status,
|
|
232
|
+
)
|
|
233
|
+
for finding in findings:
|
|
234
|
+
previous_status_date = finding.date_last_updated
|
|
235
|
+
if isinstance(finding, IntegrationFinding):
|
|
236
|
+
finding_lst.append(finding)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(f"Error processing row {index} in sheet {sheet}: {str(e)}")
|
|
240
|
+
self.error_records += 1
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
return finding_lst
|
|
244
|
+
|
|
245
|
+
def determine_category(self, data: dict, validator: ImportValidater) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Determine the category of the finding by direct string or from a mapping.
|
|
248
|
+
|
|
249
|
+
:param dict data: The row data
|
|
250
|
+
:param ImportValidater validator: The ImportValidater object
|
|
251
|
+
:return: The category of the finding
|
|
252
|
+
"""
|
|
253
|
+
dat_map = {
|
|
254
|
+
"medium": IssueSeverity.Moderate.name,
|
|
255
|
+
"high": IssueSeverity.High.name,
|
|
256
|
+
"critical": IssueSeverity.High.name,
|
|
257
|
+
"low": IssueSeverity.Low.name,
|
|
258
|
+
}
|
|
259
|
+
res = validator.mapping.get_value(data, "Original Risk Rating")
|
|
260
|
+
if res.lower() not in [mem.lower() for mem in IssueSeverity.__members__]:
|
|
261
|
+
res = dat_map.get(res.lower(), IssueSeverity.Low.name)
|
|
262
|
+
return res
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def is_poam(finding: IntegrationFinding) -> bool:
|
|
266
|
+
"""
|
|
267
|
+
Determine if this finding is a POAM.
|
|
268
|
+
|
|
269
|
+
:param IntegrationFinding finding: The finding to check
|
|
270
|
+
:return: True if this is a POAM finding
|
|
271
|
+
:rtype: bool
|
|
272
|
+
"""
|
|
273
|
+
return True # All FedRAMP findings are POAMs
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def get_issue_title(finding: IntegrationFinding) -> str:
|
|
277
|
+
"""
|
|
278
|
+
Get the title for an issue.
|
|
279
|
+
|
|
280
|
+
:param IntegrationFinding finding: The finding
|
|
281
|
+
:return: The issue title
|
|
282
|
+
:rtype: str
|
|
283
|
+
"""
|
|
284
|
+
return finding.title[:255] # Enforce title length limit
|
|
285
|
+
|
|
286
|
+
def parse_finding(self, data: dict, **kwargs) -> Iterator[IntegrationFinding]:
|
|
287
|
+
"""
|
|
288
|
+
Parse a single row from the POAM spreadsheet into IntegrationFinding objects.
|
|
289
|
+
Creates a separate finding for each asset and CVE combination.
|
|
290
|
+
|
|
291
|
+
:param dict data: The row data
|
|
292
|
+
:param kwargs: Arbitrary keyword arguments
|
|
293
|
+
:rtype: Iterator[IntegrationFinding]
|
|
294
|
+
:yields: IntegrationFinding
|
|
295
|
+
"""
|
|
296
|
+
findings = []
|
|
297
|
+
status = kwargs.get("status")
|
|
298
|
+
if not isinstance(status, str):
|
|
299
|
+
raise TypeError("status must be a string")
|
|
300
|
+
|
|
301
|
+
category = kwargs.get("category")
|
|
302
|
+
if not isinstance(category, str):
|
|
303
|
+
raise TypeError("category must be a string")
|
|
304
|
+
|
|
305
|
+
index = kwargs.get("index")
|
|
306
|
+
if not isinstance(index, int):
|
|
307
|
+
raise TypeError("index must be an integer")
|
|
308
|
+
|
|
309
|
+
sheet = kwargs.get("sheet")
|
|
310
|
+
if not isinstance(sheet, str):
|
|
311
|
+
raise TypeError("sheet must be a string")
|
|
312
|
+
|
|
313
|
+
resolve_status = kwargs.get("resolve_status")
|
|
314
|
+
if not isinstance(resolve_status, str):
|
|
315
|
+
raise TypeError("resolve_status must be a string")
|
|
316
|
+
|
|
317
|
+
val_mapping = kwargs.get("validator").mapping
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
poam_id = val_mapping.get_value(data, self.poam_id_header)
|
|
321
|
+
weakness_name = str(val_mapping.get_value(data, "Weakness Name"))
|
|
322
|
+
|
|
323
|
+
if not poam_id and weakness_name in [None, "None", ""]:
|
|
324
|
+
self.blank_records += 1
|
|
325
|
+
yield from findings
|
|
326
|
+
|
|
327
|
+
if not poam_id or not poam_id.upper():
|
|
328
|
+
print(weakness_name, poam_id)
|
|
329
|
+
logger.warning(f"Invalid POAM ID on row {index}, sheet {sheet}. Skipping.")
|
|
330
|
+
yield from findings
|
|
331
|
+
|
|
332
|
+
if not weakness_name:
|
|
333
|
+
logger.warning(f"Title is required on row {index}, sheet {sheet}. Unable to import")
|
|
334
|
+
yield from findings
|
|
335
|
+
|
|
336
|
+
# Get and validate plugin ID
|
|
337
|
+
raw_plugin_id = val_mapping.get_value(data, "Weakness Source Identifier")
|
|
338
|
+
try:
|
|
339
|
+
plugin_id_int = (
|
|
340
|
+
int(raw_plugin_id)
|
|
341
|
+
if raw_plugin_id and str(raw_plugin_id).isdigit()
|
|
342
|
+
else abs(hash(str(raw_plugin_id or ""))) % (10**9)
|
|
343
|
+
)
|
|
344
|
+
except (ValueError, TypeError):
|
|
345
|
+
plugin_id_int = abs(hash(poam_id)) % (10**9)
|
|
346
|
+
|
|
347
|
+
# Get asset identifiers
|
|
348
|
+
asset_ids = val_mapping.get_value(data, "Asset Identifier")
|
|
349
|
+
if not asset_ids:
|
|
350
|
+
logger.warning(f"No asset identifier found on row {index}, sheet {sheet}. Skipping.")
|
|
351
|
+
yield from findings
|
|
352
|
+
|
|
353
|
+
# Clean asset identifiers
|
|
354
|
+
asset_id_list = self.gen_asset_list(asset_ids)
|
|
355
|
+
|
|
356
|
+
if not asset_id_list:
|
|
357
|
+
logger.warning(f"No valid asset identifiers found on row {index}, sheet {sheet}. Skipping.")
|
|
358
|
+
yield from findings
|
|
359
|
+
|
|
360
|
+
# Get and validate CVEs
|
|
361
|
+
cves = self.process_cve(val_mapping.get_value(data, "CVE"), index, sheet)
|
|
362
|
+
cve_list = cves.split("\n") if cves else [""] # Use empty string if no CVEs
|
|
363
|
+
|
|
364
|
+
# Create a finding for each asset and CVE combination
|
|
365
|
+
for asset_id in asset_id_list:
|
|
366
|
+
for cve in cve_list:
|
|
367
|
+
# Create unique plugin ID for each CVE
|
|
368
|
+
if cve:
|
|
369
|
+
unique_plugin_id = abs(hash(f"{plugin_id_int}:{cve}")) % (10**9)
|
|
370
|
+
else:
|
|
371
|
+
unique_plugin_id = plugin_id_int
|
|
372
|
+
|
|
373
|
+
date_created = (
|
|
374
|
+
date_str(val_mapping.get_value(data, "Original Detection Date")) or get_current_datetime()
|
|
375
|
+
)
|
|
376
|
+
due_date = date_str(
|
|
377
|
+
val_mapping.get_value(data, "Scheduled Completion Date")
|
|
378
|
+
if val_mapping.get_value(data, "Scheduled Completion Date") != "#REF!"
|
|
379
|
+
else ""
|
|
380
|
+
)
|
|
381
|
+
severity: IssueSeverity = getattr(IssueSeverity, category.title(), IssueSeverity.NotAssigned)
|
|
382
|
+
if date_created and not due_date:
|
|
383
|
+
due_date = issue_due_date(severity, date_created)
|
|
384
|
+
|
|
385
|
+
# Status Date
|
|
386
|
+
status_date = date_str(val_mapping.get_value(data, "Status Date"))
|
|
387
|
+
if not status_date or status_date == "NaT":
|
|
388
|
+
status_date = self.determine_status_date(**kwargs)
|
|
389
|
+
# if status date is still None, skip this finding
|
|
390
|
+
if not status_date:
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
# Validate pluginText
|
|
394
|
+
finding = IntegrationFinding(
|
|
395
|
+
control_labels=[],
|
|
396
|
+
title=f"{weakness_name[:240]} - {cve}" if cve else weakness_name[:255],
|
|
397
|
+
category=f"FedRAMP POAM: {category}",
|
|
398
|
+
description=val_mapping.get_value(data, "Weakness Description") or "",
|
|
399
|
+
severity=severity,
|
|
400
|
+
status=(
|
|
401
|
+
regscale_models.IssueStatus.Closed
|
|
402
|
+
if status.lower() == "closed"
|
|
403
|
+
else regscale_models.IssueStatus.Open
|
|
404
|
+
),
|
|
405
|
+
asset_identifier=asset_id,
|
|
406
|
+
external_id=f"{poam_id}:{cve}" if cve else poam_id,
|
|
407
|
+
date_created=date_created,
|
|
408
|
+
date_last_updated=status_date,
|
|
409
|
+
due_date=due_date,
|
|
410
|
+
cve=cve, # Single CVE per finding
|
|
411
|
+
plugin_name=val_mapping.get_value(data, "Weakness Detector Source") or "",
|
|
412
|
+
plugin_id=str(unique_plugin_id),
|
|
413
|
+
observations=str(val_mapping.get_value(data, "Milestone Changes")) or "",
|
|
414
|
+
poam_comments=self.empty(val_mapping.get_value(data, "Comments")),
|
|
415
|
+
remediation=self.empty(val_mapping.get_value(data, "Overall Remediation Plan")),
|
|
416
|
+
basis_for_adjustment=str(self.get_basis_for_adjustment(val_mapping=val_mapping, data=data)),
|
|
417
|
+
vulnerability_type="FedRAMP",
|
|
418
|
+
source_report=str(val_mapping.get_value(data, "Weakness Detector Source")),
|
|
419
|
+
point_of_contact=str(val_mapping.get_value(data, "Point of Contact")),
|
|
420
|
+
milestone_changes=str(val_mapping.get_value(data, "Milestone Changes")),
|
|
421
|
+
planned_milestone_changes=str(val_mapping.get_value(data, "Planned Milestones")),
|
|
422
|
+
adjusted_risk_rating=val_mapping.get_value(data, "Adjusted Risk Rating"),
|
|
423
|
+
risk_adjustment=self.determine_risk_adjustment(val_mapping.get_value(data, "Risk Adjustment")),
|
|
424
|
+
operational_requirements=str(val_mapping.get_value(data, "Operational Requirement")),
|
|
425
|
+
deviation_rationale=str(val_mapping.get_value(data, "Deviation Rationale")),
|
|
426
|
+
poam_id=poam_id,
|
|
427
|
+
)
|
|
428
|
+
if finding.is_valid():
|
|
429
|
+
findings.append(finding)
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.error(f"Error processing row {index} in sheet {sheet}: {str(e)}")
|
|
433
|
+
self.error_records += 1
|
|
434
|
+
|
|
435
|
+
yield from findings
|
|
436
|
+
|
|
437
|
+
def determine_status_date(self, **kwargs):
|
|
438
|
+
"""
|
|
439
|
+
Determine the status date.
|
|
440
|
+
|
|
441
|
+
:param kwargs: Arbitrary keyword arguments
|
|
442
|
+
:return: The status date
|
|
443
|
+
:rtype: str
|
|
444
|
+
"""
|
|
445
|
+
index = kwargs.get("index")
|
|
446
|
+
sheet = kwargs.get("sheet")
|
|
447
|
+
resolve_status = kwargs.get("resolve_status")
|
|
448
|
+
status_map = {
|
|
449
|
+
"CURRENT_DATE": date_str(get_current_datetime()),
|
|
450
|
+
"USE_NEIGHBOR": date_str(kwargs.get("previous_status_date")),
|
|
451
|
+
}
|
|
452
|
+
res = date_str(status_map.get(resolve_status), "%m-%d-%Y")
|
|
453
|
+
if res:
|
|
454
|
+
logger.warning(
|
|
455
|
+
f"Status Date missing on row %i, sheet %s, defaulting to %s: %s",
|
|
456
|
+
index,
|
|
457
|
+
sheet,
|
|
458
|
+
resolve_status.lower().replace("_", " "),
|
|
459
|
+
res,
|
|
460
|
+
)
|
|
461
|
+
return res
|
|
462
|
+
logger.warning(
|
|
463
|
+
f"Status Date missing on row {index}, sheet {sheet}. Unable to find valid neighbor, falling back to current date."
|
|
464
|
+
)
|
|
465
|
+
return date_str(status_map.get("CURRENT_DATE"), "%Y-%m-%d")
|
|
466
|
+
|
|
467
|
+
# flake8: noqa: C901
|
|
468
|
+
def parse_asset(self, row: List, validator: ImportValidater) -> List[IntegrationAsset]:
|
|
469
|
+
"""
|
|
470
|
+
Parse a single row from the POAM spreadsheet into IntegrationAsset objects.
|
|
471
|
+
Handles multiple comma-separated asset identifiers.
|
|
472
|
+
|
|
473
|
+
:param List row: The row data from the spreadsheet
|
|
474
|
+
:param ImportValidater validator: The ImportValidater object
|
|
475
|
+
:rtype: List[IntegrationAsset]
|
|
476
|
+
"""
|
|
477
|
+
row_assets = []
|
|
478
|
+
try:
|
|
479
|
+
if validator and validator.mapping:
|
|
480
|
+
val_mapping = validator.mapping # convert tuple to dict
|
|
481
|
+
data = dict(zip(val_mapping.mapping, row))
|
|
482
|
+
else:
|
|
483
|
+
logger.error("Validator mapping is None")
|
|
484
|
+
return row_assets
|
|
485
|
+
asset_ids = val_mapping.get_value(data, "Asset Identifier")
|
|
486
|
+
if not asset_ids:
|
|
487
|
+
return row_assets
|
|
488
|
+
asset_id_list = self.gen_asset_list(asset_ids)
|
|
489
|
+
|
|
490
|
+
if not asset_id_list:
|
|
491
|
+
return row_assets
|
|
492
|
+
|
|
493
|
+
def clean_str(val: Optional[str], default: str = "") -> str:
|
|
494
|
+
"""Clean and validate string values."""
|
|
495
|
+
if not val:
|
|
496
|
+
return default
|
|
497
|
+
if not isinstance(val, str):
|
|
498
|
+
return default
|
|
499
|
+
|
|
500
|
+
# Remove problematic patterns
|
|
501
|
+
val = str(val).strip()
|
|
502
|
+
if any(
|
|
503
|
+
pattern in val.lower()
|
|
504
|
+
for pattern in [
|
|
505
|
+
"n/a",
|
|
506
|
+
"none",
|
|
507
|
+
"null",
|
|
508
|
+
"undefined",
|
|
509
|
+
"planned",
|
|
510
|
+
"pending",
|
|
511
|
+
"tbd",
|
|
512
|
+
"remediation",
|
|
513
|
+
"deviation",
|
|
514
|
+
"request",
|
|
515
|
+
"vulnerability",
|
|
516
|
+
]
|
|
517
|
+
):
|
|
518
|
+
return default
|
|
519
|
+
|
|
520
|
+
# Remove date-like strings
|
|
521
|
+
if re.search(r"\d{4}[-/]\d{1,2}[-/]\d{1,2}", val):
|
|
522
|
+
return default
|
|
523
|
+
|
|
524
|
+
# Remove long descriptions
|
|
525
|
+
if len(val) > 100 or "\n" in val:
|
|
526
|
+
return default
|
|
527
|
+
|
|
528
|
+
return val
|
|
529
|
+
|
|
530
|
+
def determine_asset_type(asset_id: str, raw_type: str) -> str:
|
|
531
|
+
"""Determine asset type based on asset ID and raw type."""
|
|
532
|
+
if not raw_type or raw_type == "Other":
|
|
533
|
+
# Check for common patterns in asset ID
|
|
534
|
+
if any(pattern in asset_id.lower() for pattern in ["docker", "container", "image", "registry"]):
|
|
535
|
+
return "Container"
|
|
536
|
+
elif any(pattern in asset_id.lower() for pattern in ["lambda", "function", "azure-function"]):
|
|
537
|
+
return "Function"
|
|
538
|
+
elif any(pattern in asset_id.lower() for pattern in ["s3", "bucket", "blob", "storage"]):
|
|
539
|
+
return "Storage"
|
|
540
|
+
elif any(pattern in asset_id.lower() for pattern in ["db", "database", "rds", "sql"]):
|
|
541
|
+
return "Database"
|
|
542
|
+
elif any(pattern in asset_id.lower() for pattern in ["ec2", "vm", "instance"]):
|
|
543
|
+
return "Virtual Machine"
|
|
544
|
+
else:
|
|
545
|
+
return "Other"
|
|
546
|
+
return raw_type
|
|
547
|
+
|
|
548
|
+
for asset_id in asset_id_list:
|
|
549
|
+
# Get raw values and clean them
|
|
550
|
+
raw_values = {
|
|
551
|
+
"ip": asset_id if validate_ip_address(asset_id) else "",
|
|
552
|
+
"type": clean_str(val_mapping.get_value(data, "Resources Required")),
|
|
553
|
+
"fqdn": asset_id if self.is_valid_fqdn(asset_id) else "",
|
|
554
|
+
"mac": asset_id if validate_mac_address(asset_id) else "",
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
# Determine proper asset type
|
|
558
|
+
asset_type = determine_asset_type(asset_id, raw_values["type"])
|
|
559
|
+
|
|
560
|
+
res = IntegrationAsset(
|
|
561
|
+
name=asset_id,
|
|
562
|
+
identifier=asset_id,
|
|
563
|
+
asset_type=asset_type, # Use determined asset type
|
|
564
|
+
asset_category=regscale_models.AssetCategory.Hardware,
|
|
565
|
+
parent_id=self.plan_id,
|
|
566
|
+
parent_module=regscale_models.SecurityPlan.get_module_string(),
|
|
567
|
+
status="Active (On Network)",
|
|
568
|
+
ip_address=raw_values["ip"],
|
|
569
|
+
fqdn=raw_values["fqdn"],
|
|
570
|
+
mac_address=raw_values["mac"],
|
|
571
|
+
date_last_updated=get_current_datetime(),
|
|
572
|
+
)
|
|
573
|
+
row_assets.append(res)
|
|
574
|
+
except (KeyError, ValueError, TypeError) as kex:
|
|
575
|
+
logger.error(f"Error parsing asset from row: {str(kex)} (Exception type: {type(kex).__name__})")
|
|
576
|
+
except Exception as ex:
|
|
577
|
+
logger.error(f"Unknown Error parsing asset from row: {str(ex)}")
|
|
578
|
+
|
|
579
|
+
return row_assets
|
|
580
|
+
|
|
581
|
+
def gen_asset_list(self, asset_ids: str):
|
|
582
|
+
"""
|
|
583
|
+
Generate a list of asset identifiers from a string.
|
|
584
|
+
|
|
585
|
+
:param str asset_ids: The asset identifier string
|
|
586
|
+
:return: The list of asset identifiers
|
|
587
|
+
:rtype: List[str]
|
|
588
|
+
"""
|
|
589
|
+
return [aid.strip() for aid in re.split(r"[,\n\r]+", asset_ids) if isinstance(aid, str) and aid.strip()]
|
|
590
|
+
|
|
591
|
+
@staticmethod
|
|
592
|
+
def empty(string: Optional[str]) -> Optional[str]:
|
|
593
|
+
"""
|
|
594
|
+
Convert empty strings and "None" to None.
|
|
595
|
+
|
|
596
|
+
:param Optional[str] string: The input string
|
|
597
|
+
:return: The processed string or None
|
|
598
|
+
:rtype: Optional[str]
|
|
599
|
+
"""
|
|
600
|
+
if not isinstance(string, str):
|
|
601
|
+
return None
|
|
602
|
+
if string.lower() in ["none", "n/a"]:
|
|
603
|
+
return None
|
|
604
|
+
return string
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def determine_status(sheet: str) -> Optional[str]:
|
|
608
|
+
"""
|
|
609
|
+
Determine the status based on sheet name.
|
|
610
|
+
|
|
611
|
+
:param str sheet: The sheet name
|
|
612
|
+
:return: The status (Open/Closed) or None
|
|
613
|
+
:rtype: Optional[str]
|
|
614
|
+
"""
|
|
615
|
+
if "closed" in sheet.lower():
|
|
616
|
+
return "Closed"
|
|
617
|
+
elif "open" in sheet.lower():
|
|
618
|
+
return "Open"
|
|
619
|
+
return None
|
|
620
|
+
|
|
621
|
+
def find_start_row(self, array: "numpy.ndarray") -> Optional[int]:
|
|
622
|
+
"""
|
|
623
|
+
Find the first row containing POAM data.
|
|
624
|
+
|
|
625
|
+
:param array: NumPy array containing the data
|
|
626
|
+
:return: The row number where POAM entries start
|
|
627
|
+
:rtype: Optional[int]
|
|
628
|
+
"""
|
|
629
|
+
if array[0][0] == "Unique identifier for each POAM Item" and array[1][0] == "Unique Identifier":
|
|
630
|
+
if array[2][0] == "V-1Example":
|
|
631
|
+
return 3
|
|
632
|
+
return 2
|
|
633
|
+
|
|
634
|
+
return 0
|
|
635
|
+
|
|
636
|
+
def get_basis_for_adjustment(self, val_mapping: Mapping, data: dict) -> Optional[str]:
|
|
637
|
+
"""
|
|
638
|
+
Get the basis for risk adjustment.
|
|
639
|
+
|
|
640
|
+
:param Mapping val_mapping: The mapping object
|
|
641
|
+
:param dict data: The row data
|
|
642
|
+
:return: The basis for adjustment
|
|
643
|
+
:rtype: Optional[str]
|
|
644
|
+
"""
|
|
645
|
+
basis_for_adjustment = self.empty(val_mapping.get_value(data, "Comments")) # e.g. row 23
|
|
646
|
+
risk_rating = val_mapping.get_value(data, "Original Risk Rating")
|
|
647
|
+
adjusted_risk_rating = val_mapping.get_value(data, "Adjusted Risk Rating")
|
|
648
|
+
|
|
649
|
+
if (adjusted_risk_rating != risk_rating) and not basis_for_adjustment:
|
|
650
|
+
return "POAM Import"
|
|
651
|
+
if adjusted_risk_rating == risk_rating:
|
|
652
|
+
return None
|
|
653
|
+
return basis_for_adjustment
|
|
654
|
+
|
|
655
|
+
def process_cve(self, cve: Optional[str], index: int, sheet: str) -> Optional[str]:
|
|
656
|
+
"""
|
|
657
|
+
Process and validate CVE string. Handles multiple comma-separated CVEs.
|
|
658
|
+
|
|
659
|
+
:param Optional[str] cve: The CVE string
|
|
660
|
+
:param int index: The row index
|
|
661
|
+
:param str sheet: The sheet name
|
|
662
|
+
:return: The processed CVE string, multiple CVEs joined by newlines
|
|
663
|
+
:rtype: Optional[str]
|
|
664
|
+
"""
|
|
665
|
+
cve = self.empty(cve)
|
|
666
|
+
if not cve:
|
|
667
|
+
return None
|
|
668
|
+
|
|
669
|
+
# Split by comma and clean
|
|
670
|
+
cve_list = [c.strip() for c in cve.split(",") if c.strip()]
|
|
671
|
+
if not cve_list:
|
|
672
|
+
return None
|
|
673
|
+
|
|
674
|
+
valid_cves = []
|
|
675
|
+
cve_pattern = r"(?:CVE-\d{4}-\d{4,7}|RHSA-\d{4}:\d+|GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4})"
|
|
676
|
+
|
|
677
|
+
for single_cve in cve_list:
|
|
678
|
+
# Search for CVE pattern in the string
|
|
679
|
+
cve_match = re.search(cve_pattern, single_cve, re.IGNORECASE)
|
|
680
|
+
if cve_match:
|
|
681
|
+
valid_cves.append(cve_match.group(0).upper())
|
|
682
|
+
else:
|
|
683
|
+
logger.warning(f"Invalid CVE format: {single_cve} on row {index}, sheet {sheet}. Skipping this CVE.")
|
|
684
|
+
|
|
685
|
+
# Return newline-separated CVEs or None if no valid CVEs found
|
|
686
|
+
return "\n".join(valid_cves) if valid_cves else None
|
|
687
|
+
|
|
688
|
+
def is_valid_fqdn(self, hostname: str) -> bool:
|
|
689
|
+
"""
|
|
690
|
+
Check if the hostname is valid.
|
|
691
|
+
|
|
692
|
+
:param str hostname: The hostname string
|
|
693
|
+
:return: True if the hostname is valid
|
|
694
|
+
:rtype: bool
|
|
695
|
+
"""
|
|
696
|
+
if validate_ip_address(hostname):
|
|
697
|
+
return False
|
|
698
|
+
|
|
699
|
+
if not hostname or len(hostname) > 255:
|
|
700
|
+
return False
|
|
701
|
+
|
|
702
|
+
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.")
|
|
703
|
+
if not all(char in allowed for char in hostname):
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
parts = hostname.split(".")
|
|
707
|
+
if len(parts) < 2:
|
|
708
|
+
return False
|
|
709
|
+
|
|
710
|
+
if hostname[-1] == ".":
|
|
711
|
+
hostname = hostname[:-1]
|
|
712
|
+
|
|
713
|
+
return all(
|
|
714
|
+
1 <= len(part) <= 63 and not part.startswith("-") and not part.endswith("-") for part in hostname.split(".")
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def find_header_row(self, ws: Worksheet) -> int:
|
|
718
|
+
"""
|
|
719
|
+
Find the header row in the POAM sheet.
|
|
720
|
+
|
|
721
|
+
:param ws: Worksheet
|
|
722
|
+
:return: The header row number
|
|
723
|
+
:rtype: int
|
|
724
|
+
"""
|
|
725
|
+
# Loop every row
|
|
726
|
+
header_row = None
|
|
727
|
+
for ix, row in enumerate(ws.iter_rows(min_row=ws.min_row, max_row=ws.max_row, values_only=True)):
|
|
728
|
+
for cell in row:
|
|
729
|
+
if cell and self.poam_id_header in str(cell):
|
|
730
|
+
header_row = ix + 1
|
|
731
|
+
break
|
|
732
|
+
if header_row:
|
|
733
|
+
break
|
|
734
|
+
if not header_row:
|
|
735
|
+
error_and_exit("Unable to find the header row in the POAM sheet.")
|
|
736
|
+
return header_row
|
|
737
|
+
|
|
738
|
+
def progress_bar(self, progress, total, width=50):
|
|
739
|
+
filled = int(width * progress // total)
|
|
740
|
+
bar = "=" * filled + "-" * (width - filled)
|
|
741
|
+
percent = progress / total * 100
|
|
742
|
+
return f"[{bar}] {percent:.1f}%"
|
|
743
|
+
|
|
744
|
+
def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
|
|
745
|
+
"""
|
|
746
|
+
Fetch assets from FedRAMP POAM files.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
*args: Variable length argument list
|
|
750
|
+
**kwargs: Arbitrary keyword arguments
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
Iterator[IntegrationAsset]: Iterator of parsed integration assets
|
|
754
|
+
|
|
755
|
+
Raises:
|
|
756
|
+
ValueError: If file_path is not set
|
|
757
|
+
POAMProcessingError: If there's an error processing the POAM file
|
|
758
|
+
"""
|
|
759
|
+
if not self.file_path:
|
|
760
|
+
raise ValueError("File path is required")
|
|
761
|
+
|
|
762
|
+
assets = []
|
|
763
|
+
total_processed = 0
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
logger.info(f"Starting POAM sheets processing from {self.file_path}")
|
|
767
|
+
|
|
768
|
+
with self._get_lock("processed_assets"):
|
|
769
|
+
for sheet_name in self.poam_sheets:
|
|
770
|
+
try:
|
|
771
|
+
validator = self.validators.get(sheet_name)
|
|
772
|
+
if not validator:
|
|
773
|
+
logger.warning(f"No validator found for sheet: {sheet_name}")
|
|
774
|
+
continue
|
|
775
|
+
|
|
776
|
+
data = validator.data
|
|
777
|
+
if data.empty:
|
|
778
|
+
logger.warning(f"Empty sheet found: {sheet_name}")
|
|
779
|
+
continue
|
|
780
|
+
|
|
781
|
+
start_row = self.find_start_row(data.values)
|
|
782
|
+
rows_count = len(data.values)
|
|
783
|
+
|
|
784
|
+
logger.info(
|
|
785
|
+
f"Processing sheet '{sheet_name}' with {rows_count} rows starting from row {start_row}"
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
for ix, row in enumerate(data.values[start_row:], start=start_row):
|
|
789
|
+
try:
|
|
790
|
+
new_assets = self.parse_asset(row, validator)
|
|
791
|
+
assets.extend(new_assets)
|
|
792
|
+
total_processed += len(new_assets)
|
|
793
|
+
except Exception as row_error:
|
|
794
|
+
logger.error(
|
|
795
|
+
f"Failed to process row {ix} in sheet '{sheet_name}': {str(row_error)}",
|
|
796
|
+
exc_info=True,
|
|
797
|
+
)
|
|
798
|
+
self.error_records += 1
|
|
799
|
+
|
|
800
|
+
except Exception as sheet_error:
|
|
801
|
+
logger.error(f"Failed to process sheet '{sheet_name}': {str(sheet_error)}", exc_info=True)
|
|
802
|
+
continue
|
|
803
|
+
|
|
804
|
+
except Exception as e:
|
|
805
|
+
error_msg = f"Critical error while processing POAM file: {str(e)}"
|
|
806
|
+
logger.error(error_msg, exc_info=True)
|
|
807
|
+
|
|
808
|
+
finally:
|
|
809
|
+
logger.info(f"Completed processing with {total_processed} assets and {self.error_records} errors")
|
|
810
|
+
|
|
811
|
+
return iter(assets)
|
|
812
|
+
|
|
813
|
+
def find_max_row(self, start_row: int, ws: Worksheet) -> int:
|
|
814
|
+
"""
|
|
815
|
+
A Method to find the max row in the worksheet.
|
|
816
|
+
|
|
817
|
+
:param start_row: int
|
|
818
|
+
:param ws: Worksheet
|
|
819
|
+
:return: The max row number
|
|
820
|
+
:rtype: int
|
|
821
|
+
"""
|
|
822
|
+
last_row = ws.max_row
|
|
823
|
+
for row in range(start_row, last_row):
|
|
824
|
+
if ws.cell(row=row, column=1).value:
|
|
825
|
+
continue
|
|
826
|
+
else:
|
|
827
|
+
return row
|
|
828
|
+
return last_row
|
|
829
|
+
|
|
830
|
+
def determine_risk_adjustment(self, param):
|
|
831
|
+
"""
|
|
832
|
+
Determine the risk adjustment.
|
|
833
|
+
|
|
834
|
+
Yes, No or Pending
|
|
835
|
+
|
|
836
|
+
:param param: The parameter to check
|
|
837
|
+
:return: The risk adjustment
|
|
838
|
+
"""
|
|
839
|
+
adjustment_map = {
|
|
840
|
+
"false": "No",
|
|
841
|
+
"no": "No",
|
|
842
|
+
"": "No",
|
|
843
|
+
None: "No",
|
|
844
|
+
"true": "Yes",
|
|
845
|
+
"yes": "Yes",
|
|
846
|
+
"pending": "Pending",
|
|
847
|
+
"closed": "No",
|
|
848
|
+
"n/a": "No",
|
|
849
|
+
}
|
|
850
|
+
# BMC Prefers this
|
|
851
|
+
return adjustment_map.get(param.lower(), "No")
|