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