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
@@ -84,6 +84,35 @@ class IssueStatus(str, Enum):
84
84
  return self.value
85
85
 
86
86
 
87
+ class IssueIdentification(str, Enum):
88
+ """Issue Identification"""
89
+
90
+ A123Review = "A-123 Review"
91
+ AssessmentAuditInternal = "Assessment/Audit (Internal)"
92
+ AssessmentAuditExternal = "Assessment/Audit (External)"
93
+ CriticalControlReview = "Critical Control Review"
94
+ FDCCUSGCB = "FDCC/USGCB"
95
+ GAOAudit = "GAO Audit"
96
+ IGAudit = "IG Audit"
97
+ IncidentResponseLessonsLearned = "Incident Response Lessons Learned"
98
+ ITAR = "ITAR"
99
+ PenetrationTest = "Penetration Test"
100
+ RiskAssessment = "Risk Assessment"
101
+ SecurityAuthorization = "Security Authorization"
102
+ SecurityControlAssessment = "Security Control Assessment"
103
+ VulnerabilityAssessment = "Vulnerability Assessment"
104
+ Other = "Other"
105
+
106
+ def __str__(self) -> str:
107
+ """
108
+ Return the value of the Enum as a string
109
+
110
+ :return: The value of the Enum as a string
111
+ :rtype: str
112
+ """
113
+ return self.value
114
+
115
+
87
116
  class Issue(RegScaleModel):
88
117
  """Issue Model"""
89
118
 
@@ -455,6 +484,7 @@ class Issue(RegScaleModel):
455
484
  key: str,
456
485
  start_date: Union[str, datetime.datetime, None] = None,
457
486
  dt_format: Optional[str] = "%Y-%m-%dT%H:%M:%S",
487
+ default_days: Optional[int] = 60,
458
488
  ) -> str:
459
489
  """
460
490
  Function to return due date based on the severity of the issue; the values are in the init.yaml
@@ -465,6 +495,7 @@ class Issue(RegScaleModel):
465
495
  :param str key: The key to use for init.yaml from the issues section to determine the due date
466
496
  :param Union[str, datetime.datetime, None] start_date: The date to start the due date calculation from, defaults to current date
467
497
  :param Optional[str] dt_format: String of the date format to use, defaults to "%Y-%m-%dT%H:%M:%S"
498
+ :param Optional[int] default_days: Default number of days to return if no values match, defaults to 60
468
499
  :return: Due date for the issue
469
500
  :rtype: str
470
501
  """
@@ -479,19 +510,18 @@ class Issue(RegScaleModel):
479
510
  if isinstance(severity, IssueSeverity):
480
511
  severity = severity.value
481
512
  elif severity.lower() not in [severity.value.lower() for severity in IssueSeverity]:
482
- severity = IssueSeverity.NotAssigned.value
513
+ severity = cls.assign_severity(severity)
483
514
 
484
515
  if severity == IssueSeverity.Critical.value:
485
- days = cls._get_days_for_values(["critical"], config, key)
486
- start_date = start_date + datetime.timedelta(days=days)
516
+ days = cls._get_days_for_values(["critical"], config, key, default_days)
487
517
  elif severity == IssueSeverity.High.value:
488
- days = cls._get_days_for_values(["high"], config, key)
518
+ days = cls._get_days_for_values(["high"], config, key, default_days)
489
519
  elif severity == IssueSeverity.Moderate.value:
490
- days = cls._get_days_for_values(["moderate", "medium"], config, key)
520
+ days = cls._get_days_for_values(["moderate", "medium"], config, key, default_days)
491
521
  elif severity == IssueSeverity.Low.value:
492
- days = cls._get_days_for_values(["low", "minor"], config, key)
522
+ days = cls._get_days_for_values(["low", "minor"], config, key, default_days)
493
523
  else:
494
- days = 60
524
+ days = default_days
495
525
  due_date = start_date + datetime.timedelta(days=days)
496
526
  return due_date.strftime(dt_format)
497
527
 
@@ -526,6 +556,7 @@ class Issue(RegScaleModel):
526
556
  "low": IssueSeverity.Low.value,
527
557
  "moderate": IssueSeverity.Moderate.value,
528
558
  "high": IssueSeverity.High.value,
559
+ "critical": IssueSeverity.Critical.value,
529
560
  }
530
561
  severity = IssueSeverity.NotAssigned.value
531
562
  # see if the value is an int or float
@@ -542,8 +573,10 @@ class Issue(RegScaleModel):
542
573
  severity = severity_levels["low"]
543
574
  elif value.lower() in ["medium", "moderate", "major"]:
544
575
  severity = severity_levels["moderate"]
545
- elif value.lower() in ["high", "critical", "highest", "critical", "blocker"]:
576
+ elif value.lower() in ["high", "highest", "blocker"]:
546
577
  severity = severity_levels["high"]
578
+ elif value.lower() in ["critical"]:
579
+ severity = severity_levels["critical"]
547
580
  elif value in list(severity_levels.values()):
548
581
  severity = value
549
582
  return severity
@@ -914,103 +947,285 @@ class Issue(RegScaleModel):
914
947
  """
915
948
  import logging
916
949
 
917
- cache_disabled = cls._is_cache_disabled()
918
- use_cache: bool = not cache_disabled
919
-
920
950
  logger = logging.getLogger("regscale")
951
+
921
952
  # Check cache first
922
- if use_cache:
923
- cached_data = cls._get_from_cache(plan_id)
924
- if cached_data is not None:
925
- logger.info(f"Using cached open issues data for security plan {plan_id}")
926
- return cached_data
927
-
928
- # Performance optimization: Use larger batch size and optimize query
929
- take = 50 # Increased from 50 to reduce API roundtrips
930
- skip = 0
931
- control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
953
+ cached_data = cls._check_cache(plan_id, logger)
954
+ if cached_data is not None:
955
+ return cached_data
932
956
 
933
- start_time = time.time()
934
- logger.info(f"Fetching open issues for controls and for security plan {plan_id}...")
957
+ # Fetch open issues from API
958
+ control_issues = cls._fetch_open_issues_from_api(plan_id, is_component, logger)
959
+
960
+ # Cache the results if caching is enabled
961
+ if not cls._is_cache_disabled():
962
+ cls._cache_data(plan_id, control_issues)
963
+
964
+ return control_issues
965
+
966
+ @classmethod
967
+ def _check_cache(cls, plan_id: int, logger) -> Optional[Dict[int, List[OpenIssueDict]]]:
968
+ """
969
+ Check cache for open issues data
970
+
971
+ :param int plan_id: The ID of the parent
972
+ :param logger: Logger instance
973
+ :return: Cached data if available and valid, None otherwise
974
+ :rtype: Optional[Dict[int, List[OpenIssueDict]]]
975
+ """
976
+ if cls._is_cache_disabled():
977
+ return None
978
+
979
+ cached_data = cls._get_from_cache(plan_id)
980
+ if cached_data is not None:
981
+ logger.info(f"Using cached open issues data for security plan {plan_id}")
982
+ return cached_data
983
+
984
+ @classmethod
985
+ def _fetch_open_issues_from_api(cls, plan_id: int, is_component: bool, logger) -> Dict[int, List[OpenIssueDict]]:
986
+ """
987
+ Fetch open issues from API with pagination
935
988
 
989
+ :param int plan_id: The ID of the parent
990
+ :param bool is_component: Whether parent is a component
991
+ :param logger: Logger instance
992
+ :return: Dictionary of control IDs to open issues
993
+ :rtype: Dict[int, List[OpenIssueDict]]
994
+ """
995
+ start_time = time.time()
936
996
  module_str = "component" if is_component else "security plan"
937
- logger.info(
938
- f"Fetching open issues for controls and for {module_str} {plan_id}...",
939
- )
940
- supports_multiple_controls: bool = cls.is_multiple_controls_supported()
997
+ logger.info(f"Fetching open issues for controls and for {module_str} {plan_id}...")
998
+
999
+ control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
1000
+
1001
+ try:
1002
+ total_fetched = cls._paginate_and_process_issues(plan_id, is_component, control_issues, logger)
1003
+ cls._log_completion(plan_id, total_fetched, len(control_issues), start_time, logger)
1004
+ except Exception as e:
1005
+ logger.error(f"Error fetching open issues for security plan {plan_id}: {e}")
1006
+ return defaultdict(list)
1007
+
1008
+ return control_issues
1009
+
1010
+ @classmethod
1011
+ def _paginate_and_process_issues(
1012
+ cls,
1013
+ plan_id: int,
1014
+ is_component: bool,
1015
+ control_issues: Dict[int, List[OpenIssueDict]],
1016
+ logger,
1017
+ ) -> int:
1018
+ """
1019
+ Paginate through API results and process issues using concurrent requests
1020
+
1021
+ :param int plan_id: The ID of the parent
1022
+ :param bool is_component: Whether parent is a component
1023
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate with results
1024
+ :param logger: Logger instance
1025
+ :return: Total number of items fetched
1026
+ :rtype: int
1027
+ """
1028
+ take = 50
1029
+ supports_multiple_controls = cls.is_multiple_controls_supported()
1030
+ fields = cls._get_query_fields(supports_multiple_controls)
1031
+
1032
+ # First fetch to get total count
1033
+ query = cls._build_query(plan_id, is_component, 0, take, fields)
1034
+ response = cls._get_api_handler().graph(query)
1035
+
1036
+ items = response.get(cls.get_module_string(), {}).get("items", [])
1037
+ total_count = response.get(cls.get_module_string(), {}).get("totalCount", 0)
1038
+
1039
+ # Process first page
1040
+ cls._process_issue_items(items, supports_multiple_controls, control_issues)
1041
+ logger.info("Fetched first page with %d items, total: %d", len(items), total_count)
1042
+
1043
+ # Use concurrent pagination for remaining pages if there are more
1044
+ if total_count > take:
1045
+ logger.info("Fetching remaining pages concurrently...")
1046
+ from regscale.core.utils.async_graphql_client import run_async_paginated_query
1047
+
1048
+ # Get API credentials
1049
+ api_handler = cls._get_api_handler()
1050
+ domain = api_handler.config.get("domain", "")
1051
+ token = api_handler.config.get("token", "")
1052
+ endpoint = f"{domain}/graphql"
1053
+ headers = {"Authorization": f"{token}"}
1054
+
1055
+ # Create query builder
1056
+ def query_builder(skip: int, page_size: int) -> str:
1057
+ return cls._build_query(plan_id, is_component, skip, page_size, fields)
1058
+
1059
+ # Define token refresh callback
1060
+ def token_refresh() -> str:
1061
+ # Re-read token from config in case it was refreshed
1062
+ return api_handler.config.get("token", "")
1063
+
1064
+ # Fetch remaining pages concurrently
1065
+ remaining_items = run_async_paginated_query(
1066
+ endpoint=endpoint,
1067
+ headers=headers,
1068
+ query_builder=query_builder,
1069
+ topic_key=cls.get_module_string(),
1070
+ total_count=total_count - take, # Subtract first page
1071
+ page_size=take,
1072
+ starting_skip=take, # Start from second page
1073
+ max_concurrent=5,
1074
+ timeout=60,
1075
+ task_name="Open Issues",
1076
+ token_refresh_callback=token_refresh,
1077
+ )
1078
+
1079
+ # Process remaining items
1080
+ for batch_items in [remaining_items[i : i + 100] for i in range(0, len(remaining_items), 100)]:
1081
+ cls._process_issue_items(batch_items, supports_multiple_controls, control_issues)
1082
+ logger.info("Fetched %d items from remaining pages", len(remaining_items))
1083
+
1084
+ return total_count
1085
+
1086
+ @classmethod
1087
+ def _get_query_fields(cls, supports_multiple_controls: bool) -> str:
1088
+ """
1089
+ Get GraphQL query fields based on control support
941
1090
 
942
- # Optimize field selection - only get what we need
1091
+ :param bool supports_multiple_controls: Whether multiple controls are supported
1092
+ :return: GraphQL field selection string
1093
+ :rtype: str
1094
+ """
943
1095
  if supports_multiple_controls:
944
- fields = "id, otherIdentifier, integrationFindingId, controlImplementations { id }"
945
- else:
946
- fields = "id, controlId, otherIdentifier, integrationFindingId"
1096
+ return "id, otherIdentifier, integrationFindingId, controlImplementations { id }"
1097
+ return "id, controlId, otherIdentifier, integrationFindingId"
947
1098
 
948
- total_fetched = 0
1099
+ @classmethod
1100
+ def _build_query(cls, plan_id: int, is_component: bool, skip: int, take: int, fields: str) -> str:
1101
+ """
1102
+ Build GraphQL query for fetching open issues
949
1103
 
950
- try:
951
- while True:
952
- query = f"""
953
- query GetOpenIssuesByPlanOrComponent {{
954
- {cls.get_module_string()}(
955
- skip: {skip},
956
- take: {take},
957
- where: {{
958
- {"componentId" if is_component else "securityPlanId"}: {{eq: {plan_id}}},
959
- status: {{eq: "Open"}}
960
- }}
961
- ) {{
962
- items {{ {fields} }}
963
- pageInfo {{ hasNextPage }}
964
- totalCount
965
- }}
1104
+ :param int plan_id: The ID of the parent
1105
+ :param bool is_component: Whether parent is a component
1106
+ :param int skip: Number of items to skip
1107
+ :param int take: Number of items to take
1108
+ :param str fields: GraphQL fields to select
1109
+ :return: GraphQL query string
1110
+ :rtype: str
1111
+ """
1112
+ parent_field = "componentId" if is_component else "securityPlanId"
1113
+ return f"""
1114
+ query GetOpenIssuesByPlanOrComponent {{
1115
+ {cls.get_module_string()}(
1116
+ skip: {skip},
1117
+ take: {take},
1118
+ where: {{
1119
+ {parent_field}: {{eq: {plan_id}}},
1120
+ status: {{eq: "Open"}}
966
1121
  }}
967
- """
1122
+ ) {{
1123
+ items {{ {fields} }}
1124
+ pageInfo {{ hasNextPage }}
1125
+ totalCount
1126
+ }}
1127
+ }}
1128
+ """
968
1129
 
969
- response = cls._get_api_handler().graph(query)
970
- items = response.get(cls.get_module_string(), {}).get("items", [])
971
- total_count = response.get(cls.get_module_string(), {}).get("totalCount", 0)
1130
+ @classmethod
1131
+ def _log_progress(cls, skip: int, take: int, items_count: int, total_count: int, logger) -> None:
1132
+ """
1133
+ Log progress for large datasets
972
1134
 
973
- # Log progress for large datasets
974
- if total_count > 1000:
975
- logger.info(
976
- f"Processing batch {skip // take + 1} - fetched {len(items)} items ({skip + len(items)}/{total_count})"
977
- )
1135
+ :param int skip: Number of items skipped
1136
+ :param int take: Batch size
1137
+ :param int items_count: Number of items in current batch
1138
+ :param int total_count: Total count of items
1139
+ :param logger: Logger instance
1140
+ :rtype: None
1141
+ """
1142
+ if total_count > 1000:
1143
+ logger.info(
1144
+ f"Processing batch {skip // take + 1} - fetched {items_count} items ({skip + items_count}/{total_count})"
1145
+ )
978
1146
 
979
- for item in items:
980
- issue_dict = OpenIssueDict(
981
- id=item["id"],
982
- otherIdentifier=item.get("otherIdentifier", ""),
983
- integrationFindingId=item.get("integrationFindingId", ""),
984
- )
1147
+ @classmethod
1148
+ def _process_issue_items(
1149
+ cls,
1150
+ items: List[Dict[str, Any]],
1151
+ supports_multiple_controls: bool,
1152
+ control_issues: Dict[int, List[OpenIssueDict]],
1153
+ ) -> None:
1154
+ """
1155
+ Process issue items and populate control_issues dictionary
985
1156
 
986
- if supports_multiple_controls and item.get("controlImplementations"):
987
- for control in item.get("controlImplementations", []):
988
- control_issues[control["id"]].append(issue_dict)
989
- elif item.get("controlId"):
990
- control_issues[item["controlId"]].append(issue_dict)
1157
+ :param List[Dict[str, Any]] items: List of issue items from API
1158
+ :param bool supports_multiple_controls: Whether multiple controls are supported
1159
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1160
+ :rtype: None
1161
+ """
1162
+ for item in items:
1163
+ issue_dict = OpenIssueDict(
1164
+ id=item["id"],
1165
+ otherIdentifier=item.get("otherIdentifier", ""),
1166
+ integrationFindingId=item.get("integrationFindingId", ""),
1167
+ )
991
1168
 
992
- total_fetched += len(items)
993
- if not response.get(cls.get_module_string(), {}).get("pageInfo", {}).get("hasNextPage", False):
994
- break
995
- skip += take
1169
+ if supports_multiple_controls:
1170
+ cls._add_issue_to_multiple_controls(item, issue_dict, control_issues)
1171
+ else:
1172
+ cls._add_issue_to_single_control(item, issue_dict, control_issues)
996
1173
 
997
- except Exception as e:
998
- logger.error(f"Error fetching open issues for security plan {plan_id}: {e}")
999
- # Return empty dict on error to prevent breaking the calling code
1000
- return defaultdict(list)
1174
+ @classmethod
1175
+ def _add_issue_to_multiple_controls(
1176
+ cls,
1177
+ item: Dict[str, Any],
1178
+ issue_dict: OpenIssueDict,
1179
+ control_issues: Dict[int, List[OpenIssueDict]],
1180
+ ) -> None:
1181
+ """
1182
+ Add issue to multiple control implementations
1001
1183
 
1184
+ :param Dict[str, Any] item: Issue item from API
1185
+ :param OpenIssueDict issue_dict: Issue dictionary
1186
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1187
+ :rtype: None
1188
+ """
1189
+ if item.get("controlImplementations"):
1190
+ for control in item.get("controlImplementations", []):
1191
+ control_issues[control["id"]].append(issue_dict)
1192
+
1193
+ @classmethod
1194
+ def _add_issue_to_single_control(
1195
+ cls,
1196
+ item: Dict[str, Any],
1197
+ issue_dict: OpenIssueDict,
1198
+ control_issues: Dict[int, List[OpenIssueDict]],
1199
+ ) -> None:
1200
+ """
1201
+ Add issue to single control
1202
+
1203
+ :param Dict[str, Any] item: Issue item from API
1204
+ :param OpenIssueDict issue_dict: Issue dictionary
1205
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1206
+ :rtype: None
1207
+ """
1208
+ if item.get("controlId"):
1209
+ control_issues[item["controlId"]].append(issue_dict)
1210
+
1211
+ @classmethod
1212
+ def _log_completion(cls, plan_id: int, total_fetched: int, control_count: int, start_time: float, logger) -> None:
1213
+ """
1214
+ Log completion statistics
1215
+
1216
+ :param int plan_id: The ID of the parent
1217
+ :param int total_fetched: Total number of items fetched
1218
+ :param int control_count: Number of controls with issues
1219
+ :param float start_time: Start time of the operation
1220
+ :param logger: Logger instance
1221
+ :rtype: None
1222
+ """
1002
1223
  elapsed_time = time.time() - start_time
1003
1224
  logger.info(
1004
- f"Finished fetching {total_fetched} open issue(s) for {len(control_issues)} control(s) "
1225
+ f"Finished fetching {total_fetched} open issue(s) for {control_count} control(s) "
1005
1226
  f"in security plan {plan_id} - took {elapsed_time:.2f} seconds"
1006
1227
  )
1007
1228
 
1008
- # Cache the results
1009
- if use_cache:
1010
- cls._cache_data(plan_id, control_issues)
1011
-
1012
- return control_issues
1013
-
1014
1229
  @classmethod
1015
1230
  def get_sort_position_dict(cls) -> Dict[str, int]:
1016
1231
  """
@@ -1100,36 +1315,20 @@ class Issue(RegScaleModel):
1100
1315
  }
1101
1316
 
1102
1317
  @classmethod
1103
- def get_enum_values(cls, field_name: str) -> List[Union[IssueSeverity, IssueStatus, str]]:
1318
+ def get_enum_values(cls, field_name: str) -> List[Union[IssueSeverity, IssueStatus, IssueIdentification, str]]:
1104
1319
  """
1105
1320
  Overrides the base method.
1106
1321
 
1107
1322
  :param str field_name: The property name to provide enum values for
1108
1323
  :return: List of enum values or strings
1109
- :rtype: List[Union[IssueSeverity, IssueStatus, str]]
1324
+ :rtype: List[Union[IssueSeverity, IssueStatus, IssueIdentification, str]]
1110
1325
  """
1111
1326
  if field_name == "severityLevel":
1112
1327
  return [severity.__str__() for severity in IssueSeverity]
1113
1328
  if field_name == "status":
1114
1329
  return [status.__str__() for status in IssueStatus]
1115
1330
  if field_name == "identification":
1116
- return [
1117
- "A-123 Review",
1118
- "Assessment/Audit (External)",
1119
- "Assessment/Audit (Internal)",
1120
- "Critical Control Review",
1121
- "FDCC/USGCB",
1122
- "GAO Audit",
1123
- "IG Audit",
1124
- "Incidnet Response Lessons Learned",
1125
- "ITAR",
1126
- "Other",
1127
- "Penetration Test",
1128
- "Risk Assessment",
1129
- "Security Authorization",
1130
- "Security Control Assessment",
1131
- "Vulnerability Assessment",
1132
- ]
1331
+ return [identification.__str__() for identification in IssueIdentification]
1133
1332
  return cls.get_bool_enums(field_name)
1134
1333
 
1135
1334
  @classmethod
@@ -3,7 +3,8 @@
3
3
  """Class for milestone model in RegScale platform"""
4
4
 
5
5
  from typing import Optional
6
- from pydantic import ConfigDict, Field
6
+
7
+ from pydantic import Field, field_validator, model_validator
7
8
 
8
9
  from regscale.core.app.utils.app_utils import get_current_datetime
9
10
  from regscale.models.regscale_models.regscale_model import RegScaleModel
@@ -14,27 +15,45 @@ class Milestone(RegScaleModel):
14
15
 
15
16
  _module_slug = "milestones"
16
17
  _module_string = "milestones"
18
+ _unique_fields = ["title", "parentModule", "parentID"]
19
+ _parent_id_field = "parentID"
17
20
 
18
21
  title: str
19
22
  id: int = 0
20
23
  isPublic: Optional[bool] = True
21
24
  milestoneDate: Optional[str] = Field(default_factory=get_current_datetime)
22
25
  responsiblePersonId: Optional[str] = None
23
- predecessorStepId: Optional[int] = 0
26
+ predecessorStepId: Optional[int] = None
24
27
  completed: Optional[bool] = False
25
- dateCompleted: Optional[str] = Field(default_factory=get_current_datetime)
28
+ dateCompleted: Optional[str] = None
26
29
  notes: Optional[str] = ""
27
30
  parentID: Optional[int] = None
28
31
  parentModule: str = ""
29
32
 
30
- @staticmethod
31
- def _get_additional_endpoints() -> ConfigDict:
32
- """
33
- Get additional endpoints for the Milestone model
34
-
35
- :return: A dictionary of additional endpoints
36
- :rtype: ConfigDict
37
- """
38
- return ConfigDict(
39
- get_all_by_parent="/api/{model_slug}/getAllByParent/{intParentID}",
40
- )
33
+ @field_validator("milestoneDate")
34
+ @classmethod
35
+ def validate_milestone_date(cls, v: Optional[str]) -> str:
36
+ """Ensure milestoneDate is never empty."""
37
+ if not v or v == "":
38
+ return get_current_datetime()
39
+ return v
40
+
41
+ @field_validator("dateCompleted")
42
+ @classmethod
43
+ def set_date_completed(cls, v: Optional[str], info) -> Optional[str]:
44
+ """Set dateCompleted based on completed field."""
45
+ completed = info.data.get("completed", False)
46
+ if completed and (v is None or v == ""):
47
+ return get_current_datetime()
48
+ if not completed:
49
+ return None
50
+ return v
51
+
52
+ @model_validator(mode="after")
53
+ def validate_completion(self):
54
+ """Ensure dateCompleted is set when completed is True."""
55
+ if self.completed and (self.dateCompleted is None or self.dateCompleted == ""):
56
+ self.dateCompleted = get_current_datetime()
57
+ elif not self.completed:
58
+ self.dateCompleted = None
59
+ return self
@@ -15,7 +15,10 @@ class Organization(RegScaleModel):
15
15
  id: int = 0
16
16
  name: Optional[str] = ""
17
17
  description: Optional[str] = ""
18
+ orgCode: Optional[str] = ""
19
+ orgUrl: Optional[str] = ""
18
20
  status: Optional[str] = "Active"
21
+ externalId: Optional[str] = ""
19
22
 
20
23
  @staticmethod
21
24
  def _get_additional_endpoints() -> dict: