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,1293 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Unit tests for FedRAMP Rev 5 POAM Export functionality
5
+
6
+ This module tests the FedRAMP Rev 5 POAM Excel export functionality, including:
7
+ - Dynamic POAM ID generation based on source file path properties
8
+ - KEV date determination from CISA KEV catalog
9
+ - Deviation status mapping (Approved/Pending/Rejected)
10
+ - Custom milestone and comment generation
11
+ - Excel formatting and column operations
12
+ - Date rounding for closed POAMs (25th of month)
13
+ """
14
+
15
+ from datetime import datetime, timedelta
16
+ from pathlib import Path
17
+ from unittest.mock import MagicMock, Mock, patch, call
18
+
19
+ import pytest
20
+ from openpyxl import Workbook
21
+ from openpyxl.worksheet.worksheet import Worksheet
22
+
23
+ from regscale.integrations.public.fedramp.poam_export_v5 import (
24
+ convert_to_list,
25
+ determine_kev_date,
26
+ determine_poam_comment,
27
+ determine_poam_id,
28
+ determine_poam_service_name,
29
+ get_cached_cisa_kev,
30
+ lookup_scan_date,
31
+ set_end_columns,
32
+ set_milestones,
33
+ set_risk_info,
34
+ set_short_date,
35
+ set_status,
36
+ set_vendor_info,
37
+ strip_html,
38
+ update_column_widths,
39
+ align_column,
40
+ update_header,
41
+ get_all_poams,
42
+ gen_links,
43
+ gen_files,
44
+ gen_milestones,
45
+ process_row,
46
+ process_worksheet,
47
+ export_poam_v5,
48
+ map_weakness_detector_and_id_for_rev5_issues,
49
+ _generate_closed_poam_comment,
50
+ _generate_open_poam_comment,
51
+ )
52
+ from regscale.models.regscale_models import (
53
+ Asset,
54
+ Deviation,
55
+ Issue,
56
+ IssueSeverity,
57
+ IssueStatus,
58
+ Property,
59
+ ScanHistory,
60
+ SecurityPlan,
61
+ VulnerabilityMapping,
62
+ )
63
+ from tests import CLITestFixture
64
+
65
+
66
+ class TestPOAMExportV5(CLITestFixture):
67
+ """Test class for FedRAMP Rev 5 POAM export functionality"""
68
+
69
+ def test_set_short_date_valid(self):
70
+ """Test set_short_date with valid date string"""
71
+ date_str = "2025-03-15T10:30:00"
72
+ result = set_short_date(date_str)
73
+ assert result == "03/15/25"
74
+
75
+ def test_set_short_date_different_formats(self):
76
+ """Test set_short_date with different date formats"""
77
+ # ISO format
78
+ assert set_short_date("2025-01-01T00:00:00") == "01/01/25"
79
+ # Date only
80
+ assert set_short_date("2025-12-31") == "12/31/25"
81
+
82
+ def test_strip_html_with_tags(self):
83
+ """Test strip_html removes HTML tags"""
84
+ html_str = "<p>Test <strong>content</strong> here</p>"
85
+ result = strip_html(html_str)
86
+ assert result == "Test content here"
87
+
88
+ def test_strip_html_with_entities(self):
89
+ """Test strip_html handles HTML entities"""
90
+ html_str = "&lt;div&gt;Test &amp; content&lt;/div&gt;"
91
+ result = strip_html(html_str)
92
+ assert result == "<div>Test & content</div>"
93
+
94
+ def test_strip_html_empty_string(self):
95
+ """Test strip_html with empty string"""
96
+ result = strip_html("")
97
+ assert result == ""
98
+
99
+ def test_strip_html_none(self):
100
+ """Test strip_html with None"""
101
+ result = strip_html(None)
102
+ assert result == ""
103
+
104
+ def test_strip_html_nested_tags(self):
105
+ """Test strip_html with nested HTML tags"""
106
+ html_str = "<div><p><span>Nested</span> content</p></div>"
107
+ result = strip_html(html_str)
108
+ assert result == "Nested content"
109
+
110
+ def test_convert_to_list_paragraph_tags(self):
111
+ """Test convert_to_list with <p> tag delimiters"""
112
+ asset_str = "<p>Asset1</p><p>Asset2</p><p>Asset3</p>"
113
+ result = convert_to_list(asset_str)
114
+ assert result == ["Asset1", "Asset2", "Asset3"]
115
+
116
+ def test_convert_to_list_tabs(self):
117
+ """Test convert_to_list with tab delimiters"""
118
+ asset_str = "Asset1\tAsset2\tAsset3"
119
+ result = convert_to_list(asset_str)
120
+ assert result == ["Asset1", "Asset2", "Asset3"]
121
+
122
+ def test_convert_to_list_newlines(self):
123
+ """Test convert_to_list with newline delimiters"""
124
+ asset_str = "Asset1\nAsset2\nAsset3"
125
+ result = convert_to_list(asset_str)
126
+ assert result == ["Asset1", "Asset2", "Asset3"]
127
+
128
+ def test_convert_to_list_empty_string(self):
129
+ """Test convert_to_list with empty string"""
130
+ result = convert_to_list("")
131
+ assert result == []
132
+
133
+ def test_convert_to_list_none(self):
134
+ """Test convert_to_list with None"""
135
+ result = convert_to_list(None)
136
+ assert result == []
137
+
138
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
139
+ def test_determine_kev_date_match_found(self, mock_get_kev):
140
+ """Test determine_kev_date with matching CVE"""
141
+ mock_get_kev.return_value = {
142
+ "vulnerabilities": [
143
+ {"cveID": "CVE-2025-1234", "dueDate": "2025-06-15T00:00:00"},
144
+ {"cveID": "CVE-2025-5678", "dueDate": "2025-07-20T00:00:00"},
145
+ ]
146
+ }
147
+ result = determine_kev_date("CVE-2025-1234")
148
+ assert result == "06/15/25"
149
+
150
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
151
+ def test_determine_kev_date_case_insensitive(self, mock_get_kev):
152
+ """Test determine_kev_date is case insensitive"""
153
+ mock_get_kev.return_value = {"vulnerabilities": [{"cveID": "CVE-2025-1234", "dueDate": "2025-06-15T00:00:00"}]}
154
+ result = determine_kev_date("cve-2025-1234")
155
+ assert result == "06/15/25"
156
+
157
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
158
+ def test_determine_kev_date_no_match(self, mock_get_kev):
159
+ """Test determine_kev_date with no matching CVE"""
160
+ mock_get_kev.return_value = {"vulnerabilities": [{"cveID": "CVE-2025-9999", "dueDate": "2025-08-01"}]}
161
+ result = determine_kev_date("CVE-2025-1234")
162
+ assert result == "N/A"
163
+
164
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
165
+ def test_determine_kev_date_empty_cve(self, mock_get_kev):
166
+ """Test determine_kev_date with empty CVE"""
167
+ result = determine_kev_date("")
168
+ assert result == "N/A"
169
+ mock_get_kev.assert_not_called()
170
+
171
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
172
+ def test_determine_kev_date_none_cve(self, mock_get_kev):
173
+ """Test determine_kev_date with None CVE"""
174
+ result = determine_kev_date(None)
175
+ assert result == "N/A"
176
+ mock_get_kev.assert_not_called()
177
+
178
+ def test_determine_poam_id_pdf_path(self):
179
+ """Test determine_poam_id with pdf in file path"""
180
+ poam = Mock(id=12345)
181
+ props = [Mock(key="source_file_path", value="/path/to/pdf/file.xml")]
182
+ result = determine_poam_id(poam, props)
183
+ assert result == "DC-12345"
184
+
185
+ def test_determine_poam_id_signatures_path(self):
186
+ """Test determine_poam_id with signatures in file path"""
187
+ poam = Mock(id=12345)
188
+ props = [Mock(key="source_file_path", value="/path/to/signatures/file.xml")]
189
+ result = determine_poam_id(poam, props)
190
+ assert result == "CPT-12345"
191
+
192
+ def test_determine_poam_id_campaign_path(self):
193
+ """Test determine_poam_id with campaign in file path"""
194
+ poam = Mock(id=12345)
195
+ props = [Mock(key="source_file_path", value="/path/to/campaign/file.xml")]
196
+ result = determine_poam_id(poam, props)
197
+ assert result == "ALM-12345"
198
+
199
+ def test_determine_poam_id_learning_manager_path(self):
200
+ """Test determine_poam_id with learning manager in file path"""
201
+ poam = Mock(id=12345)
202
+ props = [Mock(key="source_file_path", value="/path/to/learning manager/file.xml")]
203
+ result = determine_poam_id(poam, props)
204
+ assert result == "CCD-12345"
205
+
206
+ def test_determine_poam_id_cce_path(self):
207
+ """Test determine_poam_id with cce in file path"""
208
+ poam = Mock(id=12345)
209
+ props = [Mock(key="source_file_path", value="/path/to/cce/file.xml")]
210
+ result = determine_poam_id(poam, props)
211
+ assert result == "CCE-12345"
212
+
213
+ def test_determine_poam_id_unknown_path(self):
214
+ """Test determine_poam_id with unknown file path"""
215
+ poam = Mock(id=12345)
216
+ props = [Mock(key="source_file_path", value="/path/to/unknown/file.xml")]
217
+ result = determine_poam_id(poam, props)
218
+ assert result == "UNK-12345"
219
+
220
+ def test_determine_poam_id_no_source_path_property(self):
221
+ """Test determine_poam_id with no source_file_path property"""
222
+ poam = Mock(id=12345)
223
+ props = [Mock(key="other_property", value="value")]
224
+ result = determine_poam_id(poam, props)
225
+ assert result == "UNK-12345"
226
+
227
+ def test_determine_poam_id_empty_properties(self):
228
+ """Test determine_poam_id with empty properties list"""
229
+ poam = Mock(id=12345)
230
+ props = []
231
+ result = determine_poam_id(poam, props)
232
+ assert result == "UNK-12345"
233
+
234
+ def test_determine_poam_id_case_insensitive(self):
235
+ """Test determine_poam_id is case insensitive"""
236
+ poam = Mock(id=12345)
237
+ props = [Mock(key="source_file_path", value="/path/to/PDF/FILE.XML")]
238
+ result = determine_poam_id(poam, props)
239
+ assert result == "DC-12345"
240
+
241
+ def test_determine_poam_service_name_pdf(self):
242
+ """Test determine_poam_service_name with pdf in path"""
243
+ poam = Mock(id=12345)
244
+ props = [Mock(key="source_file_path", value="/path/to/pdf/file.xml")]
245
+ result = determine_poam_service_name(poam, props)
246
+ assert result == "PDF Services"
247
+
248
+ def test_determine_poam_service_name_signatures(self):
249
+ """Test determine_poam_service_name with signatures in path"""
250
+ poam = Mock(id=12345)
251
+ props = [Mock(key="source_file_path", value="/path/to/signatures/file.xml")]
252
+ result = determine_poam_service_name(poam, props)
253
+ assert result == "Signatures"
254
+
255
+ def test_determine_poam_service_name_unknown(self):
256
+ """Test determine_poam_service_name with unknown path"""
257
+ poam = Mock(id=12345)
258
+ props = [Mock(key="source_file_path", value="/path/to/unknown/file.xml")]
259
+ result = determine_poam_service_name(poam, props)
260
+ assert result == "UNKNOWN"
261
+
262
+ def test_determine_poam_service_name_no_property(self):
263
+ """Test determine_poam_service_name with no source_file_path property"""
264
+ poam = Mock(id=12345)
265
+ props = [Mock(key="other_property", value="value")]
266
+ result = determine_poam_service_name(poam, props)
267
+ assert result == "UNKNOWN"
268
+
269
+ def test_lookup_scan_date_with_matching_asset(self):
270
+ """Test lookup_scan_date finds scan date from vulnerability mapping"""
271
+ # Test the function without excessive mocking, just verify it returns a date
272
+ poam = Mock(assetIdentifier="Asset1", dateLastUpdated="2025-03-15T10:00:00")
273
+ asset = Mock(id=100, name="Asset1")
274
+ assets = [asset]
275
+
276
+ # Since this function makes real API calls, test that it falls back to dateLastUpdated
277
+ result = lookup_scan_date(poam, assets)
278
+ # Should return a formatted date
279
+ assert result == "03/15/25"
280
+
281
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.VulnerabilityMapping")
282
+ def test_lookup_scan_date_no_matching_asset(self, mock_vuln_mapping):
283
+ """Test lookup_scan_date with no matching asset"""
284
+ poam = Mock(assetIdentifier="Asset1", dateLastUpdated="2025-03-15T10:00:00")
285
+ asset = Mock(id=100, name="Asset2")
286
+ assets = [asset]
287
+
288
+ result = lookup_scan_date(poam, assets)
289
+ assert result == "03/15/25"
290
+ mock_vuln_mapping.find_by_asset.assert_not_called()
291
+
292
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.VulnerabilityMapping")
293
+ def test_lookup_scan_date_no_vulnerabilities(self, mock_vuln_mapping):
294
+ """Test lookup_scan_date when no vulnerabilities found"""
295
+ poam = Mock(assetIdentifier="Asset1", dateLastUpdated="2025-03-15T10:00:00")
296
+ asset = Mock(id=100, name="Asset1")
297
+ assets = [asset]
298
+
299
+ mock_vuln_mapping.find_by_asset.return_value = []
300
+
301
+ result = lookup_scan_date(poam, assets)
302
+ assert result == "03/15/25"
303
+
304
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.set_short_date")
305
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.convert_to_list")
306
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.VulnerabilityMapping")
307
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.ScanHistory")
308
+ def test_lookup_scan_date_multiple_assets(
309
+ self, mock_scan_history, mock_vuln_mapping, mock_convert, mock_short_date
310
+ ):
311
+ """Test lookup_scan_date with multiple assets in identifier"""
312
+ poam = Mock(assetIdentifier="Asset1\nAsset2", dateLastUpdated="2025-03-15T10:00:00")
313
+ asset1 = Mock(id=100, name="Asset1")
314
+ asset2 = Mock(id=101, name="Asset2")
315
+ assets = [asset1, asset2]
316
+
317
+ mock_convert.return_value = ["Asset1", "Asset2"]
318
+ mock_vuln = Mock(scanId=500)
319
+ mock_vuln_mapping.find_by_asset.return_value = [mock_vuln]
320
+
321
+ mock_scan = Mock(scanDate="2025-03-10T08:00:00")
322
+ mock_scan_history.get_object.return_value = mock_scan
323
+ mock_short_date.return_value = "03/10/25"
324
+
325
+ result = lookup_scan_date(poam, assets)
326
+ assert result == "03/10/25"
327
+
328
+ def test_determine_poam_comment_closed_poam_with_date(self):
329
+ """Test determine_poam_comment for closed POAM"""
330
+ poam = Mock(
331
+ id=123,
332
+ dateFirstDetected="2025-03-01T10:00:00",
333
+ dateCompleted="2025-03-15T10:00:00",
334
+ dateCreated="2025-03-01T10:00:00",
335
+ poamComments="",
336
+ )
337
+ poam.save = Mock()
338
+ assets = []
339
+
340
+ result = determine_poam_comment(poam, assets)
341
+ assert "Per review of the latest scan report" in result
342
+ assert "This POAM will be submitted for closure" in result
343
+ assert "03/01/25: POAM entry added" in result
344
+ poam.save.assert_called_once()
345
+
346
+ def test_determine_poam_comment_open_poam_new_comment(self):
347
+ """Test determine_poam_comment for open POAM with no existing comment"""
348
+ poam = Mock(
349
+ id=123, dateFirstDetected="2025-03-01T10:00:00", dateCompleted=None, poamComments="", dateCreated=None
350
+ )
351
+ poam.save = Mock()
352
+ assets = []
353
+
354
+ result = determine_poam_comment(poam, assets)
355
+ assert "03/01/25: POAM entry added" in result
356
+ poam.save.assert_called_once()
357
+
358
+ def test_determine_poam_comment_open_poam_existing_comment(self):
359
+ """Test determine_poam_comment for open POAM with existing comment"""
360
+ existing_comment = "03/01/25: POAM entry added"
361
+ poam = Mock(
362
+ id=123,
363
+ dateFirstDetected="2025-03-01T10:00:00",
364
+ dateCompleted=None,
365
+ poamComments=existing_comment,
366
+ dateCreated=None,
367
+ )
368
+ poam.save = Mock()
369
+ assets = []
370
+
371
+ result = determine_poam_comment(poam, assets)
372
+ assert result == existing_comment
373
+ poam.save.assert_not_called()
374
+
375
+ def test_determine_poam_comment_no_detection_date(self):
376
+ """Test determine_poam_comment with no detection date"""
377
+ poam = Mock(id=123, dateFirstDetected=None, poamComments="")
378
+ poam.save = Mock()
379
+ assets = []
380
+
381
+ result = determine_poam_comment(poam, assets)
382
+ assert result == "N/A"
383
+ poam.save.assert_not_called()
384
+
385
+ def test_generate_closed_poam_comment_new(self):
386
+ """Test _generate_closed_poam_comment creates new closed comment"""
387
+ poam = Mock(dateCompleted="2025-03-15T10:00:00", dateCreated="2025-03-01T10:00:00")
388
+ current_comment = ""
389
+ template = "Per review of the latest scan report on %s, (TGRC) can confirm that this issue no longer persists. This POAM will be submitted for closure."
390
+ open_template = "POAM entry added"
391
+
392
+ result = _generate_closed_poam_comment(poam, current_comment, template, open_template)
393
+ assert "Per review of the latest scan report on 03/15/25" in result
394
+ assert "This POAM will be submitted for closure" in result
395
+ assert "03/01/25: POAM entry added" in result
396
+
397
+ def test_generate_closed_poam_comment_already_has_closed_blurb(self):
398
+ """Test _generate_closed_poam_comment when comment already has closed blurb"""
399
+ poam = Mock(dateCompleted="2025-03-15T10:00:00", dateCreated="2025-03-01T10:00:00")
400
+ # Include "POAM entry added" to pass the first check, and the closed blurb to pass the second
401
+ current_comment = "This POAM will be submitted for closure\n03/01/25: POAM entry added"
402
+ template = "Template %s"
403
+ open_template = "POAM entry added"
404
+
405
+ result = _generate_closed_poam_comment(poam, current_comment, template, open_template)
406
+ # Should return unchanged because it already has the closure blurb
407
+ assert result == current_comment
408
+ assert "This POAM will be submitted for closure" in result
409
+
410
+ def test_generate_open_poam_comment_new(self):
411
+ """Test _generate_open_poam_comment creates new comment"""
412
+ current_comment = ""
413
+ detection_date = "03/01/25"
414
+ template = "POAM entry added"
415
+
416
+ result = _generate_open_poam_comment(current_comment, detection_date, template)
417
+ assert result == "03/01/25: POAM entry added"
418
+
419
+ def test_generate_open_poam_comment_already_has_entry_added(self):
420
+ """Test _generate_open_poam_comment when comment already has entry added"""
421
+ current_comment = "03/01/25: POAM entry added"
422
+ detection_date = "03/01/25"
423
+ template = "POAM entry added"
424
+
425
+ result = _generate_open_poam_comment(current_comment, detection_date, template)
426
+ assert result == current_comment
427
+
428
+ def test_generate_open_poam_comment_prepends_to_existing(self):
429
+ """Test _generate_open_poam_comment prepends to existing comment"""
430
+ current_comment = "Some existing text"
431
+ detection_date = "03/01/25"
432
+ template = "POAM entry added"
433
+
434
+ result = _generate_open_poam_comment(current_comment, detection_date, template)
435
+ assert result == "03/01/25: POAM entry added\nSome existing text"
436
+
437
+ def test_set_milestones_with_milestones(self):
438
+ """Test set_milestones populates worksheet with milestone data"""
439
+ poam = Mock(id=123)
440
+ sheet = MagicMock()
441
+ column_l_date = "04/01/25"
442
+ all_milestones = [
443
+ {"parent_id": 123, "MilestoneDate": "2025-03-15T10:00:00"},
444
+ {"parent_id": 123, "MilestoneDate": "2025-03-20T10:00:00"},
445
+ {"parent_id": 456, "MilestoneDate": "2025-03-25T10:00:00"},
446
+ ]
447
+
448
+ set_milestones(poam, 10, sheet, column_l_date, all_milestones)
449
+
450
+ # Should contain milestone dates
451
+ assert sheet.__getitem__.called
452
+
453
+ def test_set_milestones_no_milestones(self):
454
+ """Test set_milestones with no milestones for POAM"""
455
+ poam = Mock(id=123)
456
+ sheet = MagicMock()
457
+ column_l_date = "04/01/25"
458
+ all_milestones = [{"parent_id": 456, "MilestoneDate": "2025-03-25T10:00:00"}]
459
+
460
+ set_milestones(poam, 10, sheet, column_l_date, all_milestones)
461
+
462
+ # Should set default milestone text
463
+ assert sheet.__getitem__.called
464
+
465
+ def test_set_status_closed_before_25th(self):
466
+ """Test set_status for closed POAM completed before 25th of month"""
467
+ poam = Mock(status="Closed", dateCompleted="2025-03-10T10:00:00")
468
+ sheet = MagicMock()
469
+
470
+ set_status(poam, 10, sheet)
471
+
472
+ # Should round to 25th of same month
473
+ sheet.__getitem__.assert_called_with("O10")
474
+ sheet.__getitem__.return_value.value = "03/25/25"
475
+
476
+ def test_set_status_closed_on_25th(self):
477
+ """Test set_status for closed POAM completed on 25th of month"""
478
+ poam = Mock(status="Closed", dateCompleted="2025-03-25T10:00:00")
479
+ sheet = MagicMock()
480
+
481
+ set_status(poam, 10, sheet)
482
+
483
+ # Should stay on 25th of same month
484
+ sheet.__getitem__.assert_called()
485
+
486
+ def test_set_status_closed_after_25th(self):
487
+ """Test set_status for closed POAM completed after 25th of month"""
488
+ poam = Mock(status="Closed", dateCompleted="2025-03-26T10:00:00")
489
+ sheet = MagicMock()
490
+
491
+ set_status(poam, 10, sheet)
492
+
493
+ # Should round to 25th of next month
494
+ sheet.__getitem__.assert_called()
495
+
496
+ def test_set_status_closed_no_completion_date(self):
497
+ """Test set_status for closed POAM with no completion date"""
498
+ poam = Mock(status="Closed", dateCompleted=None)
499
+ sheet = MagicMock()
500
+
501
+ set_status(poam, 10, sheet)
502
+
503
+ sheet.__getitem__.assert_called_with("O10")
504
+
505
+ def test_set_status_open_with_last_updated(self):
506
+ """Test set_status for open POAM with last updated date"""
507
+ poam = Mock(status="Open", dateLastUpdated="2025-03-15T10:00:00", dateCompleted=None)
508
+ sheet = MagicMock()
509
+
510
+ set_status(poam, 10, sheet)
511
+
512
+ sheet.__getitem__.assert_called()
513
+
514
+ def test_set_status_open_no_last_updated(self):
515
+ """Test set_status for open POAM with no last updated date"""
516
+ poam = Mock(status="Open", dateLastUpdated=None, dateCompleted=None)
517
+ sheet = MagicMock()
518
+
519
+ set_status(poam, 10, sheet)
520
+
521
+ sheet.__getitem__.assert_called_with("O10")
522
+
523
+ def test_set_vendor_info_with_dependency(self):
524
+ """Test set_vendor_info with vendor dependency"""
525
+ poam = Mock(vendorDependency="Yes", vendorName="Test Vendor", vendorLastUpdate="2025-03-15T10:00:00")
526
+ sheet = MagicMock()
527
+
528
+ set_vendor_info(poam, 10, sheet)
529
+
530
+ assert sheet.__getitem__.call_count >= 3
531
+
532
+ def test_set_vendor_info_no_dependency(self):
533
+ """Test set_vendor_info without vendor dependency"""
534
+ poam = Mock(vendorDependency="No", vendorName=None, vendorLastUpdate=None)
535
+ sheet = MagicMock()
536
+
537
+ set_vendor_info(poam, 10, sheet)
538
+
539
+ assert sheet.__getitem__.called
540
+
541
+ def test_set_vendor_info_no_vendor_name(self):
542
+ """Test set_vendor_info with no vendor name"""
543
+ poam = Mock(vendorDependency="Yes", vendorName=None, vendorLastUpdate="2025-03-15T10:00:00")
544
+ sheet = MagicMock()
545
+
546
+ set_vendor_info(poam, 10, sheet)
547
+
548
+ assert sheet.__getitem__.called
549
+
550
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
551
+ def test_set_risk_info_approved_deviation(self, mock_deviation_class):
552
+ """Test set_risk_info with approved deviation"""
553
+ mock_deviation = Mock(deviationStatus="Approved")
554
+ mock_deviation_class.get_by_issue.return_value = mock_deviation
555
+
556
+ poam = Mock(
557
+ id=123,
558
+ originalRiskRating="High",
559
+ adjustedRiskRating="Medium",
560
+ riskAdjustment="Yes",
561
+ falsePositive="No",
562
+ operationalRequirement="No",
563
+ deviationRationale="<p>Test rationale</p>",
564
+ severityLevel=1,
565
+ )
566
+ sheet = MagicMock()
567
+
568
+ set_risk_info(poam, 10, sheet)
569
+
570
+ mock_deviation_class.get_by_issue.assert_called_once_with(123)
571
+ assert sheet.__getitem__.call_count >= 6
572
+
573
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.IssueSeverity")
574
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
575
+ def test_set_risk_info_pending_deviation(self, mock_deviation_class, mock_severity):
576
+ """Test set_risk_info with pending deviation"""
577
+ mock_deviation = Mock(deviationStatus="Pending")
578
+ mock_deviation_class.get_by_issue.return_value = mock_deviation
579
+ mock_severity.return_value.name = "Moderate"
580
+
581
+ poam = Mock(
582
+ id=123,
583
+ originalRiskRating=None,
584
+ adjustedRiskRating=None,
585
+ riskAdjustment="Pending",
586
+ falsePositive="No",
587
+ operationalRequirement="No",
588
+ deviationRationale="",
589
+ severityLevel=2,
590
+ )
591
+ sheet = MagicMock()
592
+
593
+ set_risk_info(poam, 10, sheet)
594
+
595
+ assert sheet.__getitem__.called
596
+
597
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
598
+ def test_set_risk_info_rejected_deviation(self, mock_deviation_class):
599
+ """Test set_risk_info with rejected deviation"""
600
+ mock_deviation = Mock(deviationStatus="Rejected")
601
+ mock_deviation_class.get_by_issue.return_value = mock_deviation
602
+
603
+ poam = Mock(
604
+ id=123,
605
+ originalRiskRating="Critical",
606
+ adjustedRiskRating=None,
607
+ riskAdjustment="Yes",
608
+ falsePositive="No",
609
+ operationalRequirement="No",
610
+ deviationRationale="Test",
611
+ severityLevel=0,
612
+ )
613
+ sheet = MagicMock()
614
+
615
+ set_risk_info(poam, 10, sheet)
616
+
617
+ assert sheet.__getitem__.called
618
+
619
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
620
+ def test_set_risk_info_false_positive(self, mock_deviation_class):
621
+ """Test set_risk_info with false positive"""
622
+ mock_deviation = Mock(deviationStatus="Approved")
623
+ mock_deviation_class.get_by_issue.return_value = mock_deviation
624
+
625
+ poam = Mock(
626
+ id=123,
627
+ originalRiskRating="High",
628
+ adjustedRiskRating="N/A",
629
+ riskAdjustment="No",
630
+ falsePositive="Yes",
631
+ operationalRequirement="No",
632
+ deviationRationale="False positive",
633
+ severityLevel=1,
634
+ )
635
+ sheet = MagicMock()
636
+
637
+ set_risk_info(poam, 10, sheet)
638
+
639
+ assert sheet.__getitem__.called
640
+
641
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
642
+ def test_set_risk_info_operational_requirement(self, mock_deviation_class):
643
+ """Test set_risk_info with operational requirement"""
644
+ mock_deviation = Mock(deviationStatus="Approved")
645
+ mock_deviation_class.get_by_issue.return_value = mock_deviation
646
+
647
+ poam = Mock(
648
+ id=123,
649
+ originalRiskRating="High",
650
+ adjustedRiskRating="N/A",
651
+ riskAdjustment="No",
652
+ falsePositive="No",
653
+ operationalRequirement="Yes",
654
+ deviationRationale="Operational need",
655
+ severityLevel=1,
656
+ )
657
+ sheet = MagicMock()
658
+
659
+ set_risk_info(poam, 10, sheet)
660
+
661
+ assert sheet.__getitem__.called
662
+
663
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
664
+ def test_set_risk_info_operational_requirement_pending(self, mock_deviation_class):
665
+ """Test set_risk_info with pending operational requirement"""
666
+ mock_deviation = Mock(deviationStatus="Pending")
667
+ mock_deviation_class.get_by_issue.return_value = mock_deviation
668
+
669
+ poam = Mock(
670
+ id=123,
671
+ originalRiskRating="High",
672
+ adjustedRiskRisk="N/A",
673
+ riskAdjustment="No",
674
+ falsePositive="No",
675
+ operationalRequirement="Yes",
676
+ deviationRationale="Pending approval",
677
+ severityLevel=1,
678
+ )
679
+ sheet = MagicMock()
680
+
681
+ set_risk_info(poam, 10, sheet)
682
+
683
+ assert sheet.__getitem__.called
684
+
685
+ def test_set_end_columns(self):
686
+ """Test set_end_columns populates end columns correctly"""
687
+ with patch(
688
+ "regscale.integrations.public.fedramp.poam_export_v5.determine_poam_service_name"
689
+ ) as mock_service_name, patch(
690
+ "regscale.integrations.public.fedramp.poam_export_v5.determine_kev_date"
691
+ ) as mock_kev_date, patch(
692
+ "regscale.integrations.public.fedramp.poam_export_v5.determine_poam_comment"
693
+ ) as mock_comment:
694
+ mock_service_name.return_value = "PDF Services"
695
+ mock_kev_date.return_value = "06/15/25"
696
+ mock_comment.return_value = "Test comment"
697
+
698
+ ssp = Mock()
699
+ poam = Mock(id=123, autoApproved="Yes", kevList="Yes", cve="CVE-2025-1234")
700
+ wb = Workbook()
701
+ sheet = wb.active
702
+ props = []
703
+ assets = []
704
+ # Use proper dict structure matching the code expectations
705
+ all_links = [
706
+ Mock(parentID=123, __getitem__=lambda self, key: "Link1" if key == "Title" else "http://example.com")
707
+ ]
708
+ all_files = [Mock(parentId=123, __getitem__=lambda self, key: "file1.pdf")]
709
+
710
+ set_end_columns(ssp, poam, 10, sheet, props, assets, all_links, all_files)
711
+
712
+ mock_service_name.assert_called_once()
713
+ mock_kev_date.assert_called_once_with("CVE-2025-1234")
714
+ mock_comment.assert_called_once()
715
+ # Verify some columns were set
716
+ assert sheet["Y10"].value is not None
717
+ assert sheet["AB10"].value == "Yes"
718
+ assert sheet["AE10"].value == "PDF Services"
719
+
720
+ def test_map_weakness_detector_and_id_for_rev5_issues(self):
721
+ """Test map_weakness_detector_and_id_for_rev5_issues sets correct values"""
722
+ wb = Workbook()
723
+ worksheet = wb.active
724
+ issue = Mock(sourceReport="Tenable SC", cve="CVE-2025-1234", pluginId="12345", title="Test Issue")
725
+
726
+ map_weakness_detector_and_id_for_rev5_issues(worksheet, "E", "F", 10, issue)
727
+
728
+ assert worksheet["E10"].value == "Tenable SC"
729
+ assert worksheet["F10"].value == "CVE-2025-1234"
730
+
731
+ def test_map_weakness_detector_and_id_no_cve(self):
732
+ """Test map_weakness_detector_and_id with no CVE uses pluginId"""
733
+ wb = Workbook()
734
+ worksheet = wb.active
735
+ issue = Mock(sourceReport="Tenable SC", cve=None, pluginId="12345", title="Test Issue")
736
+
737
+ map_weakness_detector_and_id_for_rev5_issues(worksheet, "E", "F", 10, issue)
738
+
739
+ assert worksheet["E10"].value == "Tenable SC"
740
+ assert worksheet["F10"].value == "12345"
741
+
742
+ def test_map_weakness_detector_and_id_no_cve_or_plugin(self):
743
+ """Test map_weakness_detector_and_id with no CVE or pluginId uses title"""
744
+ wb = Workbook()
745
+ worksheet = wb.active
746
+ issue = Mock(sourceReport="Tenable SC", cve=None, pluginId=None, title="Test Issue")
747
+
748
+ map_weakness_detector_and_id_for_rev5_issues(worksheet, "E", "F", 10, issue)
749
+
750
+ assert worksheet["E10"].value == "Tenable SC"
751
+ assert worksheet["F10"].value == "Test Issue"
752
+
753
+ def test_update_column_widths(self):
754
+ """Test update_column_widths sets correct widths"""
755
+ wb = Workbook()
756
+ ws = wb.active
757
+
758
+ # Add some test data
759
+ ws["A1"] = "Test"
760
+ ws["C1"] = "Long text that should wrap"
761
+
762
+ update_column_widths(ws)
763
+
764
+ # Check that column widths are set
765
+ assert ws.column_dimensions["A"].width == 15
766
+ assert ws.column_dimensions["C"].width == 40
767
+
768
+ def test_align_column(self):
769
+ """Test align_column sets text wrapping and alignment"""
770
+ wb = Workbook()
771
+ ws = wb.active
772
+ ws["G1"] = "Test "
773
+ ws["G2"] = " Test2 "
774
+
775
+ align_column("G", ws)
776
+
777
+ # Check alignment was set
778
+ for cell in ws["G"]:
779
+ if cell.value:
780
+ assert cell.alignment.wrap_text is True
781
+ assert cell.alignment.horizontal == "left"
782
+
783
+ def test_update_header(self):
784
+ """Test update_header populates header information"""
785
+ ssp = Mock(cspOrgName="Test Org", systemName="Test System", overallCategorization="Moderate")
786
+ wb = Workbook()
787
+ sheet = wb.active
788
+
789
+ result = update_header(ssp, sheet)
790
+
791
+ assert result["A3"].value == "Test Org"
792
+ assert result["B3"].value == "Test System"
793
+ assert result["C3"].value == "Moderate"
794
+ # D3 should have current date
795
+ assert result["D3"].value is not None
796
+
797
+ def test_update_header_no_org_name(self):
798
+ """Test update_header with no org name"""
799
+ ssp = Mock(cspOrgName=None, systemName="Test System", overallCategorization="Moderate")
800
+ wb = Workbook()
801
+ sheet = wb.active
802
+
803
+ result = update_header(ssp, sheet)
804
+
805
+ assert result["A3"].value == "N/A"
806
+
807
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Issue")
808
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Asset")
809
+ def test_get_all_poams(self, mock_asset_class, mock_issue_class):
810
+ """Test get_all_poams retrieves POAMs from SSP and assets"""
811
+ ssp_id = "123"
812
+
813
+ # Mock SSP POAMs
814
+ ssp_poam1 = Mock(
815
+ id=1, isPoam=True, otherIdentifier="ID1", assetIdentifier="Asset1", cve="CVE-1", pluginId="P1", title="T1"
816
+ )
817
+ ssp_poam2 = Mock(
818
+ id=2, isPoam=True, otherIdentifier="ID2", assetIdentifier="Asset2", cve="CVE-2", pluginId="P2", title="T2"
819
+ )
820
+ mock_issue_class.get_all_by_parent.return_value = [ssp_poam1, ssp_poam2]
821
+
822
+ # Mock assets
823
+ asset1 = Mock(id=10)
824
+ mock_asset_class.get_all_by_parent.return_value = [asset1]
825
+
826
+ # Mock asset POAMs (returns empty to avoid complexity)
827
+ def issue_side_effect(parent_id, parent_module):
828
+ if parent_id == ssp_id:
829
+ return [ssp_poam1, ssp_poam2]
830
+ return []
831
+
832
+ mock_issue_class.get_all_by_parent.side_effect = issue_side_effect
833
+
834
+ result = get_all_poams(ssp_id)
835
+
836
+ assert len(result) == 2
837
+ assert result[0].id == 1
838
+ assert result[1].id == 2
839
+
840
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Link")
841
+ def test_gen_links(self, mock_link_class):
842
+ """Test gen_links generates list of links"""
843
+ poam1 = Mock(id=1)
844
+ poam2 = Mock(id=2)
845
+ all_poams = [poam1, poam2]
846
+
847
+ link1 = {"id": 10, "title": "Link 1"}
848
+ link2 = {"id": 11, "title": "Link 2"}
849
+ mock_link_class.get_all_by_parent.side_effect = [[link1], [link2]]
850
+
851
+ result = gen_links(all_poams)
852
+
853
+ assert len(result) == 2
854
+ assert result[0] == link1
855
+ assert result[1] == link2
856
+
857
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.File")
858
+ def test_gen_files(self, mock_file_class):
859
+ """Test gen_files generates list of files"""
860
+ api = Mock()
861
+ poam1 = Mock(id=1)
862
+ poam2 = Mock(id=2)
863
+ all_poams = [poam1, poam2]
864
+
865
+ file1 = {"id": 10, "name": "File 1"}
866
+ file2 = {"id": 11, "name": "File 2"}
867
+ mock_file_class.get_files_for_parent_from_regscale.side_effect = [[file1], [file2]]
868
+
869
+ result = gen_files(all_poams, api)
870
+
871
+ assert len(result) == 2
872
+ assert result[0] == file1
873
+ assert result[1] == file2
874
+
875
+ def test_gen_milestones(self):
876
+ """Test gen_milestones generates list of milestones"""
877
+ api = Mock()
878
+ app = Mock()
879
+ app.config = {"domain": "https://test.com"}
880
+
881
+ poam1 = Mock(id=1)
882
+ poam2 = Mock(id=2)
883
+ all_poams = [poam1, poam2]
884
+
885
+ milestone1 = {"id": 10, "parent_id": 1}
886
+ milestone2 = {"id": 11, "parent_id": 2}
887
+
888
+ mock_response1 = Mock()
889
+ mock_response1.json.return_value = [milestone1]
890
+ mock_response2 = Mock()
891
+ mock_response2.json.return_value = [milestone2]
892
+
893
+ api.get.side_effect = [mock_response1, mock_response2]
894
+
895
+ result = gen_milestones(all_poams, api, app)
896
+
897
+ assert len(result) == 2
898
+ assert result[0] == milestone1
899
+ assert result[1] == milestone2
900
+
901
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Property")
902
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.set_end_columns")
903
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.set_risk_info")
904
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.set_vendor_info")
905
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.set_status")
906
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.set_milestones")
907
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.map_weakness_detector_and_id_for_rev5_issues")
908
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.strip_html")
909
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.convert_to_list")
910
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.set_short_date")
911
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.determine_poam_id")
912
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.datetime_obj")
913
+ def test_process_row(
914
+ self,
915
+ mock_datetime_obj,
916
+ mock_determine_id,
917
+ mock_short_date,
918
+ mock_convert_list,
919
+ mock_strip_html,
920
+ mock_map_weakness,
921
+ mock_set_milestones,
922
+ mock_set_status,
923
+ mock_set_vendor,
924
+ mock_set_risk,
925
+ mock_set_end,
926
+ mock_property,
927
+ ):
928
+ """Test process_row processes a POAM row correctly"""
929
+ ssp = Mock()
930
+ poam = Mock(
931
+ id=123,
932
+ sourceReport="Tenable SC",
933
+ title="Test Issue",
934
+ description="Test description",
935
+ assetIdentifier="Asset1",
936
+ remediationDescription="Fix it",
937
+ dateFirstDetected="2025-03-01T10:00:00",
938
+ dueDate="2025-04-01T10:00:00",
939
+ changes="Test changes",
940
+ )
941
+ sheet = MagicMock()
942
+ assets = []
943
+ all_milestones = []
944
+ all_links = []
945
+ all_files = []
946
+
947
+ mock_property.get_all_by_parent.return_value = []
948
+ mock_strip_html.side_effect = lambda x: x if x else ""
949
+ mock_convert_list.return_value = ["Asset1"]
950
+ mock_short_date.return_value = "03/01/25"
951
+ mock_determine_id.return_value = "DC-123"
952
+ mock_datetime_obj.return_value = datetime(2025, 4, 1, 10, 0, 0)
953
+
954
+ process_row(ssp, poam, 0, sheet, assets, all_milestones, all_links, all_files, point_of_contact="John Doe")
955
+
956
+ # Verify the index adjustment (index 0 becomes row 6)
957
+ assert sheet.__getitem__.called
958
+ mock_set_milestones.assert_called_once()
959
+ mock_set_status.assert_called_once()
960
+ mock_set_vendor.assert_called_once()
961
+ mock_set_risk.assert_called_once()
962
+ mock_set_end.assert_called_once()
963
+
964
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Property")
965
+ def test_process_row_sap_concur_normalization(self, mock_property):
966
+ """Test process_row normalizes SAP Concur to Tenable SC"""
967
+ ssp = Mock()
968
+ poam = Mock(
969
+ id=123,
970
+ sourceReport="SAP Concur",
971
+ title="Test Issue",
972
+ description="",
973
+ assetIdentifier="",
974
+ remediationDescription="",
975
+ dateFirstDetected="2025-03-01T10:00:00",
976
+ dueDate=None,
977
+ changes="",
978
+ )
979
+ sheet = MagicMock()
980
+ assets = []
981
+
982
+ mock_property.get_all_by_parent.return_value = []
983
+
984
+ with patch("regscale.integrations.public.fedramp.poam_export_v5.set_milestones"), patch(
985
+ "regscale.integrations.public.fedramp.poam_export_v5.set_status"
986
+ ), patch("regscale.integrations.public.fedramp.poam_export_v5.set_vendor_info"), patch(
987
+ "regscale.integrations.public.fedramp.poam_export_v5.set_risk_info"
988
+ ), patch(
989
+ "regscale.integrations.public.fedramp.poam_export_v5.set_end_columns"
990
+ ):
991
+ process_row(ssp, poam, 0, sheet, assets, [], [], [], point_of_contact="")
992
+
993
+ # Verify sourceReport was normalized
994
+ assert poam.sourceReport == "Tenable SC"
995
+
996
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.openpyxl.load_workbook")
997
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.update_header")
998
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Asset")
999
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.process_row")
1000
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.update_column_widths")
1001
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.align_column")
1002
+ def test_process_worksheet_open_poams(
1003
+ self, mock_align, mock_update_widths, mock_process_row, mock_asset, mock_update_header, mock_load_wb
1004
+ ):
1005
+ """Test process_worksheet processes open POAMs worksheet"""
1006
+ ssp = Mock(id=123)
1007
+ workbook_path = Path("/tmp/test.xlsx")
1008
+ all_poams = [
1009
+ Mock(id=1, status=IssueStatus.Open),
1010
+ Mock(id=2, status=IssueStatus.Open),
1011
+ Mock(id=3, status=IssueStatus.Closed),
1012
+ ]
1013
+
1014
+ mock_wb = MagicMock()
1015
+ mock_sheet = MagicMock()
1016
+ mock_wb.__getitem__.return_value = mock_sheet
1017
+ mock_load_wb.return_value = mock_wb
1018
+
1019
+ mock_asset.get_all_by_parent.return_value = []
1020
+ mock_update_header.return_value = mock_sheet
1021
+
1022
+ process_worksheet(ssp, "Open POA&M Items", workbook_path, all_poams, [], [], [], point_of_contact="John Doe")
1023
+
1024
+ # Should process only 2 open POAMs
1025
+ assert mock_process_row.call_count == 2
1026
+ mock_update_widths.assert_called_once_with(mock_sheet)
1027
+ mock_align.assert_called_once_with("G", mock_sheet)
1028
+ mock_wb.save.assert_called_once_with(workbook_path)
1029
+
1030
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.openpyxl.load_workbook")
1031
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.update_header")
1032
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Asset")
1033
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.process_row")
1034
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.update_column_widths")
1035
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.align_column")
1036
+ def test_process_worksheet_closed_poams(
1037
+ self, mock_align, mock_update_widths, mock_process_row, mock_asset, mock_update_header, mock_load_wb
1038
+ ):
1039
+ """Test process_worksheet processes closed POAMs worksheet"""
1040
+ ssp = Mock(id=123)
1041
+ workbook_path = Path("/tmp/test.xlsx")
1042
+ all_poams = [
1043
+ Mock(id=1, status=IssueStatus.Open),
1044
+ Mock(id=2, status=IssueStatus.Closed),
1045
+ Mock(id=3, status=IssueStatus.Closed),
1046
+ ]
1047
+
1048
+ mock_wb = MagicMock()
1049
+ mock_sheet = MagicMock()
1050
+ mock_wb.__getitem__.return_value = mock_sheet
1051
+ mock_load_wb.return_value = mock_wb
1052
+
1053
+ mock_asset.get_all_by_parent.return_value = []
1054
+ mock_update_header.return_value = mock_sheet
1055
+
1056
+ process_worksheet(
1057
+ ssp, "Closed POA&M Items", workbook_path, all_poams, [], [], [], point_of_contact="Jane Smith"
1058
+ )
1059
+
1060
+ # Should process only 2 closed POAMs
1061
+ assert mock_process_row.call_count == 2
1062
+ mock_wb.save.assert_called_once()
1063
+
1064
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
1065
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
1066
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
1067
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
1068
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_links")
1069
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_files")
1070
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_milestones")
1071
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.shutil.copy")
1072
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.process_worksheet")
1073
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Path")
1074
+ def test_export_poam_v5_success(
1075
+ self,
1076
+ mock_path_class,
1077
+ mock_process_ws,
1078
+ mock_copy,
1079
+ mock_gen_milestones,
1080
+ mock_gen_files,
1081
+ mock_gen_links,
1082
+ mock_get_poams,
1083
+ mock_ssp_class,
1084
+ mock_api_class,
1085
+ mock_app_class,
1086
+ ):
1087
+ """Test export_poam_v5 successfully exports POAMs"""
1088
+ ssp_id = "123"
1089
+ output_file = "/tmp/output.xlsx"
1090
+
1091
+ # Mock application and API
1092
+ mock_app = Mock()
1093
+ mock_app_class.return_value = mock_app
1094
+ mock_api = Mock()
1095
+ mock_api_class.return_value = mock_api
1096
+
1097
+ # Mock SSP
1098
+ mock_ssp = Mock(systemName="Test System")
1099
+ mock_ssp_class.get_object.return_value = mock_ssp
1100
+
1101
+ # Mock POAMs
1102
+ mock_poam = Mock(id=1)
1103
+ mock_get_poams.return_value = [mock_poam]
1104
+
1105
+ # Mock related data
1106
+ mock_gen_links.return_value = []
1107
+ mock_gen_files.return_value = []
1108
+ mock_gen_milestones.return_value = []
1109
+
1110
+ # Mock template path
1111
+ mock_template_path = Mock()
1112
+ mock_template_path.exists.return_value = True
1113
+ mock_output_path = Mock()
1114
+ mock_output_path.suffix = ".xlsx"
1115
+ mock_output_path.absolute.return_value = "/tmp/output.xlsx"
1116
+
1117
+ def path_side_effect(arg):
1118
+ if arg == "./FedRAMP-POAM-Template.xlsx":
1119
+ return mock_template_path
1120
+ return mock_output_path
1121
+
1122
+ mock_path_class.side_effect = path_side_effect
1123
+
1124
+ export_poam_v5(ssp_id, output_file)
1125
+
1126
+ # Verify calls
1127
+ mock_ssp_class.get_object.assert_called_once_with(ssp_id)
1128
+ mock_get_poams.assert_called_once_with(ssp_id)
1129
+ mock_gen_links.assert_called_once()
1130
+ mock_gen_files.assert_called_once()
1131
+ mock_gen_milestones.assert_called_once()
1132
+ mock_copy.assert_called_once()
1133
+ # Should process both open and closed worksheets
1134
+ assert mock_process_ws.call_count == 2
1135
+
1136
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
1137
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
1138
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
1139
+ def test_export_poam_v5_ssp_not_found(self, mock_ssp_class, mock_api_class, mock_app_class):
1140
+ """Test export_poam_v5 when SSP not found"""
1141
+ ssp_id = "999"
1142
+ output_file = "/tmp/output.xlsx"
1143
+
1144
+ mock_app_class.return_value = Mock()
1145
+ mock_api_class.return_value = Mock()
1146
+ mock_ssp_class.get_object.return_value = None
1147
+
1148
+ export_poam_v5(ssp_id, output_file)
1149
+
1150
+ # Should return early without processing
1151
+ mock_ssp_class.get_object.assert_called_once_with(ssp_id)
1152
+
1153
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
1154
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
1155
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
1156
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
1157
+ def test_export_poam_v5_no_poams(self, mock_get_poams, mock_ssp_class, mock_api_class, mock_app_class):
1158
+ """Test export_poam_v5 when no POAMs found"""
1159
+ ssp_id = "123"
1160
+ output_file = "/tmp/output.xlsx"
1161
+
1162
+ mock_app_class.return_value = Mock()
1163
+ mock_api_class.return_value = Mock()
1164
+ mock_ssp = Mock(systemName="Test System")
1165
+ mock_ssp_class.get_object.return_value = mock_ssp
1166
+ mock_get_poams.return_value = []
1167
+
1168
+ export_poam_v5(ssp_id, output_file)
1169
+
1170
+ # Should return early without processing
1171
+ mock_get_poams.assert_called_once_with(ssp_id)
1172
+
1173
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
1174
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
1175
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
1176
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
1177
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_links")
1178
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_files")
1179
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_milestones")
1180
+ def test_export_poam_v5_template_not_found(
1181
+ self,
1182
+ mock_gen_milestones,
1183
+ mock_gen_files,
1184
+ mock_gen_links,
1185
+ mock_get_poams,
1186
+ mock_ssp_class,
1187
+ mock_api_class,
1188
+ mock_app_class,
1189
+ ):
1190
+ """Test export_poam_v5 when template file not found"""
1191
+ ssp_id = "123"
1192
+ output_file = "/tmp/output.xlsx"
1193
+
1194
+ mock_app = Mock()
1195
+ mock_app.config = {"domain": "https://test.com"}
1196
+ mock_app_class.return_value = mock_app
1197
+ mock_api = Mock()
1198
+ mock_api.config = {"domain": "https://test.com"}
1199
+ mock_api_class.return_value = mock_api
1200
+ mock_ssp = Mock(systemName="Test System")
1201
+ mock_ssp_class.get_object.return_value = mock_ssp
1202
+ mock_poam = Mock(id=1)
1203
+ mock_get_poams.return_value = [mock_poam]
1204
+ mock_gen_links.return_value = []
1205
+ mock_gen_files.return_value = []
1206
+ mock_gen_milestones.return_value = []
1207
+
1208
+ # Don't provide template_path, so it will look for default which doesn't exist
1209
+ export_poam_v5(ssp_id, output_file)
1210
+
1211
+ # Should return early due to missing template
1212
+ mock_get_poams.assert_called_once()
1213
+
1214
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
1215
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
1216
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
1217
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
1218
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_links")
1219
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_files")
1220
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.gen_milestones")
1221
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.shutil.copy")
1222
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.process_worksheet")
1223
+ def test_export_poam_v5_with_custom_template(
1224
+ self,
1225
+ mock_process_ws,
1226
+ mock_copy,
1227
+ mock_gen_milestones,
1228
+ mock_gen_files,
1229
+ mock_gen_links,
1230
+ mock_get_poams,
1231
+ mock_ssp_class,
1232
+ mock_api_class,
1233
+ mock_app_class,
1234
+ ):
1235
+ """Test export_poam_v5 with custom template path"""
1236
+ ssp_id = "123"
1237
+ output_file = "/tmp/output.xlsx"
1238
+
1239
+ mock_app_class.return_value = Mock()
1240
+ mock_api_class.return_value = Mock()
1241
+ mock_ssp = Mock(systemName="Test System")
1242
+ mock_ssp_class.get_object.return_value = mock_ssp
1243
+ mock_poam = Mock(id=1)
1244
+ mock_get_poams.return_value = [mock_poam]
1245
+ mock_gen_links.return_value = []
1246
+ mock_gen_files.return_value = []
1247
+ mock_gen_milestones.return_value = []
1248
+
1249
+ # Create a real temporary template file
1250
+ import tempfile
1251
+
1252
+ with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as temp_template:
1253
+ template_path = Path(temp_template.name)
1254
+ # Create a simple workbook
1255
+ from openpyxl import Workbook
1256
+
1257
+ wb = Workbook()
1258
+ ws = wb.active
1259
+ ws.title = "Open POA&M Items"
1260
+ wb.create_sheet("Closed POA&M Items")
1261
+ wb.save(template_path)
1262
+
1263
+ try:
1264
+ export_poam_v5(ssp_id, output_file, template_path=template_path)
1265
+
1266
+ # Should use custom template
1267
+ mock_copy.assert_called_once()
1268
+ finally:
1269
+ # Clean up
1270
+ if template_path.exists():
1271
+ template_path.unlink()
1272
+
1273
+ @patch("regscale.integrations.public.fedramp.poam_export_v5.pull_cisa_kev")
1274
+ def test_get_cached_cisa_kev_caching(self, mock_pull_kev):
1275
+ """Test get_cached_cisa_kev caches the KEV data"""
1276
+ mock_kev_data = {"vulnerabilities": []}
1277
+ mock_pull_kev.return_value = mock_kev_data
1278
+
1279
+ # Clear the cache first
1280
+ get_cached_cisa_kev.cache_clear()
1281
+
1282
+ # First call should fetch data
1283
+ result1 = get_cached_cisa_kev()
1284
+ assert result1 == mock_kev_data
1285
+ assert mock_pull_kev.call_count == 1
1286
+
1287
+ # Second call should use cache
1288
+ result2 = get_cached_cisa_kev()
1289
+ assert result2 == mock_kev_data
1290
+ assert mock_pull_kev.call_count == 1 # Still 1, not called again
1291
+
1292
+ # Clean up
1293
+ get_cached_cisa_kev.cache_clear()