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.

Files changed (481) hide show
  1. regscale/__init__.py +1 -0
  2. regscale/airflow/__init__.py +9 -0
  3. regscale/airflow/azure/__init__.py +9 -0
  4. regscale/airflow/azure/cli.py +89 -0
  5. regscale/airflow/azure/upload_dags.py +116 -0
  6. regscale/airflow/click_dags.py +127 -0
  7. regscale/airflow/click_mixins.py +82 -0
  8. regscale/airflow/config.py +25 -0
  9. regscale/airflow/factories/__init__.py +0 -0
  10. regscale/airflow/factories/connections.py +58 -0
  11. regscale/airflow/factories/workflows.py +78 -0
  12. regscale/airflow/hierarchy.py +88 -0
  13. regscale/airflow/operators/__init__.py +0 -0
  14. regscale/airflow/operators/click.py +36 -0
  15. regscale/airflow/sensors/__init__.py +0 -0
  16. regscale/airflow/sensors/sql.py +107 -0
  17. regscale/airflow/sessions/__init__.py +0 -0
  18. regscale/airflow/sessions/sql/__init__.py +3 -0
  19. regscale/airflow/sessions/sql/queries.py +64 -0
  20. regscale/airflow/sessions/sql/sql_server_queries.py +248 -0
  21. regscale/airflow/tasks/__init__.py +0 -0
  22. regscale/airflow/tasks/branches.py +22 -0
  23. regscale/airflow/tasks/cli.py +116 -0
  24. regscale/airflow/tasks/click.py +73 -0
  25. regscale/airflow/tasks/debugging.py +9 -0
  26. regscale/airflow/tasks/groups.py +116 -0
  27. regscale/airflow/tasks/init.py +60 -0
  28. regscale/airflow/tasks/states.py +47 -0
  29. regscale/airflow/tasks/workflows.py +36 -0
  30. regscale/ansible/__init__.py +9 -0
  31. regscale/core/__init__.py +0 -0
  32. regscale/core/app/__init__.py +3 -0
  33. regscale/core/app/api.py +571 -0
  34. regscale/core/app/application.py +665 -0
  35. regscale/core/app/internal/__init__.py +136 -0
  36. regscale/core/app/internal/admin_actions.py +230 -0
  37. regscale/core/app/internal/assessments_editor.py +873 -0
  38. regscale/core/app/internal/catalog.py +316 -0
  39. regscale/core/app/internal/comparison.py +459 -0
  40. regscale/core/app/internal/control_editor.py +571 -0
  41. regscale/core/app/internal/encrypt.py +79 -0
  42. regscale/core/app/internal/evidence.py +1240 -0
  43. regscale/core/app/internal/file_uploads.py +151 -0
  44. regscale/core/app/internal/healthcheck.py +66 -0
  45. regscale/core/app/internal/login.py +305 -0
  46. regscale/core/app/internal/migrations.py +240 -0
  47. regscale/core/app/internal/model_editor.py +1701 -0
  48. regscale/core/app/internal/poam_editor.py +632 -0
  49. regscale/core/app/internal/workflow.py +105 -0
  50. regscale/core/app/logz.py +74 -0
  51. regscale/core/app/utils/XMLIR.py +258 -0
  52. regscale/core/app/utils/__init__.py +0 -0
  53. regscale/core/app/utils/api_handler.py +358 -0
  54. regscale/core/app/utils/app_utils.py +1110 -0
  55. regscale/core/app/utils/catalog_utils/__init__.py +0 -0
  56. regscale/core/app/utils/catalog_utils/common.py +91 -0
  57. regscale/core/app/utils/catalog_utils/compare_catalog.py +193 -0
  58. regscale/core/app/utils/catalog_utils/diagnostic_catalog.py +97 -0
  59. regscale/core/app/utils/catalog_utils/download_catalog.py +103 -0
  60. regscale/core/app/utils/catalog_utils/update_catalog.py +718 -0
  61. regscale/core/app/utils/catalog_utils/update_catalog_v2.py +1378 -0
  62. regscale/core/app/utils/catalog_utils/update_catalog_v3.py +1272 -0
  63. regscale/core/app/utils/catalog_utils/update_plans.py +334 -0
  64. regscale/core/app/utils/file_utils.py +238 -0
  65. regscale/core/app/utils/parser_utils.py +81 -0
  66. regscale/core/app/utils/pickle_file_handler.py +57 -0
  67. regscale/core/app/utils/regscale_utils.py +319 -0
  68. regscale/core/app/utils/report_utils.py +119 -0
  69. regscale/core/app/utils/variables.py +226 -0
  70. regscale/core/decorators.py +31 -0
  71. regscale/core/lazy_group.py +65 -0
  72. regscale/core/login.py +63 -0
  73. regscale/core/server/__init__.py +0 -0
  74. regscale/core/server/flask_api.py +473 -0
  75. regscale/core/server/helpers.py +373 -0
  76. regscale/core/server/rest.py +64 -0
  77. regscale/core/server/static/css/bootstrap.css +6030 -0
  78. regscale/core/server/static/css/bootstrap.min.css +6 -0
  79. regscale/core/server/static/css/main.css +176 -0
  80. regscale/core/server/static/images/regscale-cli.svg +49 -0
  81. regscale/core/server/static/images/regscale.svg +38 -0
  82. regscale/core/server/templates/base.html +74 -0
  83. regscale/core/server/templates/index.html +43 -0
  84. regscale/core/server/templates/login.html +28 -0
  85. regscale/core/server/templates/make_base64.html +22 -0
  86. regscale/core/server/templates/upload_STIG.html +109 -0
  87. regscale/core/server/templates/upload_STIG_result.html +26 -0
  88. regscale/core/server/templates/upload_ssp.html +144 -0
  89. regscale/core/server/templates/upload_ssp_result.html +128 -0
  90. regscale/core/static/__init__.py +0 -0
  91. regscale/core/static/regex.py +14 -0
  92. regscale/core/utils/__init__.py +117 -0
  93. regscale/core/utils/click_utils.py +13 -0
  94. regscale/core/utils/date.py +238 -0
  95. regscale/core/utils/graphql.py +254 -0
  96. regscale/core/utils/urls.py +23 -0
  97. regscale/dev/__init__.py +6 -0
  98. regscale/dev/analysis.py +454 -0
  99. regscale/dev/cli.py +235 -0
  100. regscale/dev/code_gen.py +492 -0
  101. regscale/dev/dirs.py +69 -0
  102. regscale/dev/docs.py +384 -0
  103. regscale/dev/monitoring.py +26 -0
  104. regscale/dev/profiling.py +216 -0
  105. regscale/exceptions/__init__.py +4 -0
  106. regscale/exceptions/license_exception.py +7 -0
  107. regscale/exceptions/validation_exception.py +9 -0
  108. regscale/integrations/__init__.py +1 -0
  109. regscale/integrations/commercial/__init__.py +486 -0
  110. regscale/integrations/commercial/ad.py +433 -0
  111. regscale/integrations/commercial/amazon/__init__.py +0 -0
  112. regscale/integrations/commercial/amazon/common.py +106 -0
  113. regscale/integrations/commercial/aqua/__init__.py +0 -0
  114. regscale/integrations/commercial/aqua/aqua.py +91 -0
  115. regscale/integrations/commercial/aws/__init__.py +6 -0
  116. regscale/integrations/commercial/aws/cli.py +322 -0
  117. regscale/integrations/commercial/aws/inventory/__init__.py +110 -0
  118. regscale/integrations/commercial/aws/inventory/base.py +64 -0
  119. regscale/integrations/commercial/aws/inventory/resources/__init__.py +19 -0
  120. regscale/integrations/commercial/aws/inventory/resources/compute.py +234 -0
  121. regscale/integrations/commercial/aws/inventory/resources/containers.py +113 -0
  122. regscale/integrations/commercial/aws/inventory/resources/database.py +101 -0
  123. regscale/integrations/commercial/aws/inventory/resources/integration.py +237 -0
  124. regscale/integrations/commercial/aws/inventory/resources/networking.py +253 -0
  125. regscale/integrations/commercial/aws/inventory/resources/security.py +240 -0
  126. regscale/integrations/commercial/aws/inventory/resources/storage.py +91 -0
  127. regscale/integrations/commercial/aws/scanner.py +823 -0
  128. regscale/integrations/commercial/azure/__init__.py +0 -0
  129. regscale/integrations/commercial/azure/common.py +32 -0
  130. regscale/integrations/commercial/azure/intune.py +488 -0
  131. regscale/integrations/commercial/azure/scanner.py +49 -0
  132. regscale/integrations/commercial/burp.py +78 -0
  133. regscale/integrations/commercial/cpe.py +144 -0
  134. regscale/integrations/commercial/crowdstrike.py +1117 -0
  135. regscale/integrations/commercial/defender.py +1511 -0
  136. regscale/integrations/commercial/dependabot.py +210 -0
  137. regscale/integrations/commercial/durosuite/__init__.py +0 -0
  138. regscale/integrations/commercial/durosuite/api.py +1546 -0
  139. regscale/integrations/commercial/durosuite/process_devices.py +101 -0
  140. regscale/integrations/commercial/durosuite/scanner.py +637 -0
  141. regscale/integrations/commercial/durosuite/variables.py +21 -0
  142. regscale/integrations/commercial/ecr.py +90 -0
  143. regscale/integrations/commercial/gcp/__init__.py +237 -0
  144. regscale/integrations/commercial/gcp/auth.py +96 -0
  145. regscale/integrations/commercial/gcp/control_tests.py +238 -0
  146. regscale/integrations/commercial/gcp/variables.py +18 -0
  147. regscale/integrations/commercial/gitlab.py +332 -0
  148. regscale/integrations/commercial/grype.py +165 -0
  149. regscale/integrations/commercial/ibm.py +90 -0
  150. regscale/integrations/commercial/import_all/__init__.py +0 -0
  151. regscale/integrations/commercial/import_all/import_all_cmd.py +467 -0
  152. regscale/integrations/commercial/import_all/scan_file_fingerprints.json +27 -0
  153. regscale/integrations/commercial/jira.py +1046 -0
  154. regscale/integrations/commercial/mappings/__init__.py +0 -0
  155. regscale/integrations/commercial/mappings/csf_controls.json +713 -0
  156. regscale/integrations/commercial/mappings/nist_800_53_r5_controls.json +1516 -0
  157. regscale/integrations/commercial/nessus/__init__.py +0 -0
  158. regscale/integrations/commercial/nessus/nessus_utils.py +429 -0
  159. regscale/integrations/commercial/nessus/scanner.py +416 -0
  160. regscale/integrations/commercial/nexpose.py +90 -0
  161. regscale/integrations/commercial/okta.py +798 -0
  162. regscale/integrations/commercial/opentext/__init__.py +0 -0
  163. regscale/integrations/commercial/opentext/click.py +99 -0
  164. regscale/integrations/commercial/opentext/scanner.py +143 -0
  165. regscale/integrations/commercial/prisma.py +91 -0
  166. regscale/integrations/commercial/qualys.py +1462 -0
  167. regscale/integrations/commercial/salesforce.py +980 -0
  168. regscale/integrations/commercial/sap/__init__.py +0 -0
  169. regscale/integrations/commercial/sap/click.py +31 -0
  170. regscale/integrations/commercial/sap/sysdig/__init__.py +0 -0
  171. regscale/integrations/commercial/sap/sysdig/click.py +57 -0
  172. regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +190 -0
  173. regscale/integrations/commercial/sap/tenable/__init__.py +0 -0
  174. regscale/integrations/commercial/sap/tenable/click.py +49 -0
  175. regscale/integrations/commercial/sap/tenable/scanner.py +196 -0
  176. regscale/integrations/commercial/servicenow.py +1756 -0
  177. regscale/integrations/commercial/sicura/__init__.py +0 -0
  178. regscale/integrations/commercial/sicura/api.py +855 -0
  179. regscale/integrations/commercial/sicura/commands.py +73 -0
  180. regscale/integrations/commercial/sicura/scanner.py +481 -0
  181. regscale/integrations/commercial/sicura/variables.py +16 -0
  182. regscale/integrations/commercial/snyk.py +90 -0
  183. regscale/integrations/commercial/sonarcloud.py +260 -0
  184. regscale/integrations/commercial/sqlserver.py +369 -0
  185. regscale/integrations/commercial/stig_mapper_integration/__init__.py +0 -0
  186. regscale/integrations/commercial/stig_mapper_integration/click_commands.py +38 -0
  187. regscale/integrations/commercial/stig_mapper_integration/mapping_engine.py +353 -0
  188. regscale/integrations/commercial/stigv2/__init__.py +0 -0
  189. regscale/integrations/commercial/stigv2/ckl_parser.py +349 -0
  190. regscale/integrations/commercial/stigv2/click_commands.py +95 -0
  191. regscale/integrations/commercial/stigv2/stig_integration.py +202 -0
  192. regscale/integrations/commercial/synqly/__init__.py +0 -0
  193. regscale/integrations/commercial/synqly/assets.py +46 -0
  194. regscale/integrations/commercial/synqly/ticketing.py +132 -0
  195. regscale/integrations/commercial/synqly/vulnerabilities.py +223 -0
  196. regscale/integrations/commercial/synqly_jira.py +840 -0
  197. regscale/integrations/commercial/tenablev2/__init__.py +0 -0
  198. regscale/integrations/commercial/tenablev2/authenticate.py +31 -0
  199. regscale/integrations/commercial/tenablev2/click.py +1584 -0
  200. regscale/integrations/commercial/tenablev2/scanner.py +504 -0
  201. regscale/integrations/commercial/tenablev2/stig_parsers.py +140 -0
  202. regscale/integrations/commercial/tenablev2/utils.py +78 -0
  203. regscale/integrations/commercial/tenablev2/variables.py +17 -0
  204. regscale/integrations/commercial/trivy.py +162 -0
  205. regscale/integrations/commercial/veracode.py +96 -0
  206. regscale/integrations/commercial/wizv2/WizDataMixin.py +97 -0
  207. regscale/integrations/commercial/wizv2/__init__.py +0 -0
  208. regscale/integrations/commercial/wizv2/click.py +429 -0
  209. regscale/integrations/commercial/wizv2/constants.py +1001 -0
  210. regscale/integrations/commercial/wizv2/issue.py +361 -0
  211. regscale/integrations/commercial/wizv2/models.py +112 -0
  212. regscale/integrations/commercial/wizv2/parsers.py +339 -0
  213. regscale/integrations/commercial/wizv2/sbom.py +115 -0
  214. regscale/integrations/commercial/wizv2/scanner.py +416 -0
  215. regscale/integrations/commercial/wizv2/utils.py +796 -0
  216. regscale/integrations/commercial/wizv2/variables.py +39 -0
  217. regscale/integrations/commercial/wizv2/wiz_auth.py +159 -0
  218. regscale/integrations/commercial/xray.py +91 -0
  219. regscale/integrations/integration/__init__.py +2 -0
  220. regscale/integrations/integration/integration.py +26 -0
  221. regscale/integrations/integration/inventory.py +17 -0
  222. regscale/integrations/integration/issue.py +100 -0
  223. regscale/integrations/integration_override.py +149 -0
  224. regscale/integrations/public/__init__.py +103 -0
  225. regscale/integrations/public/cisa.py +641 -0
  226. regscale/integrations/public/criticality_updater.py +70 -0
  227. regscale/integrations/public/emass.py +411 -0
  228. regscale/integrations/public/emass_slcm_import.py +697 -0
  229. regscale/integrations/public/fedramp/__init__.py +0 -0
  230. regscale/integrations/public/fedramp/appendix_parser.py +548 -0
  231. regscale/integrations/public/fedramp/click.py +479 -0
  232. regscale/integrations/public/fedramp/components.py +714 -0
  233. regscale/integrations/public/fedramp/docx_parser.py +259 -0
  234. regscale/integrations/public/fedramp/fedramp_cis_crm.py +1124 -0
  235. regscale/integrations/public/fedramp/fedramp_common.py +3181 -0
  236. regscale/integrations/public/fedramp/fedramp_docx.py +388 -0
  237. regscale/integrations/public/fedramp/fedramp_five.py +2343 -0
  238. regscale/integrations/public/fedramp/fedramp_traversal.py +138 -0
  239. regscale/integrations/public/fedramp/import_fedramp_r4_ssp.py +279 -0
  240. regscale/integrations/public/fedramp/import_workbook.py +495 -0
  241. regscale/integrations/public/fedramp/inventory_items.py +244 -0
  242. regscale/integrations/public/fedramp/mappings/__init__.py +0 -0
  243. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +7388 -0
  244. regscale/integrations/public/fedramp/mappings/fedramp_r5_params.json +8636 -0
  245. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +9605 -0
  246. regscale/integrations/public/fedramp/mappings/system_roles.py +34 -0
  247. regscale/integrations/public/fedramp/mappings/user.py +175 -0
  248. regscale/integrations/public/fedramp/mappings/values.py +141 -0
  249. regscale/integrations/public/fedramp/markdown_parser.py +150 -0
  250. regscale/integrations/public/fedramp/metadata.py +689 -0
  251. regscale/integrations/public/fedramp/models/__init__.py +59 -0
  252. regscale/integrations/public/fedramp/models/leveraged_auth_new.py +168 -0
  253. regscale/integrations/public/fedramp/models/poam_importer.py +522 -0
  254. regscale/integrations/public/fedramp/parts_mapper.py +107 -0
  255. regscale/integrations/public/fedramp/poam/__init__.py +0 -0
  256. regscale/integrations/public/fedramp/poam/scanner.py +851 -0
  257. regscale/integrations/public/fedramp/properties.py +201 -0
  258. regscale/integrations/public/fedramp/reporting.py +84 -0
  259. regscale/integrations/public/fedramp/resources.py +496 -0
  260. regscale/integrations/public/fedramp/rosetta.py +110 -0
  261. regscale/integrations/public/fedramp/ssp_logger.py +87 -0
  262. regscale/integrations/public/fedramp/system_characteristics.py +922 -0
  263. regscale/integrations/public/fedramp/system_control_implementations.py +582 -0
  264. regscale/integrations/public/fedramp/system_implementation.py +190 -0
  265. regscale/integrations/public/fedramp/xml_utils.py +87 -0
  266. regscale/integrations/public/nist_catalog.py +275 -0
  267. regscale/integrations/public/oscal.py +1946 -0
  268. regscale/integrations/public/otx.py +169 -0
  269. regscale/integrations/scanner_integration.py +2692 -0
  270. regscale/integrations/variables.py +25 -0
  271. regscale/models/__init__.py +7 -0
  272. regscale/models/app_models/__init__.py +5 -0
  273. regscale/models/app_models/catalog_compare.py +213 -0
  274. regscale/models/app_models/click.py +252 -0
  275. regscale/models/app_models/datetime_encoder.py +21 -0
  276. regscale/models/app_models/import_validater.py +321 -0
  277. regscale/models/app_models/mapping.py +260 -0
  278. regscale/models/app_models/pipeline.py +37 -0
  279. regscale/models/click_models.py +413 -0
  280. regscale/models/config.py +154 -0
  281. regscale/models/email_style.css +67 -0
  282. regscale/models/hierarchy.py +8 -0
  283. regscale/models/inspect_models.py +79 -0
  284. regscale/models/integration_models/__init__.py +0 -0
  285. regscale/models/integration_models/amazon_models/__init__.py +0 -0
  286. regscale/models/integration_models/amazon_models/inspector.py +262 -0
  287. regscale/models/integration_models/amazon_models/inspector_scan.py +206 -0
  288. regscale/models/integration_models/aqua.py +247 -0
  289. regscale/models/integration_models/azure_alerts.py +255 -0
  290. regscale/models/integration_models/base64.py +23 -0
  291. regscale/models/integration_models/burp.py +433 -0
  292. regscale/models/integration_models/burp_models.py +128 -0
  293. regscale/models/integration_models/cisa_kev_data.json +19333 -0
  294. regscale/models/integration_models/defender_data.py +93 -0
  295. regscale/models/integration_models/defenderimport.py +143 -0
  296. regscale/models/integration_models/drf.py +443 -0
  297. regscale/models/integration_models/ecr_models/__init__.py +0 -0
  298. regscale/models/integration_models/ecr_models/data.py +69 -0
  299. regscale/models/integration_models/ecr_models/ecr.py +239 -0
  300. regscale/models/integration_models/flat_file_importer.py +1079 -0
  301. regscale/models/integration_models/grype_import.py +247 -0
  302. regscale/models/integration_models/ibm.py +126 -0
  303. regscale/models/integration_models/implementation_results.py +85 -0
  304. regscale/models/integration_models/nexpose.py +140 -0
  305. regscale/models/integration_models/prisma.py +202 -0
  306. regscale/models/integration_models/qualys.py +720 -0
  307. regscale/models/integration_models/qualys_scanner.py +160 -0
  308. regscale/models/integration_models/sbom/__init__.py +0 -0
  309. regscale/models/integration_models/sbom/cyclone_dx.py +139 -0
  310. regscale/models/integration_models/send_reminders.py +620 -0
  311. regscale/models/integration_models/snyk.py +155 -0
  312. regscale/models/integration_models/synqly_models/__init__.py +0 -0
  313. regscale/models/integration_models/synqly_models/capabilities.json +1 -0
  314. regscale/models/integration_models/synqly_models/connector_types.py +22 -0
  315. regscale/models/integration_models/synqly_models/connectors/__init__.py +7 -0
  316. regscale/models/integration_models/synqly_models/connectors/assets.py +97 -0
  317. regscale/models/integration_models/synqly_models/connectors/ticketing.py +583 -0
  318. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +169 -0
  319. regscale/models/integration_models/synqly_models/ocsf_mapper.py +331 -0
  320. regscale/models/integration_models/synqly_models/param.py +72 -0
  321. regscale/models/integration_models/synqly_models/synqly_model.py +733 -0
  322. regscale/models/integration_models/synqly_models/tenants.py +39 -0
  323. regscale/models/integration_models/tenable_models/__init__.py +0 -0
  324. regscale/models/integration_models/tenable_models/integration.py +187 -0
  325. regscale/models/integration_models/tenable_models/models.py +513 -0
  326. regscale/models/integration_models/trivy_import.py +231 -0
  327. regscale/models/integration_models/veracode.py +217 -0
  328. regscale/models/integration_models/xray.py +135 -0
  329. regscale/models/locking.py +100 -0
  330. regscale/models/platform.py +110 -0
  331. regscale/models/regscale_models/__init__.py +67 -0
  332. regscale/models/regscale_models/assessment.py +570 -0
  333. regscale/models/regscale_models/assessment_plan.py +52 -0
  334. regscale/models/regscale_models/asset.py +567 -0
  335. regscale/models/regscale_models/asset_mapping.py +190 -0
  336. regscale/models/regscale_models/case.py +42 -0
  337. regscale/models/regscale_models/catalog.py +261 -0
  338. regscale/models/regscale_models/cci.py +46 -0
  339. regscale/models/regscale_models/change.py +167 -0
  340. regscale/models/regscale_models/checklist.py +372 -0
  341. regscale/models/regscale_models/comment.py +49 -0
  342. regscale/models/regscale_models/compliance_settings.py +112 -0
  343. regscale/models/regscale_models/component.py +412 -0
  344. regscale/models/regscale_models/component_mapping.py +65 -0
  345. regscale/models/regscale_models/control.py +38 -0
  346. regscale/models/regscale_models/control_implementation.py +1128 -0
  347. regscale/models/regscale_models/control_objective.py +261 -0
  348. regscale/models/regscale_models/control_parameter.py +100 -0
  349. regscale/models/regscale_models/control_test.py +34 -0
  350. regscale/models/regscale_models/control_test_plan.py +75 -0
  351. regscale/models/regscale_models/control_test_result.py +52 -0
  352. regscale/models/regscale_models/custom_field.py +245 -0
  353. regscale/models/regscale_models/data.py +109 -0
  354. regscale/models/regscale_models/data_center.py +40 -0
  355. regscale/models/regscale_models/deviation.py +203 -0
  356. regscale/models/regscale_models/email.py +97 -0
  357. regscale/models/regscale_models/evidence.py +47 -0
  358. regscale/models/regscale_models/evidence_mapping.py +40 -0
  359. regscale/models/regscale_models/facility.py +59 -0
  360. regscale/models/regscale_models/file.py +382 -0
  361. regscale/models/regscale_models/filetag.py +37 -0
  362. regscale/models/regscale_models/form_field_value.py +94 -0
  363. regscale/models/regscale_models/group.py +169 -0
  364. regscale/models/regscale_models/implementation_objective.py +335 -0
  365. regscale/models/regscale_models/implementation_option.py +275 -0
  366. regscale/models/regscale_models/implementation_role.py +33 -0
  367. regscale/models/regscale_models/incident.py +177 -0
  368. regscale/models/regscale_models/interconnection.py +43 -0
  369. regscale/models/regscale_models/issue.py +1176 -0
  370. regscale/models/regscale_models/leveraged_authorization.py +125 -0
  371. regscale/models/regscale_models/line_of_inquiry.py +52 -0
  372. regscale/models/regscale_models/link.py +205 -0
  373. regscale/models/regscale_models/meta_data.py +64 -0
  374. regscale/models/regscale_models/mixins/__init__.py +0 -0
  375. regscale/models/regscale_models/mixins/parent_cache.py +124 -0
  376. regscale/models/regscale_models/module.py +224 -0
  377. regscale/models/regscale_models/modules.py +191 -0
  378. regscale/models/regscale_models/objective.py +14 -0
  379. regscale/models/regscale_models/parameter.py +87 -0
  380. regscale/models/regscale_models/ports_protocol.py +81 -0
  381. regscale/models/regscale_models/privacy.py +89 -0
  382. regscale/models/regscale_models/profile.py +50 -0
  383. regscale/models/regscale_models/profile_link.py +68 -0
  384. regscale/models/regscale_models/profile_mapping.py +124 -0
  385. regscale/models/regscale_models/project.py +63 -0
  386. regscale/models/regscale_models/property.py +278 -0
  387. regscale/models/regscale_models/question.py +85 -0
  388. regscale/models/regscale_models/questionnaire.py +87 -0
  389. regscale/models/regscale_models/questionnaire_instance.py +177 -0
  390. regscale/models/regscale_models/rbac.py +132 -0
  391. regscale/models/regscale_models/reference.py +86 -0
  392. regscale/models/regscale_models/regscale_model.py +1643 -0
  393. regscale/models/regscale_models/requirement.py +29 -0
  394. regscale/models/regscale_models/risk.py +274 -0
  395. regscale/models/regscale_models/sbom.py +54 -0
  396. regscale/models/regscale_models/scan_history.py +436 -0
  397. regscale/models/regscale_models/search.py +53 -0
  398. regscale/models/regscale_models/security_control.py +132 -0
  399. regscale/models/regscale_models/security_plan.py +204 -0
  400. regscale/models/regscale_models/software_inventory.py +159 -0
  401. regscale/models/regscale_models/stake_holder.py +64 -0
  402. regscale/models/regscale_models/stig.py +647 -0
  403. regscale/models/regscale_models/supply_chain.py +152 -0
  404. regscale/models/regscale_models/system_role.py +188 -0
  405. regscale/models/regscale_models/system_role_external_assignment.py +40 -0
  406. regscale/models/regscale_models/tag.py +37 -0
  407. regscale/models/regscale_models/tag_mapping.py +19 -0
  408. regscale/models/regscale_models/task.py +133 -0
  409. regscale/models/regscale_models/threat.py +196 -0
  410. regscale/models/regscale_models/user.py +175 -0
  411. regscale/models/regscale_models/user_group.py +55 -0
  412. regscale/models/regscale_models/vulnerability.py +242 -0
  413. regscale/models/regscale_models/vulnerability_mapping.py +162 -0
  414. regscale/models/regscale_models/workflow.py +55 -0
  415. regscale/models/regscale_models/workflow_action.py +34 -0
  416. regscale/models/regscale_models/workflow_instance.py +269 -0
  417. regscale/models/regscale_models/workflow_instance_step.py +114 -0
  418. regscale/models/regscale_models/workflow_template.py +58 -0
  419. regscale/models/regscale_models/workflow_template_step.py +45 -0
  420. regscale/regscale.py +815 -0
  421. regscale/utils/__init__.py +7 -0
  422. regscale/utils/b64conversion.py +14 -0
  423. regscale/utils/click_utils.py +118 -0
  424. regscale/utils/decorators.py +48 -0
  425. regscale/utils/dict_utils.py +59 -0
  426. regscale/utils/files.py +79 -0
  427. regscale/utils/fxns.py +30 -0
  428. regscale/utils/graphql_client.py +113 -0
  429. regscale/utils/lists.py +16 -0
  430. regscale/utils/numbers.py +12 -0
  431. regscale/utils/shell.py +148 -0
  432. regscale/utils/string.py +121 -0
  433. regscale/utils/synqly_utils.py +165 -0
  434. regscale/utils/threading/__init__.py +8 -0
  435. regscale/utils/threading/threadhandler.py +131 -0
  436. regscale/utils/threading/threadsafe_counter.py +47 -0
  437. regscale/utils/threading/threadsafe_dict.py +242 -0
  438. regscale/utils/threading/threadsafe_list.py +83 -0
  439. regscale/utils/version.py +104 -0
  440. regscale/validation/__init__.py +0 -0
  441. regscale/validation/address.py +37 -0
  442. regscale/validation/record.py +48 -0
  443. regscale/visualization/__init__.py +5 -0
  444. regscale/visualization/click.py +34 -0
  445. regscale_cli-6.16.0.0.dist-info/LICENSE +21 -0
  446. regscale_cli-6.16.0.0.dist-info/METADATA +659 -0
  447. regscale_cli-6.16.0.0.dist-info/RECORD +481 -0
  448. regscale_cli-6.16.0.0.dist-info/WHEEL +5 -0
  449. regscale_cli-6.16.0.0.dist-info/entry_points.txt +6 -0
  450. regscale_cli-6.16.0.0.dist-info/top_level.txt +2 -0
  451. tests/fixtures/__init__.py +2 -0
  452. tests/fixtures/api.py +87 -0
  453. tests/fixtures/models.py +91 -0
  454. tests/fixtures/test_fixture.py +144 -0
  455. tests/mocks/__init__.py +0 -0
  456. tests/mocks/objects.py +3 -0
  457. tests/mocks/response.py +32 -0
  458. tests/mocks/xml.py +13 -0
  459. tests/regscale/__init__.py +0 -0
  460. tests/regscale/core/__init__.py +0 -0
  461. tests/regscale/core/test_api.py +232 -0
  462. tests/regscale/core/test_app.py +406 -0
  463. tests/regscale/core/test_login.py +37 -0
  464. tests/regscale/core/test_logz.py +66 -0
  465. tests/regscale/core/test_sbom_generator.py +87 -0
  466. tests/regscale/core/test_validation_utils.py +163 -0
  467. tests/regscale/core/test_version.py +78 -0
  468. tests/regscale/models/__init__.py +0 -0
  469. tests/regscale/models/test_asset.py +71 -0
  470. tests/regscale/models/test_config.py +26 -0
  471. tests/regscale/models/test_control_implementation.py +27 -0
  472. tests/regscale/models/test_import.py +97 -0
  473. tests/regscale/models/test_issue.py +36 -0
  474. tests/regscale/models/test_mapping.py +52 -0
  475. tests/regscale/models/test_platform.py +31 -0
  476. tests/regscale/models/test_regscale_model.py +346 -0
  477. tests/regscale/models/test_report.py +32 -0
  478. tests/regscale/models/test_tenable_integrations.py +118 -0
  479. tests/regscale/models/test_user_model.py +121 -0
  480. tests/regscale/test_about.py +19 -0
  481. 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