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
@@ -8,11 +8,10 @@ import tempfile
8
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
9
  from datetime import datetime, timedelta
10
10
  from io import BytesIO
11
+ from pathlib import Path
11
12
  from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, Literal
12
13
  from urllib.parse import urljoin
13
14
 
14
- from pathlib import Path
15
-
16
15
  if TYPE_CHECKING:
17
16
  from regscale.core.app.application import Application
18
17
 
@@ -40,7 +39,7 @@ from regscale.models import regscale_id, regscale_module
40
39
  from regscale.models.regscale_models.file import File
41
40
  from regscale.models.regscale_models.issue import Issue
42
41
  from regscale.models.regscale_models.task import Task
43
- from regscale.utils.threading.threadhandler import create_threads, thread_assignment
42
+ from regscale.integrations.variables import ScannerVariables
44
43
 
45
44
  job_progress = create_progress_object()
46
45
  logger = create_logger()
@@ -98,6 +97,18 @@ def jira():
98
97
  is_flag=True,
99
98
  help="Use token authentication for Jira API instead of basic auth, defaults to False.",
100
99
  )
100
+ @click.option(
101
+ "--jql",
102
+ type=click.STRING,
103
+ help="Custom JQL query for filtering Jira issues.",
104
+ required=False,
105
+ )
106
+ @click.option(
107
+ "--poams",
108
+ "-p",
109
+ is_flag=True,
110
+ help="Whether to create/update the incoming issues from Jira as POAMs in RegScale.",
111
+ )
101
112
  def issues(
102
113
  regscale_id: int,
103
114
  regscale_module: str,
@@ -105,6 +116,8 @@ def issues(
105
116
  jira_issue_type: str,
106
117
  sync_attachments: bool = True,
107
118
  token_auth: bool = False,
119
+ jql: Optional[str] = None,
120
+ poams: bool = False,
108
121
  ):
109
122
  """Sync issues from Jira into RegScale."""
110
123
  sync_regscale_and_jira(
@@ -114,6 +127,8 @@ def issues(
114
127
  jira_issue_type=jira_issue_type,
115
128
  sync_attachments=sync_attachments,
116
129
  token_auth=token_auth,
130
+ jql=jql,
131
+ use_poams=poams,
117
132
  )
118
133
 
119
134
 
@@ -143,12 +158,19 @@ def issues(
143
158
  is_flag=True,
144
159
  help="Use token authentication for Jira API instead of basic auth, defaults to False.",
145
160
  )
161
+ @click.option(
162
+ "--jql",
163
+ type=click.STRING,
164
+ help="Custom JQL query for filtering Jira tasks.",
165
+ required=False,
166
+ )
146
167
  def tasks(
147
168
  regscale_id: int,
148
169
  regscale_module: str,
149
170
  jira_project: str,
150
171
  sync_attachments: bool = True,
151
172
  token_auth: bool = False,
173
+ jql: Optional[str] = None,
152
174
  ):
153
175
  """Sync tasks from Jira into RegScale."""
154
176
  sync_regscale_and_jira(
@@ -159,6 +181,7 @@ def tasks(
159
181
  sync_attachments=sync_attachments,
160
182
  sync_tasks_only=True,
161
183
  token_auth=token_auth,
184
+ jql=jql,
162
185
  )
163
186
 
164
187
 
@@ -202,6 +225,8 @@ def sync_regscale_and_jira(
202
225
  sync_attachments: bool = True,
203
226
  sync_tasks_only: bool = False,
204
227
  token_auth: bool = False,
228
+ jql: Optional[str] = None,
229
+ use_poams: Optional[bool] = False,
205
230
  ) -> None:
206
231
  """
207
232
  Sync issues, bidirectionally, from Jira into RegScale as issues
@@ -213,23 +238,35 @@ def sync_regscale_and_jira(
213
238
  :param bool sync_attachments: Whether to sync attachments in RegScale & Jira, defaults to True
214
239
  :param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
215
240
  :param bool token_auth: Use token authentication for Jira API, defaults to False
241
+ :param Optional[str] jql: Custom JQL query for filtering Jira issues/tasks, defaults to None
242
+ :param Optional[bool] use_poams: Whether to mark the incoming issues as POAMs in RegScale, defaults to False
216
243
  :rtype: None
217
244
  """
218
245
  app = check_license()
219
246
  api = Api()
220
247
  config = app.config
221
248
 
249
+ # Load custom fields configuration from init.yaml
250
+ if custom_fields := config.get("jiraCustomFields", {}):
251
+ logger.info("Custom field mappings loaded from config: %s", custom_fields)
252
+ else:
253
+ logger.debug("No custom field mappings found in configuration")
254
+
222
255
  # see if provided RegScale Module is an accepted option
223
256
  verify_provided_module(parent_module)
224
257
 
225
258
  # create Jira client
226
259
  jira_client = create_jira_client(config, token_auth)
227
260
 
228
- jql_str = (
229
- f"project = {jira_project} AND issueType = {jira_issue_type}"
230
- if sync_tasks_only
231
- else f"project = {jira_project}"
232
- )
261
+ # Use custom JQL if provided, otherwise build default JQL
262
+ if jql:
263
+ jql_str = jql
264
+ else:
265
+ jql_str = (
266
+ f"project = '{jira_project}' AND issueType = '{jira_issue_type}'"
267
+ if sync_tasks_only
268
+ else f"project = '{jira_project}'"
269
+ )
233
270
  regscale_objects, regscale_attachments = get_regscale_data_and_attachments(
234
271
  parent_id=parent_id,
235
272
  parent_module=parent_module,
@@ -274,35 +311,20 @@ def sync_regscale_and_jira(
274
311
  api=api,
275
312
  sync_attachments=sync_attachments,
276
313
  attachments=regscale_attachments,
314
+ custom_fields=custom_fields,
277
315
  ):
278
- with job_progress:
279
- # create task to update RegScale issues
280
- updating_issues = job_progress.add_task(
281
- f"[#f8b737]Updating {len(regscale_objects_to_update)} RegScale {output_str}(s) from Jira...",
282
- total=len(regscale_objects_to_update),
283
- )
284
- # create threads to analyze Jira issues and RegScale issues
285
- create_threads(
286
- process=update_regscale_issues,
287
- args=(
288
- regscale_objects_to_update,
289
- updating_issues,
290
- ),
291
- thread_count=len(regscale_objects_to_update),
292
- )
293
- # output the final result
294
- logger.info(
295
- "%i/%i %s(s) updated in RegScale.",
296
- len(regscale_objects_to_update),
297
- len(update_counter),
298
- output_str,
299
- )
316
+ for regscale_object in regscale_objects_to_update:
317
+ regscale_object.save(bulk=True)
318
+ if isinstance(regscale_objects[0], Issue):
319
+ Issue.bulk_save()
320
+ elif isinstance(regscale_objects[0], Task):
321
+ Task.bulk_save()
300
322
  else:
301
323
  logger.info("No %s(s) need to be updated in RegScale.", output_str)
302
324
 
303
325
  if jira_objects:
304
326
  return sync_regscale_objects_to_jira(
305
- jira_objects, regscale_objects, sync_attachments, app, parent_id, parent_module, sync_tasks_only
327
+ jira_objects, regscale_objects, sync_attachments, app, parent_id, parent_module, sync_tasks_only, use_poams
306
328
  )
307
329
  logger.info("No %s need to be analyzed from Jira.", output_str)
308
330
 
@@ -315,6 +337,7 @@ def sync_regscale_objects_to_jira(
315
337
  parent_id: int,
316
338
  parent_module: str,
317
339
  sync_tasks_only: bool,
340
+ use_poams: Optional[bool] = False,
318
341
  ):
319
342
  """
320
343
  Sync issues from Jira to RegScale
@@ -326,6 +349,7 @@ def sync_regscale_objects_to_jira(
326
349
  :param int parent_id: Parent record ID in RegScale
327
350
  :param str parent_module: Parent record module in RegScale
328
351
  :param bool sync_tasks_only: Whether to sync only tasks from Jira
352
+ :param bool use_poams: Whether to create/update the incoming issues as POAMs in RegScale, defaults to False
329
353
  """
330
354
  issues_closed = []
331
355
  with job_progress:
@@ -347,21 +371,20 @@ def sync_regscale_objects_to_jira(
347
371
  progress_task=creating_issues,
348
372
  )
349
373
  else:
350
- create_threads(
351
- process=create_and_update_regscale_issues,
352
- args=(
353
- jira_issues,
354
- regscale_objects,
355
- sync_attachments,
356
- jira_client,
357
- app,
358
- parent_id,
359
- parent_module,
360
- creating_issues,
361
- job_progress,
362
- ),
363
- thread_count=len(jira_issues),
374
+ app.thread_manager.submit_tasks_from_list(
375
+ create_and_update_regscale_issues,
376
+ jira_issues,
377
+ regscale_objects,
378
+ use_poams,
379
+ sync_attachments,
380
+ jira_client,
381
+ app,
382
+ parent_id,
383
+ parent_module,
384
+ creating_issues,
385
+ job_progress,
364
386
  )
387
+ app.thread_manager.execute_and_verify(timeout=ScannerVariables.timeout)
365
388
  logger.info(
366
389
  "Analyzed %i Jira %s(s), created %i %s(s), updated %i %s(s), and closed %i %s(s) in RegScale.",
367
390
  len(jira_issues),
@@ -387,8 +410,6 @@ def create_jira_client(
387
410
  :return: JIRA Client
388
411
  :rtype: JIRA
389
412
  """
390
- from regscale.integrations.variables import ScannerVariables
391
-
392
413
  url = config["jiraUrl"]
393
414
  token = config["jiraApiToken"]
394
415
  jira_user = config["jiraUserName"]
@@ -399,36 +420,6 @@ def create_jira_client(
399
420
  return JIRA(basic_auth=(jira_user, token), options={"server": url})
400
421
 
401
422
 
402
- def update_regscale_issues(args: Tuple, thread: int) -> None:
403
- """
404
- Function to compare Jira issues and RegScale issues
405
-
406
- :param Tuple args: Tuple of args to use during the process
407
- :param int thread: Thread number of current thread
408
- :rtype: None
409
- """
410
- # set up local variables from the passed args
411
- (
412
- regscale_issues,
413
- task,
414
- ) = args
415
- # find which records should be executed by the current thread
416
- threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
417
- # iterate through the thread assignment items and process them
418
- for i in range(len(threads)):
419
- # set the issue for the thread for later use in the function
420
- issue = regscale_issues[threads[i]]
421
- # update the issue in RegScale
422
- issue.save()
423
- logger.debug(
424
- "RegScale Issue %i was updated with the Jira link.",
425
- issue.id,
426
- )
427
- update_counter.append(issue)
428
- # update progress bar
429
- job_progress.update(task, advance=1)
430
-
431
-
432
423
  def convert_task_status(name: str) -> str:
433
424
  """
434
425
  Convert the task status from Jira to RegScale
@@ -472,7 +463,7 @@ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_i
472
463
  date_closed = status_change_date
473
464
  percent_complete = 100
474
465
 
475
- return Task(
466
+ task = Task(
476
467
  title=title,
477
468
  status=status,
478
469
  description=description,
@@ -485,6 +476,13 @@ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_i
485
476
  extra_data={"jiraIssue": jira_issue}, # type: ignore
486
477
  )
487
478
 
479
+ # Apply custom field mappings from Jira to RegScale
480
+ custom_fields = config.get("jiraCustomFields", {})
481
+ if custom_fields:
482
+ apply_custom_fields_to_regscale_object(task, custom_fields, jira_issue)
483
+
484
+ return task
485
+
488
486
 
489
487
  def check_and_close_tasks(existing_tasks: list[Task], all_jira_titles: set[str]) -> list[Task]:
490
488
  """
@@ -545,9 +543,11 @@ def process_tasks_for_sync(
545
543
  jira_task = create_regscale_task_from_jira(config, jira_issue, parent_id, parent_module)
546
544
 
547
545
  # Check if we have a matching task in RegScale
548
- existing_task = existing_task_map.get(jira_issue.key)
546
+ if existing_task := existing_task_map.get(jira_issue.key):
547
+ # Apply custom field mappings from Jira to RegScale for existing tasks
548
+ if custom_fields := config.get("jiraCustomFields", {}):
549
+ apply_custom_fields_to_regscale_object(existing_task, custom_fields, jira_issue)
549
550
 
550
- if existing_task:
551
551
  # Check if task is closed in Jira and needs to be closed in regscale
552
552
  if jira_task.status in closed_statuses and existing_task.status not in closed_statuses:
553
553
  existing_task.status = "Closed"
@@ -659,67 +659,93 @@ def task_and_attachments_sync(
659
659
  )
660
660
 
661
661
 
662
- def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
662
+ def _create_new_regscale_issue(
663
+ jira_issue: jiraIssue, app: "Application", parent_id: int, parent_module: str, is_poam: Optional[bool] = False
664
+ ) -> Optional[Issue]:
665
+ """
666
+ Create a new RegScale issue from a Jira issue
667
+
668
+ :param jiraIssue jira_issue: The Jira issue to create from
669
+ :param Application app: RegScale application object
670
+ :param int parent_id: Parent record ID in RegScale
671
+ :param str parent_module: Parent record module in RegScale
672
+ :param bool is_poam: Whether to create the issue as a POAM in RegScale, defaults to False
673
+ :return: The created RegScale issue or None if creation failed
674
+ :rtype: Optional[Issue]
675
+ """
676
+ issue = map_jira_to_regscale_issue(
677
+ jira_issue=jira_issue,
678
+ config=app.config,
679
+ parent_id=parent_id,
680
+ parent_module=parent_module,
681
+ is_poam=is_poam,
682
+ )
683
+
684
+ if regscale_issue := issue.create():
685
+ logger.debug(
686
+ "Created issue #%i-%s in RegScale.",
687
+ regscale_issue.id,
688
+ regscale_issue.title,
689
+ )
690
+ return regscale_issue
691
+ else:
692
+ logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
693
+ return None
694
+
695
+
696
+ def _apply_custom_fields_and_update_issue(regscale_issue: Issue, app: "Application", jira_issue: jiraIssue) -> None:
697
+ """
698
+ Apply custom field mappings and update a RegScale issue
699
+
700
+ :param Issue regscale_issue: The RegScale issue to update
701
+ :param Application app: RegScale application object
702
+ :param jiraIssue jira_issue: The Jira issue to get data from
703
+ :rtype: None
704
+ """
705
+ if custom_fields := app.config.get("jiraCustomFields", {}):
706
+ apply_custom_fields_to_regscale_object(regscale_issue, custom_fields, jira_issue)
707
+ updated_regscale_issues.append(regscale_issue.save())
708
+
709
+
710
+ def create_and_update_regscale_issues(jira_issue: jiraIssue, *args, **_) -> None:
663
711
  """
664
712
  Function to create or update issues in RegScale from Jira
665
713
 
666
- :param Tuple args: Tuple of args to use during the process
667
- :param int thread: Thread number of current thread
714
+ :param jiraIssue jira_issue: Jira issue to create or update in RegScale
715
+ :param args: Additional arguments
668
716
  :rtype: None
669
717
  """
670
- # set up local variables from the passed args
671
- (jira_issues, regscale_issues, add_attachments, jira_client, app, parent_id, parent_module, task, progress) = args
718
+ # set up local variables from the passed args Tuple
719
+ (regscale_issues, use_poams, add_attachments, jira_client, app, parent_id, parent_module, task, progress) = args
672
720
  # find which records should be executed by the current thread
673
- threads = thread_assignment(thread=thread, total_items=len(jira_issues))
674
721
 
675
- # iterate through the thread assignment items and process them
676
- for i in range(len(threads)):
677
- jira_issue: jiraIssue = jira_issues[threads[i]]
678
- regscale_issue: Optional[Issue] = next(
679
- (issue for issue in regscale_issues if issue.jiraId == jira_issue.key), None
722
+ regscale_issue: Optional[Issue] = next((issue for issue in regscale_issues if issue.jiraId == jira_issue.key), None)
723
+ if regscale_issue:
724
+ regscale_issue.isPoam = use_poams
725
+
726
+ # Process the Jira issue based on its status and existing RegScale issue
727
+ if jira_issue.fields.status.name.lower() == "done" and regscale_issue:
728
+ regscale_issue.status = "Closed"
729
+ regscale_issue.dateCompleted = get_current_datetime()
730
+ _apply_custom_fields_and_update_issue(regscale_issue, app, jira_issue)
731
+ elif regscale_issue:
732
+ _apply_custom_fields_and_update_issue(regscale_issue, app, jira_issue)
733
+ else:
734
+ regscale_issue = _create_new_regscale_issue(jira_issue, app, parent_id, parent_module, use_poams)
735
+ if regscale_issue:
736
+ new_regscale_issues.append(regscale_issue)
737
+
738
+ # Handle attachments if needed
739
+ if add_attachments and regscale_issue and jira_issue.fields.attachment:
740
+ compare_files_for_dupes_and_upload(
741
+ jira_issue=jira_issue,
742
+ regscale_object=regscale_issue,
743
+ jira_client=jira_client,
744
+ api=Api(),
680
745
  )
681
- # see if the Jira issue needs to be created in RegScale
682
- if jira_issue.fields.status.name.lower() == "done" and regscale_issue:
683
- # update the status and date completed of the RegScale issue
684
- regscale_issue.status = "Closed"
685
- regscale_issue.dateCompleted = get_current_datetime()
686
- # update the issue in RegScale
687
- updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
688
- elif regscale_issue:
689
- # update the issue in RegScale
690
- updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
691
- else:
692
- # map the jira issue to a RegScale issue object
693
- issue = map_jira_to_regscale_issue(
694
- jira_issue=jira_issue,
695
- config=app.config,
696
- parent_id=parent_id,
697
- parent_module=parent_module,
698
- )
699
- # create the issue in RegScale
700
- if regscale_issue := Issue.insert_issue(
701
- app=app,
702
- issue=issue,
703
- ):
704
- logger.debug(
705
- "Created issue #%i-%s in RegScale.",
706
- regscale_issue.id,
707
- regscale_issue.title,
708
- )
709
- new_regscale_issues.append(regscale_issue)
710
- else:
711
- logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
712
- if add_attachments and regscale_issue and jira_issue.fields.attachment:
713
- # determine which attachments need to be uploaded to prevent duplicates by
714
- # getting the hashes of all Jira & RegScale attachments
715
- compare_files_for_dupes_and_upload(
716
- jira_issue=jira_issue,
717
- regscale_object=regscale_issue,
718
- jira_client=jira_client,
719
- api=Api(),
720
- )
721
- # update progress bar
722
- progress.update(task, advance=1)
746
+
747
+ # update progress bar
748
+ progress.update(task, advance=1)
723
749
 
724
750
 
725
751
  def sync_regscale_to_jira(
@@ -730,17 +756,19 @@ def sync_regscale_to_jira(
730
756
  sync_attachments: bool = True,
731
757
  attachments: Optional[dict] = None,
732
758
  api: Optional[Api] = None,
759
+ custom_fields: Optional[dict] = None,
733
760
  ) -> list[Union[Issue, Task]]:
734
761
  """
735
762
  Sync issues or tasks from RegScale to Jira
736
763
 
737
- :param list[Union[Issue, Task]] regscale_issues: list of RegScale issues or tasks to sync to Jira
764
+ :param list[Union[Issue, Task]] regscale_objects: list of RegScale issues or tasks to sync to Jira
738
765
  :param JIRA jira_client: Jira client to use for issue creation in Jira
739
766
  :param str jira_project: Jira Project to create the issues in
740
767
  :param str jira_issue_type: Type of issue to create in Jira
741
768
  :param bool sync_attachments: Sync attachments from RegScale to Jira, defaults to True
742
769
  :param Optional[dict] attachments: Dict of attachments to sync from RegScale to Jira, defaults to None
743
770
  :param Optional[Api] api: API object to download attachments, defaults to None
771
+ :param Optional[dict] custom_fields: Custom field mappings from Jira custom fields to RegScale issue fields, defaults to None
744
772
  :return: list of RegScale issues or tasks that need to be updated
745
773
  :rtype: list[Union[Issue, Task]]
746
774
  """
@@ -768,6 +796,7 @@ def sync_regscale_to_jira(
768
796
  add_attachments=sync_attachments,
769
797
  attachments=attachments,
770
798
  api=api,
799
+ custom_fields=custom_fields,
771
800
  )
772
801
  # log progress
773
802
  new_issue_counter += 1
@@ -790,7 +819,7 @@ def fetch_jira_objects(
790
819
  jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, sync_tasks_only: bool = False
791
820
  ) -> list[jiraIssue]:
792
821
  """
793
- Fetch all issues from Jira for the provided project
822
+ Fetch all issues from Jira for the provided project using the enhanced search API.
794
823
 
795
824
  :param JIRA jira_client: Jira client to use for the request
796
825
  :param str jira_project: Name of the project in Jira
@@ -800,15 +829,77 @@ def fetch_jira_objects(
800
829
  :return: List of Jira issues
801
830
  :rtype: list[jiraIssue]
802
831
  """
803
- start_pointer = 0
804
- page_size = 100
805
- jira_objects = []
806
832
  if sync_tasks_only:
807
833
  validate_issue_type(jira_client, jira_issue_type)
808
834
  output_str = "task"
809
835
  else:
810
836
  output_str = "issue"
811
837
  logger.info("Fetching %s(s) from Jira...", output_str.lower())
838
+ try:
839
+ max_results = 100 # 100 is the max allowed by Jira
840
+ jira_issues = []
841
+ issue_response = jira_client.enhanced_search_issues(
842
+ jql_str=jql_str or f"project = '{jira_project}'",
843
+ maxResults=max_results,
844
+ )
845
+ jira_issues.extend(issue_response)
846
+ logger.info(
847
+ "%i Jira %s(s) retrieved.",
848
+ len(jira_issues),
849
+ output_str.lower(),
850
+ )
851
+ # Handle pagination if there are more issues to fetch
852
+ while issue_response.nextPageToken:
853
+ issue_response = jira_client.enhanced_search_issues(
854
+ jql_str=jql_str, maxResults=max_results, nextPageToken=issue_response.nextPageToken
855
+ )
856
+ jira_issues.extend(issue_response)
857
+ logger.info(
858
+ "%i Jira %s(s) retrieved.",
859
+ len(jira_issues),
860
+ output_str.lower(),
861
+ )
862
+ # Save artifacts file and log final result if we have issues
863
+ if jira_issues:
864
+ save_jira_issues(jira_issues, jira_project, jira_issue_type)
865
+ logger.info("%i %s(s) retrieved from Jira.", len(jira_issues), output_str.lower())
866
+ return jira_issues
867
+ except Exception as e:
868
+ logger.warning(
869
+ "An error occurred while fetching Jira issues using the enhanced_search_issues method: %s", str(e)
870
+ )
871
+ logger.info("Falling back to the deprecated fetch method...")
872
+
873
+ try:
874
+ return deprecated_fetch_jira_objects(
875
+ jira_client=jira_client,
876
+ jira_project=jira_project,
877
+ jira_issue_type=jira_issue_type,
878
+ jql_str=jql_str,
879
+ output_str=output_str,
880
+ )
881
+ except JIRAError as e:
882
+ error_and_exit(f"Unable to fetch issues from Jira: {e}")
883
+
884
+
885
+ def deprecated_fetch_jira_objects(
886
+ jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, output_str: str = "issue"
887
+ ) -> list[jiraIssue]:
888
+ """
889
+ Fetch all issues from Jira for the provided project using the old API method, used as a fallback method.
890
+
891
+ :param JIRA jira_client: Jira client to use for the request
892
+ :param str jira_project: Name of the project in Jira
893
+ :param str jira_issue_type: Type of issue to fetch from Jira
894
+ :param str jql_str: JQL string to use for the request, default None
895
+ :param str output_str: String to use for logging, either "issue" or "task"
896
+ :return: List of Jira issues
897
+ :rtype: list[jiraIssue]
898
+ """
899
+ start_pointer = 0
900
+ page_size = 100
901
+ jira_objects = []
902
+ logger.info("Fetching %s(s) from Jira...", output_str.lower())
812
903
  # get all issues for the Jira project
813
904
  while True:
814
905
  start = start_pointer * page_size
@@ -829,25 +920,39 @@ def fetch_jira_objects(
829
920
  output_str.lower(),
830
921
  )
831
922
  if jira_objects:
832
- check_file_path("artifacts")
833
- file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
834
- file_path = Path(f"./artifacts/{file_name}")
835
- save_data_to(
836
- file=file_path,
837
- data=[issue.raw for issue in jira_objects],
838
- output_log=False,
839
- )
840
- logger.info(
841
- "Saved %i Jira %s(s), see %s",
842
- len(jira_objects),
843
- jira_issue_type.lower(),
844
- str(file_path.absolute()),
845
- )
923
+ save_jira_issues(jira_objects, jira_project, jira_issue_type)
846
924
  logger.info("%i %s(s) retrieved from Jira.", len(jira_objects), output_str.lower())
847
925
  return jira_objects
848
926
 
849
927
 
850
- def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str) -> Issue:
928
+ def save_jira_issues(jira_issues: list[jiraIssue], jira_project: str, jira_issue_type: str) -> None:
929
+ """
930
+ Save Jira issues to a JSON file in the artifacts directory
931
+
932
+ :param list[jiraIssue] jira_issues: List of Jira issues to save
933
+ :param str jira_project: Name of the project in Jira
934
+ :param str jira_issue_type: Type of issue to fetch from Jira
935
+ :rtype: None
936
+ """
937
+ check_file_path("artifacts")
938
+ file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
939
+ file_path = Path(f"./artifacts/{file_name}")
940
+ save_data_to(
941
+ file=file_path,
942
+ data=[issue.raw for issue in jira_issues],
943
+ output_log=False,
944
+ )
945
+ logger.info(
946
+ "Saved %i Jira %s(s), see %s",
947
+ len(jira_issues),
948
+ jira_issue_type.lower(),
949
+ str(file_path.absolute()),
950
+ )
951
+
952
+
953
+ def map_jira_to_regscale_issue(
954
+ jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str, is_poam: Optional[bool] = False
955
+ ) -> Issue:
851
956
  """
852
957
  Map Jira issues to RegScale issues
853
958
 
@@ -855,6 +960,7 @@ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: i
855
960
  :param dict config: Application config
856
961
  :param int parent_id: Parent record ID in RegScale
857
962
  :param str parent_module: Parent record module in RegScale
963
+ :param bool is_poam: Whether to create the issue as a POAM in RegScale
858
964
  :return: Issue object of the newly created issue in RegScale
859
965
  :rtype: Issue
860
966
  """
@@ -871,11 +977,20 @@ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: i
871
977
  ),
872
978
  status=("Closed" if jira_issue.fields.status.name.lower() == "done" else config["issues"]["jira"]["status"]),
873
979
  jiraId=jira_issue.key,
980
+ identification="Jira Sync",
981
+ sourceReport="Jira",
874
982
  parentId=parent_id,
875
983
  parentModule=parent_module,
876
984
  dateCreated=get_current_datetime(),
877
985
  dateCompleted=(get_current_datetime() if jira_issue.fields.status.name.lower() == "done" else None),
986
+ isPoam=is_poam,
878
987
  )
988
+
989
+ # Apply custom field mappings from Jira to RegScale
990
+ custom_fields = config.get("jiraCustomFields", {})
991
+ if custom_fields:
992
+ apply_custom_fields_to_regscale_object(issue, custom_fields, jira_issue)
993
+
879
994
  return issue
880
995
 
881
996
 
@@ -929,6 +1044,7 @@ def create_issue_in_jira(
929
1044
  add_attachments: Optional[bool] = True,
930
1045
  attachments: Optional[dict] = None,
931
1046
  api: Optional[Api] = None,
1047
+ custom_fields: Optional[dict] = None,
932
1048
  ) -> jiraIssue:
933
1049
  """
934
1050
  Create a new issue in Jira
@@ -940,6 +1056,7 @@ def create_issue_in_jira(
940
1056
  :param Optional[bool] add_attachments: Whether to add attachments to new issue, defaults to true
941
1057
  :param Optional[dict] attachments: Dictionary containing attachments, defaults to None
942
1058
  :param Optional[Api] api: API object to download attachments, defaults to None
1059
+ :param Optional[dict] custom_fields: Custom field mappings from Jira custom fields to RegScale issue fields, defaults to None
943
1060
  :return: Newly created issue in Jira
944
1061
  :rtype: jiraIssue
945
1062
  """
@@ -955,6 +1072,11 @@ def create_issue_in_jira(
955
1072
  issuetype=issue_type,
956
1073
  )
957
1074
  logger.debug("Jira issue created: %s", new_issue.key)
1075
+
1076
+ # Apply custom field mappings if provided
1077
+ if custom_fields:
1078
+ apply_custom_fields_to_jira_issue(new_issue, custom_fields, regscale_object)
1079
+
958
1080
  # add a comment to the new Jira issue
959
1081
  logger.debug("Adding comment to Jira issue: %s", new_issue.key)
960
1082
  _ = jira_client.add_comment(
@@ -1214,3 +1336,217 @@ def download_regscale_attachments_to_directory(
1214
1336
  )
1215
1337
  )
1216
1338
  return jira_dir, regscale_dir
1339
+
1340
+
1341
+ def apply_custom_fields_to_jira_issue(
1342
+ jira_issue: jiraIssue, custom_fields: dict, regscale_object: Union[Issue, Task]
1343
+ ) -> None:
1344
+ """
1345
+ Apply custom field mappings to a Jira issue based on RegScale object attributes (RegScale -> Jira)
1346
+
1347
+ :param jiraIssue jira_issue: Jira issue to apply custom fields to
1348
+ :param dict custom_fields: Dictionary mapping Jira custom field names to RegScale attribute names
1349
+ :param Union[Issue, Task] regscale_object: RegScale object to get attribute values from
1350
+ :rtype: None
1351
+ """
1352
+ if not custom_fields:
1353
+ return
1354
+
1355
+ try:
1356
+ # Convert RegScale object to dictionary for easier attribute access
1357
+ if hasattr(regscale_object, "model_dump"):
1358
+ regscale_dict = regscale_object.model_dump()
1359
+ elif hasattr(regscale_object, "dict"):
1360
+ regscale_dict = regscale_object.dict()
1361
+ else:
1362
+ regscale_dict = regscale_object.__dict__
1363
+
1364
+ # Build custom fields dictionary for Jira update
1365
+ jira_custom_fields = {}
1366
+
1367
+ for jira_field_name, regscale_field_name in custom_fields.items():
1368
+ try:
1369
+ # Get the value from RegScale object
1370
+ field_value = regscale_dict.get(regscale_field_name)
1371
+
1372
+ if field_value is not None:
1373
+ jira_custom_fields[jira_field_name] = field_value
1374
+ logger.debug(
1375
+ "Mapped custom field %s (RegScale: %s) = %s for Jira issue %s",
1376
+ jira_field_name,
1377
+ regscale_field_name,
1378
+ field_value,
1379
+ jira_issue.key,
1380
+ )
1381
+ else:
1382
+ logger.debug(
1383
+ "Custom field %s (RegScale: %s) has no value, skipping for Jira issue %s",
1384
+ jira_field_name,
1385
+ regscale_field_name,
1386
+ jira_issue.key,
1387
+ )
1388
+ except Exception as e:
1389
+ logger.warning(
1390
+ "Unable to set custom field %s (RegScale: %s) for Jira issue %s: %s",
1391
+ jira_field_name,
1392
+ regscale_field_name,
1393
+ jira_issue.key,
1394
+ str(e),
1395
+ )
1396
+
1397
+ # Update the Jira issue with custom fields if any were found
1398
+ if jira_custom_fields:
1399
+ jira_issue.update(fields=jira_custom_fields)
1400
+ logger.info(
1401
+ "Applied %d custom fields to Jira issue %s: %s",
1402
+ len(jira_custom_fields),
1403
+ jira_issue.key,
1404
+ list(jira_custom_fields.keys()),
1405
+ )
1406
+ else:
1407
+ logger.debug("No custom field values found for Jira issue %s", jira_issue.key)
1408
+
1409
+ except Exception as e:
1410
+ logger.warning(
1411
+ "Error applying custom fields to Jira issue %s: %s",
1412
+ jira_issue.key,
1413
+ str(e),
1414
+ )
1415
+
1416
+
1417
+ def _get_jira_field_value(jira_issue: jiraIssue, jira_field_name: str) -> Optional[Any]:
1418
+ """
1419
+ Get a custom field value from a Jira issue
1420
+
1421
+ :param jiraIssue jira_issue: The Jira issue to get the field value from
1422
+ :param str jira_field_name: The name of the field to retrieve
1423
+ :return: The field value or None if not found
1424
+ :rtype: Optional[Any]
1425
+ """
1426
+ # Try to access the custom field from the Jira issue
1427
+ if hasattr(jira_issue.fields, jira_field_name):
1428
+ return getattr(jira_issue.fields, jira_field_name)
1429
+
1430
+ # Try accessing through raw fields (for custom fields)
1431
+ if hasattr(jira_issue.fields, "raw") and jira_field_name in jira_issue.fields.raw:
1432
+ return jira_issue.fields.raw[jira_field_name]
1433
+
1434
+ return None
1435
+
1436
+
1437
+ def _set_regscale_field_value(
1438
+ regscale_object: Union[Issue, Task],
1439
+ regscale_field_name: str,
1440
+ jira_field_value: Any,
1441
+ jira_field_name: str,
1442
+ jira_issue: jiraIssue,
1443
+ ) -> bool:
1444
+ """
1445
+ Set a field value on a RegScale object
1446
+
1447
+ :param Union[Issue, Task] regscale_object: The RegScale object to set the field on
1448
+ :param str regscale_field_name: The name of the field to set
1449
+ :param Any jira_field_value: The value to set
1450
+ :param str jira_field_name: The Jira field name for logging
1451
+ :param jiraIssue jira_issue: The Jira issue for logging
1452
+ :return: True if the field was set successfully, False otherwise
1453
+ :rtype: bool
1454
+ """
1455
+ if hasattr(regscale_object, regscale_field_name):
1456
+ setattr(regscale_object, regscale_field_name, jira_field_value)
1457
+ logger.debug(
1458
+ "Mapped custom field %s (Jira: %s) = %s for RegScale %s #%s from Jira issue %s",
1459
+ regscale_field_name,
1460
+ jira_field_name,
1461
+ jira_field_value,
1462
+ regscale_object.get_module_string().title(),
1463
+ regscale_object.id,
1464
+ jira_issue.key,
1465
+ )
1466
+ return True
1467
+ else:
1468
+ logger.debug(
1469
+ "RegScale object does not have field %s, skipping custom field %s from Jira issue %s",
1470
+ regscale_field_name,
1471
+ jira_field_name,
1472
+ jira_issue.key,
1473
+ )
1474
+ return False
1475
+
1476
+
1477
+ def _process_single_custom_field(
1478
+ jira_field_name: str, regscale_field_name: str, regscale_object: Union[Issue, Task], jira_issue: jiraIssue
1479
+ ) -> bool:
1480
+ """
1481
+ Process a single custom field mapping from Jira to RegScale
1482
+
1483
+ :param str jira_field_name: The Jira field name
1484
+ :param str regscale_field_name: The RegScale field name
1485
+ :param Union[Issue, Task] regscale_object: The RegScale object to update
1486
+ :param jiraIssue jira_issue: The Jira issue to get data from
1487
+ :return: True if the field was processed successfully, False otherwise
1488
+ :rtype: bool
1489
+ """
1490
+ try:
1491
+ jira_field_value = _get_jira_field_value(jira_issue, jira_field_name)
1492
+
1493
+ if jira_field_value is not None:
1494
+ return _set_regscale_field_value(
1495
+ regscale_object, regscale_field_name, jira_field_value, jira_field_name, jira_issue
1496
+ )
1497
+ else:
1498
+ logger.debug(
1499
+ "Custom field %s has no value in Jira issue %s, skipping",
1500
+ jira_field_name,
1501
+ jira_issue.key,
1502
+ )
1503
+ return False
1504
+ except Exception as e:
1505
+ logger.warning(
1506
+ "Unable to set custom field %s (Jira: %s) for RegScale %s #%s: %s",
1507
+ regscale_field_name,
1508
+ jira_field_name,
1509
+ regscale_object.get_module_string().title(),
1510
+ regscale_object.id,
1511
+ str(e),
1512
+ )
1513
+ return False
1514
+
1515
+
1516
+ def apply_custom_fields_to_regscale_object(
1517
+ regscale_object: Union[Issue, Task], custom_fields: dict, jira_issue: jiraIssue
1518
+ ) -> None:
1519
+ """
1520
+ Apply custom field mappings to a RegScale object based on Jira issue custom fields (Jira -> RegScale)
1521
+
1522
+ :param Union[Issue, Task] regscale_object: RegScale object to apply custom fields to
1523
+ :param dict custom_fields: Dictionary mapping Jira custom field names to RegScale attribute names
1524
+ :param jiraIssue jira_issue: Jira issue to get custom field values from
1525
+ :rtype: None
1526
+ """
1527
+ if not custom_fields:
1528
+ return
1529
+
1530
+ try:
1531
+ fields_updated = False
1532
+
1533
+ for jira_field_name, regscale_field_name in custom_fields.items():
1534
+ if _process_single_custom_field(jira_field_name, regscale_field_name, regscale_object, jira_issue):
1535
+ fields_updated = True
1536
+
1537
+ if fields_updated:
1538
+ logger.info(
1539
+ "Applied custom fields from Jira issue %s to RegScale %s #%s",
1540
+ jira_issue.key,
1541
+ regscale_object.get_module_string().title(),
1542
+ regscale_object.id,
1543
+ )
1544
+
1545
+ except Exception as e:
1546
+ logger.warning(
1547
+ "Error applying custom fields from Jira issue %s to RegScale %s #%s: %s",
1548
+ jira_issue.key,
1549
+ regscale_object.get_module_string().title(),
1550
+ regscale_object.id,
1551
+ str(e),
1552
+ )