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,2692 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Scanner Integration Class"""
4
+ from __future__ import annotations
5
+
6
+ import concurrent.futures
7
+ import dataclasses
8
+ import enum
9
+ import hashlib
10
+ import json
11
+ import logging
12
+ import re
13
+ import threading
14
+ import time
15
+ from abc import ABC, abstractmethod
16
+ from collections import defaultdict
17
+ from typing import Any, Dict, Generic, Iterator, List, Optional, Set, TypeVar, Union
18
+
19
+ from rich.progress import Progress, TaskID
20
+
21
+ from regscale.core.app.application import Application
22
+ from regscale.core.app.utils.api_handler import APIHandler
23
+ from regscale.core.app.utils.app_utils import create_progress_object, get_current_datetime
24
+ from regscale.core.app.utils.catalog_utils.common import objective_to_control_dot
25
+ from regscale.core.utils.date import date_obj, date_str, datetime_str, days_from_today, get_day_increment
26
+ from regscale.integrations.commercial.durosuite.process_devices import scan_durosuite_devices
27
+ from regscale.integrations.commercial.durosuite.variables import DuroSuiteVariables
28
+ from regscale.integrations.commercial.stig_mapper_integration.mapping_engine import StigMappingEngine as STIGMapper
29
+ from regscale.integrations.public.cisa import pull_cisa_kev
30
+ from regscale.integrations.variables import ScannerVariables
31
+ from regscale.models import DateTimeEncoder, OpenIssueDict, regscale_models
32
+ from regscale.utils.threading import ThreadSafeDict, ThreadSafeList
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ K = TypeVar("K") # Key type
37
+ V = TypeVar("V") # Value type
38
+
39
+
40
+ def get_thread_workers_max() -> int:
41
+ """
42
+ Get the maximum number of thread workers
43
+
44
+ :return: The maximum number of thread workers
45
+ :rtype: int
46
+ """
47
+ return ScannerVariables.threadMaxWorkers
48
+
49
+
50
+ def issue_due_date(
51
+ severity: regscale_models.IssueSeverity,
52
+ created_date: str,
53
+ critical: int = ScannerVariables.issueDueDates.get("critical", 30),
54
+ high: int = ScannerVariables.issueDueDates.get("high", 60),
55
+ moderate: int = ScannerVariables.issueDueDates.get("moderate", 120),
56
+ low: int = ScannerVariables.issueDueDates.get("low", 364),
57
+ title: str = "",
58
+ config: Optional[Dict[str, Dict]] = None,
59
+ ) -> str:
60
+ """
61
+ Calculate the due date for an issue based on its severity and creation date.
62
+
63
+ :param regscale_models.IssueSeverity severity: The severity of the issue.
64
+ :param str created_date: The creation date of the issue.
65
+ :param int critical: Days until due for high severity issues. Default is 30.
66
+ :param int high: Days until due for high severity issues. Default is 60.
67
+ :param int moderate: Days until due for moderate severity issues. Default is 210.
68
+ :param int low: Days until due for low severity issues. Default is 364.
69
+ :param str title: The title of the Integration.
70
+ :param Dict[str, Dict] config: Configuration options for the due date calculation.
71
+ :return: The due date for the issue.
72
+ :rtype: str
73
+ """
74
+ if config is None:
75
+ config = {}
76
+
77
+ due_date_map = {
78
+ regscale_models.IssueSeverity.Critical: critical,
79
+ regscale_models.IssueSeverity.High: high,
80
+ regscale_models.IssueSeverity.Moderate: moderate,
81
+ regscale_models.IssueSeverity.Low: low,
82
+ }
83
+
84
+ if title and config:
85
+ # if title in a config key, use that key
86
+ issues_dict = config.get("issues", {})
87
+ matching_key = next((key.lower() for key in issues_dict if title.lower() in key.lower()), None)
88
+ if matching_key:
89
+ title_config = issues_dict.get(matching_key, {})
90
+ due_date_map = {
91
+ regscale_models.IssueSeverity.Critical: title_config.get("critical", critical),
92
+ regscale_models.IssueSeverity.High: title_config.get("high", high),
93
+ regscale_models.IssueSeverity.Moderate: title_config.get("moderate", moderate),
94
+ regscale_models.IssueSeverity.Low: title_config.get("low", low),
95
+ }
96
+
97
+ days = due_date_map.get(severity, low)
98
+ return date_str(get_day_increment(start=created_date, days=days))
99
+
100
+
101
+ class ManagedDefaultDict(Generic[K, V]):
102
+ """
103
+ A thread-safe default dictionary that uses a multiprocessing Manager.
104
+
105
+ :param default_factory: A callable that produces default values for missing keys
106
+ """
107
+
108
+ def __init__(self, default_factory):
109
+ self.store: ThreadSafeDict[Any, Any] = ThreadSafeDict() # type: ignore[type-arg]
110
+ self.default_factory = default_factory
111
+
112
+ def __getitem__(self, key: Any) -> Any:
113
+ """
114
+ Get the item from the store
115
+
116
+ :param Any key: Key to get the item from the store
117
+ :return: Value from the store
118
+ :rtype: Any
119
+ """
120
+ if key not in self.store:
121
+ self.store[key] = self.default_factory()
122
+ return self.store[key]
123
+
124
+ def __setitem__(self, key: Any, value: Any) -> None:
125
+ """
126
+ Set the item in the store
127
+
128
+ :param Any key: Key to set the item in the store
129
+ :param Any value: Value to set in the store
130
+ :rtype: None
131
+ """
132
+ self.store[key] = value
133
+
134
+ def __contains__(self, key: Any) -> bool:
135
+ """
136
+ Check if the key is in the store
137
+
138
+ :param Any key: Key to check in the store
139
+ :return: Whether the key is in the store
140
+ :rtype: bool
141
+ """
142
+ return key in self.store
143
+
144
+ def __len__(self) -> int:
145
+ """
146
+ Get the length of the store
147
+
148
+ :return: Number of items in the store
149
+ :rtype: int
150
+ """
151
+ return len(self.store)
152
+
153
+ def get(self, key: Any, default: Optional[Any] = None) -> Optional[Any]:
154
+ """
155
+ Get the value from the store
156
+
157
+ :param Any key: Key to get the value from the store
158
+ :param Optional[Any] default: Default value to return if the key is not in the store, defaults to None
159
+ :return: The value from the store, or the default value
160
+ :rtype: Optional[Any]
161
+ """
162
+ if key not in self.store:
163
+ return default
164
+ return self.store[key]
165
+
166
+ def items(self) -> Any:
167
+ """
168
+ Get the items from the store
169
+
170
+ :return: Items from the store
171
+ :rtype: Any
172
+ """
173
+ return self.store.items()
174
+
175
+ def keys(self) -> Any:
176
+ """
177
+ Get the keys from the store
178
+
179
+ :return: Keys from the store
180
+ :rtype: Any
181
+ """
182
+ return self.store.keys()
183
+
184
+ def values(self) -> Any:
185
+ """
186
+ Get the values from the store
187
+
188
+ :return: Values in the store
189
+ :rtype: Any
190
+ """
191
+ return self.store.values()
192
+
193
+ def update(self, *args, **kwargs) -> None:
194
+ """
195
+ Update the store
196
+
197
+ :rtype: None
198
+ """
199
+ self.store.update(*args, **kwargs)
200
+
201
+
202
+ @dataclasses.dataclass
203
+ class IntegrationAsset:
204
+ """
205
+ Dataclass for integration assets.
206
+
207
+ Represents an asset to be integrated, including its metadata and associated components.
208
+ If a component does not exist, it will be created based on the names provided in ``component_names``.
209
+
210
+ :param str name: The name of the asset.
211
+ :param str identifier: A unique identifier for the asset.
212
+ :param str asset_type: The type of the asset.
213
+ :param str asset_category: The category of the asset.
214
+ :param str component_type: The type of the component, defaults to ``ComponentType.Hardware``.
215
+ :param Optional[int] parent_id: The ID of the parent asset, defaults to None.
216
+ :param Optional[str] parent_module: The module of the parent asset, defaults to None.
217
+ :param str status: The status of the asset, defaults to "Active (On Network)".
218
+ :param str date_last_updated: The last update date of the asset, defaults to the current datetime.
219
+ :param Optional[str] asset_owner_id: The ID of the asset owner, defaults to None.
220
+ :param Optional[str] mac_address: The MAC address of the asset, defaults to None.
221
+ :param Optional[str] fqdn: The Fully Qualified Domain Name of the asset, defaults to None.
222
+ :param Optional[str] ip_address: The IP address of the asset, defaults to None.
223
+ :param List[str] component_names: A list of strings that represent the names of the components associated with the
224
+ asset, components will be created if they do not exist.
225
+ """
226
+
227
+ name: str
228
+ identifier: str
229
+ asset_type: str
230
+ asset_category: str
231
+ component_type: str = regscale_models.ComponentType.Hardware
232
+ description: str = ""
233
+ parent_id: Optional[int] = None
234
+ parent_module: Optional[str] = None
235
+ status: regscale_models.AssetStatus = regscale_models.AssetStatus.Active
236
+ date_last_updated: str = dataclasses.field(default_factory=get_current_datetime)
237
+ asset_owner_id: Optional[str] = None
238
+ mac_address: Optional[str] = None
239
+ fqdn: Optional[str] = None
240
+ ip_address: Optional[str] = None
241
+ component_names: List[str] = dataclasses.field(default_factory=list)
242
+ is_virtual: bool = True
243
+
244
+ # Additional fields from Wiz integration
245
+ external_id: Optional[str] = None
246
+ management_type: Optional[str] = None
247
+ software_vendor: Optional[str] = None
248
+ software_version: Optional[str] = None
249
+ software_name: Optional[str] = None
250
+ location: Optional[str] = None
251
+ notes: Optional[str] = None
252
+ model: Optional[str] = None
253
+ manufacturer: Optional[str] = None
254
+ other_tracking_number: Optional[str] = None
255
+ serial_number: Optional[str] = None
256
+ asset_tag_number: Optional[str] = None
257
+ is_public_facing: Optional[bool] = None
258
+ azure_identifier: Optional[str] = None
259
+ disk_storage: Optional[int] = None
260
+ cpu: Optional[int] = None
261
+ ram: Optional[int] = None
262
+ operating_system: Optional[regscale_models.AssetOperatingSystem] = None
263
+ os_version: Optional[str] = None
264
+ end_of_life_date: Optional[str] = None
265
+ vlan_id: Optional[str] = None
266
+ uri: Optional[str] = None
267
+ aws_identifier: Optional[str] = None
268
+ google_identifier: Optional[str] = None
269
+ other_cloud_identifier: Optional[str] = None
270
+ patch_level: Optional[str] = None
271
+ cpe: Optional[str] = None
272
+
273
+ source_data: Optional[Dict[str, Any]] = None
274
+ url: Optional[str] = None
275
+ ports_and_protocols: List[Dict[str, Any]] = dataclasses.field(default_factory=list)
276
+ software_inventory: List[Dict[str, Any]] = dataclasses.field(default_factory=list)
277
+
278
+ def __post_init__(self):
279
+ if self.ip_address in ["", "0.0.0.0"]:
280
+ self.ip_address = None
281
+
282
+
283
+ @dataclasses.dataclass
284
+ class IntegrationFinding:
285
+ """
286
+ Dataclass for integration findings.
287
+
288
+ :param list[str] control_labels: A list of control labels associated with the finding.
289
+ :param str title: The title of the finding.
290
+ :param str category: The category of the finding.
291
+ :param regscale_models.IssueSeverity severity: The severity of the finding, based on regscale_models.IssueSeverity.
292
+ :param str description: A description of the finding.
293
+ :param regscale_models.ControlTestResultStatus status: The status of the finding, based on
294
+ regscale_models.ControlTestResultStatus.
295
+ :param str priority: The priority of the finding, defaults to "Medium".
296
+ :param str issue_type: The type of issue, defaults to "Risk".
297
+ :param str issue_title: The title of the issue, defaults to an empty string.
298
+ :param str date_created: The creation date of the finding, defaults to the current datetime.
299
+ :param str due_date: The due date of the finding, defaults to 60 days from the current datetime.
300
+ :param str date_last_updated: The last update date of the finding, defaults to the current datetime.
301
+ :param str external_id: An external identifier for the finding, defaults to an empty string.
302
+ :param str gaps: A description of any gaps identified, defaults to an empty string.
303
+ :param str observations: Observations related to the finding, defaults to an empty string.
304
+ :param str evidence: Evidence supporting the finding, defaults to an empty string.
305
+ :param str identified_risk: The risk identified by the finding, defaults to an empty string.
306
+ :param str impact: The impact of the finding, defaults to an empty string.
307
+ :param str recommendation_for_mitigation: Recommendations for mitigating the finding, defaults to an empty string.
308
+ :param str asset_identifier: The identifier of the asset associated with the finding, defaults to an empty string.
309
+ :param Optional[str] cci_ref: The Common Configuration Enumeration reference for the finding, defaults to None.
310
+ :param str rule_id: The rule ID of the finding, defaults to an empty string.
311
+ :param str rule_version: The version of the rule associated with the finding, defaults to an empty string.
312
+ :param str results: The results of the finding, defaults to an empty string.
313
+ :param Optional[str] comments: Additional comments related to the finding, defaults to None.
314
+ :param Optional[str] source_report: The source report of the finding, defaults to None.
315
+ :param Optional[str] point_of_contact: The point of contact for the finding, used to create property defaults to None.
316
+ :param Optional[str] milestone_changes: Milestone Changes for the finding, defaults to None.
317
+ :param Optional[str] adjusted_risk_rating: The adjusted risk rating of the finding, defaults to None.
318
+ :param Optional[str] risk_adjustment: The risk adjustment of the finding, (Should be Yes, No, Pending), defaults to No.
319
+ :param Optional[str] operational_requirements: The operational requirements of the finding, defaults to None.
320
+ :param Optional[str] deviation_rationale: The rationale for any deviations from the finding, defaults to None.
321
+ :param str baseline: The baseline of the finding, defaults to an empty string.
322
+ :param str poam_comments: Comments related to the Plan of Action and Milestones (POAM) for the finding, defaults to
323
+ :param Optional[int] vulnerability_id: The ID of the vulnerability associated with the finding, defaults to None.
324
+ an empty string.
325
+ :param Optional[str] basis_for_adjustment: The basis for adjusting the finding, defaults to None.
326
+ :param Optional[str] vulnerability_number: STIG vulnerability number
327
+ :param Optional[str] vulnerability_type: The type of vulnerability, defaults to None.
328
+ :param Optional[str] plugin_id: The ID of the plugin associated with the finding, defaults to None.
329
+ :param Optional[str] plugin_name: The name of the plugin associated with the finding, defaults to None.
330
+ :param Optional[str] dns: The DNS name associated with the finding, defaults to None.
331
+ :param int severity_int: The severity integer of the finding, defaults to 0.
332
+ :param Optional[str] cve: The CVE of the finding, defaults to None.
333
+ :param Optional[float] cvss_v3_score: The CVSS v3 score of the finding, defaults to None.
334
+ :param Optional[float] cvss_v2_score: The CVSS v2 score of the finding, defaults to None.
335
+ :param Optional[str] cvss_score: The CVSS score of the finding, defaults to None.
336
+ :param Optional[str] cvss_v3_base_score: The CVSS v3 base score of the finding, defaults to None.
337
+ :param Optional[str] ip_address: The IP address associated with the finding, defaults to None.
338
+ :param Optional[str] first_seen: The first seen date of the finding, defaults to the current datetime.
339
+ :param Optional[str] last_seen: The last seen date of the finding, defaults to the current datetime.
340
+ :param Optional[str] oval_def: The OVAL definition of the finding, defaults to None.
341
+ :param Optional[str] scan_date: The scan date of the finding, defaults to the current datetime.
342
+ :param Optional[str] rule_id_full: The full rule ID of the finding, defaults to an empty string.
343
+ :param Optional[str] group_id: The group ID of the finding, defaults to an empty string.
344
+ :param Optional[str] vulnerable_asset: The vulnerable asset of the finding, defaults to None.
345
+ :param Optional[str] remediation: The remediation of the finding, defaults to None.
346
+ :param Optional[str] source_rule_id: The source rule ID of the finding, defaults to None.
347
+ :param Optional[str] poam_id: The POAM ID of the finding, defaults to None.
348
+ """
349
+
350
+ control_labels: List[str]
351
+ title: str
352
+ category: str
353
+ plugin_name: str
354
+ severity: regscale_models.IssueSeverity
355
+ description: str
356
+ status: Union[regscale_models.ControlTestResultStatus, regscale_models.ChecklistStatus, regscale_models.IssueStatus]
357
+ priority: str = "Medium"
358
+
359
+ # Vulns
360
+ first_seen: str = dataclasses.field(default_factory=get_current_datetime)
361
+ last_seen: str = dataclasses.field(default_factory=get_current_datetime)
362
+ cve: Optional[str] = None
363
+ cvss_v3_score: Optional[float] = None
364
+ cvss_v2_score: Optional[float] = None
365
+ ip_address: Optional[str] = None
366
+ plugin_id: Optional[str] = None
367
+ dns: Optional[str] = None
368
+ severity_int: int = 0
369
+ security_check: Optional[str] = None
370
+
371
+ # Issues
372
+ issue_title: str = ""
373
+ issue_type: str = "Risk"
374
+ date_created: str = dataclasses.field(default_factory=get_current_datetime)
375
+ date_last_updated: str = dataclasses.field(default_factory=get_current_datetime)
376
+ due_date: str = dataclasses.field(default_factory=lambda: date_str(days_from_today(60)))
377
+ external_id: str = ""
378
+ gaps: str = ""
379
+ observations: str = ""
380
+ evidence: str = ""
381
+ identified_risk: str = ""
382
+ impact: str = ""
383
+ recommendation_for_mitigation: str = ""
384
+ asset_identifier: str = ""
385
+ comments: Optional[str] = None
386
+ source_report: Optional[str] = None
387
+ point_of_contact: Optional[str] = None
388
+ milestone_changes: Optional[str] = None
389
+ planned_milestone_changes: Optional[str] = None
390
+ adjusted_risk_rating: Optional[str] = None
391
+ risk_adjustment: str = "No"
392
+ operational_requirements: Optional[str] = None
393
+ deviation_rationale: Optional[str] = None
394
+
395
+ poam_comments: Optional[str] = None
396
+ vulnerability_id: Optional[int] = None
397
+ _control_implementation_ids: List[int] = dataclasses.field(default_factory=list)
398
+
399
+ # Stig
400
+ checklist_status: regscale_models.ChecklistStatus = dataclasses.field(
401
+ default=regscale_models.ChecklistStatus.NOT_REVIEWED
402
+ )
403
+ cci_ref: Optional[str] = None
404
+ rule_id: str = ""
405
+ rule_version: str = ""
406
+ results: str = ""
407
+ baseline: str = ""
408
+ vulnerability_number: str = ""
409
+ oval_def: str = ""
410
+ scan_date: str = ""
411
+ rule_id_full: str = ""
412
+ group_id: str = ""
413
+
414
+ # Wiz
415
+ vulnerable_asset: Optional[str] = None
416
+ remediation: Optional[str] = None
417
+ cvss_score: Optional[float] = None
418
+ cvss_v3_base_score: Optional[float] = None
419
+ source_rule_id: Optional[str] = None
420
+ vulnerability_type: Optional[str] = None
421
+
422
+ # CoalFre POAM
423
+ basis_for_adjustment: Optional[str] = None
424
+ poam_id: Optional[str] = None
425
+
426
+ # Additional fields from Wiz integration
427
+ vpr_score: Optional[float] = None
428
+
429
+ def __post_init__(self):
430
+ if self.plugin_name is None:
431
+ self.plugin_name = self.cve or self.title
432
+ if self.plugin_id is None:
433
+ self.plugin_id = self.plugin_name
434
+
435
+ def get_issue_status(self) -> regscale_models.IssueStatus:
436
+ return (
437
+ regscale_models.IssueStatus.Closed
438
+ if (
439
+ self.status == regscale_models.ControlTestResultStatus.PASS
440
+ or self.status == regscale_models.IssueStatus.Closed
441
+ )
442
+ else regscale_models.IssueStatus.Open
443
+ )
444
+
445
+ def __eq__(self, other: Any) -> bool:
446
+ """
447
+ Check if the finding is equal to another finding
448
+
449
+ :param Any other: The other finding to compare
450
+ :return: Whether the findings are equal
451
+ :rtype: bool
452
+ """
453
+ if not isinstance(other, IntegrationFinding):
454
+ return NotImplemented
455
+ return (self.title, self.category, self.external_id) == (other.title, other.category, other.external_id)
456
+
457
+ def __hash__(self) -> int:
458
+ """
459
+ Get the hash of the finding
460
+
461
+ :return: Hash of the finding
462
+ :rtype: int
463
+ """
464
+ return hash((self.title, self.category, self.external_id))
465
+
466
+ def is_valid(self) -> bool:
467
+ """
468
+ Determines if the finding is valid based on the presence of `date_last_updated` and `risk_adjustment`.
469
+
470
+ :return: True if the finding is valid, False otherwise.
471
+ :rtype: bool
472
+ """
473
+ # Check if these fields are not empty or None
474
+ if not self.date_last_updated:
475
+ logger.warning("Finding %s is missing date_last_updated, skipping..", self.poam_id)
476
+ return False
477
+
478
+ if not self.risk_adjustment:
479
+ logger.warning("Finding %s is missing risk_adjustment, skipping..", self.poam_id)
480
+ return False
481
+
482
+ # Additional validation logic can be added here if needed
483
+ # For example, ensure risk_adjustment is one of the allowed values ("Yes", "No", "Pending")
484
+ allowed_risk_adjustments = {"Yes", "No", "Pending"}
485
+ if self.risk_adjustment not in allowed_risk_adjustments:
486
+ logger.warning("Finding %s has a disallowed risk adjustment, skipping..", self.poam_id)
487
+ return False
488
+
489
+ return True
490
+
491
+
492
+ class ScannerIntegrationType(str, enum.Enum):
493
+ """
494
+ Enumeration for scanner integration types.
495
+ """
496
+
497
+ CHECKLIST = "checklist"
498
+ CONTROL_TEST = "control_test"
499
+ VULNERABILITY = "vulnerability"
500
+
501
+
502
+ class FindingStatus(str, enum.Enum):
503
+ OPEN = regscale_models.IssueStatus.Open
504
+ CLOSED = regscale_models.IssueStatus.Closed
505
+ FAIL = regscale_models.IssueStatus.Open
506
+ PASS = regscale_models.IssueStatus.Closed
507
+ NOT_APPLICABLE = regscale_models.IssueStatus.Closed
508
+ NOT_REVIEWED = regscale_models.IssueStatus.Open
509
+
510
+
511
+ class ScannerIntegration(ABC):
512
+ """
513
+ Abstract class for scanner integrations.
514
+
515
+ :param int plan_id: The ID of the security plan
516
+ :param int tenant_id: The ID of the tenant, defaults to 1
517
+ """
518
+
519
+ stig_mapper = None
520
+ # Basic configuration options
521
+ options_map_assets_to_components: bool = False
522
+ type: ScannerIntegrationType = ScannerIntegrationType.CONTROL_TEST
523
+ title: str = "Scanner Integration"
524
+ asset_identifier_field: str = "otherTrackingNumber"
525
+ issue_identifier_field: str = ""
526
+ _max_poam_id: Optional[int] = None # Value holder for get_max_poam_id
527
+
528
+ # Progress trackers
529
+ asset_progress: Progress
530
+ finding_progress: Progress
531
+
532
+ # Processing counts
533
+ num_assets_to_process: Optional[int] = None
534
+ num_findings_to_process: Optional[int] = None
535
+
536
+ # Lock registry
537
+ _lock_registry: ThreadSafeDict = ThreadSafeDict()
538
+ _global_lock = threading.Lock() # Class-level lock
539
+ _kev_data = ThreadSafeDict() # Class-level lock
540
+
541
+ # Error handling
542
+ errors: List[str] = []
543
+
544
+ # Mapping dictionaries
545
+ finding_status_map: dict[Any, regscale_models.IssueStatus] = {}
546
+ checklist_status_map: dict[Any, regscale_models.ChecklistStatus] = {}
547
+ finding_severity_map: dict[Any, regscale_models.IssueSeverity] = {}
548
+ issue_to_vulnerability_map: dict[regscale_models.IssueSeverity, regscale_models.VulnerabilitySeverity] = {
549
+ regscale_models.IssueSeverity.Low: regscale_models.VulnerabilitySeverity.Low,
550
+ regscale_models.IssueSeverity.Moderate: regscale_models.VulnerabilitySeverity.Medium,
551
+ regscale_models.IssueSeverity.High: regscale_models.VulnerabilitySeverity.High,
552
+ regscale_models.IssueSeverity.Critical: regscale_models.VulnerabilitySeverity.Critical,
553
+ }
554
+ asset_map: dict[str, regscale_models.Asset] = {}
555
+ # cci_to_control_map: dict[str, set[int]] = {}
556
+ control_implementation_id_map: dict[str, int] = {}
557
+ control_map: dict[int, str] = {}
558
+ control_id_to_implementation_map: dict[int, int] = {}
559
+
560
+ # Existing issues map
561
+ existing_issue_ids_by_implementation_map: dict[int, List[OpenIssueDict]] = defaultdict(list)
562
+
563
+ # Scan Date
564
+ scan_date: str = ""
565
+ enable_finding_date_update = False
566
+
567
+ # Close Outdated Findings
568
+ close_outdated_findings = True
569
+
570
+ def __init__(self, plan_id: int, tenant_id: int = 1, **kwargs):
571
+ """
572
+ Initialize the ScannerIntegration.
573
+
574
+ :param int plan_id: The ID of the security plan
575
+ :param int tenant_id: The ID of the tenant, defaults to 1
576
+ :param kwargs: Additional keyword arguments
577
+ """
578
+ self.app = Application()
579
+ self.alerted_assets: Set[str] = set()
580
+ self.regscale_version: str = APIHandler().regscale_version # noqa
581
+ logger.info(f"RegScale Version: {self.regscale_version}")
582
+ self.plan_id: int = plan_id
583
+ self.tenant_id: int = tenant_id
584
+ self.components: ThreadSafeList[Any] = ThreadSafeList()
585
+ self.asset_map_by_identifier: ThreadSafeDict[str, regscale_models.Asset] = ThreadSafeDict()
586
+ self.software_to_create: ThreadSafeList[regscale_models.SoftwareInventory] = ThreadSafeList()
587
+ self.software_to_update: ThreadSafeList[regscale_models.SoftwareInventory] = ThreadSafeList()
588
+ self.data_to_create: ThreadSafeList[regscale_models.Data] = ThreadSafeList()
589
+ self.data_to_update: ThreadSafeList[regscale_models.Data] = ThreadSafeList()
590
+ self.link_to_create: ThreadSafeList[regscale_models.Link] = ThreadSafeList()
591
+ self.link_to_update: ThreadSafeList[regscale_models.Link] = ThreadSafeList()
592
+
593
+ self.existing_issues_map: ThreadSafeDict[int, List[regscale_models.Issue]] = ThreadSafeDict()
594
+ self.components_by_title: ThreadSafeDict[str, regscale_models.Component] = ThreadSafeDict()
595
+ self.control_tests_map: ManagedDefaultDict[int, regscale_models.ControlTest] = ManagedDefaultDict(list)
596
+
597
+ self.implementation_objective_map: ThreadSafeDict[str, int] = ThreadSafeDict()
598
+ self.implementation_option_map: ThreadSafeDict[str, int] = ThreadSafeDict()
599
+ self.control_implementation_map: ThreadSafeDict[int, regscale_models.ControlImplementation] = ThreadSafeDict()
600
+
601
+ self.control_implementation_id_map = regscale_models.ControlImplementation.get_control_label_map_by_plan(
602
+ plan_id=plan_id
603
+ )
604
+ self.control_map = {v: k for k, v in self.control_implementation_id_map.items()}
605
+ self.existing_issue_ids_by_implementation_map = regscale_models.Issue.get_open_issues_ids_by_implementation_id(
606
+ plan_id=plan_id
607
+ ) # GraphQL Call
608
+ self.control_id_to_implementation_map = regscale_models.ControlImplementation.get_control_id_map_by_plan(
609
+ plan_id=plan_id
610
+ )
611
+ self.cci_to_control_map: ThreadSafeDict[str, set[int]] = ThreadSafeDict()
612
+ self._no_ccis: bool = False
613
+ self.cci_to_control_map_lock: threading.Lock = threading.Lock()
614
+
615
+ self.assessment_map: ThreadSafeDict[int, regscale_models.Assessment] = ThreadSafeDict()
616
+ self.assessor_id: str = self.get_assessor_id()
617
+ self.asset_progress: Progress = create_progress_object()
618
+ self.finding_progress: Progress = create_progress_object()
619
+ self.stig_mapper = self.load_stig_mapper()
620
+ kev_data = pull_cisa_kev()
621
+ thread_safe_kev_data = ThreadSafeDict()
622
+ thread_safe_kev_data.update(kev_data)
623
+ self._kev_data = thread_safe_kev_data
624
+
625
+ @classmethod
626
+ def _get_lock(cls, key: str) -> threading.RLock:
627
+ """
628
+ Get or create a lock associated with a key.
629
+
630
+ :param str key: The cache key
631
+ :return: A reentrant lock
632
+ :rtype: RLock
633
+ """
634
+ lock = cls._lock_registry.get(key)
635
+ if lock is None:
636
+ with cls._global_lock: # Use a class-level lock to ensure thread safety
637
+ lock = cls._lock_registry.get(key)
638
+ if lock is None:
639
+ lock = threading.RLock()
640
+ cls._lock_registry[key] = lock
641
+ return lock
642
+
643
+ @staticmethod
644
+ def load_stig_mapper() -> Optional[STIGMapper]:
645
+ """
646
+ Load the STIG Mapper file
647
+
648
+ :return: None
649
+ """
650
+ from os import path
651
+
652
+ stig_mapper_file = ScannerVariables.stigMapperFile
653
+ if not path.exists(stig_mapper_file):
654
+ return None
655
+ try:
656
+ stig_mapper = STIGMapper(json_file=stig_mapper_file)
657
+ return stig_mapper
658
+ except Exception as e:
659
+ logger.debug(f"Warning Unable to loading STIG Mapper file: {e}")
660
+ return None
661
+
662
+ @staticmethod
663
+ def get_assessor_id() -> str:
664
+ """
665
+ Gets the ID of the assessor
666
+
667
+ :return: The ID of the assessor
668
+ :rtype: str
669
+ """
670
+
671
+ return regscale_models.Issue.get_user_id()
672
+
673
+ def get_cci_to_control_map(self) -> ThreadSafeDict[str, set[int]] | dict:
674
+ """
675
+ Gets the CCI to control map
676
+
677
+ :return: The CCI to control map
678
+ :rtype: ThreadSafeDict[str, set[int]] | dict
679
+ """
680
+ if self._no_ccis:
681
+ return self.cci_to_control_map
682
+ with self.cci_to_control_map_lock:
683
+ if any(self.cci_to_control_map):
684
+ return self.cci_to_control_map
685
+ logger.info("Getting CCI to control map...")
686
+ self.cci_to_control_map = regscale_models.map_ccis_to_control_ids(parent_id=self.plan_id) # type: ignore
687
+ if not any(self.cci_to_control_map):
688
+ self._no_ccis = True
689
+ return self.cci_to_control_map
690
+
691
+ def get_control_to_cci_map(self) -> dict[int, set[str]]:
692
+ """
693
+ Gets the security control id to CCI map
694
+
695
+ :return: The security control id to CCI map
696
+ :rtype: dict[int, set[str]]
697
+ """
698
+ control_id_to_cci_map = defaultdict(set)
699
+ for cci, control_ids in self.get_cci_to_control_map().items():
700
+ for control_id in control_ids:
701
+ control_id_to_cci_map[control_id].add(cci)
702
+ return control_id_to_cci_map
703
+
704
+ def get_control_implementation_id_for_cci(self, cci: Optional[str]) -> Optional[int]:
705
+ """
706
+ Gets the control implementation ID for a CCI
707
+
708
+ :param Optional[str] cci: The CCI
709
+ :return: The control ID
710
+ :rtype: Optional[int]
711
+ """
712
+ if not cci:
713
+ return None
714
+
715
+ cci_to_control_map = self.get_cci_to_control_map()
716
+ if cci not in cci_to_control_map:
717
+ cci = "CCI-000366"
718
+
719
+ if control_ids := cci_to_control_map.get(cci, set()):
720
+ for control_id in control_ids:
721
+ return self.control_id_to_implementation_map.get(control_id)
722
+ return None
723
+
724
+ def get_asset_map(self) -> dict[str, regscale_models.Asset]:
725
+ """
726
+ Retrieves a mapping of asset identifiers to their corresponding Asset objects. This method supports two modes
727
+ of operation based on the `options_map_assets_to_components` flag. If the flag is set, it fetches the asset
728
+ map using a specified key field from the assets associated with the given plan ID. Otherwise, it constructs
729
+ the map by fetching all assets under the specified plan and using the asset identifier field as the key.
730
+
731
+ :return: A dictionary mapping asset identifiers to Asset objects.
732
+ :rtype: dict[str, regscale_models.Asset]
733
+ """
734
+ if self.options_map_assets_to_components:
735
+ # Fetches the asset map directly using a specified key field.
736
+ return regscale_models.Asset.get_map(plan_id=self.plan_id, key_field=self.asset_identifier_field)
737
+ else:
738
+ # Constructs the asset map by fetching all assets under the plan and using the asset identifier field as
739
+ # the key.
740
+ return { # type: ignore
741
+ getattr(x, self.asset_identifier_field): x
742
+ for x in regscale_models.Asset.get_all_by_parent(
743
+ parent_id=self.plan_id,
744
+ parent_module=regscale_models.SecurityPlan.get_module_string(),
745
+ )
746
+ }
747
+
748
+ @abstractmethod
749
+ def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
750
+ """
751
+ Fetches findings from the integration
752
+
753
+ :return: A list of findings
754
+ :rtype: List[IntegrationFinding]
755
+ """
756
+
757
+ @abstractmethod
758
+ def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
759
+ """
760
+ Fetches assets from the integration
761
+
762
+ :return: An iterator of assets
763
+ :rtype: Iterator[IntegrationAsset]
764
+ """
765
+
766
+ def get_finding_status(self, status: Optional[str]) -> regscale_models.IssueStatus:
767
+ """
768
+ Gets the RegScale issue status based on the integration finding status
769
+
770
+ :param Optional[str] status: The status of the finding
771
+ :return: The RegScale issue status
772
+ :rtype: regscale_models.IssueStatus
773
+ """
774
+ return self.finding_status_map.get(status, regscale_models.IssueStatus.Open)
775
+
776
+ def get_checklist_status(self, status: Optional[str]) -> regscale_models.ChecklistStatus:
777
+ """
778
+ Gets the RegScale checklist status based on the integration finding status
779
+
780
+ :param Optional[str] status: The status of the finding
781
+ :return: The RegScale checklist status
782
+ :rtype: regscale_models.ChecklistStatus
783
+ """
784
+ return self.checklist_status_map.get(status, regscale_models.ChecklistStatus.NOT_REVIEWED)
785
+
786
+ def get_finding_severity(self, severity: Optional[str]) -> regscale_models.IssueSeverity:
787
+ """
788
+ Gets the RegScale issue severity based on the integration finding severity
789
+
790
+ :param Optional[str] severity: The severity of the finding
791
+ :return: The RegScale issue severity
792
+ :rtype: regscale_models.IssueSeverity
793
+ """
794
+ return self.finding_severity_map.get(severity, regscale_models.IssueSeverity.NotAssigned)
795
+
796
+ def get_finding_identifier(self, finding: IntegrationFinding) -> str:
797
+ """
798
+ Gets the finding identifier for the finding
799
+
800
+ :param IntegrationFinding finding: The finding
801
+ :return: The finding identifier
802
+ :rtype: str
803
+ """
804
+ prefix = f"{self.plan_id}:"
805
+ if ScannerVariables.tenableGroupByPlugin and finding.plugin_id:
806
+ return f"{prefix}{finding.plugin_id}"
807
+ prefix += finding.cve or finding.plugin_id or finding.rule_id or self.hash_string(finding.external_id).__str__()
808
+ if ScannerVariables.issueCreation.lower() == "perasset":
809
+ return f"{prefix}:{finding.asset_identifier}"
810
+ return prefix
811
+
812
+ def get_or_create_assessment(self, control_implementation_id: int) -> regscale_models.Assessment:
813
+ """
814
+ Gets or creates a RegScale assessment
815
+
816
+ :param int control_implementation_id: The ID of the control implementation
817
+ :return: The assessment
818
+ :rtype: regscale_models.Assessment
819
+ """
820
+ logger.info("Getting or create assessment for control implementation %d", control_implementation_id)
821
+ assessment: Optional[regscale_models.Assessment] = self.assessment_map.get(control_implementation_id)
822
+ if assessment:
823
+ logger.debug(
824
+ "Found cached assessment %s for control implementation %s", assessment.id, control_implementation_id
825
+ )
826
+ else:
827
+ logger.debug("Assessment not found for control implementation %d", control_implementation_id)
828
+ assessment = regscale_models.Assessment(
829
+ plannedStart=get_current_datetime(),
830
+ plannedFinish=get_current_datetime(),
831
+ status=regscale_models.AssessmentStatus.COMPLETE.value,
832
+ assessmentResult=regscale_models.AssessmentResultsStatus.FAIL.value,
833
+ actualFinish=get_current_datetime(),
834
+ leadAssessorId=self.assessor_id,
835
+ parentId=control_implementation_id,
836
+ parentModule=regscale_models.ControlImplementation.get_module_string(),
837
+ title=f"{self.title} Assessment",
838
+ assessmentType=regscale_models.AssessmentType.QA_SURVEILLANCE.value,
839
+ ).create()
840
+ self.assessment_map[control_implementation_id] = assessment
841
+ return assessment
842
+
843
+ def get_components(self) -> ThreadSafeList[regscale_models.Component]:
844
+ """
845
+ Get all components from the integration
846
+
847
+ :return: A list of components
848
+ :rtype: ThreadSafeList[regscale_models.Component]
849
+ """
850
+ if any(self.components):
851
+ return self.components
852
+ components: List[regscale_models.Component] = regscale_models.Component.get_all_by_parent(
853
+ parent_id=self.plan_id,
854
+ parent_module=regscale_models.SecurityPlan.get_module_string(),
855
+ )
856
+ self.components = ThreadSafeList(components)
857
+ return self.components
858
+
859
+ def get_component_by_title(self) -> dict:
860
+ """
861
+ Get all components from the integration
862
+
863
+ :return: A dictionary of components
864
+ :rtype: dict
865
+ """
866
+ return {component.title: component for component in self.get_components()}
867
+
868
+ # Asset Methods
869
+ def set_asset_defaults(self, asset: IntegrationAsset) -> IntegrationAsset:
870
+ """
871
+ Set default values for the asset (Thread Safe)
872
+
873
+ :param IntegrationAsset asset: The integration asset
874
+ :return: The asset with which defaults should be set
875
+ :rtype: IntegrationAsset
876
+ """
877
+ if not asset.asset_owner_id:
878
+ asset.asset_owner_id = self.get_assessor_id()
879
+ if not asset.status:
880
+ asset.status = regscale_models.AssetStatus.Active
881
+ return asset
882
+
883
+ def process_asset(
884
+ self,
885
+ asset: IntegrationAsset,
886
+ loading_assets: TaskID,
887
+ ) -> None:
888
+ """
889
+ Safely processes a single asset in a concurrent environment. This method ensures thread safety
890
+ by utilizing a threading lock. It assigns default values to the asset if necessary, maps the asset
891
+ to components if specified, and updates the progress of asset loading.
892
+ (Thread Safe)
893
+
894
+ :param IntegrationAsset asset: The integration asset to be processed.
895
+ :param TaskID loading_assets: The identifier for the task tracking the progress of asset loading.
896
+ :rtype: None
897
+ """
898
+
899
+ # Assign default values to the asset if they are not already set.
900
+ asset = self.set_asset_defaults(asset)
901
+
902
+ # If mapping assets to components is enabled and the asset has associated component names,
903
+ # attempt to update or create each asset under its respective component.
904
+ if any(asset.component_names):
905
+ for component_name in asset.component_names:
906
+ self.update_or_create_asset(asset, component_name)
907
+ else:
908
+ # If no component mapping is required, add the asset directly to the security plan without a component.
909
+ self.update_or_create_asset(asset, None)
910
+
911
+ if self.num_assets_to_process and self.asset_progress.tasks[loading_assets].total != float(
912
+ self.num_assets_to_process
913
+ ):
914
+ self.asset_progress.update(
915
+ loading_assets,
916
+ total=self.num_assets_to_process,
917
+ description=f"[#f8b737]Creating and updating {self.num_assets_to_process} assets from {self.title}.",
918
+ )
919
+ self.asset_progress.advance(loading_assets, 1)
920
+
921
+ def update_or_create_asset(
922
+ self,
923
+ asset: IntegrationAsset,
924
+ component_name: Optional[str] = None,
925
+ ) -> None:
926
+ """
927
+ Update or create an asset in RegScale.
928
+
929
+ This method either updates an existing asset or creates a new one within a thread-safe manner. It handles
930
+ the asset's association with a component, creating the component if it does not exist.
931
+ (Thread Safe)
932
+
933
+ :param IntegrationAsset asset: The asset to be updated or created.
934
+ :param Optional[str] component_name: The name of the component to associate the asset with. If None, the asset
935
+ is added directly to the security plan without a component association.
936
+ """
937
+ # Continue with normal asset creation/update
938
+ if not asset.identifier:
939
+ logger.warning("Asset has no identifier, skipping")
940
+ return
941
+
942
+ component = None
943
+ if component_name:
944
+ logger.debug("Searching for component: %s...", component_name)
945
+ component = self.components_by_title.get(component_name)
946
+ if not component:
947
+ logger.debug("No existing component found with name %s, proceeding to create it...", component_name)
948
+ component = regscale_models.Component(
949
+ title=component_name,
950
+ componentType=asset.component_type,
951
+ securityPlansId=self.plan_id,
952
+ description=component_name,
953
+ componentOwnerId=self.get_assessor_id(),
954
+ ).get_or_create()
955
+ self.components.append(component)
956
+ if component.securityPlansId:
957
+ component_mapping = regscale_models.ComponentMapping(
958
+ componentId=component.id,
959
+ securityPlanId=self.plan_id,
960
+ )
961
+ component_mapping.get_or_create()
962
+ self.components_by_title[component_name] = component
963
+
964
+ created, existing_or_new_asset = self.create_new_asset(asset, component=None)
965
+
966
+ # If the asset is associated with a component, create a mapping between them.
967
+ if existing_or_new_asset and component:
968
+ _was_created, _asset_mapping = regscale_models.AssetMapping(
969
+ assetId=existing_or_new_asset.id,
970
+ componentId=component.id,
971
+ ).get_or_create_with_status()
972
+
973
+ if created and DuroSuiteVariables.duroSuiteEnabled:
974
+ # Check if this is a DuroSuite compatible asset
975
+ scan_durosuite_devices(asset=asset, plan_id=self.plan_id, progress=self.asset_progress)
976
+
977
+ def create_new_asset(
978
+ self, asset: IntegrationAsset, component: Optional[regscale_models.Component]
979
+ ) -> tuple[bool, Optional[regscale_models.Asset]]:
980
+ """
981
+ Creates a new asset in the system based on the provided integration asset details.
982
+ Associates the asset with a component or directly with the security plan.
983
+
984
+ :param IntegrationAsset asset: The integration asset from which the new asset will be created.
985
+ :param Optional[regscale_models.Component] component: The component to link the asset to, or None.
986
+ :return: Tuple of (was_created, newly created asset instance).
987
+ :rtype: tuple[bool, Optional[regscale_models.Asset]]
988
+ """
989
+ # Ensure the asset has a name
990
+ if not asset.name:
991
+ logger.warning(
992
+ "Asset name is required for asset creation. Skipping asset creation of asset_type: %s", asset.asset_type
993
+ )
994
+ return False, None
995
+
996
+ new_asset = regscale_models.Asset(
997
+ name=asset.name,
998
+ description=asset.description,
999
+ bVirtual=asset.is_virtual,
1000
+ otherTrackingNumber=asset.other_tracking_number or asset.identifier,
1001
+ assetOwnerId=asset.asset_owner_id or "Unknown",
1002
+ parentId=component.id if component else self.plan_id,
1003
+ parentModule=(
1004
+ regscale_models.Component.get_module_string()
1005
+ if component
1006
+ else regscale_models.SecurityPlan.get_module_string()
1007
+ ),
1008
+ assetType=asset.asset_type,
1009
+ dateLastUpdated=asset.date_last_updated or get_current_datetime(),
1010
+ status=asset.status,
1011
+ assetCategory=asset.asset_category,
1012
+ managementType=asset.management_type,
1013
+ notes=asset.notes,
1014
+ model=asset.model,
1015
+ manufacturer=asset.manufacturer,
1016
+ serialNumber=asset.serial_number,
1017
+ assetTagNumber=asset.asset_tag_number,
1018
+ bPublicFacing=asset.is_public_facing,
1019
+ azureIdentifier=asset.azure_identifier,
1020
+ location=asset.location,
1021
+ ipAddress=asset.ip_address,
1022
+ fqdn=asset.fqdn,
1023
+ macAddress=asset.mac_address,
1024
+ diskStorage=asset.disk_storage,
1025
+ cpu=asset.cpu,
1026
+ ram=asset.ram or 0,
1027
+ operatingSystem=asset.operating_system,
1028
+ osVersion=asset.os_version,
1029
+ endOfLifeDate=asset.end_of_life_date,
1030
+ vlanId=asset.vlan_id,
1031
+ uri=asset.uri,
1032
+ awsIdentifier=asset.aws_identifier,
1033
+ googleIdentifier=asset.google_identifier,
1034
+ otherCloudIdentifier=asset.other_cloud_identifier,
1035
+ patchLevel=asset.patch_level,
1036
+ cpe=asset.cpe,
1037
+ softwareVersion=asset.software_version,
1038
+ softwareName=asset.software_name,
1039
+ softwareVendor=asset.software_vendor,
1040
+ )
1041
+ if self.asset_identifier_field:
1042
+ setattr(new_asset, self.asset_identifier_field, asset.identifier)
1043
+
1044
+ created, new_asset = new_asset.create_or_update_with_status(bulk_update=True)
1045
+ # add to asset_map_by_identifier
1046
+ self.asset_map_by_identifier[asset.identifier] = new_asset
1047
+ logger.debug("Created new asset with identifier %s", asset.identifier)
1048
+
1049
+ self.handle_software_inventory(new_asset, asset.software_inventory, created)
1050
+ self.create_asset_data_and_link(new_asset, asset)
1051
+ self.create_or_update_ports_protocol(new_asset, asset)
1052
+ if self.stig_mapper:
1053
+ self.stig_mapper.map_associated_stigs_to_asset(asset=new_asset, ssp_id=self.plan_id)
1054
+ return created, new_asset
1055
+
1056
+ def handle_software_inventory(
1057
+ self, new_asset: regscale_models.Asset, software_inventory: List[Dict[str, Any]], created: bool
1058
+ ) -> None:
1059
+ """
1060
+ Handles the software inventory for the asset.
1061
+
1062
+ :param regscale_models.Asset new_asset: The newly created asset
1063
+ :param List[Dict[str, Any]] software_inventory: List of software inventory items
1064
+ :param bool created: Flag indicating if the asset was newly created
1065
+ :rtype: None
1066
+ """
1067
+ if not software_inventory:
1068
+ return
1069
+
1070
+ existing_software: list[regscale_models.SoftwareInventory] = (
1071
+ []
1072
+ if created
1073
+ else regscale_models.SoftwareInventory.get_all_by_parent(
1074
+ parent_id=new_asset.id,
1075
+ parent_module=None,
1076
+ )
1077
+ )
1078
+ existing_software_dict = {(s.name, s.version): s for s in existing_software}
1079
+ software_in_scan = set()
1080
+
1081
+ for software in software_inventory:
1082
+ software_name = software.get("name")
1083
+ if not software_name:
1084
+ logger.error("Software name is required for software inventory")
1085
+ continue
1086
+
1087
+ software_version = software.get("version")
1088
+ software_in_scan.add((software_name, software_version))
1089
+
1090
+ if (software_name, software_version) not in existing_software_dict:
1091
+ self.software_to_create.append(
1092
+ regscale_models.SoftwareInventory(
1093
+ name=software_name,
1094
+ parentHardwareAssetId=new_asset.id,
1095
+ version=software_version,
1096
+ # references=software.get("references", []),
1097
+ )
1098
+ )
1099
+ else:
1100
+ self.software_to_update.append(existing_software_dict[(software_name, software_version)])
1101
+
1102
+ # Remove software that is no longer in the scan
1103
+ for software_key, software_obj in existing_software_dict.items():
1104
+ if software_key not in software_in_scan:
1105
+ software_obj.delete()
1106
+
1107
+ def create_asset_data_and_link(self, asset: regscale_models.Asset, integration_asset: IntegrationAsset) -> None:
1108
+ """
1109
+ Creates Data and Link objects for the given asset.
1110
+
1111
+ :param regscale_models.Asset asset: The asset to create Data and Link for
1112
+ :param IntegrationAsset integration_asset: The integration asset containing source data and URL
1113
+ :rtype: None
1114
+ """
1115
+ if integration_asset.source_data:
1116
+ # Optimization, create an api that gets the data by plan and parent module
1117
+ regscale_models.Data(
1118
+ parentId=asset.id,
1119
+ parentModule=asset.get_module_string(),
1120
+ dataSource=self.title,
1121
+ dataType=regscale_models.DataDataType.JSON.value,
1122
+ rawData=json.dumps(integration_asset.source_data, indent=2, cls=DateTimeEncoder),
1123
+ lastUpdatedById=integration_asset.asset_owner_id or "Unknown",
1124
+ createdById=integration_asset.asset_owner_id or "Unknown",
1125
+ ).create_or_update(bulk_create=True, bulk_update=True)
1126
+ if integration_asset.url:
1127
+ link = regscale_models.Link(
1128
+ parentID=asset.id,
1129
+ parentModule=asset.get_module_string(),
1130
+ url=integration_asset.url,
1131
+ title="Asset Provider URL",
1132
+ )
1133
+ if link.find_by_unique():
1134
+ self.link_to_update.append(link)
1135
+ else:
1136
+ self.link_to_create.append(link)
1137
+
1138
+ @staticmethod
1139
+ def create_or_update_ports_protocol(asset: regscale_models.Asset, integration_asset: IntegrationAsset) -> None:
1140
+ """
1141
+ Creates or updates PortsProtocol objects for the given asset.
1142
+
1143
+ :param regscale_models.Asset asset: The asset to create or update PortsProtocol for
1144
+ :param IntegrationAsset integration_asset: The integration asset containing ports and protocols information
1145
+ :rtype: None
1146
+ """
1147
+ if integration_asset.ports_and_protocols:
1148
+ for port_protocol in integration_asset.ports_and_protocols:
1149
+ if not port_protocol.get("start_port") or not port_protocol.get("end_port"):
1150
+ logger.error("Invalid port protocol data: %s", port_protocol)
1151
+ continue
1152
+ regscale_models.PortsProtocol(
1153
+ parentId=asset.id,
1154
+ parentModule=asset.get_module_string(),
1155
+ startPort=port_protocol.get("start_port", 0),
1156
+ endPort=port_protocol.get("end_port", 0),
1157
+ service=port_protocol.get("service", asset.name),
1158
+ protocol=port_protocol.get("protocol"),
1159
+ purpose=port_protocol.get("purpose", f"Grant access to {asset.name}"),
1160
+ usedBy=asset.name,
1161
+ ).create_or_update()
1162
+
1163
+ def update_regscale_assets(self, assets: Iterator[IntegrationAsset]) -> int:
1164
+ """
1165
+ Updates RegScale assets based on the integration assets
1166
+
1167
+ :param Iterator[IntegrationAsset] assets: The integration assets
1168
+ :return: The number of assets processed
1169
+ :rtype: int
1170
+ """
1171
+ logger.info("Updating RegScale assets...")
1172
+ loading_assets = self._setup_progress_bar()
1173
+ logger.info("Pre-populating cache")
1174
+ regscale_models.AssetMapping.populate_cache_by_plan(self.plan_id)
1175
+ regscale_models.ComponentMapping.populate_cache_by_plan(self.plan_id)
1176
+
1177
+ if self.options_map_assets_to_components:
1178
+ thread_safe_dict: ThreadSafeDict[str, regscale_models.Component] = ThreadSafeDict()
1179
+ thread_safe_dict.update(self.get_component_by_title())
1180
+ self.components_by_title = thread_safe_dict
1181
+
1182
+ assets_processed = self._process_assets(assets, loading_assets)
1183
+
1184
+ self._perform_batch_operations(self.asset_progress)
1185
+
1186
+ return assets_processed
1187
+
1188
+ def _setup_progress_bar(self) -> TaskID:
1189
+ """
1190
+ Sets up the progress bar for asset processing.
1191
+
1192
+ :return: The task ID for the progress bar
1193
+ :rtype: TaskID
1194
+ """
1195
+ asset_count = self.num_assets_to_process or None
1196
+ return self.asset_progress.add_task(
1197
+ f"[#f8b737]Creating and updating{f' {asset_count}' if asset_count else ''} asset(s) from {self.title}.",
1198
+ total=asset_count,
1199
+ )
1200
+
1201
+ def _process_assets(self, assets: Iterator[IntegrationAsset], loading_assets: TaskID) -> int:
1202
+ """
1203
+ Processes the assets using single or multi-threaded approach based on THREAD_MAX_WORKERS.
1204
+
1205
+ :param Iterator[IntegrationAsset] assets: The assets to process
1206
+ :param TaskID loading_assets: The task ID for the progress bar
1207
+ :return: The number of assets processed
1208
+ :rtype: int
1209
+ """
1210
+ assets_processed = 0
1211
+ # prime cache
1212
+ regscale_models.Asset.get_all_by_parent(
1213
+ parent_id=self.plan_id, parent_module=regscale_models.SecurityPlan.get_module_string()
1214
+ )
1215
+
1216
+ process_func = lambda my_asset: self._process_single_asset(my_asset, loading_assets) # noqa: E731
1217
+
1218
+ if get_thread_workers_max() == 1:
1219
+ for asset in assets:
1220
+ if process_func(asset):
1221
+ assets_processed = self._update_processed_count(assets_processed)
1222
+ else:
1223
+ with concurrent.futures.ThreadPoolExecutor(max_workers=get_thread_workers_max()) as executor:
1224
+ future_to_asset = {executor.submit(process_func, asset): asset for asset in assets}
1225
+ for future in concurrent.futures.as_completed(future_to_asset):
1226
+ if future.result():
1227
+ assets_processed = self._update_processed_count(assets_processed)
1228
+
1229
+ return assets_processed
1230
+
1231
+ def _process_single_asset(self, asset: IntegrationAsset, loading_assets: TaskID) -> bool:
1232
+ """
1233
+ Processes a single asset and handles any exceptions.
1234
+
1235
+ :param IntegrationAsset asset: The asset to process
1236
+ :param TaskID loading_assets: The task ID for the progress bar
1237
+ :return: True if the asset was processed successfully, False otherwise
1238
+ :rtype: bool
1239
+ """
1240
+ try:
1241
+ self.process_asset(asset, loading_assets)
1242
+ return True
1243
+ except Exception as exc:
1244
+ self.log_error("An error occurred when processing asset %s: %s", asset.name, exc)
1245
+ return False
1246
+
1247
+ @staticmethod
1248
+ def _update_processed_count(assets_processed: int) -> int:
1249
+ """
1250
+ Updates and logs the count of processed assets.
1251
+
1252
+ :param int assets_processed: The current count of processed assets
1253
+ :return: The updated count of processed assets
1254
+ :rtype: int
1255
+ """
1256
+ assets_processed += 1
1257
+ if assets_processed % 100 == 0:
1258
+ logger.info("Processed %d assets.", assets_processed)
1259
+ return assets_processed
1260
+
1261
+ def _perform_batch_operations(self, progress: Progress) -> None:
1262
+ """
1263
+ Performs batch operations for assets, software inventory, and data.
1264
+
1265
+ :rtype: None
1266
+ """
1267
+ logger.info("Bulk saving assets...")
1268
+ regscale_models.Asset.bulk_save(progress_context=progress)
1269
+ regscale_models.Issue.bulk_save(progress_context=progress)
1270
+ regscale_models.Property.bulk_save(progress_context=progress)
1271
+
1272
+ if self.software_to_create:
1273
+ regscale_models.SoftwareInventory.batch_create(items=self.software_to_create, progress_context=progress)
1274
+ if self.software_to_update:
1275
+ regscale_models.SoftwareInventory.batch_update(items=self.software_to_update, progress_context=progress)
1276
+ regscale_models.Data.bulk_save(progress_context=progress)
1277
+
1278
+ @staticmethod
1279
+ def get_issue_title(finding: IntegrationFinding) -> str:
1280
+ """
1281
+ Gets the issue title based on the POAM Title Type variable.
1282
+
1283
+ :param IntegrationFinding finding: The finding data
1284
+ :return: The issue title
1285
+ :rtype: str
1286
+ """
1287
+ issue_title = finding.title or ""
1288
+ if ScannerVariables.poamTitleType.lower() == "pluginid" or not issue_title:
1289
+ issue_title = (
1290
+ f"{finding.plugin_id or finding.cve or finding.rule_id}: {finding.plugin_name or finding.description}"
1291
+ )
1292
+ return issue_title[:450]
1293
+
1294
+ # Finding Methods
1295
+ def create_or_update_issue_from_finding(
1296
+ self,
1297
+ title: str,
1298
+ finding: IntegrationFinding,
1299
+ ) -> regscale_models.Issue:
1300
+ """
1301
+ Creates or updates a RegScale issue from a finding
1302
+
1303
+ :param str title: The title of the issue
1304
+ :param IntegrationFinding finding: The finding data
1305
+ :return: The created or updated RegScale issue
1306
+ :rtype: regscale_models.Issue
1307
+ """
1308
+ issue_status = finding.get_issue_status()
1309
+ finding_id = self.get_finding_identifier(finding)
1310
+ finding_id_lock = self._get_lock(finding_id)
1311
+
1312
+ with finding_id_lock:
1313
+ if ScannerVariables.issueCreation.lower() != "perasset":
1314
+ # Check if we should consolidate open issues based on integrationFindingId
1315
+ if issue_status == regscale_models.IssueStatus.Open:
1316
+ existing_issues = regscale_models.Issue.find_by_integration_finding_id(finding_id)
1317
+ # Find an open issue to update
1318
+ issue = next(
1319
+ (issue for issue in existing_issues if issue.status != regscale_models.IssueStatus.Closed), None
1320
+ )
1321
+ if issue:
1322
+ return self._create_or_update_issue(finding, issue_status, title, issue)
1323
+
1324
+ # Check if we should consolidate closed issues based on integrationFindingId and issueDueDates
1325
+ elif issue_status == regscale_models.IssueStatus.Closed:
1326
+ existing_issues = regscale_models.Issue.find_by_integration_finding_id(finding_id)
1327
+ # Find a closed issue with matching due date to consolidate with
1328
+ matching_closed_issue = next(
1329
+ (
1330
+ issue
1331
+ for issue in existing_issues
1332
+ if issue.status == regscale_models.IssueStatus.Closed
1333
+ and date_str(issue.dueDate) == date_str(finding.due_date)
1334
+ ),
1335
+ None,
1336
+ )
1337
+ if matching_closed_issue:
1338
+ return self._create_or_update_issue(finding, issue_status, title, matching_closed_issue)
1339
+
1340
+ return self._create_or_update_issue(finding, issue_status, title)
1341
+
1342
+ def _create_or_update_issue(
1343
+ self,
1344
+ finding: IntegrationFinding,
1345
+ issue_status: regscale_models.IssueStatus,
1346
+ title: str,
1347
+ existing_issue: Optional[regscale_models.Issue] = None,
1348
+ ) -> regscale_models.Issue:
1349
+ """
1350
+ Creates or updates a RegScale issue
1351
+
1352
+ :param IntegrationFinding finding: The finding data
1353
+ :param str issue_status: The status of the issue
1354
+ :param str title: The title of the issue
1355
+ :param Optional[regscale_models.Issue] existing_issue: Existing issue to update, if any
1356
+ :return: The created or updated RegScale issue
1357
+ :rtype: regscale_models.Issue
1358
+ """
1359
+ # Prepare issue data
1360
+ issue_title = self.get_issue_title(finding) or title
1361
+ description = finding.description or ""
1362
+ remediation_description = finding.recommendation_for_mitigation or finding.remediation or ""
1363
+ is_poam = self.is_poam(finding)
1364
+
1365
+ if existing_issue:
1366
+ logger.debug(
1367
+ "Updating existing issue %s with assetIdentifier %s", existing_issue.id, finding.asset_identifier
1368
+ )
1369
+
1370
+ # If we have an existing issue, update its fields instead of creating a new one
1371
+ issue = existing_issue or regscale_models.Issue()
1372
+
1373
+ # Get consolidated asset identifier
1374
+ asset_identifier = self.get_consolidated_asset_identifier(finding, existing_issue)
1375
+
1376
+ # Update all fields
1377
+ issue.parentId = self.plan_id
1378
+ issue.parentModule = regscale_models.SecurityPlan.get_module_string()
1379
+ issue.vulnerabilityId = finding.vulnerability_id
1380
+ issue.title = issue_title
1381
+ issue.dateCreated = finding.date_created
1382
+ issue.status = issue_status
1383
+ issue.dateCompleted = (
1384
+ self.get_date_completed(finding, issue_status)
1385
+ if issue_status == regscale_models.IssueStatus.Closed
1386
+ else None
1387
+ )
1388
+ issue.severityLevel = finding.severity
1389
+ issue.issueOwnerId = self.assessor_id
1390
+ issue.securityPlanId = self.plan_id
1391
+ issue.identification = "Vulnerability Assessment"
1392
+ issue.dateFirstDetected = finding.date_created
1393
+ issue.dueDate = finding.due_date
1394
+ issue.description = description
1395
+ issue.sourceReport = finding.source_report or self.title
1396
+ issue.recommendedActions = finding.recommendation_for_mitigation
1397
+ issue.assetIdentifier = asset_identifier
1398
+ issue.securityChecks = finding.security_check or finding.external_id
1399
+ issue.remediationDescription = remediation_description
1400
+ issue.integrationFindingId = self.get_finding_identifier(finding)
1401
+ issue.poamComments = finding.poam_comments
1402
+ issue.cve = finding.cve
1403
+ control_id = self.get_control_implementation_id_for_cci(finding.cci_ref) if finding.cci_ref else None
1404
+ issue.controlId = control_id # TODO REMOVE
1405
+ # Add the control implementation ids and the cci ref if it exists
1406
+ # Get control implementation ID for CCI if it exists
1407
+ # Only add CCI control ID if it exists
1408
+ cci_control_ids = [control_id] if control_id is not None else []
1409
+
1410
+ issue.controlImplementationIds = list(set(finding._control_implementation_ids + cci_control_ids)) # noqa
1411
+ issue.isPoam = is_poam
1412
+ issue.basisForAdjustment = (
1413
+ finding.basis_for_adjustment if finding.basis_for_adjustment else f"{self.title} import"
1414
+ )
1415
+ issue.pluginId = finding.plugin_id
1416
+ issue.originalRiskRating = regscale_models.Issue.assign_risk_rating(finding.severity)
1417
+ # Current: changes
1418
+ # Planned: planned changes
1419
+ issue.changes = "<p>Current: {}</p><p>Planned: {}</p>".format(
1420
+ finding.milestone_changes, finding.planned_milestone_changes
1421
+ )
1422
+ issue.adjustedRiskRating = finding.adjusted_risk_rating
1423
+ issue.riskAdjustment = finding.risk_adjustment
1424
+ issue.operationalRequirement = finding.operational_requirements
1425
+ issue.deviationRationale = finding.deviation_rationale
1426
+
1427
+ if finding.cve:
1428
+ issue = self.lookup_kev_and_upate_issue(cve=finding.cve, issue=issue, cisa_kevs=self._kev_data)
1429
+
1430
+ if existing_issue:
1431
+ logger.debug("Saving existing issue %s with assetIdentifier: %s", issue.id, issue.assetIdentifier)
1432
+ issue.save(bulk=True)
1433
+ else:
1434
+ issue = issue.create_or_update(
1435
+ bulk_update=True, defaults={"otherIdentifier": self._get_other_identifier(finding, is_poam)}
1436
+ )
1437
+
1438
+ if poc := finding.point_of_contact:
1439
+ _ = regscale_models.Property(
1440
+ key="POC",
1441
+ value=poc,
1442
+ parentId=issue.id,
1443
+ parentModule="issues",
1444
+ ).create_or_update(bulk_create=True, bulk_update=True)
1445
+
1446
+ return issue
1447
+
1448
+ @staticmethod
1449
+ def get_consolidated_asset_identifier(
1450
+ finding: IntegrationFinding,
1451
+ existing_issue: Optional[regscale_models.Issue] = None,
1452
+ ) -> str:
1453
+ """
1454
+ Gets the consolidated asset identifier, combining the finding's asset identifier
1455
+ with any existing asset identifiers from the issue.
1456
+
1457
+ :param IntegrationFinding finding: The finding data
1458
+ :param Optional[regscale_models.Issue] existing_issue: The existing issue to consolidate with, if any
1459
+ :return: The consolidated asset identifier
1460
+ :rtype: str
1461
+ """
1462
+ delimiter = "\n"
1463
+ if not existing_issue or ScannerVariables.issueCreation.lower() == "perasset":
1464
+ return finding.asset_identifier
1465
+
1466
+ # Get existing asset identifiers
1467
+ existing_asset_identifiers = set((existing_issue.assetIdentifier or "").split(delimiter))
1468
+ if finding.asset_identifier not in existing_asset_identifiers:
1469
+ existing_asset_identifiers.add(finding.asset_identifier)
1470
+
1471
+ return delimiter.join(existing_asset_identifiers)
1472
+
1473
+ def _get_other_identifier(self, finding: IntegrationFinding, is_poam: bool) -> Optional[str]:
1474
+ """
1475
+ Gets the other identifier for an issue
1476
+
1477
+ :param IntegrationFinding finding: The finding data
1478
+ :param bool is_poam: Whether this is a POAM issue
1479
+ :return: The other identifier if applicable
1480
+ :rtype: Optional[str]
1481
+ """
1482
+ # If existing POAM ID is greater than the cached max, update the cached max
1483
+ if finding.poam_id:
1484
+ if (poam_id := self.parse_poam_id(finding.poam_id)) and poam_id > (self._max_poam_id or 0):
1485
+ self._max_poam_id = poam_id
1486
+ return finding.poam_id
1487
+
1488
+ # Only called if isPoam is True and creating a new issue
1489
+ if is_poam and ScannerVariables.incrementPoamIdentifier:
1490
+ return f"V-{self.get_next_poam_id():04d}"
1491
+ return None
1492
+
1493
+ @staticmethod
1494
+ def lookup_kev_and_upate_issue(
1495
+ cve: str, issue: regscale_models.Issue, cisa_kevs: Optional[ThreadSafeDict[str, Any]] = None
1496
+ ) -> regscale_models.Issue:
1497
+ """
1498
+ Determine if the cve is part of the published CISA KEV list
1499
+
1500
+ :param str cve: The CVE to lookup in CISAs KEV list
1501
+ :param regscale_models.Issue issue: The issue to update kevList field and dueDate if found in KEV List
1502
+ :param Optional[ThreadSafeDict[str, Any]] cisa_kevs: The CISA KEV data to search the findings
1503
+ :return: The updated issue
1504
+ :rtype: regscale_models.Issue
1505
+ """
1506
+ from datetime import datetime
1507
+
1508
+ from regscale.core.app.utils.app_utils import convert_datetime_to_regscale_string
1509
+
1510
+ issue.kevList = "No"
1511
+
1512
+ if cisa_kevs:
1513
+ kev_data = next(
1514
+ (
1515
+ entry
1516
+ for entry in cisa_kevs.get("vulnerabilities", [])
1517
+ if entry.get("cveID", "").lower() == cve.lower()
1518
+ ),
1519
+ None,
1520
+ )
1521
+ if kev_data:
1522
+ issue.dueDate = convert_datetime_to_regscale_string(datetime.strptime(kev_data["dueDate"], "%Y-%m-%d"))
1523
+ issue.kevList = "Yes"
1524
+
1525
+ return issue
1526
+
1527
+ @staticmethod
1528
+ def group_by_plugin(existing_issue: regscale_models.Issue, finding: IntegrationFinding) -> regscale_models.Issue:
1529
+ """
1530
+ Merges the CVEs for the issue if the group by plugin is enabled
1531
+
1532
+ :param regscale_models.Issue regscale_models.Issue existing_issue: The existing issue
1533
+ :param IntegrationFinding finding: The finding data
1534
+ :return: The existing issue
1535
+ :rtype: regscale_models.Issue
1536
+ """
1537
+ if ScannerVariables.tenableGroupByPlugin and finding.cve:
1538
+ # consolidate cve, but only for this switch
1539
+ existing_cves = (existing_issue.cve or "").split(",")
1540
+ existing_issue.cve = ",".join(set(existing_cves + [finding.cve]))
1541
+ return existing_issue
1542
+
1543
+ @staticmethod
1544
+ def is_poam(finding: IntegrationFinding) -> bool:
1545
+ """
1546
+ Determines if an issue should be considered a Plan of Action and Milestones (POAM).
1547
+
1548
+ :param IntegrationFinding finding: The finding to check
1549
+ :return: True if the issue should be a POAM, False otherwise
1550
+ :rtype: bool
1551
+ """
1552
+ if ScannerVariables.vulnerabilityCreation.lower() == "poamcreation":
1553
+ return True
1554
+ if finding.due_date < get_current_datetime():
1555
+ return True
1556
+ return False
1557
+
1558
+ def handle_failing_finding(
1559
+ self,
1560
+ issue_title: str,
1561
+ finding: IntegrationFinding,
1562
+ ) -> None:
1563
+ """
1564
+ Handles findings that have failed by creating or updating an issue.
1565
+
1566
+ :param str issue_title: The title of the issue
1567
+ :param IntegrationFinding finding: The finding data that has failed
1568
+ :rtype: None
1569
+ """
1570
+ logger.debug("Creating issue for failing finding %s", finding.external_id)
1571
+ found_issue = self.create_or_update_issue_from_finding(
1572
+ title=issue_title,
1573
+ finding=finding,
1574
+ )
1575
+ # Update the control implementation status to NOT_IMPLEMENTED since we have a failing finding
1576
+ if found_issue.controlImplementationIds:
1577
+ for control_id in found_issue.controlImplementationIds:
1578
+ self.update_control_implementation_status_after_close(control_id)
1579
+
1580
+ def handle_failing_checklist(
1581
+ self,
1582
+ finding: IntegrationFinding,
1583
+ plan_id: int,
1584
+ ) -> None:
1585
+ """
1586
+ Handles failing checklists by creating or updating implementation options and objectives.
1587
+
1588
+ :param IntegrationFinding finding: The finding data
1589
+ :param int plan_id: The ID of the security plan
1590
+ :rtype: None
1591
+ """
1592
+ if finding.cci_ref:
1593
+ failing_objectives = regscale_models.ControlObjective.fetch_control_objectives_by_other_id(
1594
+ parent_id=plan_id, other_id_contains=finding.cci_ref
1595
+ )
1596
+ failing_objectives += regscale_models.ControlObjective.fetch_control_objectives_by_name(
1597
+ parent_id=plan_id, name_contains=finding.cci_ref
1598
+ )
1599
+ for failing_objective in failing_objectives:
1600
+ if failing_objective.name.lower().startswith("cci-"):
1601
+ implementation_id = self.get_control_implementation_id_for_cci(failing_objective.name)
1602
+ else:
1603
+ control_label = objective_to_control_dot(failing_objective.name)
1604
+ if control_label not in self.control_implementation_id_map:
1605
+ logger.warning("Control %s not found for %s", control_label, control_label)
1606
+ continue
1607
+ implementation_id = self.control_implementation_id_map[control_label]
1608
+
1609
+ failing_option = regscale_models.ImplementationOption(
1610
+ name="Failed STIG",
1611
+ description="Failed STIG Security Checks",
1612
+ acceptability=regscale_models.ImplementationStatus.NOT_IMPLEMENTED,
1613
+ objectiveId=failing_objective.id,
1614
+ securityControlId=failing_objective.securityControlId,
1615
+ responsibility="Customer",
1616
+ ).create_or_update()
1617
+
1618
+ _ = regscale_models.ImplementationObjective(
1619
+ securityControlId=failing_objective.securityControlId,
1620
+ implementationId=implementation_id,
1621
+ objectiveId=failing_objective.id,
1622
+ optionId=failing_option.id,
1623
+ status=regscale_models.ImplementationStatus.NOT_IMPLEMENTED,
1624
+ statement=failing_objective.description,
1625
+ responsibility="Customer",
1626
+ ).create_or_update()
1627
+
1628
+ # Create assessment and control test result
1629
+ assessment = self.get_or_create_assessment(implementation_id)
1630
+ if implementation_id:
1631
+ control_test = self.create_or_get_control_test(finding, implementation_id)
1632
+ self.create_control_test_result(
1633
+ finding, control_test, assessment, regscale_models.ControlTestResultStatus.FAIL
1634
+ )
1635
+
1636
+ def handle_passing_checklist(
1637
+ self,
1638
+ finding: IntegrationFinding,
1639
+ plan_id: int,
1640
+ ) -> None:
1641
+ """
1642
+ Handles passing checklists by creating or updating implementation options and objectives.
1643
+
1644
+ :param IntegrationFinding finding: The finding data
1645
+ :param int plan_id: The ID of the security plan
1646
+ :rtype: None
1647
+ """
1648
+ if finding.cci_ref:
1649
+ passing_objectives = regscale_models.ControlObjective.fetch_control_objectives_by_other_id(
1650
+ parent_id=plan_id, other_id_contains=finding.cci_ref
1651
+ )
1652
+ passing_objectives += regscale_models.ControlObjective.fetch_control_objectives_by_name(
1653
+ parent_id=plan_id, name_contains=finding.cci_ref
1654
+ )
1655
+ for passing_objective in passing_objectives:
1656
+ if passing_objective.name.lower().startswith("cci-"):
1657
+ implementation_id = self.get_control_implementation_id_for_cci(passing_objective.name)
1658
+ else:
1659
+ control_label = objective_to_control_dot(passing_objective.name)
1660
+ if control_label not in self.control_implementation_id_map:
1661
+ logger.warning("Control %s not found for %s", control_label, control_label)
1662
+ continue
1663
+ implementation_id = self.control_implementation_id_map[control_label]
1664
+
1665
+ # Skip if we couldn't determine the implementation ID
1666
+ if implementation_id is None:
1667
+ logger.warning("Could not determine implementation ID for objective %s", passing_objective.name)
1668
+ continue
1669
+
1670
+ passing_option = regscale_models.ImplementationOption(
1671
+ name="Passed STIG",
1672
+ description="Passed STIG Security Checks",
1673
+ acceptability=regscale_models.ImplementationStatus.FULLY_IMPLEMENTED,
1674
+ objectiveId=passing_objective.id,
1675
+ securityControlId=passing_objective.securityControlId,
1676
+ responsibility="Customer",
1677
+ ).create_or_update()
1678
+
1679
+ _ = regscale_models.ImplementationObjective(
1680
+ securityControlId=passing_objective.securityControlId,
1681
+ implementationId=implementation_id,
1682
+ objectiveId=passing_objective.id,
1683
+ optionId=passing_option.id,
1684
+ status=regscale_models.ImplementationStatus.FULLY_IMPLEMENTED,
1685
+ statement=passing_objective.description,
1686
+ responsibility="Customer",
1687
+ ).create_or_update()
1688
+
1689
+ # Create assessment and control test result
1690
+ assessment = self.get_or_create_assessment(implementation_id)
1691
+ control_test = self.create_or_get_control_test(finding, implementation_id)
1692
+ self.create_control_test_result(
1693
+ finding, control_test, assessment, regscale_models.ControlTestResultStatus.PASS
1694
+ )
1695
+
1696
+ @staticmethod
1697
+ def create_or_get_control_test(
1698
+ finding: IntegrationFinding, control_implementation_id: int
1699
+ ) -> regscale_models.ControlTest:
1700
+ """
1701
+ Create or get an existing control test.
1702
+
1703
+ :param IntegrationFinding finding: The finding associated with the control test
1704
+ :param int control_implementation_id: The ID of the control implementation
1705
+ :return: The created or existing control test
1706
+ :rtype: regscale_models.ControlTest
1707
+ """
1708
+ return regscale_models.ControlTest(
1709
+ uuid=finding.external_id,
1710
+ parentControlId=control_implementation_id,
1711
+ testCriteria=finding.cci_ref or finding.description,
1712
+ ).get_or_create()
1713
+
1714
+ def get_asset_by_identifier(self, identifier: str) -> Optional[regscale_models.Asset]:
1715
+ """
1716
+ Gets an asset by its identifier
1717
+
1718
+ :param str identifier: The identifier of the asset
1719
+ :return: The asset
1720
+ :rtype: Optional[regscale_models.Asset]
1721
+ """
1722
+ asset = self.asset_map_by_identifier.get(identifier)
1723
+ if not asset and identifier not in self.alerted_assets:
1724
+ self.alerted_assets.add(identifier)
1725
+ self.log_error("1. Asset not found for identifier %s", identifier)
1726
+ return asset
1727
+
1728
+ def process_checklist(self, finding: IntegrationFinding) -> int:
1729
+ """
1730
+ Processes a single checklist item based on the provided finding.
1731
+
1732
+ This method checks if the asset related to the finding exists, updates or creates a checklist item,
1733
+ and handles the finding based on its status (pass/fail).
1734
+
1735
+ :param IntegrationFinding finding: The finding to process
1736
+ :return: 1 if the checklist was processed, 0 if not
1737
+ :rtype: int
1738
+ """
1739
+ logger.debug("Processing checklist %s", finding.external_id)
1740
+ if not (asset := self.get_asset_by_identifier(finding.asset_identifier)):
1741
+ logger.error("2. Asset not found for identifier %s", finding.asset_identifier)
1742
+ return 0
1743
+
1744
+ tool = regscale_models.ChecklistTool.STIGs
1745
+ if finding.vulnerability_type == "Vulnerability Scan":
1746
+ tool = regscale_models.ChecklistTool.VulnerabilityScanner
1747
+
1748
+ if not finding.cci_ref:
1749
+ finding.cci_ref = "CCI-000366"
1750
+
1751
+ logger.debug("Create or update checklist for %s", finding.external_id)
1752
+ regscale_models.Checklist(
1753
+ status=finding.checklist_status,
1754
+ assetId=asset.id,
1755
+ tool=tool,
1756
+ baseline=finding.baseline,
1757
+ vulnerabilityId=finding.vulnerability_number,
1758
+ results=finding.results,
1759
+ check=finding.title,
1760
+ cci=finding.cci_ref,
1761
+ ruleId=finding.rule_id,
1762
+ version=finding.rule_version,
1763
+ comments=finding.comments,
1764
+ datePerformed=finding.date_created,
1765
+ ).create_or_update()
1766
+
1767
+ # For both passing and failing findings, let the vulnerability mapping handle the closure
1768
+ if finding.status != regscale_models.ChecklistStatus.PASS:
1769
+ logger.debug("Handling failing checklist for %s", finding.external_id)
1770
+ if self.type == ScannerIntegrationType.CHECKLIST:
1771
+ self.handle_failing_checklist(finding=finding, plan_id=self.plan_id)
1772
+ self.handle_failing_finding(
1773
+ issue_title=finding.issue_title or finding.title,
1774
+ finding=finding,
1775
+ )
1776
+ return 1
1777
+
1778
+ def handle_control_finding(self, finding: IntegrationFinding) -> None:
1779
+ """
1780
+ Handle a control finding, either passing or failing.
1781
+
1782
+ :param IntegrationFinding finding: The finding to handle
1783
+ :rtype: None
1784
+ """
1785
+ if finding.status == regscale_models.ControlTestResultStatus.PASS:
1786
+ # For passing findings, we'll let the normal vulnerability mapping closure handle it
1787
+ pass
1788
+ else:
1789
+ self.handle_failing_finding(
1790
+ issue_title="Finding %s failed",
1791
+ finding=finding,
1792
+ )
1793
+
1794
+ def update_regscale_findings(self, findings: Iterator[IntegrationFinding]) -> int:
1795
+ """
1796
+ Updates RegScale findings, checklists, and vulnerabilities in a single pass.
1797
+
1798
+ :param Iterator[IntegrationFinding] findings: The integration findings
1799
+ :return: The number of findings processed
1800
+ :rtype: int
1801
+ """
1802
+ logger.info("Updating RegScale findings...")
1803
+ scan_history = self.create_scan_history()
1804
+ current_vulnerabilities: Dict[int, Set[int]] = defaultdict(set)
1805
+ processed_findings_count = 0
1806
+ findings_to_process = self.num_findings_to_process
1807
+ loading_findings = self.finding_progress.add_task(
1808
+ f"[#f8b737]Processing {f'{findings_to_process} ' if findings_to_process else ''}finding(s) from {self.title}",
1809
+ total=self.num_findings_to_process if self.num_findings_to_process else None,
1810
+ )
1811
+
1812
+ # Locks for thread-safe operations
1813
+ count_lock = threading.RLock()
1814
+
1815
+ def process_finding_with_progress(finding_to_process: IntegrationFinding) -> None:
1816
+ """
1817
+ Process a single finding and update progress.
1818
+
1819
+ :param IntegrationFinding finding_to_process: The finding to process
1820
+ :rtype: None
1821
+ """
1822
+ nonlocal processed_findings_count
1823
+ try:
1824
+ self.process_finding(finding_to_process, scan_history, current_vulnerabilities)
1825
+ with count_lock:
1826
+ processed_findings_count += 1
1827
+ if findings_to_process and self.finding_progress.tasks[loading_findings].total != float(
1828
+ findings_to_process
1829
+ ):
1830
+ self.finding_progress.update(
1831
+ loading_findings,
1832
+ total=findings_to_process,
1833
+ description=f"[#f8b737]Processing {findings_to_process} findings from {self.title}.",
1834
+ )
1835
+ self.finding_progress.advance(loading_findings, 1)
1836
+ except Exception as exc:
1837
+ self.log_error(
1838
+ "An error occurred when processing finding %s: %s",
1839
+ finding_to_process.external_id,
1840
+ exc,
1841
+ )
1842
+
1843
+ if get_thread_workers_max() == 1:
1844
+ for finding in findings:
1845
+ process_finding_with_progress(finding)
1846
+ else:
1847
+ with concurrent.futures.ThreadPoolExecutor(max_workers=get_thread_workers_max()) as executor:
1848
+ list(executor.map(process_finding_with_progress, findings))
1849
+
1850
+ # Close outdated issues
1851
+ scan_history.save()
1852
+ regscale_models.Issue.bulk_save(progress_context=self.finding_progress)
1853
+ self.close_outdated_issues(current_vulnerabilities)
1854
+
1855
+ return processed_findings_count
1856
+
1857
+ @staticmethod
1858
+ def parse_poam_id(poam_identifier: str) -> Optional[int]:
1859
+ """
1860
+ Parses a POAM identifier string to extract the numeric ID.
1861
+
1862
+ :param str poam_identifier: The POAM identifier string (e.g. "V-1234")
1863
+ :return: The numeric ID portion, or None if invalid format
1864
+ :rtype: Optional[int]
1865
+ """
1866
+ if not poam_identifier or not poam_identifier.startswith("V-"):
1867
+ return None
1868
+ try:
1869
+ return int("".join(c for c in poam_identifier.split("-")[1] if c.isdigit()))
1870
+ except (IndexError, ValueError):
1871
+ return None
1872
+
1873
+ def get_next_poam_id(self) -> int:
1874
+ """
1875
+ Retrieves the Next POAM ID for the current security plan in a thread-safe manner.
1876
+
1877
+ :return: The Next POAM ID
1878
+ :rtype: int
1879
+ """
1880
+ # Use the class's _get_lock method to get a thread-safe lock
1881
+ with self._get_lock("poam_id"):
1882
+ # If we haven't cached the max ID yet
1883
+ if not isinstance(self._max_poam_id, int):
1884
+ logger.info("Fetching max POAM ID...")
1885
+ # Get all existing POAM IDs and find the maximum
1886
+ issues: List[regscale_models.Issue] = regscale_models.Issue.get_all_by_parent(
1887
+ parent_id=self.plan_id,
1888
+ parent_module=regscale_models.SecurityPlan.get_module_string(),
1889
+ )
1890
+ self._max_poam_id = max(
1891
+ (
1892
+ parsed_id
1893
+ for issue in issues
1894
+ if issue.otherIdentifier
1895
+ and (parsed_id := self.parse_poam_id(issue.otherIdentifier)) is not None
1896
+ ),
1897
+ default=0,
1898
+ )
1899
+
1900
+ # Increment the cached max ID and store it
1901
+ self._max_poam_id = (self._max_poam_id or 0) + 1
1902
+ return self._max_poam_id
1903
+
1904
+ def create_scan_history(self) -> regscale_models.ScanHistory:
1905
+ """
1906
+ Creates a new ScanHistory object for the current scan.
1907
+
1908
+ :return: A newly created ScanHistory object
1909
+ :rtype: regscale_models.ScanHistory
1910
+ """
1911
+ scan_history = regscale_models.ScanHistory(
1912
+ parentId=self.plan_id,
1913
+ parentModule=regscale_models.SecurityPlan.get_module_string(),
1914
+ scanningTool=self.title,
1915
+ scanDate=get_current_datetime(),
1916
+ createdById=self.assessor_id,
1917
+ tenantsId=self.tenant_id,
1918
+ vLow=0,
1919
+ vMedium=0,
1920
+ vHigh=0,
1921
+ vCritical=0,
1922
+ ).create()
1923
+
1924
+ count = 0
1925
+ regscale_models.ScanHistory.delete_object_cache(scan_history)
1926
+ while not regscale_models.ScanHistory.get_object(object_id=scan_history.id) or count > 10:
1927
+ logger.info("Waiting for ScanHistory to be created...")
1928
+ time.sleep(1)
1929
+ count += 1
1930
+ regscale_models.ScanHistory.delete_object_cache(scan_history)
1931
+ return scan_history
1932
+
1933
+ def process_finding(
1934
+ self,
1935
+ finding: IntegrationFinding,
1936
+ scan_history: regscale_models.ScanHistory,
1937
+ current_vulnerabilities: Dict[int, Set[int]],
1938
+ ) -> None:
1939
+ """
1940
+ Process a single finding, handling both checklist and vulnerability cases.
1941
+
1942
+ :param IntegrationFinding finding: The finding to process
1943
+ :param regscale_models.ScanHistory scan_history: The current scan history
1944
+ :param Dict[int, Set[int]] current_vulnerabilities: Dictionary of current vulnerabilities
1945
+ :rtype: None
1946
+ """
1947
+ # Update finding dates if scan date is set
1948
+ finding = self.update_integration_finding_dates(
1949
+ finding=finding,
1950
+ existing_issues_dict={}, # We'll handle issue lookup in create_or_update_issue_from_finding
1951
+ scan_history=scan_history,
1952
+ )
1953
+
1954
+ # Process checklist if applicable
1955
+ if self.type == ScannerIntegrationType.CHECKLIST:
1956
+ if not (asset := self.get_asset_by_identifier(finding.asset_identifier)):
1957
+ logger.error("2. Asset not found for identifier %s", finding.asset_identifier)
1958
+ return
1959
+
1960
+ tool = regscale_models.ChecklistTool.STIGs
1961
+ if finding.vulnerability_type == "Vulnerability Scan":
1962
+ tool = regscale_models.ChecklistTool.VulnerabilityScanner
1963
+
1964
+ if not finding.cci_ref:
1965
+ finding.cci_ref = "CCI-000366"
1966
+
1967
+ # Convert checklist status to string
1968
+ checklist_status_str = str(finding.checklist_status.value)
1969
+
1970
+ logger.debug("Create or update checklist for %s", finding.external_id)
1971
+ regscale_models.Checklist(
1972
+ status=checklist_status_str,
1973
+ assetId=asset.id,
1974
+ tool=tool,
1975
+ baseline=finding.baseline,
1976
+ vulnerabilityId=finding.vulnerability_number,
1977
+ results=finding.results,
1978
+ check=finding.title,
1979
+ cci=finding.cci_ref,
1980
+ ruleId=finding.rule_id,
1981
+ version=finding.rule_version,
1982
+ comments=finding.comments,
1983
+ datePerformed=finding.date_created,
1984
+ ).create_or_update()
1985
+
1986
+ # For failing findings, handle control implementation updates
1987
+ if finding.status != regscale_models.IssueStatus.Closed:
1988
+ logger.debug("Handling failing checklist for %s", finding.external_id)
1989
+ if self.type == ScannerIntegrationType.CHECKLIST:
1990
+ self.handle_failing_checklist(finding=finding, plan_id=self.plan_id)
1991
+ else:
1992
+ logger.debug("Handling passing checklist for %s", finding.external_id)
1993
+ self.handle_passing_checklist(finding=finding, plan_id=self.plan_id)
1994
+
1995
+ # Process vulnerability if applicable
1996
+ if finding.status != regscale_models.IssueStatus.Closed:
1997
+ if asset := self.get_asset_by_identifier(finding.asset_identifier):
1998
+ if vulnerability_id := self.handle_vulnerability(finding, asset, scan_history):
1999
+ current_vulnerabilities[asset.id].add(vulnerability_id)
2000
+
2001
+ # Handle failing finding (creates/updates issues) for both checklist and vulnerability cases
2002
+ if finding.status != regscale_models.IssueStatus.Closed:
2003
+ self.handle_failing_finding(
2004
+ issue_title=finding.issue_title or finding.title,
2005
+ finding=finding,
2006
+ )
2007
+
2008
+ # Update scan history severity counts
2009
+ self.set_severity_count_for_scan(finding.severity, scan_history)
2010
+
2011
+ def create_vulnerability_from_finding(
2012
+ self, finding: IntegrationFinding, asset: regscale_models.Asset, scan_history: regscale_models.ScanHistory
2013
+ ) -> regscale_models.Vulnerability:
2014
+ """
2015
+ Creates a vulnerability from an integration finding.
2016
+
2017
+ :param IntegrationFinding finding: The integration finding
2018
+ :param regscale_models.Asset asset: The associated asset
2019
+ :param regscale_models.ScanHistory scan_history: The scan history
2020
+ :return: The created vulnerability
2021
+ :rtype: regscale_models.Vulnerability
2022
+ """
2023
+ vulnerability = regscale_models.Vulnerability(
2024
+ title=finding.title,
2025
+ cve=finding.cve,
2026
+ vprScore=(
2027
+ finding.vpr_score if hasattr(finding, "vprScore") else None
2028
+ ), # If this is the VPR score, otherwise use a different field
2029
+ cvsSv3BaseScore=finding.cvss_v3_base_score or finding.cvss_v3_score or finding.cvss_score,
2030
+ scanId=scan_history.id,
2031
+ severity=self.issue_to_vulnerability_map.get(finding.severity, regscale_models.VulnerabilitySeverity.Low),
2032
+ description=finding.description,
2033
+ dateLastUpdated=finding.date_last_updated,
2034
+ parentId=self.plan_id,
2035
+ parentModule=regscale_models.SecurityPlan.get_module_string(),
2036
+ dns=asset.fqdn or "unknown",
2037
+ status=regscale_models.VulnerabilityStatus.Open,
2038
+ ipAddress=finding.ip_address or asset.ipAddress or "",
2039
+ firstSeen=finding.first_seen,
2040
+ lastSeen=finding.last_seen,
2041
+ plugInName=finding.cve or finding.plugin_name, # Use CVE if available, otherwise use plugin name
2042
+ # plugInId=finding.plugin_id, # Vulnerability.pluginId is an int, but it is a string on Issue
2043
+ exploitAvailable=None, # Set this if you have information about exploit availability
2044
+ plugInText=finding.observations, # or finding.evidence, whichever is more appropriate
2045
+ port=finding.port if hasattr(finding, "port") else None,
2046
+ protocol=finding.protocol if hasattr(finding, "protocol") else None,
2047
+ operatingSystem=asset.operating_system if hasattr(asset, "operating_system") else None,
2048
+ )
2049
+
2050
+ vulnerability = vulnerability.create_or_update()
2051
+ if re.match(r"^\d+\.\d+(\.\d+){0,2}$", self.regscale_version) or self.regscale_version >= "5.64.0":
2052
+ regscale_models.VulnerabilityMapping(
2053
+ vulnerabilityId=vulnerability.id,
2054
+ assetId=asset.id,
2055
+ scanId=scan_history.id,
2056
+ securityPlansId=self.plan_id,
2057
+ createdById=self.assessor_id,
2058
+ tenantsId=self.tenant_id,
2059
+ isPublic=True,
2060
+ dateCreated=get_current_datetime(),
2061
+ firstSeen=finding.first_seen,
2062
+ lastSeen=finding.last_seen,
2063
+ status=finding.status,
2064
+ ).create_unique()
2065
+ return vulnerability
2066
+
2067
+ def handle_vulnerability(
2068
+ self,
2069
+ finding: IntegrationFinding,
2070
+ asset: Optional[regscale_models.Asset],
2071
+ scan_history: regscale_models.ScanHistory,
2072
+ ) -> Optional[int]:
2073
+ """
2074
+ Handles the vulnerabilities for a finding.
2075
+
2076
+ :param IntegrationFinding finding: The integration finding
2077
+ :param Optional[regscale_models.Asset] asset: The associated asset
2078
+ :param regscale_models.ScanHistory scan_history: The scan history
2079
+ :rtype: Optional[int]
2080
+ :return: The vulnerability ID
2081
+ """
2082
+ if not (finding.plugin_name or finding.cve):
2083
+ logger.warning("No Plugin Name or CVE found for finding %s", finding.title)
2084
+ return None
2085
+
2086
+ if not asset:
2087
+ logger.warning("VulnerabilityMapping Error: Asset not found for identifier %s", finding.asset_identifier)
2088
+ return None
2089
+
2090
+ vulnerability = self.create_vulnerability_from_finding(finding, asset, scan_history)
2091
+ finding.vulnerability_id = vulnerability.id
2092
+
2093
+ # Handle associated issue
2094
+ self.create_or_update_issue_from_finding(
2095
+ title=finding.title,
2096
+ finding=finding,
2097
+ )
2098
+
2099
+ return vulnerability.id
2100
+
2101
+ def _filter_vulns_open_by_other_tools(
2102
+ self, all_vulns: list[regscale_models.Vulnerability]
2103
+ ) -> list[regscale_models.Vulnerability]:
2104
+ """
2105
+ Fetch vulnerabilities that are open and not associated with other tools.
2106
+ :param list[regscale_models.Vulnerability] all_vulns: List of all vulnerabilities to check the scanningTool
2107
+ :return: List of matching vulnerabilities
2108
+ :rtype: list[regscale_models.Vulnerability]
2109
+ """
2110
+ vuln_list = []
2111
+ for vuln in all_vulns:
2112
+ other_tool = False
2113
+ open_vuln_mappings = regscale_models.VulnerabilityMapping.find_by_vulnerability(vuln.id, status="Open")
2114
+ for vuln_mapping in open_vuln_mappings:
2115
+ if vuln_mapping.scanId is not None:
2116
+ scan_history = regscale_models.ScanHistory.get_object(vuln_mapping.scanId)
2117
+ if scan_history and scan_history.scanningTool != self.title:
2118
+ other_tool = True
2119
+ break
2120
+ if not other_tool:
2121
+ vuln_list.append(vuln)
2122
+ return vuln_list
2123
+
2124
+ def close_outdated_vulnerabilities(self, current_vulnerabilities: Dict[int, Set[int]]) -> None:
2125
+ """
2126
+ Closes vulnerabilities that are not in the current set of vulnerability IDs for each asset.
2127
+
2128
+ :param Dict[int, Set[int]] current_vulnerabilities: Dictionary of asset IDs to lists of current vulnerability IDs
2129
+ :rtype: None
2130
+ """
2131
+ # Get all current vulnerability IDs
2132
+ current_vuln_ids = {vuln_id for vuln_ids in current_vulnerabilities.values() for vuln_id in vuln_ids}
2133
+
2134
+ # Get all vulnerabilities for this security plan
2135
+ all_vulnerabilities: list[regscale_models.Vulnerability] = regscale_models.Vulnerability.get_all_by_parent(
2136
+ parent_id=self.plan_id, parent_module=regscale_models.SecurityPlan.get_module_string()
2137
+ )
2138
+
2139
+ # Pre-filter vulnerabilities that are not in current set
2140
+ outdated_vulns = [v for v in all_vulnerabilities if v.id not in current_vuln_ids]
2141
+
2142
+ # Filter by tool
2143
+ tool_vulns = self._filter_vulns_open_by_other_tools(all_vulns=outdated_vulns)
2144
+
2145
+ closed_count = 0
2146
+ for vuln in tool_vulns:
2147
+ if vuln.status != regscale_models.VulnerabilityStatus.Closed:
2148
+ self.close_mappings_list(vuln) # Close matching mappings
2149
+ vuln.status = regscale_models.VulnerabilityStatus.Closed
2150
+ vuln.dateClosed = get_current_datetime()
2151
+ vuln.save()
2152
+ closed_count += 1
2153
+ logger.info("Closed vulnerability %d", vuln.id)
2154
+
2155
+ logger.info("Closed %d outdated vulnerabilities.", closed_count)
2156
+
2157
+ @classmethod
2158
+ def close_mappings_list(cls, vuln: regscale_models.Vulnerability) -> None:
2159
+ """
2160
+ Close all mappings for a vulnerability.
2161
+
2162
+ :param regscale_models.Vulnerability vuln: The vulnerability to close mappings for
2163
+ :rtype: None
2164
+ """
2165
+ mappings: List[regscale_models.VulnerabilityMapping] = [
2166
+ mapping
2167
+ for mapping in regscale_models.VulnerabilityMapping.find_by_vulnerability(
2168
+ vuln.id, status=regscale_models.IssueStatus.Open
2169
+ )
2170
+ if mapping is not None
2171
+ ]
2172
+ for mapping in mappings:
2173
+ # Don't close for other tools
2174
+ if mapping.scanId:
2175
+ scan = regscale_models.ScanHistory.get_object(mapping.scanId)
2176
+ if scan and scan.scanningTool != cls.title:
2177
+ continue
2178
+
2179
+ # This one uses IssueStatus
2180
+ mapping.status = regscale_models.IssueStatus.Closed
2181
+ mapping.dateClosed = get_current_datetime()
2182
+ mapping.save()
2183
+
2184
+ def close_outdated_issues(self, current_vulnerabilities: Dict[int, Set[int]]) -> int:
2185
+ """
2186
+ Closes issues that are not associated with current vulnerabilities for each asset.
2187
+ After closing issues, updates the status of affected control implementations.
2188
+
2189
+ :param Dict[int, Set[int]] current_vulnerabilities: Dictionary mapping asset IDs to sets of current vulnerability IDs
2190
+ :return: Number of issues closed
2191
+ :rtype: int
2192
+ """
2193
+ if not self.close_outdated_findings:
2194
+ # This should normally be set to True, but on POAM import, we do not want to automatically close issues,
2195
+ # unless the sheet specifies to do so
2196
+ logger.info("Skipping closing outdated issues.")
2197
+ return 0
2198
+
2199
+ closed_count = 0
2200
+ affected_control_ids = set()
2201
+
2202
+ # Get all open issues for this security plan
2203
+ open_issues = regscale_models.Issue.fetch_issues_by_ssp(
2204
+ None, ssp_id=self.plan_id, status=regscale_models.IssueStatus.Open.value
2205
+ )
2206
+
2207
+ # Create a progress bar
2208
+ task_id = self.finding_progress.add_task("[cyan]Closing outdated issues...", total=len(open_issues))
2209
+
2210
+ for issue in open_issues:
2211
+ if self.should_close_issue(issue, current_vulnerabilities):
2212
+ issue.status = regscale_models.IssueStatus.Closed
2213
+ issue.dateCompleted = get_current_datetime()
2214
+ changes_text = f"{get_current_datetime('%b %d, %Y')} - Closed by {self.title} for having no current vulnerabilities."
2215
+ if issue.changes:
2216
+ issue.changes += f"\n{changes_text}"
2217
+ else:
2218
+ issue.changes = changes_text
2219
+ issue.save()
2220
+ closed_count += 1
2221
+
2222
+ # Track affected control implementations
2223
+ if issue.controlImplementationIds:
2224
+ affected_control_ids.update(issue.controlImplementationIds)
2225
+
2226
+ # Update the progress bar
2227
+ self.finding_progress.update(task_id, advance=1)
2228
+
2229
+ # Update status of affected control implementations
2230
+ for control_id in affected_control_ids:
2231
+ self.update_control_implementation_status_after_close(control_id)
2232
+
2233
+ logger.info("Closed %d outdated issues.", closed_count)
2234
+ return closed_count
2235
+
2236
+ def update_control_implementation_status_after_close(self, control_id: int) -> None:
2237
+ """
2238
+ Updates the status of a control implementation after closing issues.
2239
+ Sets to FULLY_IMPLEMENTED if no open issues remain, NOT_IMPLEMENTED if any issues are open.
2240
+
2241
+ :param int control_id: The ID of the control implementation to update
2242
+ :rtype: None
2243
+ """
2244
+ # Get the control implementation
2245
+ control_implementation = self.control_implementation_map.get(
2246
+ control_id
2247
+ ) or regscale_models.ControlImplementation.get_object(object_id=control_id)
2248
+
2249
+ if not control_implementation:
2250
+ logger.warning("Control implementation %d not found", control_id)
2251
+ return
2252
+
2253
+ # Check if there are any open issues for this control implementation
2254
+ open_issues = self.existing_issue_ids_by_implementation_map.get(control_id, [])
2255
+
2256
+ # Set status based on presence of open issues
2257
+ new_status = (
2258
+ regscale_models.ImplementationStatus.FULLY_IMPLEMENTED
2259
+ if not open_issues
2260
+ else regscale_models.ImplementationStatus.NOT_IMPLEMENTED
2261
+ )
2262
+
2263
+ if control_implementation.status != new_status:
2264
+ control_implementation.status = new_status
2265
+ self.control_implementation_map[control_id] = control_implementation.save()
2266
+ logger.info("Updated control implementation %d status to %s", control_id, new_status)
2267
+
2268
+ def should_close_issue(self, issue: regscale_models.Issue, current_vulnerabilities: Dict[int, Set[int]]) -> bool:
2269
+ """
2270
+ Determines if an issue should be closed based on current vulnerabilities.
2271
+ An issue should be closed if it has no more active vulnerability mappings for any assets.
2272
+
2273
+ :param regscale_models.Issue issue: The issue to check
2274
+ :param Dict[int, Set[int]] current_vulnerabilities: Dictionary of current vulnerabilities
2275
+ :return: True if the issue should be closed, False otherwise
2276
+ :rtype: bool
2277
+ """
2278
+ # Do not close issues from other tools
2279
+ if issue.sourceReport != self.title:
2280
+ return False
2281
+
2282
+ # If the issue has a vulnerability ID, check if it's still current for any asset
2283
+ if issue.vulnerabilityId:
2284
+ # Get vulnerability mappings for this issue
2285
+ vuln_mappings = regscale_models.VulnerabilityMapping.find_by_issue(
2286
+ issue.id, status=regscale_models.IssueStatus.Open
2287
+ )
2288
+
2289
+ # Check if the issue's vulnerability is still current for any asset
2290
+ # If it is, we shouldn't close the issue
2291
+ if any(
2292
+ mapping.assetId in current_vulnerabilities
2293
+ and issue.vulnerabilityId in current_vulnerabilities[mapping.assetId]
2294
+ for mapping in vuln_mappings
2295
+ ):
2296
+ return False
2297
+
2298
+ # If we've checked all conditions and found no current vulnerabilities, we should close it
2299
+ return True
2300
+
2301
+ @staticmethod
2302
+ def set_severity_count_for_scan(severity: str, scan_history: regscale_models.ScanHistory) -> None:
2303
+ """
2304
+ Increments the count of the severity
2305
+ :param str severity: Severity of the vulnerability
2306
+ :param regscale_models.ScanHistory scan_history: Scan history object
2307
+ :rtype: None
2308
+ """
2309
+ if severity == regscale_models.IssueSeverity.Low:
2310
+ scan_history.vLow += 1
2311
+ elif severity == regscale_models.IssueSeverity.Moderate:
2312
+ scan_history.vMedium += 1
2313
+ elif severity == regscale_models.IssueSeverity.High:
2314
+ scan_history.vHigh += 1
2315
+ elif severity == regscale_models.IssueSeverity.Critical:
2316
+ scan_history.vCritical += 1
2317
+
2318
+ @classmethod
2319
+ def cci_assessment(cls, plan_id: int) -> None:
2320
+ """
2321
+ Creates or updates CCI assessments in RegScale
2322
+
2323
+ :param int plan_id: The ID of the security plan
2324
+ :rtype: None
2325
+ """
2326
+ instance = cls(plan_id=plan_id)
2327
+ for control_id, ccis in instance.get_control_to_cci_map().items():
2328
+ if not (implementation_id := instance.control_id_to_implementation_map.get(control_id)):
2329
+ logger.error("Control Implementation for %d not found in RegScale", control_id)
2330
+ continue
2331
+ assessment = instance.get_or_create_assessment(implementation_id)
2332
+ assessment_result = regscale_models.AssessmentResultsStatus.PASS
2333
+ open_issues: List[OpenIssueDict] = instance.existing_issue_ids_by_implementation_map.get(
2334
+ implementation_id, []
2335
+ )
2336
+ ccis.add("CCI-000366")
2337
+ for cci in sorted(ccis):
2338
+ logger.debug("Creating assessment for CCI %s for implementation %d", cci, implementation_id)
2339
+ result = regscale_models.ControlTestResultStatus.PASS
2340
+ for issue in open_issues:
2341
+ if cci.lower() in issue.get("integrationFindingId", "").lower():
2342
+ result = regscale_models.ControlTestResultStatus.FAIL
2343
+ assessment_result = regscale_models.AssessmentResultsStatus.FAIL
2344
+ break
2345
+
2346
+ control_test_key = f"{implementation_id}-{cci}"
2347
+ control_test = instance.control_tests_map.get(
2348
+ control_test_key,
2349
+ regscale_models.ControlTest(
2350
+ parentControlId=implementation_id,
2351
+ testCriteria=cci,
2352
+ ).get_or_create(),
2353
+ )
2354
+ regscale_models.ControlTestResult(
2355
+ parentTestId=control_test.id if control_test else None,
2356
+ parentAssessmentId=assessment.id,
2357
+ result=result,
2358
+ dateAssessed=get_current_datetime(),
2359
+ assessedById=instance.assessor_id,
2360
+ ).create()
2361
+ assessment.assessmentResult = assessment_result
2362
+ assessment.save()
2363
+
2364
+ @classmethod
2365
+ def sync_findings(cls, plan_id: int, **kwargs) -> int:
2366
+ """
2367
+ Synchronizes findings from the integration to RegScale.
2368
+
2369
+ :param int plan_id: The ID of the RegScale SSP
2370
+ :return: The number of findings processed
2371
+ :rtype: int
2372
+ """
2373
+ logger.info("Syncing %s findings...", cls.title)
2374
+ instance = cls(plan_id=plan_id)
2375
+ instance.set_keys(**kwargs)
2376
+ # If a progress object was passed, use it instead of creating a new one
2377
+ instance.finding_progress = kwargs.pop("progress") if "progress" in kwargs else create_progress_object()
2378
+ instance.enable_finding_date_update = kwargs.get("enable_finding_date_update", False)
2379
+ if finding_count := kwargs.get("finding_count"):
2380
+ instance.num_findings_to_process = finding_count
2381
+ kwargs["plan_id"] = plan_id
2382
+
2383
+ with instance.finding_progress:
2384
+ findings = instance.fetch_findings(**kwargs)
2385
+ # Update the asset map with the latest assets
2386
+ logger.info("Getting asset map...")
2387
+ instance.asset_map_by_identifier.update(instance.get_asset_map())
2388
+ findings_processed = instance.update_regscale_findings(findings=findings)
2389
+
2390
+ if instance.errors:
2391
+ logger.error("Summary of errors encountered:")
2392
+ for error in instance.errors:
2393
+ logger.error(error)
2394
+ else:
2395
+ logger.info("All findings have been processed successfully.")
2396
+
2397
+ logger.info("Processed %d findings.", findings_processed)
2398
+ return findings_processed
2399
+
2400
+ @classmethod
2401
+ def sync_assets(cls, plan_id: int, **kwargs) -> int:
2402
+ """
2403
+ Synchronizes assets from the integration to RegScale.
2404
+
2405
+ :param int plan_id: The ID of the RegScale SSP
2406
+ :return: The number of assets processed
2407
+ :rtype: int
2408
+ """
2409
+ logger.info("Syncing %s assets...", cls.title)
2410
+ instance = cls(plan_id=plan_id, **kwargs)
2411
+ instance.set_keys(**kwargs)
2412
+ instance.asset_progress = kwargs.pop("progress") if "progress" in kwargs else create_progress_object()
2413
+ if asset_count := kwargs.get("asset_count"):
2414
+ instance.num_assets_to_process = asset_count
2415
+
2416
+ with instance.asset_progress:
2417
+ assets = instance.fetch_assets(**kwargs)
2418
+ assets_processed = instance.update_regscale_assets(assets=assets)
2419
+
2420
+ if instance.errors:
2421
+ logger.error("Summary of errors encountered:")
2422
+ for error in instance.errors:
2423
+ logger.error(error)
2424
+ else:
2425
+ logger.info("All assets have been processed successfully.")
2426
+
2427
+ APIHandler().log_api_summary()
2428
+ logger.info("%d assets processed.", assets_processed)
2429
+ return assets_processed
2430
+
2431
+ @classmethod
2432
+ def set_keys(cls, **kwargs):
2433
+ """
2434
+ Set the attributes for an integration
2435
+ :rtype: None
2436
+ """
2437
+ for key, value in kwargs.items():
2438
+ if hasattr(cls, key):
2439
+ setattr(cls, key, value)
2440
+ else:
2441
+ logger.debug("Unable to set the %s attribute", key)
2442
+
2443
+ def log_error(self, msg: str, *args) -> None:
2444
+ """
2445
+ Logs an error message
2446
+
2447
+ :param str msg: The error message
2448
+ :rtype: None
2449
+ """
2450
+ logger.error(msg, *args, exc_info=True)
2451
+ self.errors.append(msg % args)
2452
+
2453
+ def update_integration_finding_dates(
2454
+ self,
2455
+ finding: IntegrationFinding,
2456
+ existing_issues_dict: Dict[str, regscale_models.Issue],
2457
+ scan_history: regscale_models.ScanHistory,
2458
+ ) -> IntegrationFinding:
2459
+ """
2460
+ Update the dates of the integration finding based on the scan date and whether the finding is new or existing.
2461
+
2462
+ :param IntegrationFinding finding: The integration finding
2463
+ :param Dict[str, regscale_models.Issue] existing_issues_dict: Dictionary of existing issues
2464
+ :param regscale_models.ScanHistory scan_history: List of existing scan history objects
2465
+ :return: The updated integration finding or the original finding if the scan date is not set
2466
+ :rtype: IntegrationFinding
2467
+ """
2468
+ if self.scan_date and self.enable_finding_date_update:
2469
+ issue = self.get_issue(existing_issues_dict, finding)
2470
+ vulnerabilities = (
2471
+ self.get_vulnerabilities(issue=issue, status=regscale_models.IssueStatus.Open) if issue else []
2472
+ )
2473
+ existing_vuln = self.get_existing_vuln(vulnerabilities, finding)
2474
+ finding = self.update_finding_dates(finding, existing_vuln, issue)
2475
+ self.update_scan(scan_history=scan_history)
2476
+
2477
+ return finding
2478
+
2479
+ def get_issue(
2480
+ self, existing_issues_dict: Dict[str, regscale_models.Issue], finding: IntegrationFinding
2481
+ ) -> Optional[regscale_models.Issue]:
2482
+ """
2483
+ Get the existing issue for the integration finding
2484
+
2485
+ :param Dict[str, regscale_models.Issue] existing_issues_dict: Dictionary of existing issues
2486
+ :param IntegrationFinding finding: The integration finding
2487
+ :return: The existing issue
2488
+ :rtype: Optional[regscale_models.Issue]
2489
+ """
2490
+ return existing_issues_dict.get(f"{self.get_finding_identifier(finding)}_{finding.asset_identifier}")
2491
+
2492
+ @staticmethod
2493
+ def get_vulnerabilities(issue: regscale_models.Issue, status: str) -> List[regscale_models.VulnerabilityMapping]:
2494
+ """
2495
+ Get the vulnerabilities for the issue
2496
+
2497
+ :param regscale_models.Issue issue: The issue
2498
+ :param str status: The status of the vulnerability
2499
+ :return: The list of vulnerabilities
2500
+ :rtype: List[regscale_models.VulnerabilityMapping]
2501
+ """
2502
+ return regscale_models.VulnerabilityMapping.find_by_issue(issue.id, status=status) if issue else []
2503
+
2504
+ def get_existing_vuln(
2505
+ self, vulnerabilities: List[regscale_models.VulnerabilityMapping], finding: IntegrationFinding
2506
+ ) -> Optional[regscale_models.VulnerabilityMapping]:
2507
+ """
2508
+ Get the existing vulnerability for the integration finding
2509
+
2510
+ :param List[regscale_models.VulnerabilityMapping] vulnerabilities: The list of existing vulnerabilities
2511
+ :param IntegrationFinding finding: The integration finding
2512
+ :return: The existing vulnerability
2513
+ :rtype: Optional[regscale_models.VulnerabilityMapping]
2514
+ """
2515
+ existing_vuln = min(vulnerabilities, key=lambda vuln: vuln.firstSeen) if vulnerabilities else None
2516
+ scan_date = date_obj(self.scan_date)
2517
+ first_seen = date_obj(finding.first_seen)
2518
+ if existing_vuln and scan_date and first_seen and scan_date < first_seen:
2519
+ finding.first_seen = self.scan_date
2520
+ return existing_vuln
2521
+
2522
+ def update_finding_dates(
2523
+ self,
2524
+ finding: IntegrationFinding,
2525
+ existing_vuln: Optional[regscale_models.VulnerabilityMapping],
2526
+ issue: Optional[regscale_models.Issue],
2527
+ ) -> IntegrationFinding:
2528
+ """
2529
+ Update the dates of the integration finding based on the scan date and whether the finding is new or existing.
2530
+
2531
+ :param IntegrationFinding finding: The integration finding
2532
+ :param Optional[regscale_models.VulnerabilityMapping] existing_vuln: The existing vulnerability mapping
2533
+ :param Optional[regscale_models.Issue] issue: The existing issue
2534
+ :return: The updated integration finding
2535
+ :rtype: IntegrationFinding
2536
+ """
2537
+ if not finding.due_date:
2538
+ if not existing_vuln:
2539
+ finding.first_seen = self.scan_date
2540
+ finding.date_created = self.scan_date
2541
+ # From @mlongworth:
2542
+ # It also appears that the suspense date (due date for remediation) is set based upon the import date rather
2543
+ # than the scan date. This calculation needs to be based upon scan date e.g. scan date of 2/5/2024 severity
2544
+ # High, should set the due date for remediation in the POA&M to 4/5/2024.
2545
+ finding.due_date = issue_due_date(
2546
+ severity=finding.severity,
2547
+ created_date=finding.date_created or self.scan_date,
2548
+ title=self.title,
2549
+ config=self.app.config,
2550
+ )
2551
+ else:
2552
+ finding.first_seen = existing_vuln.firstSeen if existing_vuln else finding.first_seen
2553
+ if issue:
2554
+ finding.date_created = issue.dateFirstDetected or finding.date_created
2555
+ scan_date = date_obj(self.scan_date)
2556
+ first_seen = date_obj(finding.first_seen)
2557
+ if scan_date and first_seen and scan_date >= first_seen:
2558
+ finding.last_seen = self.scan_date
2559
+ return finding
2560
+
2561
+ def update_scan(self, scan_history: regscale_models.ScanHistory) -> None:
2562
+ """
2563
+ Update the scan history object for the current security plan
2564
+
2565
+ :param regscale_models.ScanHistory scan_history: The list of existing scan history objects
2566
+ :return: None
2567
+ :rtype: None
2568
+ """
2569
+ scan_history.scanDate = datetime_str(self.scan_date)
2570
+ scan_history.save()
2571
+
2572
+ @staticmethod
2573
+ def get_date_completed(finding: IntegrationFinding, issue_status: regscale_models.IssueStatus) -> Optional[str]:
2574
+ """
2575
+ Returns the date when the issue was completed based on the issue status.
2576
+
2577
+ :param IntegrationFinding finding: The finding data
2578
+ :param regscale_models.IssueStatus issue_status: The status of the issue
2579
+ :return: The date when the issue was completed if the issue status is Closed, else None
2580
+ :rtype: Optional[str]
2581
+ """
2582
+ return finding.date_last_updated if issue_status == regscale_models.IssueStatus.Closed else None
2583
+
2584
+ @staticmethod
2585
+ def hash_string(input_string: str) -> str:
2586
+ """
2587
+ Hashes a string using SHA-256
2588
+
2589
+ :param str input_string: The string to hash
2590
+ :return: The hashed string
2591
+ :rtype: str
2592
+ """
2593
+ return hashlib.sha256(input_string.encode()).hexdigest()
2594
+
2595
+ def update_control_implementation_status(
2596
+ self,
2597
+ issue: regscale_models.Issue,
2598
+ open_issue_ids: List[int],
2599
+ status: regscale_models.ImplementationStatus,
2600
+ ) -> None:
2601
+ """
2602
+ Updates the control implementation status based on the open issues.
2603
+
2604
+ :param regscale_models.Issue issue: The issue being closed
2605
+ :param List[int] open_issue_ids: List of open issue IDs
2606
+ :param regscale_models.ImplementationStatus status: The status to set
2607
+ :rtype: None
2608
+ """
2609
+ # Method is deprecated - using update_control_implementation_status_after_close instead
2610
+ pass
2611
+
2612
+ def update_regscale_checklists(self, findings: List[IntegrationFinding]) -> int:
2613
+ """
2614
+ Process checklists from IntegrationFindings, optionally using multiple threads.
2615
+
2616
+ :param List[IntegrationFinding] findings: The findings to process
2617
+ :return: The number of checklists processed
2618
+ :rtype: int
2619
+ """
2620
+ logger.info("Updating RegScale checklists...")
2621
+ loading_findings = self.finding_progress.add_task(
2622
+ f"[#f8b737]Creating and updating checklists from {self.title}."
2623
+ )
2624
+ checklists_processed = 0
2625
+
2626
+ def process_finding(finding_to_process: IntegrationFinding) -> None:
2627
+ """
2628
+ Process a single finding and update the progress bar.
2629
+
2630
+ :param IntegrationFinding finding_to_process: The finding to process
2631
+ :rtype: None
2632
+ """
2633
+ nonlocal checklists_processed
2634
+ try:
2635
+ self.process_checklist(finding_to_process)
2636
+ if self.num_findings_to_process and self.finding_progress.tasks[loading_findings].total != float(
2637
+ self.num_findings_to_process
2638
+ ):
2639
+ self.finding_progress.update(
2640
+ loading_findings,
2641
+ total=self.num_findings_to_process,
2642
+ description=f"[#f8b737]Creating and updating {self.num_findings_to_process} checklists from {self.title}.",
2643
+ )
2644
+ self.finding_progress.advance(loading_findings, 1)
2645
+ checklists_processed += 1
2646
+ except Exception as exc:
2647
+ self.log_error(
2648
+ "An error occurred when processing asset %s for finding %s: %s",
2649
+ finding.asset_identifier,
2650
+ finding_to_process.external_id,
2651
+ exc,
2652
+ )
2653
+
2654
+ if get_thread_workers_max() == 1:
2655
+ for finding in findings:
2656
+ process_finding(finding)
2657
+ else:
2658
+ with concurrent.futures.ThreadPoolExecutor(max_workers=get_thread_workers_max()) as executor:
2659
+ list(executor.map(process_finding, findings))
2660
+
2661
+ return checklists_processed
2662
+
2663
+ def create_control_test_result(
2664
+ self,
2665
+ finding: IntegrationFinding,
2666
+ control_test: regscale_models.ControlTest,
2667
+ assessment: regscale_models.Assessment,
2668
+ result: regscale_models.ControlTestResultStatus,
2669
+ ) -> None:
2670
+ """
2671
+ Create a control test result.
2672
+
2673
+ :param IntegrationFinding finding: The finding associated with the test result
2674
+ :param regscale_models.ControlTest control_test: The control test
2675
+ :param regscale_models.Assessment assessment: The assessment
2676
+ :param regscale_models.ControlTestResultStatus result: The result of the test
2677
+ :rtype: None
2678
+ """
2679
+ regscale_models.ControlTestResult(
2680
+ parentTestId=control_test.id,
2681
+ parentAssessmentId=assessment.id,
2682
+ uuid=finding.external_id,
2683
+ result=result,
2684
+ dateAssessed=finding.date_created,
2685
+ assessedById=self.assessor_id,
2686
+ gaps=finding.gaps,
2687
+ observations=finding.observations,
2688
+ evidence=finding.evidence,
2689
+ identifiedRisk=finding.identified_risk,
2690
+ impact=finding.impact,
2691
+ recommendationForMitigation=finding.recommendation_for_mitigation,
2692
+ ).create()