regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__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.
Files changed (314) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/api.py +5 -2
  4. regscale/core/app/application.py +36 -6
  5. regscale/core/app/internal/control_editor.py +73 -21
  6. regscale/core/app/internal/evidence.py +727 -204
  7. regscale/core/app/internal/login.py +4 -2
  8. regscale/core/app/internal/model_editor.py +219 -64
  9. regscale/core/app/utils/app_utils.py +86 -12
  10. regscale/core/app/utils/catalog_utils/common.py +1 -1
  11. regscale/core/login.py +21 -4
  12. regscale/core/utils/async_graphql_client.py +363 -0
  13. regscale/core/utils/date.py +77 -1
  14. regscale/dev/cli.py +26 -0
  15. regscale/dev/code_gen.py +109 -24
  16. regscale/dev/version.py +72 -0
  17. regscale/integrations/commercial/__init__.py +30 -2
  18. regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
  19. regscale/integrations/commercial/aws/cli.py +3107 -54
  20. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  21. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  22. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  23. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  24. regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
  25. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  26. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  27. regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
  28. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  29. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  30. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  31. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  32. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  33. regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
  34. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  35. regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
  36. regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
  37. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  38. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  39. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  40. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  41. regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
  42. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  43. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  44. regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
  45. regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
  46. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  47. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  48. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  49. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  50. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  51. regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
  52. regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
  53. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  54. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  55. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  56. regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
  57. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  58. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  59. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  60. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  61. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  62. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  63. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  64. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  65. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  66. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  67. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  68. regscale/integrations/commercial/aws/scanner.py +1072 -205
  69. regscale/integrations/commercial/aws/security_hub.py +319 -0
  70. regscale/integrations/commercial/aws/session_manager.py +282 -0
  71. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  72. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  73. regscale/integrations/commercial/jira.py +489 -153
  74. regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
  75. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  76. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  77. regscale/integrations/commercial/qualys/__init__.py +167 -68
  78. regscale/integrations/commercial/qualys/scanner.py +305 -39
  79. regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
  80. regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
  81. regscale/integrations/commercial/sicura/api.py +79 -42
  82. regscale/integrations/commercial/sicura/commands.py +8 -2
  83. regscale/integrations/commercial/sicura/scanner.py +83 -44
  84. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  85. regscale/integrations/commercial/synqly/assets.py +133 -16
  86. regscale/integrations/commercial/synqly/edr.py +2 -8
  87. regscale/integrations/commercial/synqly/query_builder.py +536 -0
  88. regscale/integrations/commercial/synqly/ticketing.py +27 -0
  89. regscale/integrations/commercial/synqly/vulnerabilities.py +165 -28
  90. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  91. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  92. regscale/integrations/commercial/tenablev2/commands.py +146 -5
  93. regscale/integrations/commercial/tenablev2/scanner.py +1 -3
  94. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  95. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  96. regscale/integrations/commercial/wizv2/click.py +191 -76
  97. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  98. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  99. regscale/integrations/commercial/wizv2/compliance_report.py +1592 -0
  100. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  101. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
  102. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
  103. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  104. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  105. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +66 -9
  106. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  107. regscale/integrations/commercial/wizv2/issue.py +776 -28
  108. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  109. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  110. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  111. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  112. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  113. regscale/integrations/commercial/wizv2/reports.py +243 -0
  114. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  115. regscale/integrations/commercial/wizv2/scanner.py +1031 -441
  116. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  117. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  118. regscale/integrations/commercial/wizv2/variables.py +89 -3
  119. regscale/integrations/compliance_integration.py +1036 -151
  120. regscale/integrations/control_matcher.py +432 -0
  121. regscale/integrations/due_date_handler.py +333 -0
  122. regscale/integrations/milestone_manager.py +291 -0
  123. regscale/integrations/public/__init__.py +14 -0
  124. regscale/integrations/public/cci_importer.py +834 -0
  125. regscale/integrations/public/csam/__init__.py +0 -0
  126. regscale/integrations/public/csam/csam.py +938 -0
  127. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  128. regscale/integrations/public/csam/csam_common.py +154 -0
  129. regscale/integrations/public/csam/csam_controls.py +432 -0
  130. regscale/integrations/public/csam/csam_poam.py +124 -0
  131. regscale/integrations/public/fedramp/click.py +77 -6
  132. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  133. regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
  134. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  135. regscale/integrations/public/fedramp/poam/scanner.py +75 -7
  136. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  137. regscale/integrations/scanner_integration.py +1961 -430
  138. regscale/models/integration_models/CCI_List.xml +1 -0
  139. regscale/models/integration_models/aqua.py +2 -2
  140. regscale/models/integration_models/cisa_kev_data.json +805 -11
  141. regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
  142. regscale/models/integration_models/nexpose.py +36 -10
  143. regscale/models/integration_models/qualys.py +3 -4
  144. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  145. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +87 -18
  146. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  147. regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
  148. regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
  149. regscale/models/locking.py +12 -8
  150. regscale/models/platform.py +4 -2
  151. regscale/models/regscale_models/__init__.py +7 -0
  152. regscale/models/regscale_models/assessment.py +2 -1
  153. regscale/models/regscale_models/catalog.py +1 -1
  154. regscale/models/regscale_models/compliance_settings.py +251 -1
  155. regscale/models/regscale_models/component.py +1 -0
  156. regscale/models/regscale_models/control_implementation.py +236 -41
  157. regscale/models/regscale_models/control_objective.py +74 -5
  158. regscale/models/regscale_models/file.py +2 -0
  159. regscale/models/regscale_models/form_field_value.py +5 -3
  160. regscale/models/regscale_models/inheritance.py +44 -0
  161. regscale/models/regscale_models/issue.py +301 -102
  162. regscale/models/regscale_models/milestone.py +33 -14
  163. regscale/models/regscale_models/organization.py +3 -0
  164. regscale/models/regscale_models/regscale_model.py +310 -73
  165. regscale/models/regscale_models/security_plan.py +4 -2
  166. regscale/models/regscale_models/vulnerability.py +3 -3
  167. regscale/regscale.py +25 -4
  168. regscale/templates/__init__.py +0 -0
  169. regscale/utils/threading/threadhandler.py +20 -15
  170. regscale/validation/record.py +23 -1
  171. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
  172. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
  173. tests/core/__init__.py +0 -0
  174. tests/core/utils/__init__.py +0 -0
  175. tests/core/utils/test_async_graphql_client.py +472 -0
  176. tests/fixtures/test_fixture.py +13 -8
  177. tests/regscale/core/test_login.py +171 -4
  178. tests/regscale/integrations/commercial/__init__.py +0 -0
  179. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  180. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  181. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  182. tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
  183. tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
  184. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  185. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  186. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  187. tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
  188. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  189. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  190. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  191. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  192. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  193. tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
  194. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  195. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  196. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  197. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  198. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  199. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  200. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  201. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  202. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  203. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  204. tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
  205. tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
  206. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  207. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  208. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  209. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  210. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  211. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  212. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  213. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  214. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  215. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  216. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  217. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  218. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  219. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  220. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  221. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  222. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  223. tests/regscale/integrations/commercial/conftest.py +28 -0
  224. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  225. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  226. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  227. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  228. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  229. tests/regscale/integrations/commercial/test_aws.py +3742 -0
  230. tests/regscale/integrations/commercial/test_burp.py +48 -0
  231. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  232. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  233. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  234. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  235. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  236. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  237. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  238. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  239. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  240. tests/regscale/integrations/commercial/test_sicura.py +349 -0
  241. tests/regscale/integrations/commercial/test_snow.py +423 -0
  242. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  243. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  244. tests/regscale/integrations/commercial/test_stig.py +33 -0
  245. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  246. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  247. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  248. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  249. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  250. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  251. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  252. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  253. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  254. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  255. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  256. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  257. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  258. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  259. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  260. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  261. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  262. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  263. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  264. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  265. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  266. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  267. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  268. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  269. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  270. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  271. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  272. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  273. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  274. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  275. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  276. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  277. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  278. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  279. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  280. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1218 -0
  281. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  282. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  283. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  284. tests/regscale/integrations/public/__init__.py +0 -0
  285. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  286. tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
  287. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  288. tests/regscale/integrations/public/test_alienvault.py +220 -0
  289. tests/regscale/integrations/public/test_cci.py +1053 -0
  290. tests/regscale/integrations/public/test_cisa.py +1021 -0
  291. tests/regscale/integrations/public/test_emass.py +518 -0
  292. tests/regscale/integrations/public/test_fedramp.py +1152 -0
  293. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  294. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  295. tests/regscale/integrations/public/test_oscal.py +453 -0
  296. tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
  297. tests/regscale/integrations/test_control_matcher.py +1421 -0
  298. tests/regscale/integrations/test_control_matching.py +155 -0
  299. tests/regscale/integrations/test_milestone_manager.py +408 -0
  300. tests/regscale/models/test_control_implementation.py +118 -3
  301. tests/regscale/models/test_form_field_value_integration.py +304 -0
  302. tests/regscale/models/test_issue.py +378 -1
  303. tests/regscale/models/test_module_integration.py +582 -0
  304. tests/regscale/models/test_tenable_integrations.py +811 -105
  305. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
  306. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
  307. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
  308. regscale/integrations/public/fedramp/parts_mapper.py +0 -107
  309. /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
  310. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  311. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
  312. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
  313. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
  314. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,888 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ FedRAMP Rev 5 POAM Export
4
+
5
+ This module provides FedRAMP Rev 5 POAM Excel export functionality with advanced features:
6
+ - Dynamic POAM ID generation based on source file path properties
7
+ - KEV date determination from CISA KEV catalog
8
+ - Deviation status mapping (Approved/Pending/Rejected)
9
+ - Custom milestone and comment generation
10
+ - Excel formatting optimized for FedRAMP Rev 5 template
11
+ """
12
+
13
+ import functools
14
+ import logging
15
+ import re
16
+ import shutil
17
+ from datetime import datetime, timedelta
18
+ from html import unescape
19
+ from pathlib import Path
20
+ from typing import List, Optional
21
+
22
+ import openpyxl
23
+ from openpyxl.worksheet.worksheet import Worksheet
24
+
25
+ from regscale.core.app.api import Api
26
+ from regscale.core.app.application import Application
27
+ from regscale.core.utils.date import datetime_obj
28
+ from regscale.integrations.public.cisa import pull_cisa_kev
29
+ from regscale.models.regscale_models import (
30
+ Asset,
31
+ Deviation,
32
+ File,
33
+ Issue,
34
+ IssueSeverity,
35
+ IssueStatus,
36
+ Link,
37
+ Property,
38
+ ScanHistory,
39
+ SecurityPlan,
40
+ VulnerabilityMapping,
41
+ )
42
+
43
+ logger = logging.getLogger("regscale")
44
+
45
+ # FedRAMP POAM Export Constants
46
+ POAM_CLOSED_DATE_ROUNDING_DAY = 25 # FedRAMP requirement: round closed dates to 25th of month
47
+ EXCEL_TEMPLATE_HEADER_ROWS = 6 # Number of header rows in FedRAMP template before data starts
48
+
49
+
50
+ @functools.lru_cache(maxsize=1)
51
+ def get_cached_cisa_kev():
52
+ """
53
+ Pull the CISA KEV with caching
54
+
55
+ :return: CISA KEV data
56
+ :rtype: dict
57
+ """
58
+ return pull_cisa_kev()
59
+
60
+
61
+ def set_short_date(date_str: str) -> str:
62
+ """
63
+ Convert datetime string to short date format (MM/DD/YY)
64
+
65
+ :param str date_str: Date string to convert
66
+ :return: Formatted date string
67
+ :rtype: str
68
+ """
69
+ return datetime_obj(date_str).strftime("%m/%d/%y")
70
+
71
+
72
+ def strip_html(input_str: str) -> str:
73
+ """
74
+ Strip HTML tags from input string
75
+
76
+ :param str input_str: String with HTML tags
77
+ :return: String with HTML removed
78
+ :rtype: str
79
+ """
80
+ if not input_str:
81
+ return ""
82
+ no_html = re.sub("<[^>]*>", "", input_str) # Use negated character class instead of reluctant quantifier
83
+ return unescape(no_html)
84
+
85
+
86
+ def convert_to_list(asset_identifier: str) -> List[str]:
87
+ """
88
+ Convert asset identifier string to list, supporting multiple formats
89
+
90
+ Data could be <p> tag delimited, tab delimited, or newline delimited
91
+
92
+ :param str asset_identifier: Asset identifier string
93
+ :return: List of asset identifiers
94
+ :rtype: List[str]
95
+ """
96
+ if not asset_identifier:
97
+ return []
98
+
99
+ # Check for <p> tags and split by them
100
+ if "<p>" in asset_identifier and "</p>" in asset_identifier:
101
+ return re.findall(r"<p>([^<]*)</p>", asset_identifier)
102
+ # Check for tab characters and split by them
103
+ if "\t" in asset_identifier:
104
+ return asset_identifier.split("\t")
105
+ # Otherwise, split by newlines
106
+ return asset_identifier.splitlines()
107
+
108
+
109
+ def determine_kev_date(cve: str) -> str:
110
+ """
111
+ Determine KEV due date from CISA KEV catalog
112
+
113
+ :param str cve: CVE identifier
114
+ :return: KEV due date or "N/A"
115
+ :rtype: str
116
+ """
117
+ if not cve:
118
+ return "N/A"
119
+
120
+ kev_data = get_cached_cisa_kev()
121
+ for item in kev_data.get("vulnerabilities", []):
122
+ if item.get("cveID", "").lower() == cve.lower():
123
+ logger.info("Matched CVE: %s. KEV due date: %s", item.get("cveID"), item.get("dueDate"))
124
+ due_date = item.get("dueDate")
125
+ return set_short_date(due_date)
126
+ return "N/A"
127
+
128
+
129
+ def determine_poam_id(poam: Issue, props: List[Property]) -> str:
130
+ """
131
+ Determine POAM ID based on source file path patterns
132
+
133
+ Maps source file path keywords to POAM prefixes:
134
+ - pdf -> DC
135
+ - signatures -> CPT
136
+ - campaign -> ALM
137
+ - learning manager -> CCD
138
+ - cce -> CCE
139
+
140
+ :param Issue poam: POAM issue object
141
+ :param List[Property] props: Properties for the POAM
142
+ :return: Generated POAM ID
143
+ :rtype: str
144
+ """
145
+ # Define mapping from file path keywords to POAM prefixes
146
+ source_path_mappings = {
147
+ "pdf": "DC",
148
+ "signatures": "CPT",
149
+ "campaign": "ALM",
150
+ "learning manager": "CCD",
151
+ "cce": "CCE",
152
+ }
153
+
154
+ # Look for source_file_path property
155
+ source_file_path = None
156
+ for prop in props:
157
+ if prop.key == "source_file_path":
158
+ source_file_path = prop.value.lower()
159
+ break
160
+
161
+ if source_file_path:
162
+ # Check each mapping pattern
163
+ for keyword, prefix in source_path_mappings.items():
164
+ if keyword in source_file_path:
165
+ return f"{prefix}-{poam.id}"
166
+
167
+ return f"UNK-{poam.id}"
168
+
169
+
170
+ def determine_poam_service_name(_poam: Issue, props: List[Property]) -> str:
171
+ """
172
+ Determine service name from source file path
173
+
174
+ :param Issue _poam: POAM issue object (unused, kept for API consistency)
175
+ :param List[Property] props: Properties for the POAM
176
+ :return: Service name
177
+ :rtype: str
178
+ """
179
+ for prop in props:
180
+ if prop.key == "source_file_path":
181
+ value_lower = prop.value.lower()
182
+ if "pdf" in value_lower:
183
+ return "PDF Services"
184
+ if "signatures" in value_lower:
185
+ return "Signatures"
186
+ return "UNKNOWN"
187
+
188
+
189
+ def lookup_scan_date(poam: Issue, assets: List[Asset]) -> str:
190
+ """
191
+ Lookup the scan date from vulnerability mappings
192
+
193
+ :param Issue poam: POAM issue object
194
+ :param List[Asset] assets: List of assets
195
+ :return: Scan date string
196
+ :rtype: str
197
+ """
198
+ poam_assets = convert_to_list(poam.assetIdentifier)
199
+ for asset_name in poam_assets:
200
+ matching_asset = [asset for asset in assets if asset.name == asset_name]
201
+ if matching_asset:
202
+ vulns = VulnerabilityMapping.find_by_asset(matching_asset[0].id)
203
+ scans = [vuln.scanId for vuln in vulns]
204
+ if scans:
205
+ scan_date = ScanHistory.get_object(scans[0]).scanDate
206
+ return set_short_date(scan_date)
207
+ return set_short_date(poam.dateLastUpdated)
208
+
209
+
210
+ def determine_poam_comment(poam: Issue, assets: List[Asset]) -> str: # pylint: disable=unused-argument
211
+ """
212
+ Determine and update POAM comment with appropriate status-based messages
213
+
214
+ :param Issue poam: POAM issue object
215
+ :param List[Asset] assets: List of assets
216
+ :return: Updated POAM comment
217
+ :rtype: str
218
+ """
219
+ # Comment templates
220
+ closed_comment_template = (
221
+ "Per review of the latest scan report on %s, (TGRC) can confirm that this issue "
222
+ "no longer persists. This POAM will be submitted for closure."
223
+ )
224
+ open_comment_template = "POAM entry added"
225
+
226
+ if not poam.dateFirstDetected:
227
+ return "N/A"
228
+
229
+ original_comment = poam.poamComments
230
+ current_comment = poam.poamComments or ""
231
+ detection_date = set_short_date(poam.dateFirstDetected)
232
+
233
+ # Determine new comment based on POAM status
234
+ if poam.dateCompleted:
235
+ # Closed POAM: Add closure comment if not already present
236
+ updated_comment = _generate_closed_poam_comment(
237
+ poam, current_comment, closed_comment_template, open_comment_template
238
+ )
239
+ else:
240
+ # Open POAM: Add detection/creation comment
241
+ updated_comment = _generate_open_poam_comment(current_comment, detection_date, open_comment_template)
242
+
243
+ # Save POAM if comment changed
244
+ if updated_comment != original_comment:
245
+ logger.info("Updating POAM comment for POAM #%s", poam.id)
246
+ poam.poamComments = updated_comment
247
+ poam.save()
248
+
249
+ return updated_comment or "N/A"
250
+
251
+
252
+ def _generate_closed_poam_comment(poam: Issue, current_comment: str, template: str, _open_template: str) -> str:
253
+ """
254
+ Generate comment for closed POAMs
255
+
256
+ :param Issue poam: POAM issue object
257
+ :param str current_comment: Current comment text
258
+ :param str template: Template for closed comment
259
+ :param str _open_template: Template for open comment (unused, kept for API consistency)
260
+ :return: Generated comment
261
+ :rtype: str
262
+ """
263
+ closed_blurb = "This POAM will be submitted for closure"
264
+ open_blurb = "POAM entry added"
265
+
266
+ if open_blurb not in current_comment:
267
+ current_comment = f"{set_short_date(poam.dateCreated)}: {open_blurb}"
268
+ if closed_blurb in current_comment:
269
+ return current_comment # Already has closed comment
270
+
271
+ return template % set_short_date(poam.dateCompleted) + "\n" + current_comment
272
+
273
+
274
+ def _generate_open_poam_comment(current_comment: str, detection_date: str, template: str) -> str:
275
+ """
276
+ Generate comment for open POAMs
277
+
278
+ :param str current_comment: Current comment text
279
+ :param str detection_date: Detection date string
280
+ :param str template: Template for open comment
281
+ :return: Generated comment
282
+ :rtype: str
283
+ """
284
+ # If comment already has "entry added", return unchanged
285
+ if current_comment and "entry added" in current_comment:
286
+ return current_comment
287
+
288
+ new_entry = f"{detection_date}: {template}"
289
+
290
+ # If there's existing comment (without "entry added"), prepend new entry
291
+ if current_comment:
292
+ return f"{new_entry}\n{current_comment}"
293
+
294
+ # No existing comment, return just the new entry
295
+ return new_entry
296
+
297
+
298
+ def set_milestones(poam: Issue, index: int, sheet: Worksheet, column_l_date: str, all_milestones: List[dict]) -> None:
299
+ """
300
+ Set milestones in the worksheet
301
+
302
+ :param Issue poam: POAM issue object
303
+ :param int index: Row index
304
+ :param Worksheet sheet: Worksheet object
305
+ :param str column_l_date: Scheduled completion date
306
+ :param List[dict] all_milestones: All milestones
307
+ """
308
+ milestones = [milestone for milestone in all_milestones if milestone.get("parent_id", 0) == poam.id]
309
+ milestone_text = f"{column_l_date}: System will be updated as part of the monthly patching cycle.\n".join(
310
+ [set_short_date(milestone.get("MilestoneDate", "")) for milestone in milestones]
311
+ )
312
+ if milestone_text:
313
+ sheet[f"M{index}"].value = milestone_text
314
+ else:
315
+ sheet[f"M{index}"].value = f"{column_l_date}: System will be updated as part of the monthly patching cycle."
316
+
317
+
318
+ def set_status(poam: Issue, index: int, sheet: Worksheet) -> None:
319
+ """
320
+ Set status completion date with rounding logic for closed POAMs
321
+
322
+ Closed dates are rounded to the 25th of the month:
323
+ - If closed on or before the 25th, use the 25th of that month
324
+ - If closed after the 25th, use the 25th of the next month
325
+
326
+ :param Issue poam: POAM issue object
327
+ :param int index: Row index
328
+ :param Worksheet sheet: Worksheet object
329
+ """
330
+ if poam.status == "Closed" and poam.dateCompleted:
331
+ day_of_month = datetime_obj(poam.dateCompleted).day
332
+ if day_of_month <= POAM_CLOSED_DATE_ROUNDING_DAY:
333
+ new_date_completed = datetime_obj(poam.dateCompleted).replace(day=POAM_CLOSED_DATE_ROUNDING_DAY)
334
+ else:
335
+ # Move to 25th of next month
336
+ next_month = datetime_obj(poam.dateCompleted) + timedelta(days=31)
337
+ new_date_completed = next_month.replace(day=POAM_CLOSED_DATE_ROUNDING_DAY)
338
+ sheet[f"O{index}"].value = set_short_date(new_date_completed)
339
+ elif poam.status == "Closed":
340
+ sheet[f"O{index}"].value = ""
341
+ if poam.status == "Open" and poam.dateLastUpdated:
342
+ sheet[f"O{index}"].value = set_short_date(poam.dateLastUpdated)
343
+ elif poam.status == "Open":
344
+ sheet[f"O{index}"].value = ""
345
+
346
+
347
+ def set_vendor_info(poam: Issue, index: int, sheet: Worksheet) -> None:
348
+ """
349
+ Set vendor dependency information
350
+
351
+ :param Issue poam: POAM issue object
352
+ :param int index: Row index
353
+ :param Worksheet sheet: Worksheet object
354
+ """
355
+ sheet[f"P{index}"].value = poam.vendorDependency or "No"
356
+ sheet[f"R{index}"].value = poam.vendorName if poam.vendorName else "N/A"
357
+ if sheet[f"P{index}"].value == "No":
358
+ sheet[f"Q{index}"].value = "N/A"
359
+ elif poam.vendorLastUpdate:
360
+ sheet[f"Q{index}"].value = set_short_date(poam.vendorLastUpdate)
361
+ else:
362
+ sheet[f"Q{index}"].value = ""
363
+
364
+
365
+ def set_risk_info(poam: Issue, index: int, sheet: Worksheet) -> None:
366
+ """
367
+ Set risk adjustment and deviation information
368
+
369
+ Maps deviation status to Yes/Pending/No based on approval state
370
+
371
+ :param Issue poam: POAM issue object
372
+ :param int index: Row index
373
+ :param Worksheet sheet: Worksheet object
374
+ """
375
+ deviation_map = {"Approved": "Yes", "Pending": "Pending", "Rejected": "No"}
376
+ deviation_status = ""
377
+ deviation_obj = Deviation.get_by_issue(poam.id)
378
+ if deviation_obj:
379
+ deviation_status = deviation_obj.deviationStatus
380
+ deviation_rationale = strip_html(poam.deviationRationale)
381
+
382
+ original_risk_rating = (
383
+ poam.originalRiskRating if poam.originalRiskRating else IssueSeverity(poam.severityLevel).name
384
+ )
385
+ sheet[f"S{index}"].value = poam.originalRiskRating if poam.originalRiskRating else original_risk_rating
386
+
387
+ # Set defaults
388
+ sheet[f"T{index}"].value = poam.adjustedRiskRating or "N/A"
389
+ sheet[f"U{index}"].value = poam.riskAdjustment or "No"
390
+ sheet[f"V{index}"].value = poam.falsePositive or "No"
391
+ sheet[f"W{index}"].value = "No"
392
+
393
+ if poam.operationalRequirement or poam.riskAdjustment or poam.falsePositive:
394
+ sheet[f"X{index}"].value = deviation_rationale
395
+
396
+ if poam.falsePositive in ["Yes", "Pending"]:
397
+ sheet[f"V{index}"].value = deviation_map.get(deviation_status, "No")
398
+
399
+ if poam.riskAdjustment in ["Yes", "Pending"]:
400
+ sheet[f"U{index}"].value = deviation_map.get(deviation_status, "No")
401
+
402
+ if poam.operationalRequirement in ["Yes", "Pending"]:
403
+ sheet[f"W{index}"].value = deviation_map.get(deviation_status, "No")
404
+ if poam.operationalRequirement == "Yes" and deviation_map.get(deviation_status, "No") == "Pending":
405
+ sheet[f"U{index}"].value = "No"
406
+ sheet[f"V{index}"].value = "No"
407
+ sheet[f"W{index}"].value = "Pending"
408
+
409
+ if not deviation_rationale:
410
+ sheet[f"X{index}"].value = "N/A"
411
+
412
+
413
+ def set_end_columns(
414
+ _ssp: SecurityPlan,
415
+ poam: Issue,
416
+ index: int,
417
+ sheet: Worksheet,
418
+ props: List[Property],
419
+ assets: List[Asset],
420
+ all_links: List[dict],
421
+ all_files: List[dict],
422
+ ):
423
+ """
424
+ Set end columns including links, files, KEV data, and service names
425
+
426
+ :param SecurityPlan _ssp: Security plan object (unused, kept for API consistency)
427
+ :param Issue poam: POAM issue object
428
+ :param int index: Row index
429
+ :param Worksheet sheet: Worksheet object
430
+ :param List[Property] props: Properties list
431
+ :param List[Asset] assets: Assets list
432
+ :param List[dict] all_links: All links
433
+ :param List[dict] all_files: All files
434
+ """
435
+ grouped_links = [link for link in all_links if link.parentID == poam.id]
436
+ grouped_files = [file for file in all_files if file.parentId == poam.id]
437
+
438
+ aggregate_link_txt = "".join([f"\t{lin['Title']}: {lin['URL']};\n" for lin in grouped_links])
439
+ aggregate_file_txt = "".join([f"\t{fil['TrustedDisplayName']};\n" for fil in grouped_files])
440
+
441
+ sheet[f"Y{index}"].value = "N/A"
442
+ if grouped_links:
443
+ sheet[f"Y{index}"].value = "Links:\n" + aggregate_link_txt
444
+ if grouped_files:
445
+ sheet[f"Y{index}"].value = "\nFiles:\n" + aggregate_file_txt
446
+
447
+ sheet[f"Z{index}"].value = determine_poam_comment(poam, assets)
448
+ sheet[f"AA{index}"].value = poam.autoApproved
449
+ sheet[f"AB{index}"].value = poam.kevList if poam.kevList == "Yes" else "No"
450
+ sheet[f"AC{index}"].value = determine_kev_date(poam.cve)
451
+ sheet[f"AD{index}"].value = poam.cve or "N/A"
452
+ service_name = determine_poam_service_name(poam, props)
453
+ sheet[f"AE{index}"].value = service_name
454
+ sheet[f"I{index}"].value = service_name
455
+
456
+
457
+ def _normalize_source_report(poam: Issue) -> None:
458
+ """
459
+ Normalize source report name (e.g., SAP Concur -> Tenable SC)
460
+
461
+ :param Issue poam: POAM issue object
462
+ """
463
+ if poam.sourceReport == "SAP Concur":
464
+ poam.sourceReport = "Tenable SC"
465
+
466
+
467
+ def _populate_basic_poam_columns(sheet: Worksheet, index: int, poam: Issue, point_of_contact: str) -> None:
468
+ """
469
+ Populate basic POAM columns B-I (control, title, description, assets, POC, service)
470
+
471
+ :param Worksheet sheet: Worksheet object
472
+ :param int index: Row index
473
+ :param Issue poam: POAM issue object
474
+ :param str point_of_contact: Point of Contact name
475
+ """
476
+ sheet[f"B{index}"].value = "RA-5"
477
+ title = poam.title or poam.cve
478
+ sheet[f"C{index}"].value = title
479
+ sheet[f"D{index}"].value = strip_html(poam.description)
480
+ sheet[f"G{index}"].value = "\n".join(convert_to_list(poam.assetIdentifier))
481
+ sheet[f"H{index}"].value = point_of_contact if point_of_contact else ""
482
+
483
+
484
+ def _populate_date_and_milestone_columns(sheet: Worksheet, index: int, poam: Issue, all_milestones: List[dict]) -> None:
485
+ """
486
+ Populate date and milestone columns K-M (detection date, due date, milestones)
487
+
488
+ :param Worksheet sheet: Worksheet object
489
+ :param int index: Row index
490
+ :param Issue poam: POAM issue object
491
+ :param List[dict] all_milestones: All milestones
492
+ """
493
+ sheet[f"K{index}"].value = set_short_date(poam.dateFirstDetected)
494
+ column_l_date = (
495
+ (datetime_obj(poam.dueDate) + timedelta(days=-1)).strftime("%m/%d/%y") if datetime_obj(poam.dueDate) else ""
496
+ )
497
+ sheet[f"L{index}"].value = column_l_date
498
+ set_milestones(poam, index, sheet, column_l_date, all_milestones)
499
+
500
+
501
+ def map_weakness_detector_and_id_for_rev5_issues(
502
+ worksheet: Worksheet, column1: str, column2: str, row_number: int, issue: Issue
503
+ ):
504
+ """
505
+ Map weakness detector (column E) and source ID (column F)
506
+
507
+ :param Worksheet worksheet: Worksheet object
508
+ :param str column1: First column letter (E)
509
+ :param str column2: Second column letter (F)
510
+ :param int row_number: Row number
511
+ :param Issue issue: Issue object
512
+ """
513
+ worksheet[f"{column1}{row_number}"] = issue.sourceReport or ""
514
+ worksheet[f"{column2}{row_number}"] = issue.cve or issue.pluginId or issue.title
515
+
516
+
517
+ def process_row(
518
+ ssp: SecurityPlan,
519
+ poam: Issue,
520
+ index: int,
521
+ sheet: Worksheet,
522
+ assets: List[Asset],
523
+ all_milestones: List[dict],
524
+ all_links: List[dict],
525
+ all_files: List[dict],
526
+ point_of_contact: str = "",
527
+ ):
528
+ """
529
+ Process a single POAM row in the worksheet
530
+
531
+ :param SecurityPlan ssp: Security plan object
532
+ :param Issue poam: POAM issue object
533
+ :param int index: Row index
534
+ :param Worksheet sheet: Worksheet object
535
+ :param List[Asset] assets: Assets list
536
+ :param List[dict] all_milestones: All milestones
537
+ :param List[dict] all_links: All links
538
+ :param List[dict] all_files: All files
539
+ :param str point_of_contact: Point of Contact name for POAMs
540
+ """
541
+ index = EXCEL_TEMPLATE_HEADER_ROWS + index # Adjust for header rows
542
+
543
+ if not index or index < EXCEL_TEMPLATE_HEADER_ROWS:
544
+ return
545
+
546
+ try:
547
+ props = Property.get_all_by_parent(parent_id=poam.id, parent_module="issues")
548
+
549
+ # Normalize source report name
550
+ _normalize_source_report(poam)
551
+
552
+ # Populate basic columns (B-I)
553
+ _populate_basic_poam_columns(sheet, index, poam, point_of_contact)
554
+
555
+ # Map weakness detector and source ID (E-F)
556
+ map_weakness_detector_and_id_for_rev5_issues(
557
+ worksheet=sheet, column1="E", column2="F", row_number=index, issue=poam
558
+ )
559
+
560
+ # Populate remediation and date columns (J-M)
561
+ sheet[f"J{index}"].value = strip_html(poam.remediationDescription)
562
+ _populate_date_and_milestone_columns(sheet, index, poam, all_milestones)
563
+
564
+ # Populate changes and status columns (N-R)
565
+ sheet[f"N{index}"].value = strip_html(poam.changes)
566
+ set_status(poam, index, sheet)
567
+ set_vendor_info(poam, index, sheet)
568
+
569
+ # Populate risk and deviation columns (S-X)
570
+ set_risk_info(poam, index, sheet)
571
+
572
+ # Populate end columns (Y-AE)
573
+ set_end_columns(ssp, poam, index, sheet, props, assets, all_links, all_files)
574
+
575
+ # Set POAM ID (column A)
576
+ new_poam_id = determine_poam_id(poam, props)
577
+ logger.info("Generated POAM ID For POAM #%s: %s", poam.id, new_poam_id)
578
+ sheet[f"A{index}"].value = new_poam_id
579
+
580
+ except (KeyError, AttributeError, ValueError, TypeError) as e:
581
+ logger.error("Error processing POAM #%s: %s", poam.id, e)
582
+
583
+
584
+ def update_column_widths(ws: Worksheet) -> None:
585
+ """
586
+ Update column widths and formatting for the worksheet
587
+
588
+ :param Worksheet ws: Worksheet to format
589
+ """
590
+ # Define specific column widths
591
+ fixed_widths = {
592
+ "A": 15, # POAM ID
593
+ "B": 10, # Control
594
+ "C": 40, # Title
595
+ "D": 50, # Description
596
+ "E": 20, # Source Report
597
+ "F": 20, # Plugin ID/CVE
598
+ "G": 30, # Asset Identifier
599
+ "H": 15, # Point of Contact
600
+ "I": 50, # Service Name
601
+ "J": 15, # Remediation
602
+ "K": 15, # Detection Date
603
+ "L": 15, # Due Date
604
+ "M": 15, # Milestones
605
+ "N": 30, # Changes
606
+ "O": 15, # Completion Date
607
+ "P": 15, # Vendor Dependency
608
+ "Q": 15, # Vendor Last Update
609
+ "R": 20, # Vendor Name
610
+ "S": 15, # Original Risk
611
+ "T": 15, # Adjusted Risk
612
+ "U": 15, # Risk Adjustment
613
+ "V": 15, # False Positive
614
+ "W": 15, # Operational Requirement
615
+ "X": 30, # Deviation Rationale
616
+ "Y": 40, # Links and Files
617
+ "Z": 50, # POAM Comments
618
+ "AA": 15, # Auto Approved
619
+ "AB": 15, # KEV List
620
+ "AC": 15, # KEV Due Date
621
+ "AD": 20, # CVE
622
+ "AE": 30, # Service Name
623
+ }
624
+
625
+ # Apply fixed widths
626
+ for col, width in fixed_widths.items():
627
+ ws.column_dimensions[col].width = width
628
+
629
+ # Enable text wrapping for specific columns
630
+ wrap_columns = ["C", "D", "I", "X", "Y", "Z"]
631
+ for col in wrap_columns:
632
+ for cell in ws[col]:
633
+ if not isinstance(cell, openpyxl.cell.cell.MergedCell) and cell.value:
634
+ cell.alignment = openpyxl.styles.Alignment(wrap_text=True)
635
+
636
+
637
+ def align_column(column_letter: str, worksheet: Worksheet) -> None:
638
+ """
639
+ Align column text to the left and wrap text
640
+
641
+ :param str column_letter: Column letter to align
642
+ :param Worksheet worksheet: Worksheet object
643
+ """
644
+ for cell in worksheet[column_letter]:
645
+ cell.alignment = openpyxl.styles.Alignment(wrap_text=True, horizontal="left")
646
+ cell.value = cell.value.strip() if cell.value else ""
647
+
648
+
649
+ def update_header(ssp: SecurityPlan, sheet: Worksheet) -> Worksheet:
650
+ """
651
+ Update the header rows of the worksheet with SSP information
652
+
653
+ :param SecurityPlan ssp: Security plan object
654
+ :param Worksheet sheet: Worksheet object
655
+ :return: Updated worksheet
656
+ :rtype: Worksheet
657
+ """
658
+ sheet["A3"] = ssp.cspOrgName or "N/A"
659
+ sheet["B3"] = ssp.systemName
660
+ sheet["C3"] = ssp.overallCategorization
661
+ sheet["D3"] = datetime.now().strftime("%m/%d/%Y")
662
+ return sheet
663
+
664
+
665
+ def get_all_poams(ssp_id: str) -> List[Issue]:
666
+ """
667
+ Get all POAMs for the given SSP ID, including those from child assets
668
+
669
+ :param str ssp_id: SSP ID
670
+ :return: List of POAM issues
671
+ :rtype: List[Issue]
672
+ """
673
+ logger.info("Getting POAMs for SSP %s", ssp_id)
674
+ poams = [iss for iss in Issue.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans") if iss.isPoam]
675
+
676
+ assets = Asset.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans")
677
+ unique_poams = {
678
+ (
679
+ poam.otherIdentifier,
680
+ poam.assetIdentifier,
681
+ poam.cve,
682
+ poam.pluginId,
683
+ poam.title,
684
+ )
685
+ for poam in poams
686
+ }
687
+
688
+ for asset in assets:
689
+ asset_poams = [iss for iss in Issue.get_all_by_parent(parent_id=asset.id, parent_module="assets") if iss.isPoam]
690
+ for asset_poam in asset_poams:
691
+ if not asset_poam.otherIdentifier:
692
+ continue
693
+ poam_tuple = (
694
+ asset_poam.otherIdentifier,
695
+ asset_poam.assetIdentifier,
696
+ asset_poam.cve,
697
+ asset_poam.pluginId,
698
+ asset_poam.title,
699
+ )
700
+ if poam_tuple not in unique_poams:
701
+ poams.append(asset_poam)
702
+ unique_poams.add(poam_tuple)
703
+
704
+ logger.info("Found %s POAMs", len(poams))
705
+ return poams
706
+
707
+
708
+ def gen_links(all_poams: List[Issue]) -> List[dict]:
709
+ """
710
+ Generate list of links for all POAMs
711
+
712
+ :param List[Issue] all_poams: All POAM issues
713
+ :return: List of link dicts
714
+ :rtype: List[dict]
715
+ """
716
+ logger.info("Building list of links")
717
+ res = [Link.get_all_by_parent(parent_id=iss.id, parent_module="issues") for iss in all_poams]
718
+ return [link for sublist in res for link in sublist]
719
+
720
+
721
+ def gen_files(all_poams: List[Issue], api: Api) -> List[dict]:
722
+ """
723
+ Generate list of files for all POAMs
724
+
725
+ :param List[Issue] all_poams: All POAM issues
726
+ :param Api api: API client
727
+ :return: List of file dicts
728
+ :rtype: List[dict]
729
+ """
730
+ logger.info("Building list of files")
731
+ res = [
732
+ File.get_files_for_parent_from_regscale(parent_id=iss.id, parent_module="issues", api=api) for iss in all_poams
733
+ ]
734
+ return [file for sublist in res for file in sublist]
735
+
736
+
737
+ def gen_milestones(all_poams: List[Issue], api: Api, app: Application) -> List[dict]:
738
+ """
739
+ Generate list of milestones for all POAMs
740
+
741
+ :param List[Issue] all_poams: All POAM issues
742
+ :param Api api: API client
743
+ :param Application app: Application object
744
+ :return: List of milestone dicts
745
+ :rtype: List[dict]
746
+ """
747
+ logger.info("Building list of milestones")
748
+ milestones = []
749
+ url = app.config["domain"] + "/api/milestones/getAllByParent/"
750
+ for iss in all_poams:
751
+ dat = api.get(f"{url}{iss.id}/issues").json()
752
+ milestones.extend(dat)
753
+ return milestones
754
+
755
+
756
+ def process_worksheet(
757
+ ssp: SecurityPlan,
758
+ sheet_name: str,
759
+ workbook_path: Path,
760
+ all_poams: List[Issue],
761
+ all_milestones: List[dict],
762
+ all_links: List[dict],
763
+ all_files: List[dict],
764
+ point_of_contact: str = "",
765
+ ):
766
+ """
767
+ Process a single worksheet (Open or Closed POAMs)
768
+
769
+ :param SecurityPlan ssp: Security plan object
770
+ :param str sheet_name: Worksheet name ("Open POA&M Items" or "Closed POA&M Items")
771
+ :param Path workbook_path: Path to workbook file
772
+ :param List[Issue] all_poams: All POAM issues
773
+ :param str point_of_contact: Point of Contact name for POAMs
774
+ :param List[dict] all_milestones: All milestones
775
+ :param List[dict] all_links: All links
776
+ :param List[dict] all_files: All files
777
+ """
778
+ logger.info("Processing worksheet: %s", sheet_name)
779
+
780
+ wb = openpyxl.load_workbook(workbook_path)
781
+ sheet = wb[sheet_name]
782
+
783
+ status = IssueStatus.Closed if sheet_name == "Closed POA&M Items" else IssueStatus.Open
784
+
785
+ sheet = update_header(ssp=ssp, sheet=sheet)
786
+
787
+ assets = Asset.get_all_by_parent(parent_id=ssp.id, parent_module="securityplans")
788
+
789
+ # Process POAMs matching the status
790
+ matching_poams = [poam for poam in sorted(all_poams, key=lambda x: x.id) if poam.status == status]
791
+
792
+ for ix, poam in enumerate(matching_poams):
793
+ process_row(
794
+ ssp=ssp,
795
+ poam=poam,
796
+ index=ix,
797
+ sheet=sheet,
798
+ assets=assets,
799
+ all_milestones=all_milestones,
800
+ all_links=all_links,
801
+ all_files=all_files,
802
+ point_of_contact=point_of_contact,
803
+ )
804
+
805
+ logger.info("Processed %s %s POAMs out of %s Total POAMs", len(matching_poams), status, len(all_poams))
806
+
807
+ # Format worksheet
808
+ update_column_widths(sheet)
809
+ align_column("G", sheet)
810
+
811
+ # Format date column
812
+ for cell in sheet["L"]:
813
+ if cell.row >= 6:
814
+ cell.number_format = "mm/dd/yyyy"
815
+
816
+ wb.save(workbook_path)
817
+ logger.info("Saved worksheet: %s", sheet_name)
818
+
819
+
820
+ def export_poam_v5(ssp_id: str, output_file: str, template_path: Optional[Path] = None, point_of_contact: str = ""):
821
+ """
822
+ Export FedRAMP Rev 5 POAM Excel file
823
+
824
+ :param str ssp_id: SSP ID
825
+ :param str output_file: Output file path
826
+ :param Optional[Path] template_path: Path to FedRAMP POAM template
827
+ :param str point_of_contact: Point of Contact name for POAMs (defaults to empty string)
828
+ """
829
+ logger.info("Starting FedRAMP Rev 5 POAM export for SSP %s", ssp_id)
830
+
831
+ app = Application()
832
+ api = Api()
833
+
834
+ # Get SSP info
835
+ ssp = SecurityPlan.get_object(ssp_id)
836
+ if not ssp:
837
+ logger.error("SSP %s not found", ssp_id)
838
+ return
839
+
840
+ logger.info("Exporting POAMs for SSP: %s", ssp.systemName)
841
+
842
+ # Get all POAMs
843
+ all_poams = get_all_poams(ssp_id)
844
+ if not all_poams:
845
+ logger.warning("No POAMs found for SSP %s", ssp_id)
846
+ return
847
+
848
+ # Get related data
849
+ all_links = gen_links(all_poams)
850
+ all_files = gen_files(all_poams, api)
851
+ all_milestones = gen_milestones(all_poams, api, app)
852
+
853
+ # Copy template to output location
854
+ if not template_path:
855
+ import importlib.resources as pkg_resources
856
+ from regscale import templates
857
+
858
+ files = pkg_resources.files(templates)
859
+ template_path = files / "FedRAMP-POAM-Template.xlsx"
860
+ # Look for template in templates directory first, then current directory
861
+ template_path = Path(template_path)
862
+
863
+ if not template_path.exists():
864
+ logger.error("Template file not found: %s", template_path)
865
+ logger.error("Please provide a FedRAMP POAM template Excel file or place it in ./templates/ directory")
866
+ return
867
+
868
+ output_path = Path(output_file)
869
+ if output_path.suffix != ".xlsx":
870
+ output_path = output_path.with_suffix(".xlsx")
871
+
872
+ shutil.copy(template_path, output_path)
873
+ logger.info("Copied template to: %s", output_path)
874
+
875
+ # Process both worksheets
876
+ for sheet_name in ["Open POA&M Items", "Closed POA&M Items"]:
877
+ process_worksheet(
878
+ ssp=ssp,
879
+ sheet_name=sheet_name,
880
+ workbook_path=output_path,
881
+ all_poams=all_poams,
882
+ all_milestones=all_milestones,
883
+ all_links=all_links,
884
+ all_files=all_files,
885
+ point_of_contact=point_of_contact,
886
+ )
887
+
888
+ logger.info("POAMs exported successfully to: %s", output_path.absolute())