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,1546 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import random
6
+ import time
7
+ from functools import wraps
8
+ from typing import Optional, List, Union, Dict, Any, Type, TypeVar
9
+ from urllib.parse import urljoin
10
+
11
+ import requests
12
+ from pydantic import BaseModel, Field
13
+ from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError
14
+
15
+ logger = logging.getLogger("regscale")
16
+
17
+
18
+ def retry_with_backoff(retries=3, backoff_in_seconds=1):
19
+ """
20
+ Decorator for retrying a function with exponential backoff.
21
+
22
+ :param int retries: Number of retries
23
+ :param int backoff_in_seconds: Initial backoff time in seconds
24
+ :return: Decorated function
25
+ """
26
+
27
+ def decorator(func):
28
+ @wraps(func)
29
+ def wrapper(*args, **kwargs):
30
+ x = 0
31
+ while True:
32
+ try:
33
+ return func(*args, **kwargs)
34
+ except (Timeout, RequestsConnectionError) as e:
35
+ if x == retries:
36
+ raise e
37
+ sleep = backoff_in_seconds * 2**x + random.uniform(0, 1)
38
+ time.sleep(sleep)
39
+ x += 1
40
+
41
+ return wrapper
42
+
43
+ return decorator
44
+
45
+
46
+ class DuroSuiteModel(BaseModel):
47
+ """Base model for DuroSuite API responses."""
48
+
49
+ class Config:
50
+ populate_by_name = True
51
+
52
+
53
+ class AuditResponse(DuroSuiteModel):
54
+ """Model for audit response."""
55
+
56
+ device_id: int
57
+ group_id: int
58
+ template_id: int
59
+ audit_id: int
60
+ job_id: str
61
+ status: Optional[str] = None
62
+ error_message: Optional[str] = None
63
+
64
+ class Config:
65
+ populate_by_name = True
66
+ extra = "allow"
67
+
68
+
69
+ class Group(DuroSuiteModel):
70
+ """Model for group information."""
71
+
72
+ id: Optional[int] = Field(None, alias="group_id")
73
+ name: Optional[str] = Field(None, alias="name")
74
+ os_id: Optional[int] = None
75
+ host_group_vars: Optional[List[Dict[str, Any]]] = Field(default_factory=list)
76
+ children: Optional[List[Dict[str, Any]]] = Field(default_factory=list)
77
+
78
+ class Config:
79
+ populate_by_name = True
80
+ extra = "allow"
81
+
82
+
83
+ class Var(DuroSuiteModel):
84
+ """Model for variable information."""
85
+
86
+ id: int = Field(default=None, alias="var_id")
87
+ device_id: Optional[int] = None
88
+ name: str = Field(..., alias="var_name")
89
+ value: str = Field(..., alias="var_value")
90
+
91
+
92
+ class Device(DuroSuiteModel):
93
+ """DuroSuite Device model."""
94
+
95
+ id: Optional[int] = Field(None, alias="device_id")
96
+ name: str = Field(..., alias="name")
97
+ os_id: int = Field(..., alias="os_id")
98
+ group_id: Optional[int] = Field(None, alias="group_id")
99
+ device_vars: Optional[List[Var]] = Field(default_factory=list, alias="device_vars")
100
+ groups: Optional[List[Group]] = Field(default_factory=list, alias="groups")
101
+
102
+
103
+ class STIG(DuroSuiteModel):
104
+ """Model for STIG information."""
105
+
106
+ id: int
107
+ file_name: str
108
+ version: str
109
+ os_id: int
110
+ releaseinfo: str
111
+ playbook: str
112
+
113
+
114
+ class Template(DuroSuiteModel):
115
+ """Template model."""
116
+
117
+ id: Optional[int] = Field(None)
118
+ name: str
119
+ os_id: int
120
+ playbook_id: Optional[int] = None
121
+ group_id: Optional[int] = None
122
+ template_vars: Optional[List[Dict[str, Any]]] = Field(default_factory=list)
123
+
124
+ class Config:
125
+ populate_by_name = True
126
+ extra = "allow"
127
+
128
+
129
+ DuroSuiteModelType = TypeVar("DuroSuiteModelType", bound=DuroSuiteModel)
130
+
131
+
132
+ class DuroSuite:
133
+ """Methods to interact with DuroSuite API"""
134
+
135
+ # API Endpoints
136
+ TEMPLATE_VAR_ENDPOINT = "/api/stigs/templates/var"
137
+ TEMPLATES_ENDPOINT = "/api/stigs/templates"
138
+ DEVICE_VARS_ENDPOINT = "/api/devices/vars"
139
+
140
+ def __init__(self, base_url: str, username: Optional[str] = None, password: Optional[str] = None):
141
+ """
142
+ Initialize DuroSuite API client.
143
+
144
+ :param str base_url: Base URL for the API
145
+ :param Optional[str] username: Username for authentication
146
+ :param Optional[str] password: Password for authentication
147
+ """
148
+ self.base_url = base_url
149
+ self.api_key = None
150
+ if username and password:
151
+ self.username = username
152
+ self.password = password
153
+ self.login(username, password)
154
+
155
+ def login(self, username: str, password: str) -> None:
156
+ """
157
+ Log in to the DuroSuite API.
158
+
159
+ :param str username: Username for authentication
160
+ :param str password: Password for authentication
161
+ :raises ValueError: If login fails
162
+ """
163
+ self.username = username
164
+ self.password = password
165
+ data = {"username": username, "password": password}
166
+ response_data = self._make_request("POST", "/api/login", data=data)
167
+ if response_data:
168
+ self.api_key = response_data.get("access_token")
169
+ if not self.api_key:
170
+ raise ValueError("Login failed: No access token received")
171
+
172
+ def _handle_403_error(self) -> bool:
173
+ """
174
+ Handle 403 error by attempting to refresh token or log in again.
175
+
176
+ :return: True if handled successfully, False otherwise
177
+ :rtype: bool
178
+ """
179
+ if hasattr(self, "username") and hasattr(self, "password"):
180
+ self.login(self.username, self.password)
181
+ return True
182
+ return False
183
+
184
+ @retry_with_backoff(retries=3, backoff_in_seconds=1)
185
+ def _make_request(
186
+ self,
187
+ method: str,
188
+ endpoint: str,
189
+ data: Optional[Dict[str, Any]] = None,
190
+ params: Optional[Dict[str, Any]] = None,
191
+ files: Optional[Dict[str, Any]] = None,
192
+ ) -> Optional[Union[dict, str]]:
193
+ """
194
+ Make a request to the DuroSuite API.
195
+
196
+ :param str method: HTTP method
197
+ :param str endpoint: API endpoint
198
+ :param Optional[Dict[str, Any]] data: Request data
199
+ :param Optional[Dict[str, Any]] params: Query parameters
200
+ :param Optional[Dict[str, Any]] files: Files to upload
201
+ :return: Response data or None if request failed
202
+ :rtype: Optional[Union[dict, str]]
203
+ """
204
+ url = urljoin(self.base_url, endpoint)
205
+ headers = {}
206
+ if self.api_key:
207
+ headers["Authorization"] = f"Bearer {self.api_key}"
208
+ else:
209
+ logger.warning("No API key provided, proceeding without authentication")
210
+
211
+ if data:
212
+ headers["Content-Type"] = "application/json"
213
+
214
+ try:
215
+ response = requests.request(
216
+ method, url, headers=headers, json=data, params=params, verify=True, timeout=60, files=files
217
+ )
218
+
219
+ if response.status_code == 403:
220
+ if self._handle_403_error():
221
+ # Retry the request with the new token
222
+ return self._make_request(method, endpoint, data, params, files)
223
+ else:
224
+ logger.error("Authentication failed and no credentials provided for retry")
225
+ raise requests.exceptions.HTTPError("Authentication failed after retry", response=response)
226
+
227
+ # Special handling for 404 with JSON response
228
+ if response.status_code == 404:
229
+ logger.error(f"Resource not found: {url}")
230
+ try:
231
+ return response.json()
232
+ except: # noqa: E722
233
+ response.raise_for_status()
234
+ else:
235
+ response.raise_for_status()
236
+
237
+ # Try to parse as JSON, if it fails, return the text content
238
+ try:
239
+ r = response.json()
240
+ return r
241
+ except requests.exceptions.JSONDecodeError:
242
+ return response.text
243
+
244
+ except Exception as e:
245
+ logger.error(f"Method: {method}, Endpoint: {endpoint}, Data: {data}, Params: {params}, Files: {files}")
246
+ logger.error(f"Request failed: {e}", exc_info=True)
247
+ raise
248
+
249
+ @staticmethod
250
+ def handle_response(
251
+ response: Optional[Dict[str, Any]], model: Type[DuroSuiteModelType]
252
+ ) -> Optional[DuroSuiteModelType]:
253
+ """
254
+ Handle API response and convert to appropriate DuroSuite model.
255
+
256
+ :param Optional[Dict[str, Any]] response: API response
257
+ :param Type[DuroSuiteModelType] model: DuroSuite model to validate response against
258
+ :return: Validated DuroSuite model instance or None if validation fails
259
+ :rtype: Optional[DuroSuiteModelType]
260
+ """
261
+ if response is None:
262
+ return None
263
+ logger.debug(f"Handling Response: {response}")
264
+ try:
265
+ return model.model_validate(response)
266
+ except ValueError as e:
267
+ logging.error(f"Error validating response: {e}", exc_info=True)
268
+ return None
269
+
270
+ def _handle_list_response(
271
+ self, response: Optional[List[Dict[str, Any]]], model: Type[DuroSuiteModelType]
272
+ ) -> List[DuroSuiteModelType]:
273
+ """
274
+ Handle API list response and convert to a list of appropriate DuroSuite models.
275
+
276
+ :param Optional[List[Dict[str, Any]]] response: API response
277
+ :param Type[DuroSuiteModelType] model: DuroSuite model to validate response against
278
+ :return: List of validated DuroSuite model instances
279
+ :rtype: List[DuroSuiteModelType]
280
+ """
281
+ if response is None:
282
+ return []
283
+ return [
284
+ item
285
+ for item in (self.handle_response(item, model) for item in response if item is not None)
286
+ if item is not None
287
+ ]
288
+
289
+ def revoke_access_token(self) -> Optional[Dict[str, Any]]:
290
+ """
291
+ Revoke Access Token.
292
+
293
+ :return: API response
294
+ :rtype: Optional[Dict[str, Any]]
295
+ """
296
+ return self._make_request("DELETE", "/api/access-revoke")
297
+
298
+ def revoke_refresh_token(self) -> Optional[Dict[str, Any]]:
299
+ """
300
+ Revoke Refresh Token.
301
+
302
+ :return: API response
303
+ :rtype: Optional[Dict[str, Any]]
304
+ """
305
+ return self._make_request("DELETE", "/api/refresh-revoke")
306
+
307
+ def refresh_token(self) -> Optional[Dict[str, Any]]:
308
+ """
309
+ Refresh Token.
310
+
311
+ :return: API response
312
+ :rtype: Optional[Dict[str, Any]]
313
+ """
314
+ return self._make_request("POST", "/api/refresh-token")
315
+
316
+ def change_current_user_password(self, old_password: str, new_password: str) -> Optional[Dict[str, Any]]:
317
+ """
318
+ Change Current User Password.
319
+
320
+ :param str old_password: Current password
321
+ :param str new_password: New password
322
+ :return: API response
323
+ :rtype: Optional[Dict[str, Any]]
324
+ """
325
+ data = {"old_password": old_password, "new_password": new_password}
326
+ return self._make_request("POST", "/api/users/change_password", data=json.dumps(data))
327
+
328
+ def reset_user_password(self, admin_password: str, user_id: int, new_password: str) -> Optional[Dict[str, Any]]:
329
+ """
330
+ Reset User Password.
331
+
332
+ :param str admin_password: Admin password
333
+ :param int user_id: ID of the user whose password is being reset
334
+ :param str new_password: New password
335
+ :return: API response
336
+ :rtype: Optional[Dict[str, Any]]
337
+ """
338
+ data = {"admin_password": admin_password, "user_id": user_id, "new_password": new_password}
339
+ return self._make_request("POST", "/api/users/reset_password", data=json.dumps(data))
340
+
341
+ # 2. Audit Records
342
+ def get_all_audits(self, skip: int = 0, limit: int = 10, **params) -> Optional[Dict[str, Any]]:
343
+ """
344
+ Get All Audits.
345
+
346
+ :param int skip: Number of records to skip
347
+ :param int limit: Number of records to return
348
+ :param params: Additional query parameters
349
+ :return: List of audits
350
+ :rtype: Optional[Dict[str, Any]]
351
+ """
352
+ params.update({"skip": skip, "limit": limit})
353
+ return self._make_request("GET", "/api/audits", params=params)
354
+
355
+ def get_audit_record(self, audit_id: int) -> Optional[Dict[str, Any]]:
356
+ """
357
+ Get audit record.
358
+
359
+ :param int audit_id: ID of the audit
360
+ :return: Audit record
361
+ :rtype: Optional[Dict[str, Any]]
362
+ """
363
+ return self._make_request("GET", f"/api/audits/{audit_id}")
364
+
365
+ def delete_audit(self, audit_id: int) -> Optional[Dict[str, Any]]:
366
+ """
367
+ Delete Audit.
368
+
369
+ :param int audit_id: ID of the audit to delete
370
+ :return: API response
371
+ :rtype: Optional[Dict[str, Any]]
372
+ """
373
+ return self._make_request("DELETE", f"/api/audits/{audit_id}")
374
+
375
+ def get_checklist_file_by_audit_id(self, audit_id: int) -> Optional[str]:
376
+ """
377
+ Get checklist file by audit ID.
378
+
379
+ :param int audit_id: The ID of the audit
380
+ :return: Checklist file content or None if not found
381
+ :rtype: Optional[str]
382
+ """
383
+ return self._make_request("GET", f"/api/audits/checklist/{audit_id}")
384
+
385
+ def combine_checklist_files(self, audit_ids: List[int]) -> Optional[Dict[str, Any]]:
386
+ """
387
+ Combine Checklist Files.
388
+
389
+ :param List[int] audit_ids: List of audit IDs to combine
390
+ :return: Combined checklist file
391
+ :rtype: Optional[Dict[str, Any]]
392
+ """
393
+ return self._make_request("GET", "/api/combine-ckl", params={"audit_ids": audit_ids})
394
+
395
+ def get_vulnerabilities_for_audit(self, audit_id: int) -> Optional[Dict[str, Any]]:
396
+ """
397
+ Get Vulnerabilities For Audit.
398
+
399
+ :param int audit_id: ID of the audit
400
+ :return: List of vulnerabilities
401
+ :rtype: Optional[Dict[str, Any]]
402
+ """
403
+ return self._make_request("GET", "/api/vulnerabilities", params={"audit_id": audit_id})
404
+
405
+ def get_single_vulnerability(self, vuln_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
406
+ """
407
+ Get Single Vulnerability.
408
+
409
+ :param Dict[str, Any] vuln_data: Vulnerability data
410
+ :return: Vulnerability details
411
+ :rtype: Optional[Dict[str, Any]]
412
+ """
413
+ return self._make_request("POST", "/api/single-vuln", data=json.dumps(vuln_data))
414
+
415
+ # 3. Remediate Records
416
+ def get_all_remediations(self, skip: int = 0, limit: int = 10, **params) -> Optional[Dict[str, Any]]:
417
+ """
418
+ Get All Remediations.
419
+
420
+ :param int skip: Number of records to skip
421
+ :param int limit: Number of records to return
422
+ :param params: Additional query parameters
423
+ :return: List of remediations
424
+ :rtype: Optional[Dict[str, Any]]
425
+ """
426
+ params.update({"skip": skip, "limit": limit})
427
+ return self._make_request("GET", "/api/remediations", params=params)
428
+
429
+ def get_remediation_record(self, remediation_id: int) -> Optional[Dict[str, Any]]:
430
+ """
431
+ Get Remediation Record.
432
+
433
+ :param int remediation_id: ID of the remediation
434
+ :return: Remediation record
435
+ :rtype: Optional[Dict[str, Any]]
436
+ """
437
+ return self._make_request("GET", f"/api/remediations/{remediation_id}")
438
+
439
+ def delete_remediation(self, remediation_id: int) -> Optional[Dict[str, Any]]:
440
+ """
441
+ Delete Remediation.
442
+
443
+ :param int remediation_id: ID of the remediation to delete
444
+ :return: API response
445
+ :rtype: Optional[Dict[str, Any]]
446
+ """
447
+ return self._make_request("DELETE", f"/api/remediations/{remediation_id}")
448
+
449
+ # 4. Devices
450
+ def get_devices(self) -> List[Device]:
451
+ """
452
+ Get all devices.
453
+
454
+ :return: List of all devices
455
+ :rtype: List[Device]
456
+ """
457
+ try:
458
+ response = self._make_request("GET", "/api/devices")
459
+ # Handle 404 "No devices available" response
460
+ if not response or (isinstance(response, dict) and "detail" in response):
461
+ logger.debug(f"No devices found: {response}")
462
+ return []
463
+ return self._handle_list_response(response, Device)
464
+ except Exception as e:
465
+ logger.error(f"Failed to get devices: {e}", exc_info=True)
466
+ return []
467
+
468
+ def update_device(self, device_data: Device) -> Optional[Dict[str, Any]]:
469
+ """
470
+ Update Device.
471
+
472
+ :param Device device_data: Updated device data
473
+ :return: API response
474
+ :rtype: Optional[Dict[str, Any]]
475
+ """
476
+ return self._make_request("PUT", "/api/devices", params=device_data.model_dump())
477
+
478
+ def add_new_device(self, device_data: Dict[str, Any]) -> Optional[Device]:
479
+ """
480
+ Add a new device.
481
+
482
+ :param Dict[str, Any] device_data: Device data to add
483
+ :return: Added device
484
+ :rtype: Optional[Device]
485
+ """
486
+ try:
487
+ # Format device data according to API spec
488
+ request_data = {
489
+ "device_name": device_data["name"], # API expects device_name
490
+ "os_id": int(device_data["os_id"]),
491
+ "group_id": int(device_data["group_id"]),
492
+ }
493
+
494
+ logger.debug(f"Creating device with data: {request_data}")
495
+
496
+ # Create device first
497
+ response = self._make_request("POST", "/api/devices", data=request_data)
498
+
499
+ if not response:
500
+ logger.error("Failed to create device - empty response")
501
+ return None
502
+
503
+ logger.debug(f"Device creation response: {response}")
504
+
505
+ device_id = response.get("device_id")
506
+ if not device_id:
507
+ logger.error("Failed to get device ID from response")
508
+ return None
509
+
510
+ # Add device variables if provided
511
+ if "device_vars" in device_data:
512
+ for var in device_data["device_vars"]:
513
+ var_data = {"device_id": device_id, "var_name": var["var_name"], "var_value": var["var_value"]}
514
+
515
+ logger.debug(f"Adding device variable: {var_data}")
516
+ self._make_request("POST", "/api/devices/vars", data=var_data)
517
+
518
+ # Get all devices and find our newly created one
519
+ all_devices = self._make_request("GET", "/api/devices")
520
+ if not all_devices:
521
+ logger.error("Failed to get devices list")
522
+ return None
523
+
524
+ # Find our device in the list
525
+ device_response = next((d for d in all_devices if d.get("device_id") == device_id), None)
526
+
527
+ if not device_response:
528
+ logger.error(f"Could not find device {device_id} in devices list")
529
+ # Return a basic device object with what we know
530
+ device_response = {
531
+ "device_id": device_id,
532
+ "name": device_data["name"],
533
+ "os_id": device_data["os_id"],
534
+ "group_id": device_data["group_id"],
535
+ "device_vars": device_data.get("device_vars", []),
536
+ "groups": [{"id": device_data["group_id"]}],
537
+ }
538
+
539
+ return Device(**device_response)
540
+ except Exception as e:
541
+ logger.error(f"Failed to create device: {e}", exc_info=True)
542
+ return None
543
+
544
+ def get_csv_for_batch_upload(self, os_id: int) -> Optional[str]:
545
+ """
546
+ Get CSV For Batch Upload.
547
+
548
+ :param int os_id: Operating system ID
549
+ :return: CSV content
550
+ :rtype: Optional[str]
551
+ """
552
+ return self._make_request("GET", "/api/devices/batch-upload", params={"os_id": os_id})
553
+
554
+ def batch_upload_devices(self, os_id: int, csv_file: Any) -> Optional[Dict[str, Any]]:
555
+ """
556
+ Batch Upload Devices.
557
+
558
+ :param int os_id: Operating system ID
559
+ :param Any csv_file: CSV file containing device information
560
+ :return: API response
561
+ :rtype: Optional[Dict[str, Any]]
562
+ """
563
+ files = {"devices_file": csv_file}
564
+ return self._make_request("POST", f"/api/devices/batch-upload?os_id={os_id}", files=files)
565
+
566
+ def get_devices_by_group_id(self, group_id: int) -> Optional[List[Device]]:
567
+ """
568
+ Get Devices By Group ID.
569
+
570
+ :param int group_id: Group ID
571
+ :return: List of devices in the group
572
+ :rtype: Optional[List[Device]]
573
+ """
574
+ return self._make_request("GET", f"/api/devices/{group_id}")
575
+
576
+ def delete_device(self, device_id: int) -> Optional[Dict[str, Any]]:
577
+ """
578
+ Delete Device.
579
+
580
+ :param int device_id: Device ID to delete
581
+ :return: API response
582
+ :rtype: Optional[Dict[str, Any]]
583
+ """
584
+ return self._make_request("DELETE", f"/api/devices/{device_id}")
585
+
586
+ def update_device_variable(self, var_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
587
+ """
588
+ Update Device Variable.
589
+
590
+ :param Dict[str, Any] var_data: Variable data to update
591
+ :return: API response
592
+ :rtype: Optional[Dict[str, Any]]
593
+ """
594
+ return self._make_request("PUT", self.DEVICE_VARS_ENDPOINT, params=var_data)
595
+
596
+ def add_new_device_variable(self, var: Var) -> Optional[Var]:
597
+ """
598
+ Add New Device Variable.
599
+
600
+ :param Var var: New variable data
601
+ :return: Added variable
602
+ :rtype: Optional[Var]
603
+ """
604
+ data = var.model_dump(by_alias=True, exclude_none=True)
605
+ response = self._make_request("POST", self.DEVICE_VARS_ENDPOINT, data=json.dumps(data))
606
+ return self.handle_response(response, Var)
607
+
608
+ def delete_device_variable(self, var_id: int) -> Optional[Dict[str, Any]]:
609
+ """
610
+ Delete Device Variable.
611
+
612
+ :param int var_id: Variable ID to delete
613
+ :return: API response
614
+ :rtype: Optional[Dict[str, Any]]
615
+ """
616
+ return self._make_request("DELETE", f"{self.DEVICE_VARS_ENDPOINT}/{var_id}")
617
+
618
+ def test_connection(self, device_id: int, group_id: int) -> Optional[Dict[str, Any]]:
619
+ """
620
+ Test Connection.
621
+
622
+ :param int device_id: Device ID
623
+ :param int group_id: Group ID
624
+ :return: Connection test results
625
+ :rtype: Optional[Dict[str, Any]]
626
+ """
627
+ params = {"device_id": device_id, "group_id": group_id}
628
+ return self._make_request("GET", "/api/connection-test", params=params)
629
+
630
+ # 5. Groups
631
+ def get_groups(self) -> List[Group]:
632
+ """
633
+ Get all groups.
634
+
635
+ :return: List of all groups
636
+ :rtype: List[Group]
637
+ """
638
+ try:
639
+ response = self._make_request("GET", "/api/groups")
640
+ if not response:
641
+ return []
642
+
643
+ # Log response for debugging
644
+ logger.debug(f"Get groups response: {response}")
645
+
646
+ # Handle both single group and list responses
647
+ if isinstance(response, dict):
648
+ response = [response]
649
+
650
+ groups = []
651
+ for group_data in response:
652
+ try:
653
+ # Ensure group_id is properly set
654
+ if "group_id" not in group_data and "id" in group_data:
655
+ group_data["group_id"] = group_data["id"]
656
+ groups.append(Group(**group_data))
657
+ except Exception as e:
658
+ logger.error(f"Failed to parse group data: {e}", exc_info=True)
659
+
660
+ return groups
661
+ except Exception as e:
662
+ logger.error(f"Failed to get groups: {e}", exc_info=True)
663
+ return []
664
+
665
+ def update_group(self, group_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
666
+ """
667
+ Update Group.
668
+
669
+ :param Dict[str, Any] group_data: Updated group data
670
+ :return: API response
671
+ :rtype: Optional[Dict[str, Any]]
672
+ """
673
+ return self._make_request("PUT", "/api/groups", params=group_data)
674
+
675
+ def add_new_group(self, group_data: Dict[str, Any]) -> Optional[Group]:
676
+ """
677
+ Add a new group.
678
+
679
+ :param Dict[str, Any] group_data: Group data to add
680
+ :return: Added group
681
+ :rtype: Optional[Group]
682
+ """
683
+ try:
684
+ # Format group data according to API spec
685
+ request_data = {
686
+ "group_name": group_data["group_name"], # API expects group_name
687
+ "os_id": int(group_data["os_id"]),
688
+ }
689
+
690
+ logger.debug(f"Creating group with data: {request_data}")
691
+
692
+ response = self._make_request("POST", "/api/groups", data=request_data)
693
+
694
+ if not response:
695
+ logger.error("Failed to create group - empty response")
696
+ return None
697
+
698
+ logger.debug(f"Group creation response: {response}")
699
+
700
+ # Map response fields to Group model fields
701
+ group_data = {
702
+ "group_id": response.get("group_id"),
703
+ "name": response.get("group_name"),
704
+ "os_id": response.get("group_os_id"), # Changed from os_id to group_os_id
705
+ "host_group_vars": [],
706
+ "children": [],
707
+ }
708
+
709
+ # Create the group first
710
+ group = Group(**group_data)
711
+ if not group.id:
712
+ logger.error("Failed to parse group response - missing id")
713
+ return None
714
+
715
+ # Step 2: Create template for the group
716
+ template_data = {"name": "Baseline", "os_id": int(group_data["os_id"]), "group_id": group.id}
717
+
718
+ template_response = self._make_request("POST", "/api/stigs/templates", data=template_data)
719
+
720
+ if not template_response:
721
+ logger.error("Failed to create template")
722
+ return None
723
+
724
+ template_id = template_response.get("id")
725
+
726
+ # Step 3: Add template variables
727
+ if "variables" in group_data and template_id:
728
+ for var_name, var_value in group_data["variables"].items():
729
+ var_data = {"template_id": template_id, "var_name": var_name, "var_value": str(var_value)}
730
+
731
+ logger.debug(f"Adding template variable: {var_data}")
732
+ self._make_request("POST", "/api/stigs/templates/var", data=var_data)
733
+
734
+ return group
735
+ except Exception as e:
736
+ logger.error(f"Failed to create group: {e}", exc_info=True)
737
+ return None
738
+
739
+ def get_groups_by_device_id(self, device_id: int) -> Optional[List[Group]]:
740
+ """
741
+ Get Groups By Device ID.
742
+
743
+ :param int device_id: Device ID
744
+ :return: List of groups associated with the device
745
+ :rtype: Optional[List[Group]]
746
+ """
747
+ return self._make_request("GET", f"/api/groups/{device_id}")
748
+
749
+ def delete_group(self, group_id: int) -> Optional[Dict[str, Any]]:
750
+ """
751
+ Delete Group.
752
+
753
+ :param int group_id: Group ID to delete
754
+ :return: API response
755
+ :rtype: Optional[Dict[str, Any]]
756
+ """
757
+ return self._make_request("DELETE", f"/api/groups/{group_id}")
758
+
759
+ def add_device_to_group(self, group_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
760
+ """
761
+ Add Device To Group.
762
+
763
+ :param Dict[str, Any] group_data: Data for adding device to group
764
+ :return: API response
765
+ :rtype: Optional[Dict[str, Any]]
766
+ """
767
+ return self._make_request("POST", "/api/groups/device", data=json.dumps(group_data))
768
+
769
+ def remove_device_from_group(self, group_id: int, device_id: int) -> Optional[Dict[str, Any]]:
770
+ """
771
+ Remove Device From Group.
772
+
773
+ :param int group_id: Group ID
774
+ :param int device_id: Device ID to remove
775
+ :return: API response
776
+ :rtype: Optional[Dict[str, Any]]
777
+ """
778
+ return self._make_request("DELETE", f"/api/groups/device/{group_id}/{device_id}")
779
+
780
+ def add_child_to_group(self, group_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
781
+ """
782
+ Add Child To Group.
783
+
784
+ :param Dict[str, Any] group_data: Data for adding child to group
785
+ :return: API response
786
+ :rtype: Optional[Dict[str, Any]]
787
+ """
788
+ return self._make_request("POST", "/api/groups/child", data=json.dumps(group_data))
789
+
790
+ def remove_child_from_group(self, parent_id: int, child_id: int) -> Optional[Dict[str, Any]]:
791
+ """
792
+ Remove Child From Group.
793
+
794
+ :param int parent_id: Parent group ID
795
+ :param int child_id: Child group ID to remove
796
+ :return: API response
797
+ :rtype: Optional[Dict[str, Any]]
798
+ """
799
+ return self._make_request("DELETE", f"/api/groups/child/{parent_id}/{child_id}")
800
+
801
+ def get_roles_by_group_id(self, group_id: int) -> Optional[List[Dict[str, Any]]]:
802
+ """
803
+ Get Roles By Group ID.
804
+
805
+ :param int group_id: Group ID
806
+ :return: List of roles associated with the group
807
+ :rtype: Optional[List[Dict[str, Any]]
808
+ """
809
+ return self._make_request("GET", f"/api/groups/roles/{group_id}")
810
+
811
+ def add_user_to_group(self, group_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
812
+ """
813
+ Add User To Group.
814
+
815
+ :param Dict[str, Any] group_data: Data for adding user to group
816
+ :return: API response
817
+ :rtype: Optional[Dict[str, Any]]
818
+ """
819
+ return self._make_request("POST", "/api/groups/user", data=json.dumps(group_data))
820
+
821
+ def remove_user_from_group(self, user_id: int, perm_id: int) -> Optional[Dict[str, Any]]:
822
+ """
823
+ Remove User From Group.
824
+
825
+ :param int user_id: User ID to remove
826
+ :param int perm_id: Permission ID
827
+ :return: API response
828
+ :rtype: Optional[Dict[str, Any]]
829
+ """
830
+ return self._make_request("DELETE", f"/api/groups/user/{user_id}/{perm_id}")
831
+
832
+ def get_current_users_group_perms(self, group_id: int) -> Optional[Dict[str, Any]]:
833
+ """
834
+ Get Current Users Group Permissions.
835
+
836
+ :param int group_id: The ID of the group
837
+ :return: Group permissions for the current user
838
+ :rtype: Optional[Dict[str, Any]]
839
+ """
840
+ return self._make_request("GET", f"/api/groups/perms/{group_id}")
841
+
842
+ # 6. Settings
843
+ def get_ansible_cfg_settings(self) -> Optional[Dict[str, Any]]:
844
+ """
845
+ Get Ansible Configuration Settings.
846
+
847
+ :return: Ansible configuration settings
848
+ :rtype: Optional[Dict[str, Any]]
849
+ """
850
+ return self._make_request("GET", "/api/ansible-cfg")
851
+
852
+ def update_ansible_cfg(self, ansible_cfg_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
853
+ """
854
+ Update Ansible Configuration.
855
+
856
+ :param Dict[str, Any] ansible_cfg_data: Updated Ansible configuration data
857
+ :return: API response
858
+ :rtype: Optional[Dict[str, Any]]
859
+ """
860
+ return self._make_request("PUT", "/api/ansible-cfg", data=json.dumps(ansible_cfg_data))
861
+
862
+ def get_site_theme(self) -> Optional[Dict[str, Any]]:
863
+ """
864
+ Get Site Theme.
865
+
866
+ :return: Site theme information
867
+ :rtype: Optional[Dict[str, Any]]
868
+ """
869
+ return self._make_request("GET", "/api/settings/theme")
870
+
871
+ def update_site_theme(self, theme_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
872
+ """
873
+ Update Site Theme.
874
+
875
+ :param Dict[str, Any] theme_data: Updated theme data
876
+ :return: API response
877
+ :rtype: Optional[Dict[str, Any]]
878
+ """
879
+ return self._make_request("PUT", "/api/settings/theme", data=json.dumps(theme_data))
880
+
881
+ def get_database_backup(self) -> Optional[Dict[str, Any]]:
882
+ """
883
+ Get Database Backup.
884
+
885
+ :return: Database backup information
886
+ :rtype: Optional[Dict[str, Any]]
887
+ """
888
+ return self._make_request("GET", "/api/settings/backup")
889
+
890
+ def get_dns_conf(self) -> Optional[Dict[str, Any]]:
891
+ """
892
+ Get DNS Configuration.
893
+
894
+ :return: DNS configuration information
895
+ :rtype: Optional[Dict[str, Any]]
896
+ """
897
+ return self._make_request("GET", "/api/settings/dns")
898
+
899
+ def set_dns_conf(self, dns_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
900
+ """
901
+ Set DNS Configuration.
902
+
903
+ :param Dict[str, Any] dns_data: DNS configuration data
904
+ :return: API response
905
+ :rtype: Optional[Dict[str, Any]]
906
+ """
907
+ return self._make_request("POST", "/api/settings/dns", data=json.dumps(dns_data))
908
+
909
+ def get_license_info(self) -> Optional[Dict[str, Any]]:
910
+ """
911
+ Get License Info.
912
+
913
+ :return: License information
914
+ :rtype: Optional[Dict[str, Any]]
915
+ """
916
+ return self._make_request("GET", "/api/settings/license")
917
+
918
+ def license_key_ingest(self, license_file: Any) -> Optional[Dict[str, Any]]:
919
+ """
920
+ License Key Ingest.
921
+
922
+ :param Any license_file: License file to ingest
923
+ :return: API response
924
+ :rtype: Optional[Dict[str, Any]]
925
+ """
926
+ files = {"license_file": license_file}
927
+ return self._make_request("POST", "/api/settings/license", files=files)
928
+
929
+ def get_version_info(self) -> Optional[Dict[str, Any]]:
930
+ """
931
+ Get Version Info.
932
+
933
+ :return: Version information
934
+ :rtype: Optional[Dict[str, Any]]
935
+ """
936
+ return self._make_request("GET", "/api/settings/version")
937
+
938
+ def get_license_status(self) -> Optional[Dict[str, Any]]:
939
+ """
940
+ Get License Status.
941
+
942
+ :return: License status information
943
+ :rtype: Optional[Dict[str, Any]]
944
+ """
945
+ return self._make_request("GET", "/api/settings/license/status")
946
+
947
+ # 7. Supported Systems
948
+ def get_supported_operating_systems(self) -> Optional[List[Dict[str, Any]]]:
949
+ """
950
+ Get Supported Operating Systems.
951
+
952
+ :return: List of supported operating systems
953
+ :rtype: Optional[List[Dict[str, Any]]]
954
+ """
955
+ return self._make_request("GET", "/api/operating-systems")
956
+
957
+ def get_operating_system_default_variables(self, os_id: int) -> Optional[Dict[str, Any]]:
958
+ """
959
+ Get Operating System Default Variables.
960
+
961
+ :param int os_id: Operating system ID
962
+ :return: Default variables for the specified operating system
963
+ :rtype: Optional[Dict[str, Any]]
964
+ """
965
+ return self._make_request("GET", f"/api/operating-systems/defaults/{os_id}")
966
+
967
+ def get_os_connection_vars(self, os_id: int) -> Optional[Dict[str, Any]]:
968
+ """
969
+ Get OS Connection Variables.
970
+
971
+ :param int os_id: Operating system ID
972
+ :return: Connection variables for the specified operating system
973
+ :rtype: Optional[Dict[str, Any]]
974
+ """
975
+ return self._make_request("GET", f"/api/operating-systems/connection-vars/{os_id}")
976
+
977
+ def get_available_stigs(self) -> List[STIG]:
978
+ """
979
+ Get Available STIGs.
980
+
981
+ :return: List of available STIGs
982
+ :rtype: List[STIG]
983
+ """
984
+ response = self._make_request("GET", "/api/stigs")
985
+ return self._handle_list_response(response, STIG)
986
+
987
+ def get_stigs_by_os_id(self, os_id: int) -> List[STIG]:
988
+ """
989
+ Get STIGs By OS ID.
990
+
991
+ :param int os_id: Operating system ID
992
+ :return: List of STIGs for the specified operating system
993
+ :rtype: List[STIG]
994
+ """
995
+ response = self._make_request("GET", f"/api/stigs/{os_id}")
996
+ return self._handle_list_response(response, STIG)
997
+
998
+ def get_stig_defaults_by_stig_id(self, stig_id: int) -> Optional[STIG]:
999
+ """
1000
+ Get STIG Defaults By STIG ID.
1001
+
1002
+ :param int stig_id: STIG ID
1003
+ :return: Default STIG information
1004
+ :rtype: Optional[STIG]
1005
+ """
1006
+ response = self._make_request("GET", f"/api/stigs/defaults/{stig_id}")
1007
+ return self.handle_response(response, STIG)
1008
+
1009
+ def create_template(self, name: str, os_id: int, playbook_id: int, group_id: int) -> Template:
1010
+ """
1011
+ Create a new template.
1012
+
1013
+ :param str name: The name of the template.
1014
+ :param int os_id: The ID of the operating system.
1015
+ :param int playbook_id: The ID of the playbook.
1016
+ :param int group_id: The ID of the group.
1017
+
1018
+ :return: The created template.
1019
+ :rtype: Template
1020
+ """
1021
+ try:
1022
+ # Log input parameters
1023
+ logger.debug(
1024
+ f"Creating template with params: name={name}, os_id={os_id}, playbook_id={playbook_id}, group_id={group_id}"
1025
+ )
1026
+
1027
+ # Create request data according to swagger spec for TemplateRequest
1028
+ template_request = {
1029
+ "name": name,
1030
+ "os_id": int(os_id), # Ensure integer type
1031
+ "group_id": int(group_id), # Ensure integer type
1032
+ "playbook_id": int(playbook_id), # Ensure integer type
1033
+ "template_vars": [], # Added empty template_vars array
1034
+ }
1035
+
1036
+ # Log request data
1037
+ logger.debug(f"Template request data: {template_request}")
1038
+
1039
+ # No need to set headers here as _make_request handles authorization
1040
+ response = self._make_request(
1041
+ "POST", self.TEMPLATES_ENDPOINT, data=template_request # _make_request will handle JSON serialization
1042
+ )
1043
+
1044
+ if not response:
1045
+ error_msg = "Failed to create template - empty response"
1046
+ logger.error(error_msg)
1047
+ raise ValueError(error_msg)
1048
+
1049
+ response_data = response
1050
+
1051
+ # Log the response for debugging
1052
+ logger.debug(f"Template creation response: {response_data}")
1053
+
1054
+ # Ensure the response has all required fields
1055
+ response_data.update({"os_id": os_id, "playbook_id": playbook_id, "group_id": group_id})
1056
+
1057
+ # Log the final template data
1058
+ logger.debug(f"Final template data: {response_data}")
1059
+
1060
+ return Template(**response_data)
1061
+ except Exception as e:
1062
+ logger.error(f"Failed to create template: {str(e)}", exc_info=True) # Added full stack trace
1063
+ raise ValueError(f"Failed to create template: {str(e)}")
1064
+
1065
+ def update_template(self, template: Template) -> Optional[Template]:
1066
+ """
1067
+ Update Template.
1068
+
1069
+ :param Template template: Template to update
1070
+ :return: Updated template
1071
+ :rtype: Optional[Template]
1072
+ """
1073
+ data = template.model_dump(by_alias=True, exclude_none=True)
1074
+ response = self._make_request("PUT", self.TEMPLATES_ENDPOINT, data=json.dumps(data))
1075
+ return self.handle_response(response, Template)
1076
+
1077
+ def add_new_template(self, template: Template) -> Optional[Template]:
1078
+ """
1079
+ Add New Template.
1080
+
1081
+ :param Template template: New template to add
1082
+ :return: Added template
1083
+ :rtype: Optional[Template]
1084
+ """
1085
+ data = template.model_dump(by_alias=True, exclude_none=True)
1086
+ response = self._make_request("POST", self.TEMPLATES_ENDPOINT, data=json.dumps(data))
1087
+ return self.handle_response(response, Template)
1088
+
1089
+ def update_template_variable(self, var_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1090
+ """
1091
+ Update Template Variable.
1092
+
1093
+ :param Dict[str, Any] var_data: Variable data to update
1094
+ :return: API response
1095
+ :rtype: Optional[Dict[str, Any]]
1096
+ """
1097
+ return self._make_request("PUT", self.TEMPLATE_VAR_ENDPOINT, data=json.dumps(var_data))
1098
+
1099
+ def add_template_var(self, var_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1100
+ """
1101
+ Add Template Variable.
1102
+
1103
+ :param Dict[str, Any] var_data: Variable data to add
1104
+ :return: API response
1105
+ :rtype: Optional[Dict[str, Any]]
1106
+ """
1107
+ return self._make_request("POST", self.TEMPLATE_VAR_ENDPOINT, data=json.dumps(var_data))
1108
+
1109
+ def get_available_stig_templates(self, stig_id: int) -> List[Template]:
1110
+ """
1111
+ Get Available STIG Templates.
1112
+
1113
+ :param int stig_id: STIG ID
1114
+ :return: List of available templates for the specified STIG
1115
+ :rtype: List[Template]
1116
+ """
1117
+ response = self._make_request("GET", f"/api/stigs/templates/stig/{stig_id}")
1118
+ return self._handle_list_response(response, Template)
1119
+
1120
+ def get_template(self, template_id: int) -> Optional[Template]:
1121
+ """
1122
+ Get Template.
1123
+
1124
+ :param int template_id: Template ID
1125
+ :return: Template information
1126
+ :rtype: Optional[Template]
1127
+ """
1128
+ response = self._make_request("GET", f"{self.TEMPLATES_ENDPOINT}/{template_id}")
1129
+ return self.handle_response(response, Template)
1130
+
1131
+ def delete_template(self, template_id: int) -> bool:
1132
+ """
1133
+ Delete Template.
1134
+
1135
+ :param int template_id: Template ID to delete
1136
+ :return: True if deletion was successful, False otherwise
1137
+ :rtype: bool
1138
+ """
1139
+ response = self._make_request("DELETE", f"{self.TEMPLATES_ENDPOINT}/{template_id}")
1140
+ return response is not None
1141
+
1142
+ def get_template_ids_by_group(self, group_id: int) -> List[Template]:
1143
+ """
1144
+ Get Template IDs By Group.
1145
+
1146
+ :param int group_id: The ID of the group
1147
+ :return: List of templates associated with the group
1148
+ :rtype: List[Template]
1149
+ """
1150
+ try:
1151
+ response = self._make_request("GET", f"/api/stigs/templates/group/{group_id}")
1152
+ if not response:
1153
+ return []
1154
+
1155
+ # Add group_id to each template response
1156
+ if isinstance(response, list):
1157
+ for template_data in response:
1158
+ template_data.update(
1159
+ {
1160
+ "group_id": group_id,
1161
+ "playbook_id": template_data.get("playbook_id", None), # Handle missing playbook_id
1162
+ }
1163
+ )
1164
+
1165
+ templates = []
1166
+ for template_data in response:
1167
+ try:
1168
+ templates.append(Template(**template_data))
1169
+ except Exception as e:
1170
+ logger.error(f"Failed to parse template data: {e}", exc_info=True)
1171
+
1172
+ return templates
1173
+ except Exception as e:
1174
+ logger.error(f"Failed to get templates for group {group_id}: {e}", exc_info=True)
1175
+ return []
1176
+
1177
+ def delete_template_variable(self, var_id: int) -> Optional[Dict[str, Any]]:
1178
+ """
1179
+ Delete Template Variable.
1180
+
1181
+ :param int var_id: The ID of the variable to delete
1182
+ :return: API response
1183
+ :rtype: Optional[Dict[str, Any]]
1184
+ """
1185
+ return self._make_request("DELETE", f"{self.TEMPLATE_VAR_ENDPOINT}/{var_id}")
1186
+
1187
+ # 8. Perform Audit/Remediate
1188
+ def audit_device(self, device_id: int, group_id: int, playbook_id: int, template_id: int) -> AuditResponse:
1189
+ """
1190
+ Audit Device.
1191
+
1192
+ :param int device_id: The ID of the device to audit
1193
+ :param int group_id: The ID of the group
1194
+ :param int playbook_id: The ID of the playbook
1195
+ :param int template_id: The ID of the template
1196
+ :return: Audit response
1197
+ :rtype: AuditResponse
1198
+ :raises ValueError: If the API response cannot be parsed into an AuditResponse
1199
+ """
1200
+ params = {"device_id": device_id, "group_id": group_id, "playbook_id": playbook_id, "template_id": template_id}
1201
+ response = self._make_request("GET", "/api/audit/device", params=params)
1202
+ audit_response = self.handle_response(response, AuditResponse)
1203
+ if audit_response is None:
1204
+ raise ValueError("Failed to parse API response into AuditResponse")
1205
+ return audit_response
1206
+
1207
+ def audit_group(self, group_id: int, playbook_id: int, template_id: int) -> Optional[Dict[str, Any]]:
1208
+ """
1209
+ Audit Group.
1210
+
1211
+ :param int group_id: The ID of the group to audit
1212
+ :param int playbook_id: The ID of the playbook
1213
+ :param int template_id: The ID of the template
1214
+ :return: API response
1215
+ :rtype: Optional[Dict[str, Any]]
1216
+ """
1217
+ params = {"group_id": group_id, "playbook_id": playbook_id, "template_id": template_id}
1218
+ return self._make_request("GET", "/api/audit/group", params=params)
1219
+
1220
+ def cancel_audit(self, audit_id: int) -> Optional[Dict[str, Any]]:
1221
+ """
1222
+ Cancel Audit.
1223
+
1224
+ :param int audit_id: The ID of the audit to cancel
1225
+ :return: API response
1226
+ :rtype: Optional[Dict[str, Any]]
1227
+ """
1228
+ return self._make_request("GET", "/api/audit/cancel", params={"audit_id": audit_id})
1229
+
1230
+ def remediate_device(
1231
+ self, device_id: int, group_id: int, playbook_id: int, template_id: int
1232
+ ) -> Optional[Dict[str, Any]]:
1233
+ """
1234
+ Remediate Device.
1235
+
1236
+ :param int device_id: The ID of the device to remediate
1237
+ :param int group_id: The ID of the group
1238
+ :param int playbook_id: The ID of the playbook
1239
+ :param int template_id: The ID of the template
1240
+ :return: API response
1241
+ :rtype: Optional[Dict[str, Any]]
1242
+ """
1243
+ params = {"device_id": device_id, "group_id": group_id, "playbook_id": playbook_id, "template_id": template_id}
1244
+ return self._make_request("GET", "/api/remediate/device", params=params)
1245
+
1246
+ def remediate_group(self, group_id: int, playbook_id: int, template_id: int) -> Optional[Dict[str, Any]]:
1247
+ """
1248
+ Remediate Group.
1249
+
1250
+ :param int group_id: The ID of the group to remediate
1251
+ :param int playbook_id: The ID of the playbook
1252
+ :param int template_id: The ID of the template
1253
+ :return: API response
1254
+ :rtype: Optional[Dict[str, Any]]
1255
+ """
1256
+ params = {"group_id": group_id, "playbook_id": playbook_id, "template_id": template_id}
1257
+ return self._make_request("GET", "/api/remediate/group", params=params)
1258
+
1259
+ def cancel_remediation(self, rem_id: int) -> Optional[Dict[str, Any]]:
1260
+ """
1261
+ Cancel Remediation.
1262
+
1263
+ :param int rem_id: The ID of the remediation to cancel
1264
+ :return: API response
1265
+ :rtype: Optional[Dict[str, Any]]
1266
+ """
1267
+ return self._make_request("GET", "/api/remediate/cancel", params={"rem_id": rem_id})
1268
+
1269
+ def stream_audit(self, audit_id: int) -> Optional[Dict[str, Any]]:
1270
+ """
1271
+ Stream Audit.
1272
+
1273
+ :param int audit_id: The ID of the audit to stream
1274
+ :return: API response
1275
+ :rtype: Optional[Dict[str, Any]]
1276
+ """
1277
+ return self._make_request("GET", "/api/stream-audit", params={"audit_id": audit_id})
1278
+
1279
+ def get_scheduled_tasks(
1280
+ self, group_id: Optional[int] = None, device_id: Optional[int] = None, repeat_type: Optional[str] = None
1281
+ ) -> Optional[Dict[str, Any]]:
1282
+ """
1283
+ Get Scheduled Tasks.
1284
+
1285
+ :param Optional[int] group_id: The ID of the group (default: None)
1286
+ :param Optional[int] device_id: The ID of the device (default: None)
1287
+ :param Optional[str] repeat_type: The type of repeat for the task (default: None)
1288
+ :return: API response
1289
+ :rtype: Optional[Dict[str, Any]]
1290
+ """
1291
+ params = {}
1292
+ if group_id is not None:
1293
+ params["group_id"] = str(group_id)
1294
+ if device_id is not None:
1295
+ params["device_id"] = str(device_id)
1296
+ if repeat_type:
1297
+ params["repeat_type"] = repeat_type
1298
+ return self._make_request("GET", "/api/schedule", params=params)
1299
+
1300
+ def delete_scheduled_task(self, scheduled_task_id: int) -> Optional[Dict[str, Any]]:
1301
+ """
1302
+ Delete Scheduled Task.
1303
+
1304
+ :param int scheduled_task_id: The ID of the scheduled task to delete
1305
+ :return: API response
1306
+ :rtype: Optional[Dict[str, Any]]
1307
+ """
1308
+ return self._make_request("DELETE", f"/api/schedule/{scheduled_task_id}")
1309
+
1310
+ def schedule_single(self, schedule_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1311
+ """
1312
+ Schedule Single Task.
1313
+
1314
+ :param Dict[str, Any] schedule_data: The data for scheduling the task
1315
+ :return: API response
1316
+ :rtype: Optional[Dict[str, Any]]
1317
+ """
1318
+ return self._make_request("POST", "/api/schedule/single", data=json.dumps(schedule_data))
1319
+
1320
+ def schedule_repeat(self, schedule_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1321
+ """
1322
+ Schedule Repeat Task.
1323
+
1324
+ :param Dict[str, Any] schedule_data: The data for scheduling the repeating task
1325
+ :return: API response
1326
+ :rtype: Optional[Dict[str, Any]]
1327
+ """
1328
+ return self._make_request("POST", "/api/schedule/repeat", data=json.dumps(schedule_data))
1329
+
1330
+ # 9. User Actions
1331
+ def read_current_user(self) -> Optional[Dict[str, Any]]:
1332
+ """
1333
+ Read Current User.
1334
+
1335
+ :return: Current user information
1336
+ :rtype: Optional[Dict[str, Any]]
1337
+ """
1338
+ return self._make_request("GET", "/api/users/me")
1339
+
1340
+ def read_all_users(self) -> Optional[List[Dict[str, Any]]]:
1341
+ """
1342
+ Read All Users.
1343
+
1344
+ :return: List of all users
1345
+ :rtype: Optional[List[Dict[str, Any]]]
1346
+ """
1347
+ return self._make_request("GET", "/api/users/all")
1348
+
1349
+ def view_all_roles(self) -> Optional[List[Dict[str, Any]]]:
1350
+ """
1351
+ View All Roles.
1352
+
1353
+ :return: List of all roles
1354
+ :rtype: Optional[List[Dict[str, Any]]]
1355
+ """
1356
+ return self._make_request("GET", "/api/users/roles/all")
1357
+
1358
+ def create_new_user(self, email: str, password: str) -> Optional[Dict[str, Any]]:
1359
+ """
1360
+ Create New User.
1361
+
1362
+ :param str email: The email of the new user
1363
+ :param str password: The password for the new user
1364
+ :return: API response
1365
+ :rtype: Optional[Dict[str, Any]]
1366
+ """
1367
+ data = {"email": email, "password": password}
1368
+ return self._make_request("POST", "/api/users", data=json.dumps(data))
1369
+
1370
+ def delete_user(self, user_id: int) -> Optional[Dict[str, Any]]:
1371
+ """
1372
+ Delete User.
1373
+
1374
+ :param int user_id: The ID of the user to delete
1375
+ :return: API response
1376
+ :rtype: Optional[Dict[str, Any]]
1377
+ """
1378
+ return self._make_request("DELETE", f"/api/users/{user_id}")
1379
+
1380
+ def enable_user(self, user_id: int) -> Optional[Dict[str, Any]]:
1381
+ """
1382
+ Enable User.
1383
+
1384
+ :param int user_id: The ID of the user to enable
1385
+ :return: API response
1386
+ :rtype: Optional[Dict[str, Any]]
1387
+ """
1388
+ return self._make_request("PUT", "/api/users/enable", params={"user_id": user_id})
1389
+
1390
+ def disable_user(self, user_id: int) -> Optional[Dict[str, Any]]:
1391
+ """
1392
+ Disable User.
1393
+
1394
+ :param int user_id: The ID of the user to disable
1395
+ :return: API response
1396
+ :rtype: Optional[Dict[str, Any]]
1397
+ """
1398
+ return self._make_request("PUT", "/api/users/disable", params={"user_id": user_id})
1399
+
1400
+ def add_role_to_user(self, user_id: int, role_id: int) -> Optional[Dict[str, Any]]:
1401
+ """
1402
+ Add Role To User.
1403
+
1404
+ :param int user_id: The ID of the user
1405
+ :param int role_id: The ID of the role to add
1406
+ :return: API response
1407
+ :rtype: Optional[Dict[str, Any]]
1408
+ """
1409
+ data = {"user_id": user_id, "role_id": role_id}
1410
+ return self._make_request("PUT", "/api/users/roles/add", data=json.dumps(data))
1411
+
1412
+ def remove_role_from_user(self, user_id: int, role_id: int) -> Optional[Dict[str, Any]]:
1413
+ """
1414
+ Remove Role From User.
1415
+
1416
+ :param int user_id: The ID of the user
1417
+ :param int role_id: The ID of the role to remove
1418
+ :return: API response
1419
+ :rtype: Optional[Dict[str, Any]]
1420
+ """
1421
+ data = {"user_id": user_id, "role_id": role_id}
1422
+ return self._make_request("PUT", "/api/users/roles/remove", data=json.dumps(data))
1423
+
1424
+ def edit_user_settings(self, user_id: int, dark_mode: bool) -> Optional[Dict[str, Any]]:
1425
+ """
1426
+ Edit User Settings.
1427
+
1428
+ :param int user_id: The ID of the user
1429
+ :param bool dark_mode: Whether to enable dark mode
1430
+ :return: API response
1431
+ :rtype: Optional[Dict[str, Any]]
1432
+ """
1433
+ data = {"user_id": user_id, "dark_mode": dark_mode}
1434
+ return self._make_request("PUT", "/api/users/settings", data=json.dumps(data))
1435
+
1436
+ # 10. Logs
1437
+ def get_all_logs(self, skip: int = 0, limit: int = 10, **params) -> Optional[Dict[str, Any]]:
1438
+ """
1439
+ Get All Logs.
1440
+
1441
+ :param int skip: Number of logs to skip (default: 0)
1442
+ :param int limit: Maximum number of logs to return (default: 10)
1443
+ :param params: Additional parameters for filtering logs
1444
+ :return: API response
1445
+ :rtype: Optional[Dict[str, Any]]
1446
+ """
1447
+ return self._make_request("GET", "/api/logs", params={"skip": skip, "limit": limit, **params})
1448
+
1449
+ # 11. Notifications
1450
+ def get_notification(self, id: int) -> Optional[Dict[str, Any]]:
1451
+ """
1452
+ Get Notification.
1453
+
1454
+ :param int id: The ID of the notification
1455
+ :return: API response
1456
+ :rtype: Optional[Dict[str, Any]]
1457
+ """
1458
+ return self._make_request("GET", "/api/notification", params={"id": id})
1459
+
1460
+ def delete_notification(self, id: int) -> Optional[Dict[str, Any]]:
1461
+ """
1462
+ Delete Notification.
1463
+
1464
+ :param int id: The ID of the notification to delete
1465
+ :return: API response
1466
+ :rtype: Optional[Dict[str, Any]]
1467
+ """
1468
+ return self._make_request("DELETE", "/api/notification", params={"id": id})
1469
+
1470
+ def acknowledge_notification(self, id: int) -> Optional[Dict[str, Any]]:
1471
+ """
1472
+ Acknowledge Notification.
1473
+
1474
+ :param int id: The ID of the notification to acknowledge
1475
+ :return: API response
1476
+ :rtype: Optional[Dict[str, Any]]
1477
+ """
1478
+ return self._make_request("PUT", "/api/notification/acknowledge", params={"id": id})
1479
+
1480
+ def get_audit_status(self, audit_id: int) -> Optional[AuditResponse]:
1481
+ """
1482
+ Get audit status.
1483
+
1484
+ :param int audit_id: The ID of the audit
1485
+ :return: Audit status response
1486
+ :rtype: Optional[AuditResponse]
1487
+ """
1488
+ try:
1489
+ # According to swagger spec, the endpoint is /api/audits/{audit_id}
1490
+ response = self._make_request("GET", f"/api/audits/{audit_id}")
1491
+
1492
+ if not response:
1493
+ logger.error(f"Failed to get audit status for audit {audit_id} - empty response")
1494
+ return None
1495
+
1496
+ # Log the full response for debugging
1497
+ logger.debug(f"Audit status response: {response}")
1498
+
1499
+ # Add required fields if missing
1500
+ response.update(
1501
+ {
1502
+ "audit_id": audit_id,
1503
+ "status": response.get("status", "unknown"),
1504
+ "error_message": response.get("error_message", response.get("detail", "Unknown error")),
1505
+ "device_id": response.get("device_id"),
1506
+ "group_id": response.get("group_id"),
1507
+ "template_id": response.get("template_id"),
1508
+ "job_id": response.get("job_id"),
1509
+ }
1510
+ )
1511
+
1512
+ # Create AuditResponse object
1513
+ audit_response = AuditResponse(**response)
1514
+
1515
+ # Log status for debugging
1516
+ logger.debug(f"Audit {audit_id} status: {audit_response.status}")
1517
+
1518
+ return audit_response
1519
+ except Exception as e:
1520
+ logger.error(f"Failed to get audit status for audit {audit_id}: {e}", exc_info=True)
1521
+ return None
1522
+
1523
+ def update_device_vars(self, device_id: int, vars_data: List[Dict[str, str]]) -> bool:
1524
+ """
1525
+ Update device variables.
1526
+
1527
+ :param int device_id: Device ID
1528
+ :param List[Dict[str, str]] vars_data: List of variable data to update
1529
+ :return: True if successful, False otherwise
1530
+ :rtype: bool
1531
+ """
1532
+ try:
1533
+ for var in vars_data:
1534
+ var_data = {"device_id": device_id, "var_name": var["var_name"], "var_value": var["var_value"]}
1535
+
1536
+ logger.debug(f"Updating device variable: {var_data}")
1537
+ response = self._make_request("POST", self.DEVICE_VARS_ENDPOINT, data=var_data)
1538
+
1539
+ if not response:
1540
+ logger.error(f"Failed to update variable {var['var_name']}")
1541
+ return False
1542
+
1543
+ return True
1544
+ except Exception as e:
1545
+ logger.error(f"Failed to update device variables: {e}", exc_info=True)
1546
+ return False