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,1176 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Model for a RegScale Issue"""
4
+ from collections import defaultdict
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from regscale.models import File
11
+
12
+ from urllib.parse import urljoin
13
+
14
+ from pathlib import Path
15
+ from pydantic import Field, field_validator
16
+ from requests import JSONDecodeError
17
+ from rich.progress import Progress
18
+
19
+ from regscale.core.app.api import Api
20
+ from regscale.core.app.application import Application
21
+ from regscale.core.app.logz import create_logger
22
+ from regscale.core.app.utils.app_utils import check_file_path, get_current_datetime, reformat_str_date, save_data_to
23
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
24
+ from regscale.utils.version import RegscaleVersion
25
+
26
+
27
+ class OpenIssueDict(TypedDict):
28
+ """TypedDict for open issues"""
29
+
30
+ id: int
31
+ otherIdentifier: str
32
+ integrationFindingId: str
33
+
34
+
35
+ class IssueSeverity(str, Enum):
36
+ """Issue Severity"""
37
+
38
+ NotAssigned = "IV - Not Assigned"
39
+ Low = "III - Low - Other Weakness"
40
+ Moderate = "II - Moderate - Reportable Condition"
41
+ High = "I - High - Significant Deficiency"
42
+ Critical = "0 - Critical - Critical Deficiency"
43
+
44
+ def __str__(self) -> str:
45
+ """
46
+ Return the value of the Enum as a string
47
+
48
+ :return: The value of the Enum as a string
49
+ :rtype: str
50
+ """
51
+ return self.value
52
+
53
+
54
+ class IssueStatus(str, Enum):
55
+ """Issue Status"""
56
+
57
+ Draft = "Draft"
58
+ PendingScreening = "Pending Screening"
59
+ Open = "Open"
60
+ PendingVerification = "Pending Verification"
61
+ Closed = "Closed"
62
+ Cancelled = "Cancelled"
63
+ PendingDecommission = "Pending Decommission"
64
+ SupplyChainProcurementDependency = "Supply Chain/Procurement Dependency"
65
+ VendorDependency = "Vendor Dependency for Fix"
66
+ Delayed = "Delayed"
67
+ ExceptionWaiver = "Exception/Waiver"
68
+ PendingApproval = "Pending Approval"
69
+
70
+ def __str__(self) -> str:
71
+ """
72
+ Return the value of the Enum as a string
73
+
74
+ :return: The value of the Enum as a string
75
+ :rtype: str
76
+ """
77
+ return self.value
78
+
79
+
80
+ class Issue(RegScaleModel):
81
+ """Issue Model"""
82
+
83
+ _module_slug = "issues"
84
+ _x_api_version = "2"
85
+ _unique_fields = [
86
+ ["integrationFindingId", "vulnerabilityId", "status"],
87
+ ["otherIdentifier", "parentModule", "parentId", "status"],
88
+ ]
89
+ _exclude_graphql_fields = [
90
+ "facility",
91
+ "org",
92
+ "createdBy",
93
+ "lastUpdatedBy",
94
+ "extra_data",
95
+ "tenantsId",
96
+ "issueOwner",
97
+ "controlImplementationIds",
98
+ ]
99
+
100
+ title: Optional[str] = ""
101
+ severityLevel: Union[IssueSeverity, str] = IssueSeverity.NotAssigned
102
+ issueOwnerId: Optional[str] = Field(default_factory=RegScaleModel.get_user_id)
103
+ dueDate: Optional[str] = ""
104
+ id: int = 0
105
+ tenantsId: int = 1
106
+ uuid: Optional[str] = None
107
+ dateCreated: Optional[str] = Field(default_factory=get_current_datetime)
108
+ description: Optional[str] = None
109
+ issueOwner: Optional[str] = None
110
+ costEstimate: Optional[int] = None
111
+ levelOfEffort: Optional[int] = None
112
+ identification: Optional[str] = "" # Has to be an empty string or else it will fail to create
113
+ capStatus: Optional[str] = None
114
+ sourceReport: Optional[str] = None
115
+ status: Optional[Union[IssueStatus, str]] = None
116
+ dateCompleted: Optional[str] = None
117
+ activitiesObserved: Optional[str] = None
118
+ failuresObserved: Optional[str] = None
119
+ requirementsViolated: Optional[str] = None
120
+ safetyImpact: Optional[str] = None
121
+ securityImpact: Optional[str] = None
122
+ qualityImpact: Optional[str] = None
123
+ facility: Optional[str] = None
124
+ facilityId: Optional[int] = None
125
+ org: Optional[str] = None
126
+ orgId: Optional[int] = None
127
+ controlId: Optional[int] = None
128
+ assessmentId: Optional[int] = None
129
+ requirementId: Optional[int] = None
130
+ securityPlanId: Optional[int] = None
131
+ projectId: Optional[int] = None
132
+ supplyChainId: Optional[int] = None
133
+ policyId: Optional[int] = None
134
+ componentId: Optional[int] = None
135
+ incidentId: Optional[int] = None
136
+ jiraId: Optional[str] = None
137
+ serviceNowId: Optional[str] = None
138
+ wizId: Optional[str] = None
139
+ burpId: Optional[str] = None
140
+ defenderId: Optional[str] = None
141
+ defenderAlertId: Optional[str] = None
142
+ defenderCloudId: Optional[str] = None
143
+ salesforceId: Optional[str] = None
144
+ prismaId: Optional[str] = None
145
+ tenableId: Optional[str] = None
146
+ tenableNessusId: Optional[str] = None
147
+ qualysId: Optional[str] = None
148
+ pluginId: Optional[str] = None
149
+ cve: Optional[str] = None
150
+ assetIdentifier: Optional[str] = None
151
+ falsePositive: Optional[str] = None
152
+ operationalRequirement: Optional[str] = None
153
+ autoApproved: Optional[str] = None
154
+ kevList: Optional[str] = None
155
+ dateFirstDetected: Optional[str] = None
156
+ changes: Optional[str] = None
157
+ vendorDependency: Optional[str] = None
158
+ vendorName: Optional[str] = None
159
+ vendorLastUpdate: Optional[str] = None
160
+ vendorActions: Optional[str] = None
161
+ deviationRationale: Optional[str] = None
162
+ parentId: Optional[int] = None
163
+ parentModule: Optional[str] = None
164
+ createdBy: Optional[str] = None
165
+ createdById: Optional[str] = Field(default_factory=RegScaleModel.get_user_id)
166
+ lastUpdatedBy: Optional[str] = None
167
+ lastUpdatedById: Optional[str] = Field(default_factory=RegScaleModel.get_user_id)
168
+ dateLastUpdated: Optional[str] = Field(default_factory=get_current_datetime)
169
+ securityChecks: Optional[str] = None
170
+ recommendedActions: Optional[str] = None
171
+ isPublic: Optional[bool] = True
172
+ dependabotId: Optional[str] = None
173
+ isPoam: Optional[bool] = False
174
+ originalRiskRating: Optional[str] = None
175
+ adjustedRiskRating: Optional[str] = None
176
+ bRiskAdjustment: Optional[bool] = None
177
+ basisForAdjustment: Optional[str] = None
178
+ poamComments: Optional[str] = None
179
+ otherIdentifier: Optional[str] = None
180
+ integrationFindingId: Optional[str] = None
181
+ wizCicdScanId: Optional[str] = None
182
+ wizCicdScanVuln: Optional[str] = None
183
+ sonarQubeIssueId: Optional[str] = None
184
+ qualityAssurerId: Optional[str] = None
185
+ remediationDescription: Optional[str] = None
186
+ manualDetectionSource: Optional[str] = None
187
+ manualDetectionId: Optional[str] = None
188
+ vulnerabilityId: Optional[int] = None
189
+ riskAdjustment: Optional[str] = None
190
+ controlImplementationIds: List[int] = Field(default_factory=list)
191
+
192
+ @staticmethod
193
+ def is_multiple_controls_supported() -> bool:
194
+ """
195
+ Check if multiple control is supported
196
+
197
+ :return: True if multiple controls is supported
198
+ :rtype: bool
199
+ """
200
+ return RegscaleVersion.meets_minimum_version("7.0.0", dev_is_latest=False)
201
+
202
+ def __init__(self, **data: Any):
203
+ # Handle aliases internally
204
+ if "parent_id" in data:
205
+ data["parentId"] = data.pop("parent_id")
206
+ if "parent_module" in data:
207
+ data["parentModule"] = data.pop("parent_module")
208
+ super().__init__(**data)
209
+
210
+ @staticmethod
211
+ def _get_additional_endpoints() -> dict:
212
+ """
213
+ Get additional endpoints for the Issues model.
214
+
215
+ :return: A dictionary of additional endpoints
216
+ :rtype: dict
217
+ """
218
+ return {
219
+ "user_open_items_days": "/api/{model_slug}/userOpenItemsDays/{strUserId}/{intDays}",
220
+ "set_quality_assurer": "/api/{model_slug}/setQualityAssurer/{intIssueId}/{strQaUserId}",
221
+ "remove_quality_assurer": "/api/{model_slug}/removeQualityAssurer/{intIssueId}",
222
+ "process_lineage": "/api/{model_slug}/processLineage/{intIssueId}",
223
+ "get_count": "/api/{model_slug}/getCount",
224
+ "get_by_date_range": "/api/{model_slug}/getByDateRange/{dtStart}/{dtEnd}",
225
+ "get_by_date_range_and_date_field": "/api/{model_slug}/getByDateRangeAndDateField/{dateField}/{dtStart}/{dtEnd}",
226
+ "graph_by_owner_then_status": "/api/{model_slug}/graphByOwnerThenStatus/{dateField}/{dtStart}/{dtEnd}",
227
+ "group_by_owner_and_plan_then_status_forever": "/api/{model_slug}/groupByOwnerAndPlanThenStatusForever",
228
+ "group_by_owner_and_plan_then_status": "/api/{model_slug}/groupByOwnerAndPlanThenStatus/{dateField}/{dtStart}/{dtEnd}",
229
+ "group_by_owner_and_component_then_status": "/api/{model_slug}/groupByOwnerAndComponentThenStatus/{dateField}/{dtStart}/{dtEnd}",
230
+ "group_by_owner_and_component_then_status_forever": "/api/{model_slug}/groupByOwnerAndComponentThenStatusForever",
231
+ "group_by_owner_and_component_then_status_drilldown": "/api/{model_slug}/groupByOwnerAndComponentThenStatusDrilldown/{intId}/{ownerId}/{dateField}/{dtStart}/{dtEnd}",
232
+ "group_by_owner_and_plan_then_status_drilldown": "/api/{model_slug}/groupByOwnerAndPlanThenStatusDrilldown/{intId}/{ownerId}/{dateField}/{dtStart}/{dtEnd}",
233
+ "get_by_date_closed": "/api/{model_slug}/getByDateClosed/{dtStart}/{dtEnd}",
234
+ "get_all_by_integration_field": "/api/{model_slug}/getAllByIntegrationField/{strFieldName}",
235
+ "get_active_by_integration_field": "/api/{model_slug}/getActiveByIntegrationField/{strFieldName}",
236
+ "get_filtered_list": "/api/{model_slug}/getFilteredList/{strFind}",
237
+ "get_all_by_grand_parent": "/api/{model_slug}/getAllByGrandParent/{intParentId}/{strModule}",
238
+ "query_by_custom_field": "/api/{model_slug}/queryByCustomField/{strFieldName}/{strValue}",
239
+ "issue_timeline": "/api/{model_slug}/issueTimeline/{intId}/{strModule}/{strType}",
240
+ "calendar_issues": "/api/{model_slug}/calendarIssues/{dtDate}/{fId}/{orgId}/{userId}",
241
+ "graph": "/api/{model_slug}/graph",
242
+ "graph_by_date": "/api/{model_slug}/graphByDate/{strGroupBy}/{year}",
243
+ "filter_issues": "/api/{model_slug}/filterIssues",
244
+ "update_issue_screening": "/api/{model_slug}/screening/{id}",
245
+ "retrieve_issue": "/api/{model_slug}/{intId}",
246
+ "emass_component_export": "/api/{model_slug}/emassComponentExport/{intId}",
247
+ "emass_ssp_export": "/api/{model_slug}/emassSSPExport/{intId}",
248
+ "find_by_other_identifier": "/api/{model_slug}/findByOtherIdentifier/{id}",
249
+ "find_by_service_now_id": "/api/{model_slug}/findByServiceNowId/{id}",
250
+ "find_by_salesforce_case": "/api/{model_slug}/findBySalesforceCase/{id}",
251
+ "find_by_jira_id": "/api/{model_slug}/findByJiraId/{id}",
252
+ "find_by_dependabot_id": "/api/{model_slug}/findByDependabotId/{id}",
253
+ "find_by_prisma_id": "/api/{model_slug}/findByPrismaId/{id}",
254
+ "find_by_wiz_id": "/api/{model_slug}/findByWizId/{id}",
255
+ "find_by_wiz_cicd_scan_id": "/api/{model_slug}/findByWizCicdScanId/{wizCicdScanId}",
256
+ "get_all_by_wiz_cicd_scan_vuln": "/api/{model_slug}/getAllByWizCicdScanVuln/{wizCicdScanVuln}",
257
+ "get_active_by_wiz_cicd_scan_vuln": "/api/{model_slug}/getActiveByWizCicdScanVuln/{wizCicdScanVuln}",
258
+ "find_by_sonar_qube_issue_id": "/api/{model_slug}/findBySonarQubeIssueId/{projectId}/{issueId}",
259
+ "find_by_defender_365_id": "/api/{model_slug}/findByDefender365Id/{id}",
260
+ "find_by_defender_365_alert_id": "/api/{model_slug}/findByDefender365AlertId/{id}",
261
+ "find_by_defender_cloud_id": "/api/{model_slug}/findByDefenderCloudId/{id}",
262
+ "report": "/api/{model_slug}/report/{strReport}",
263
+ "schedule": "/api/{model_slug}/schedule/{dtStart}/{dtEnd}/{dvar}",
264
+ "graph_due_date": "/api/{model_slug}/graphDueDate/{year}",
265
+ "graph_date_identified": "/api/{model_slug}/graphDateIdentified/{year}/{status}",
266
+ "graph_severity_level_by_date_identified": "/api/{model_slug}/graphSeverityLevelByDateIdentified/{year}",
267
+ "graph_cost_by_date_identified": "/api/{model_slug}/graphCostByDateIdentified/{year}",
268
+ "graph_facility_by_date_identified": "/api/{model_slug}/graphFacilityByDateIdentified/{year}",
269
+ "get_severity_level_by_status": "/api/{model_slug}/getSeverityLevelByStatus/{dtStart}/{dtEnd}",
270
+ "graph_due_date_by_status": "/api/{model_slug}/graphDueDateByStatus/{year}",
271
+ "dashboard": "/api/{model_slug}/dashboard/{strGroupBy}",
272
+ "drilldown": "/api/{model_slug}/drilldown/{strMonth}/{temporal}/{strCategory}/{chartType}",
273
+ "main_dashboard": "/api/{model_slug}/mainDashboard/{intYear}",
274
+ "main_dashboard_chart": "/api/{model_slug}/mainDashboardChart/{year}",
275
+ "dashboard_by_parent": "/api/{model_slug}/dashboardByParent/{strGroupBy}/{intId}/{strModule}",
276
+ "batch_create": "/api/{model_slug}/batchCreate",
277
+ "batch_update": "/api/{model_slug}/batchUpdate",
278
+ "find_by_integration_finding_id": "/api/{model_slug}/findByIntegrationFindingId/{id}",
279
+ }
280
+
281
+ @classmethod
282
+ def find_by_other_identifier(cls, other_identifier: str) -> List["Issue"]:
283
+ """
284
+ Find an issue by its other identifier.
285
+
286
+ :param str other_identifier: The other identifier to search for
287
+ :return: The found Issues
288
+ :rtype: List[Issue]
289
+ """
290
+ api_handler = cls._get_api_handler()
291
+ endpoint = cls.get_endpoint("find_by_other_identifier").format(id=other_identifier)
292
+
293
+ response = api_handler.get(endpoint)
294
+ issues: List["Issue"] = cls._handle_list_response(response)
295
+ return issues
296
+
297
+ @classmethod
298
+ def find_by_integration_finding_id(cls, integration_finding_id: str) -> List["Issue"]:
299
+ """
300
+ Find an issue by its integration finding id.
301
+
302
+ :param str integration_finding_id: The integration finding id to search for
303
+ :return: The found Issues
304
+ :rtype: List[Issue]
305
+ """
306
+ endpoint = cls.get_endpoint("find_by_integration_finding_id").format(id=integration_finding_id)
307
+ response = cls._get_api_handler().get(endpoint)
308
+ issues: List["Issue"] = cls._handle_list_response(response)
309
+ # Ensure we are returning the cached object if it exists to ensure bulk updates work
310
+ issues = [cls.get_cached_object(cls._get_cache_key(issue, {})) or issue for issue in issues]
311
+ return issues
312
+
313
+ @classmethod
314
+ def get_all_by_integration_field(cls, field: str) -> List["Issue"]:
315
+ """
316
+ Get all issues by integration field.
317
+
318
+ :param str field: The integration field to search for
319
+ :return: The found Issues
320
+ :rtype: List[Issue]
321
+ """
322
+ endpoint = cls.get_endpoint("get_all_by_integration_field").format(strFieldName=field)
323
+ response = cls._get_api_handler().get(endpoint)
324
+ return cls._handle_list_response(response)
325
+
326
+ @classmethod
327
+ def get_all_by_manual_detection_source(cls, value: str) -> List["Issue"]:
328
+ """
329
+ Get all issues with the manual detection source
330
+
331
+ :param str value: The manual detection source to search for
332
+ :return: The found Issues
333
+ :rtype: List[Issue]
334
+ """
335
+ query = f"""
336
+ query {{
337
+ issues(take: 50, skip: 0, where: {{ manualDetectionSource: {{eq: "{value}"}}}})
338
+ {{
339
+ items {{
340
+ {Issue.build_graphql_fields()}
341
+ }}
342
+ pageInfo {{
343
+ hasNextPage
344
+ }}
345
+ ,totalCount}}
346
+ }}
347
+ """
348
+ try:
349
+ existing_issues = cls._get_api_handler().graph(query=query)["issues"]["items"]
350
+ except (JSONDecodeError, TypeError, KeyError):
351
+ existing_issues = []
352
+ return [Issue(**issue) for issue in existing_issues]
353
+
354
+ @staticmethod
355
+ def get_issues_by_asset_map(plan_id: int) -> Dict[str, List["Issue"]]:
356
+ """
357
+ Get a dictionary of issues grouped by asset identifier for a given security plan.
358
+
359
+ :param int plan_id: The ID of the security plan
360
+ :return: A dictionary where keys are asset identifiers and values are lists of associated issues
361
+ :rtype: Dict[str, List[Issue]]
362
+ """
363
+ issues = Issue.fetch_issues_by_ssp(None, ssp_id=plan_id, status=IssueStatus.Open.value)
364
+ issues_by_asset = defaultdict(list)
365
+ for issue in issues:
366
+ if issue.assetIdentifier:
367
+ for asset_identifier in issue.assetIdentifier.split("\n"):
368
+ issues_by_asset[asset_identifier].append(issue)
369
+ return issues_by_asset
370
+
371
+ @classmethod
372
+ def assign_risk_rating(cls, value: Any) -> str:
373
+ """
374
+ Function to assign risk rating for an issue in RegScale using the provided value
375
+
376
+ :param Any value: The value to analyze to determine the issue's risk rating
377
+ :return: String of risk rating for RegScale issue, or "" if not found
378
+ :rtype: str
379
+ """
380
+ if isinstance(value, str):
381
+ if "low" in value.lower():
382
+ return "Low"
383
+ if "medium" in value.lower() or "moderate" in value.lower():
384
+ return "Moderate"
385
+ if "high" in value.lower() or "critical" in value.lower():
386
+ return "High"
387
+ return ""
388
+
389
+ @staticmethod
390
+ def assign_severity(value: Optional[Any] = None) -> str:
391
+ """
392
+ Function to assign severity for an issue in RegScale using the provided value
393
+
394
+ :param Optional[Any] value: The value to analyze to determine the issue's severity, defaults to None
395
+ :return: String of severity level for RegScale issue
396
+ :rtype: str
397
+ """
398
+ severity_levels = {
399
+ "low": IssueSeverity.Low.value,
400
+ "moderate": IssueSeverity.Moderate.value,
401
+ "high": IssueSeverity.High.value,
402
+ }
403
+ severity = IssueSeverity.NotAssigned.value
404
+ # see if the value is an int or float
405
+ if isinstance(value, (int, float)):
406
+ # check severity score and assign it to the appropriate RegScale severity
407
+ if value >= 7:
408
+ severity = severity_levels["high"]
409
+ elif 4 <= value < 7:
410
+ severity = severity_levels["moderate"]
411
+ else:
412
+ severity = severity_levels["low"]
413
+ elif isinstance(value, str):
414
+ if value.lower() in ["low", "lowest"]:
415
+ severity = severity_levels["low"]
416
+ elif value.lower() in ["medium", "moderate"]:
417
+ severity = severity_levels["moderate"]
418
+ elif value.lower() in ["high", "critical", "highest"]:
419
+ severity = severity_levels["high"]
420
+ elif value in list(severity_levels.values()):
421
+ severity = value
422
+ return severity
423
+
424
+ @staticmethod
425
+ def update_issue(app: Application, issue: "Issue") -> Optional["Issue"]:
426
+ """
427
+ Update an issue in RegScale
428
+
429
+ :param Application app: Application Instance
430
+ :param Issue issue: Issue to update in RegScale
431
+ :return: Updated issue in RegScale
432
+ :rtype: Optional[Issue]
433
+ """
434
+ if isinstance(issue, dict):
435
+ issue = Issue(**issue)
436
+ api = Api()
437
+ issue_id = issue.id
438
+
439
+ response = api.put(app.config["domain"] + f"/api/issues/{issue_id}", json=issue.dict())
440
+ if response.status_code == 200:
441
+ try:
442
+ issue = Issue(**response.json())
443
+ except JSONDecodeError:
444
+ return None
445
+ return issue
446
+
447
+ @staticmethod
448
+ def insert_issue(app: Application, issue: "Issue") -> Optional["Issue"]:
449
+ """
450
+ Insert an issue in RegScale
451
+
452
+ :param Application app: Application Instance
453
+ :param Issue issue: Issue to insert to RegScale
454
+ :return: Newly created issue in RegScale
455
+ :rtype: Optional[Issue]
456
+ """
457
+ if isinstance(issue, dict):
458
+ issue = Issue(**issue)
459
+ api = Api()
460
+ logger = create_logger()
461
+ response = api.post(app.config["domain"] + "/api/issues", json=issue.dict())
462
+ if response.status_code == 200:
463
+ try:
464
+ issue = Issue(**response.json())
465
+ except JSONDecodeError as jex:
466
+ logger.error("Unable to read issue:\n%s", jex)
467
+ return None
468
+ else:
469
+ logger.warning("Unable to insert issue: %s", issue.title)
470
+ return issue
471
+
472
+ @staticmethod
473
+ def bulk_insert(
474
+ app: Application,
475
+ issues: List["Issue"],
476
+ max_workers: Optional[int] = 10,
477
+ batch_size: int = 100,
478
+ batch: bool = False,
479
+ ) -> List["Issue"]:
480
+ """
481
+ Bulk insert issues using the RegScale API and ThreadPoolExecutor
482
+
483
+ :param Application app: Application Instance
484
+ :param List["Issue"] issues: List of issues to insert
485
+ :param Optional[int] max_workers: Max Workers, defaults to 10
486
+ :param int batch_size: Number of issues to insert per batch, defaults to 100
487
+ :param bool batch: Insert issues in batches, defaults to False
488
+ :return: List of Issues from RegScale
489
+ :rtype: List[Issue]
490
+ """
491
+ api = Api()
492
+ url = urljoin(app.config["domain"], "/api/{model_slug}/batchcreate")
493
+ results = []
494
+ api.logger.info("Creating %i new issue(s) in RegScale...", len(issues))
495
+ with Progress(transient=False) as progress:
496
+ task = progress.add_task(f"Creating {len(issues)} new issue(s) in RegScale...", total=len(issues))
497
+ if batch:
498
+ # Chunk list into batches
499
+ batches = [issues[i : i + batch_size] for i in range(0, len(issues), batch_size)]
500
+ for my_batch in batches:
501
+ res = api.post(url=url.format(model_slug="issues"), json=[iss.dict() for iss in my_batch])
502
+ if not res.ok:
503
+ app.logger.error(
504
+ "%i: %s\nError creating batch of issues: %s",
505
+ res.status_code,
506
+ res.text,
507
+ res.reason,
508
+ )
509
+ results.append(res)
510
+ progress.update(task, advance=len(my_batch))
511
+ else:
512
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
513
+ futures = [
514
+ executor.submit(
515
+ issue.create,
516
+ )
517
+ for issue in issues
518
+ ]
519
+ for future in as_completed(futures):
520
+ issue = future.result()
521
+ results.append(issue)
522
+ progress.update(task, advance=1)
523
+ return results
524
+
525
+ @staticmethod
526
+ def bulk_update(
527
+ app: Application,
528
+ issues: List["Issue"],
529
+ max_workers: int = 10,
530
+ batch_size: int = 100,
531
+ batch: bool = False,
532
+ ) -> List["Issue"]:
533
+ """
534
+ Bulk update issues using the RegScale API and ThreadPoolExecutor
535
+
536
+ :param Application app: Application Instance
537
+ :param List["Issue"] issues: List of issues to update
538
+ :param int max_workers: Max Workers, defaults to 10
539
+ :param int batch_size: Number of issues to update per batch, defaults to 100
540
+ :param bool batch: Update issues in batches, defaults to False
541
+ :return: List of Issues from RegScale
542
+ :rtype: List[Issue]
543
+ """
544
+ api = Api()
545
+ url = urljoin(app.config["domain"], "/api/{model_slug}/batchupdate")
546
+ results = []
547
+ api.logger.info("Updating %i issue(s) in RegScale...", len(issues))
548
+ with Progress(transient=False) as progress:
549
+ task = progress.add_task(f"Updating {len(issues)} issue(s) in RegScale...", total=len(issues))
550
+ if batch:
551
+ # Chunk list into batches
552
+ batches = [issues[i : i + batch_size] for i in range(0, len(issues), batch_size)]
553
+ for my_batch in batches:
554
+ res = api.put(url=url.format(model_slug="issues"), json=[iss.dict() for iss in my_batch])
555
+ if not res.ok:
556
+ app.logger.error(
557
+ "%i: %s\nError creating batch of issues: %s",
558
+ res.status_code,
559
+ res.text,
560
+ res.reason,
561
+ )
562
+ results.append(res)
563
+ progress.update(task, advance=len(my_batch))
564
+ else:
565
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
566
+ futures = [executor.submit(issue.save) for issue in issues]
567
+ for future in as_completed(futures):
568
+ issue = future.result()
569
+ results.append(issue)
570
+ progress.update(task, advance=1)
571
+
572
+ return results
573
+
574
+ @staticmethod
575
+ def fetch_issues_by_parent(
576
+ app: Application,
577
+ regscale_id: int,
578
+ regscale_module: str,
579
+ ) -> List["Issue"]:
580
+ """
581
+ Find all issues by parent id and parent module
582
+
583
+ :param Application app: Application Instance
584
+ :param int regscale_id: Parent ID
585
+ :param str regscale_module: Parent Module
586
+ :return: List of issues from RegScale
587
+ :rtype: List[Issue]
588
+ """
589
+ api = Api()
590
+ body = f"""
591
+ query {{
592
+ issues(take: 50, skip: 0, where: {{ parentModule: {{eq: "{regscale_module}"}} parentId: {{
593
+ eq: {regscale_id}
594
+ }}}}) {{
595
+ items {{
596
+ {Issue.build_graphql_fields()}
597
+ }}
598
+ pageInfo {{
599
+ hasNextPage
600
+ }}
601
+ ,totalCount}}
602
+ }}
603
+ """
604
+ try:
605
+ existing_issues = api.graph(query=body)["issues"]["items"]
606
+ except (JSONDecodeError, TypeError, KeyError):
607
+ existing_issues = []
608
+ return [Issue(**issue) for issue in existing_issues]
609
+
610
+ @staticmethod
611
+ def fetch_issues_by_ssp(
612
+ app: Optional[Application],
613
+ ssp_id: int,
614
+ status: Optional[str] = None,
615
+ ) -> List["Issue"]:
616
+ """
617
+ Find all issues by parent id and parent module
618
+
619
+ :param Application app: Application Instance
620
+ :param int ssp_id: RegScale SSP Id
621
+ :param Optional[str] status: Issue Status, defaults to None
622
+ :return: List of Issues from RegScale SSP
623
+ :rtype: List[Issue]
624
+ """
625
+ api = Api()
626
+ where_conditions = [f"securityPlanId: {{eq: {ssp_id}}}"]
627
+ if status:
628
+ where_conditions.append(f'status: {{eq: "{status}"}}')
629
+ where_str = ", ".join(where_conditions)
630
+ body = f"""
631
+ query {{
632
+ issues(take: 50, skip: 0, where: {{ {where_str} }}) {{
633
+ items {{
634
+ {Issue.build_graphql_fields()}
635
+ }}
636
+ pageInfo {{
637
+ hasNextPage
638
+ }}
639
+ totalCount
640
+ }}
641
+ }}
642
+ """
643
+ try:
644
+ existing_issues = api.graph(query=body)["issues"]["items"]
645
+ except (JSONDecodeError, TypeError, KeyError):
646
+ existing_issues = []
647
+ return [Issue(**issue) for issue in existing_issues]
648
+
649
+ @staticmethod
650
+ def fetch_all_issues(
651
+ app: Application,
652
+ ) -> List["Issue"]:
653
+ """
654
+ Find all issues in RegScale
655
+
656
+ :param Application app: Application Instance
657
+ :return: List of Issues from RegScale
658
+ :rtype: List[Issue]
659
+ """
660
+ api = Api()
661
+ body = f"""
662
+ query {{
663
+ issues(take: 50, skip: 0) {{
664
+ items {{
665
+ {Issue.build_graphql_fields()}
666
+ }}
667
+ pageInfo {{
668
+ hasNextPage
669
+ }}
670
+ ,totalCount}}
671
+ }}
672
+ """
673
+ try:
674
+ api.logger.info("Retrieving all issues from RegScale...")
675
+ existing_issues = api.graph(query=body)["issues"]["items"]
676
+ api.logger.info("Retrieved %i issue(s) from RegScale.", len(existing_issues))
677
+ except (JSONDecodeError, KeyError):
678
+ existing_issues = []
679
+ return [Issue(**issue) for issue in existing_issues]
680
+
681
+ @staticmethod
682
+ def fetch_issue_by_id(
683
+ app: Application,
684
+ issue_id: int,
685
+ ) -> Optional["Issue"]:
686
+ """
687
+ Find a RegScale issue by its id
688
+
689
+ :param Application app: Application Instance
690
+ :param int issue_id: RegScale Issue Id
691
+ :return: Issue from RegScale or None if it doesn't exist
692
+ :rtype: Optional[Issue]
693
+ """
694
+ api = Api()
695
+ issue_response = api.get(url=f"{app.config['domain']}/api/issues/{issue_id}")
696
+ issue = None
697
+ try:
698
+ issue = Issue(**issue_response.json())
699
+ except JSONDecodeError:
700
+ logger = create_logger()
701
+ logger.warning("Unable to find issue with id %i", issue_id)
702
+ return issue
703
+
704
+ @staticmethod
705
+ def fetch_issues_and_attachments_by_parent(
706
+ parent_id: int,
707
+ parent_module: str,
708
+ fetch_attachments: Optional[bool] = True,
709
+ save_issues: Optional[bool] = True,
710
+ ) -> Tuple[List["Issue"], Optional[Dict[int, List["File"]]]]:
711
+ """
712
+ Fetch all issues from RegScale for the provided parent record
713
+
714
+ :param int parent_id: Parent record ID in RegScale
715
+ :param str parent_module: Parent record module in RegScale
716
+ :param Optional[Application] app: Application object, deprecated 3.26.2024, defaults to None
717
+ :param Optional[bool] fetch_attachments: Whether to fetch attachments from RegScale, defaults to True
718
+ :param Optional[bool] save_issues: Save RegScale issues to a .json in artifacts, defaults to True
719
+ :return: List of RegScale issues, dictionary of issue's attachments as File objects
720
+ :rtype: Tuple[List[Issue], Optional[Dict[int, List[File]]]]
721
+ """
722
+ from regscale.models import File
723
+
724
+ attachments: Optional[Dict[int, List[File]]] = None
725
+ logger = create_logger()
726
+ # get the existing issues for the parent record that are already in RegScale
727
+ logger.info("Fetching full issue list from RegScale %s #%i.", parent_module, parent_id)
728
+ issues_data = Issue().get_all_by_parent(
729
+ parent_id=parent_id,
730
+ parent_module=parent_module,
731
+ )
732
+
733
+ # check for null/not found response
734
+ if len(issues_data) == 0:
735
+ logger.warning(
736
+ "No existing issues for this RegScale record #%i in %s.",
737
+ parent_id,
738
+ parent_module,
739
+ )
740
+ else:
741
+ if fetch_attachments:
742
+ # get the attachments for the issue
743
+ api = Api()
744
+ attachments = {
745
+ issue.id: files
746
+ for issue in issues_data
747
+ if (
748
+ files := File.get_files_for_parent_from_regscale(
749
+ parent_id=issue.id,
750
+ parent_module="issues",
751
+ api=api,
752
+ )
753
+ )
754
+ }
755
+ logger.info(
756
+ "Found %i issue(s) from RegScale %s #%i for processing.",
757
+ len(issues_data),
758
+ parent_module,
759
+ parent_id,
760
+ )
761
+ if save_issues:
762
+ # write issue data to a json file
763
+ check_file_path("artifacts")
764
+ file_name = "existingRegScaleIssues.json"
765
+ file_path = Path("./artifacts") / file_name
766
+ save_data_to(
767
+ file=file_path,
768
+ data=[issue.dict() for issue in issues_data],
769
+ output_log=False,
770
+ )
771
+ logger.info(
772
+ "Saved RegScale issue(s) for %s #%i, see %s", parent_module, parent_id, str(file_path.absolute())
773
+ )
774
+ return issues_data, attachments
775
+
776
+ @classmethod
777
+ def get_open_issues_ids_by_implementation_id(cls, plan_id: int) -> Dict[int, List[OpenIssueDict]]:
778
+ """
779
+ Get all open issues by implementation id for a given security plan
780
+
781
+ :param int plan_id: The ID of the parent
782
+ :return: A dictionary of control ids and their associated issue ids
783
+ :rtype: Dict[int, List[OpenIssueDict]]
784
+ """
785
+ import logging
786
+
787
+ logger = logging.getLogger("regscale")
788
+ take = 50
789
+ skip = 0
790
+ control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
791
+ logger.info("Fetching open issues for controls and for security plan %i...", plan_id)
792
+
793
+ # Define fields based on version
794
+ fields = """
795
+ id,
796
+ controlId
797
+ otherIdentifier
798
+ {extra_fields}
799
+ """.format(
800
+ extra_fields="controlImplementations { id }" if cls.is_multiple_controls_supported() else ""
801
+ )
802
+
803
+ while True:
804
+ query = f"""
805
+ query MyQuery() {{
806
+ {cls.get_module_string()}(
807
+ skip: {skip},
808
+ take: {take},
809
+ where: {{
810
+ securityPlanId: {{eq: {plan_id}}},
811
+ status: {{eq: "Open"}}
812
+ }}
813
+ ) {{
814
+ items {{ {fields} }}
815
+ pageInfo {{ hasNextPage }}
816
+ totalCount
817
+ }}
818
+ }}
819
+ """
820
+
821
+ response = cls._get_api_handler().graph(query)
822
+ items = response.get(cls.get_module_string(), {}).get("items", [])
823
+
824
+ for item in items:
825
+ if cls.is_multiple_controls_supported() and item.get("controlImplementations"):
826
+ for control in item.get("controlImplementations", []):
827
+ control_issues[control["id"]].append(
828
+ OpenIssueDict(id=item["id"], otherIdentifier=item["otherIdentifier"])
829
+ )
830
+ else:
831
+ control_issues[item["controlId"]].append(
832
+ OpenIssueDict(id=item["id"], otherIdentifier=item["otherIdentifier"])
833
+ )
834
+
835
+ if not response.get(cls.get_module_string(), {}).get("pageInfo", {}).get("hasNextPage", False):
836
+ break
837
+ skip += take
838
+ logger.info("Finished fetching %i open control issue(s) for security plan %i", len(control_issues), plan_id)
839
+ return control_issues
840
+
841
+ @classmethod
842
+ def get_sort_position_dict(cls) -> Dict[str, int]:
843
+ """
844
+ Overrides the base method.
845
+
846
+ :return: The sort position in the list of properties
847
+ :rtype: Dict[str, int]
848
+ """
849
+ return {
850
+ "id": 1,
851
+ "title": 2,
852
+ "severityLevel": 3,
853
+ "issueOwnerId": 4,
854
+ "dueDate": 5,
855
+ "uuid": -1,
856
+ "dateCreated": 6,
857
+ "description": 7,
858
+ "issueOwner": -1,
859
+ "costEstimate": 9,
860
+ "levelOfEffort": 10,
861
+ "identification": 11,
862
+ "capStatus": 12,
863
+ "sourceReport": 13,
864
+ "status": 14,
865
+ "dateCompleted": 15,
866
+ "activitiesObserved": 16,
867
+ "failuresObserved": 17,
868
+ "requirementsViolated": 18,
869
+ "safetyImpact": 19,
870
+ "securityImpact": 20,
871
+ "qualityImpact": 21,
872
+ "facility": -1,
873
+ "facilityId": -1,
874
+ "org": -1,
875
+ "orgId": -1,
876
+ "controlId": 26,
877
+ "assessmentId": 27,
878
+ "requirementId": 28,
879
+ "securityPlanId": 29,
880
+ "projectId": 30,
881
+ "supplyChainId": 31,
882
+ "policyId": 32,
883
+ "componentId": 33,
884
+ "incidentId": 34,
885
+ "jiraId": 35,
886
+ "serviceNowId": 36,
887
+ "wizId": 37,
888
+ "burpId": 38,
889
+ "defenderId": 39,
890
+ "defenderAlertId": 40,
891
+ "defenderCloudId": 41,
892
+ "salesforceId": 42,
893
+ "prismaId": 43,
894
+ "tenableId": 44,
895
+ "tenableNessusId": 45,
896
+ "qualysId": 46,
897
+ "pluginId": 47,
898
+ "cve": 48,
899
+ "assetIdentifier": 49,
900
+ "falsePositive": 50,
901
+ "operationalRequirement": 51,
902
+ "autoApproved": 52,
903
+ "kevList": 53,
904
+ "dateFirstDetected": 54,
905
+ "changes": 55,
906
+ "vendorDependency": 56,
907
+ "vendorName": 57,
908
+ "vendorLastUpdate": 58,
909
+ "vendorActions": 59,
910
+ "deviationRationale": 60,
911
+ "parentId": 61,
912
+ "parentModule": 62,
913
+ "createdBy": -1,
914
+ "createdById": -1,
915
+ "lastUpdatedBy": -1,
916
+ "lastUpdatedById": -1,
917
+ "dateLastUpdated": -1,
918
+ "securityChecks": 63,
919
+ "recommendedActions": 64,
920
+ "isPublic": 65,
921
+ "dependabotId": 66,
922
+ "isPoam": 67,
923
+ "originalRiskRating": 68,
924
+ "adjustedRiskRating": 69,
925
+ "bRiskAdjustment": 70,
926
+ "basisForAdjustment": 71,
927
+ }
928
+
929
+ @classmethod
930
+ def get_enum_values(cls, field_name: str) -> List[Union[IssueSeverity, IssueStatus, str]]:
931
+ """
932
+ Overrides the base method.
933
+
934
+ :param str field_name: The property name to provide enum values for
935
+ :return: List of enum values or strings
936
+ :rtype: List[Union[IssueSeverity, IssueStatus, str]]
937
+ """
938
+ if field_name == "severityLevel":
939
+ return [severity.__str__() for severity in IssueSeverity]
940
+ if field_name == "status":
941
+ return [status.__str__() for status in IssueStatus]
942
+ if field_name == "identification":
943
+ return [
944
+ "A-123 Review",
945
+ "Assessment/Audit (External)",
946
+ "Assessment/Audit (Internal)",
947
+ "Critical Control Review",
948
+ "FDCC/USGCB",
949
+ "GAO Audit",
950
+ "IG Audit",
951
+ "Incidnet Response Lessons Learned",
952
+ "ITAR",
953
+ "Other",
954
+ "Penetration Test",
955
+ "Risk Assessment",
956
+ "Security Authorization",
957
+ "Security Control Assessment",
958
+ "Vulnerability Assessment",
959
+ ]
960
+ return cls.get_bool_enums(field_name)
961
+
962
+ @classmethod
963
+ def get_lookup_field(cls, field_name: str) -> str:
964
+ """
965
+ Overrides the base method.
966
+
967
+ :param str field_name: The property name to provide enum values for
968
+ :return: The field name to look up
969
+ :rtype: str
970
+ """
971
+ lookup_fields = {"issueOwnerId": "user", "facilityId": "facilities", "orgId": "organizations"}
972
+ if field_name in lookup_fields.keys():
973
+ return lookup_fields[field_name]
974
+ return ""
975
+
976
+ @classmethod
977
+ def is_date_field(cls, field_name: str) -> bool:
978
+ """
979
+ Overrides the base method.
980
+
981
+ :param str field_name: The property name to provide enum values for
982
+ :return: If the field should be formatted as a date
983
+ :rtype: bool
984
+ """
985
+ return field_name in ["dueDate", "dateCreated", "dateCompleted", "dateFirstDetected"]
986
+
987
+ # pylint: disable=C0301
988
+ @classmethod
989
+ def get_export_query(cls, app: Application, parent_id: int, parent_module: str) -> List[Dict[str, Any]]:
990
+ """
991
+ Overrides the base method.
992
+
993
+ :param Application app: RegScale Application object
994
+ :param int parent_id: RegScale ID of parent
995
+ :param str parent_module: Module of parent
996
+ :return: GraphQL response from RegScale
997
+ :rtype: List[Dict[str, Any]]
998
+ """
999
+ body = """
1000
+ query {
1001
+ issues (skip: 0, take: 50, where: {parentId: {eq: parent_id} parentModule: {eq: "parent_module"}}) {
1002
+ items {
1003
+ id
1004
+ issueOwnerId
1005
+ issueOwner {
1006
+ firstName
1007
+ lastName
1008
+ userName
1009
+ }
1010
+ title
1011
+ dateCreated
1012
+ description
1013
+ severityLevel
1014
+ costEstimate
1015
+ levelOfEffort
1016
+ dueDate
1017
+ identification
1018
+ status
1019
+ dateCompleted
1020
+ activitiesObserved
1021
+ failuresObserved
1022
+ requirementsViolated
1023
+ safetyImpact
1024
+ securityImpact
1025
+ qualityImpact
1026
+ securityChecks
1027
+ recommendedActions
1028
+ isPoam
1029
+ parentId
1030
+ parentModule
1031
+ }
1032
+ totalCount
1033
+ pageInfo {
1034
+ hasNextPage
1035
+ }
1036
+ }
1037
+ }
1038
+ """.replace(
1039
+ "parent_module", parent_module
1040
+ ).replace(
1041
+ "parent_id", str(parent_id)
1042
+ )
1043
+
1044
+ api = Api()
1045
+ existing_issue_data = api.graph(query=body)
1046
+
1047
+ if existing_issue_data["issues"]["totalCount"] > 0:
1048
+ raw_data = existing_issue_data["issues"]["items"]
1049
+ moded_data = []
1050
+ for a in raw_data:
1051
+ moded_data.append(build_issue_dict_from_query(a))
1052
+ return moded_data
1053
+ return []
1054
+
1055
+ # pylint: emable=C0301
1056
+
1057
+ @classmethod
1058
+ def find_by_service_now_id(cls, snow_id: str) -> List["Issue"]:
1059
+ """
1060
+ Find issues by its serviceNowId
1061
+
1062
+ :param str snow_id: The serviceNowId to search for
1063
+ :return: The found Issues
1064
+ :rtype: List[Issue]
1065
+ """
1066
+ api_handler = cls._get_api_handler()
1067
+ endpoint = cls.get_endpoint("find_by_service_now_id").format(id=snow_id)
1068
+
1069
+ response = api_handler.get(endpoint)
1070
+ issues: List["Issue"] = cls._handle_list_response(response)
1071
+ return issues
1072
+
1073
+ @classmethod
1074
+ def use_query(cls) -> bool:
1075
+ """
1076
+ Overrides the base method.
1077
+
1078
+ :return: Whether to use query
1079
+ :rtype: bool
1080
+ """
1081
+ return True
1082
+
1083
+ @classmethod
1084
+ def get_extra_fields(cls) -> List[str]:
1085
+ """
1086
+ Overrides the base method.
1087
+
1088
+ :return: List of extra field names
1089
+ :rtype: List[str]
1090
+ """
1091
+ return []
1092
+
1093
+ @classmethod
1094
+ def get_include_fields(cls) -> List[str]:
1095
+ """
1096
+ Overrides the base method.
1097
+
1098
+ :return: List of field names to include
1099
+ :rtype: List[str]
1100
+ """
1101
+ return []
1102
+
1103
+ @field_validator("riskAdjustment")
1104
+ def validate_risk_adjustment(cls, v: str) -> str:
1105
+ """
1106
+ Validates the riskAdjustment field.
1107
+
1108
+ :param str v: The value to validate
1109
+ :raise ValueError: If the value is not valid
1110
+
1111
+ :return: The validated values
1112
+ :rtype: str
1113
+
1114
+ """
1115
+ allowed_values = ["No", "Yes", "Pending", None]
1116
+ if v not in allowed_values:
1117
+ raise ValueError(f"riskAdjustment must be one of {allowed_values}")
1118
+ return v
1119
+
1120
+
1121
+ def build_issue_dict_from_query(a: Dict[str, Any]) -> Dict[str, Any]:
1122
+ """
1123
+ This method takes in a single record from the graphQL query and reformat
1124
+ it into an issue dict.
1125
+
1126
+ :param Dict[str, Any] a: A single record returned from the query
1127
+ :return: Reformatted dict for the data needs
1128
+ :rtype: Dict[str, Any]
1129
+ """
1130
+ modeled_item = {}
1131
+ modeled_item["id"] = a["id"]
1132
+ modeled_item["issueOwnerId"] = (
1133
+ (
1134
+ str(a["issueOwner"]["lastName"]).strip()
1135
+ + ", "
1136
+ + str(a["issueOwner"]["firstName"]).strip()
1137
+ + " ("
1138
+ + str(a["issueOwner"]["userName"]).strip()
1139
+ + ")"
1140
+ )
1141
+ if a["issueOwner"]
1142
+ else "None"
1143
+ )
1144
+ modeled_item["title"] = a["title"]
1145
+ modeled_item["dateCreated"] = reformat_str_date(a["dateCreated"])
1146
+ modeled_item["isPoam"] = a["isPoam"]
1147
+ modeled_item["description"] = a["description"] if a["description"] else "None"
1148
+ modeled_item["severityLevel"] = a["severityLevel"]
1149
+ modeled_item["costEstimate"] = a["costEstimate"] if a["costEstimate"] and a["costEstimate"] != "None" else 0.00
1150
+ modeled_item["levelOfEffort"] = a["levelOfEffort"] if a["levelOfEffort"] and a["levelOfEffort"] != "None" else 0
1151
+ modeled_item["dueDate"] = reformat_str_date(a["dueDate"])
1152
+ modeled_item["identification"] = a["identification"] if a["identification"] else "None"
1153
+ modeled_item["status"] = a["status"] if a["status"] else "None"
1154
+ modeled_item["dateCompleted"] = reformat_str_date(a["dateCompleted"]) if a["dateCompleted"] else ""
1155
+ modeled_item["activitiesObserved"] = blank_if_empty(a, "activitiesObserved")
1156
+ modeled_item["failuresObserved"] = blank_if_empty(a, "failuresObserved")
1157
+ modeled_item["requirementsViolated"] = blank_if_empty(a, "requirementsViolated")
1158
+ modeled_item["safetyImpact"] = blank_if_empty(a, "safetyImpact")
1159
+ modeled_item["securityImpact"] = blank_if_empty(a, "securityImpact")
1160
+ modeled_item["qualityImpact"] = blank_if_empty(a, "qualityImpact")
1161
+ modeled_item["securityChecks"] = blank_if_empty(a, "securityChecks")
1162
+ modeled_item["recommendedActions"] = blank_if_empty(a, "recommendedActions")
1163
+ return modeled_item
1164
+
1165
+
1166
+ def blank_if_empty(data: dict, field: str) -> str:
1167
+ """
1168
+ This method will return the value of the specified field from the passed dict if it exists. If
1169
+ it doesn't, an empty string will be returned.
1170
+
1171
+ :param dict data: the data to be queried for the field
1172
+ :param str field: the field to return if it exists in the dict
1173
+ :return: str the field value or an empty string
1174
+ :rtype: str
1175
+ """
1176
+ return data.get(field, "")