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
@@ -60,6 +60,8 @@ class RegScaleModel(BaseModel, ABC):
60
60
 
61
61
  _pending_updates: ClassVar[Dict[str, Set[int]]] = {}
62
62
  _pending_creations: ClassVar[Dict[str, Set[str]]] = {}
63
+ _session_created_ids: ClassVar[Set[int]] = set() # Track IDs created in current session for dedupe detection
64
+ _ignore_has_changed: bool = False
63
65
 
64
66
  id: int = 0
65
67
  extra_data: Dict[str, Any] = Field(default={}, exclude=True)
@@ -181,6 +183,7 @@ class RegScaleModel(BaseModel, ABC):
181
183
  def _get_cache_key(cls, obj: T, defaults: Optional[Dict[str, Any]] = None) -> str:
182
184
  """
183
185
  Generate a cache key based on the object's unique fields using SHA256 hash.
186
+ Includes parentId to scope cache keys to individual security plans/parents.
184
187
 
185
188
  :param T obj: The object to generate a key for
186
189
  :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply to the object, defaults to None
@@ -188,6 +191,14 @@ class RegScaleModel(BaseModel, ABC):
188
191
  :rtype: str
189
192
  """
190
193
  defaults = defaults or {}
194
+
195
+ # Get parent ID to scope cache keys to individual parents (e.g., security plans)
196
+ # This prevents assets/issues/etc. from being matched across different security plans
197
+ parent_id = getattr(obj, cls._parent_id_field, None)
198
+ # Check if parent_id is None and try defaults if available
199
+ if parent_id is None and defaults:
200
+ parent_id = defaults.get(cls._parent_id_field)
201
+
191
202
  # Iterate over each set of unique fields
192
203
  for fields in cls.get_unique_fields():
193
204
  unique_fields = []
@@ -206,11 +217,12 @@ class RegScaleModel(BaseModel, ABC):
206
217
  # If all fields in the current set have values, use them to generate the cache key
207
218
  if len(unique_fields) == len(fields):
208
219
  unique_string = ":".join(unique_fields)
209
- cache_key = f"{cls.__name__}:{unique_string}"
220
+ # Include parent_id in cache key to scope to individual security plans/parents
221
+ cache_key = f"{cls.__name__}:{parent_id}:{unique_string}"
210
222
  return cache_key
211
223
 
212
224
  # Fallback if no complete set of unique fields is found, use object ID
213
- return f"{cls.__name__}:{obj.get_object_id()}"
225
+ return f"{cls.__name__}:{parent_id}:{obj.get_object_id()}"
214
226
 
215
227
  @classmethod
216
228
  def get_cached_object(cls, cache_key: str) -> Optional[T]:
@@ -502,31 +514,93 @@ class RegScaleModel(BaseModel, ABC):
502
514
  :return: A list of unique fields
503
515
  :rtype: List[List[str]]
504
516
  """
505
- sample_format = {"uniqueOverride": {"asset": ["ipAddress"]}}
506
517
  config = Application().config
507
518
 
519
+ # First, ensure _unique_fields is in new format (List[List[str]])
520
+ cls._ensure_unique_fields_format()
521
+
508
522
  try:
509
523
  primary = config.get("uniqueOverride", {}).get(cls.__name__.lower())
510
524
  if primary:
511
- if not isinstance(primary, list):
512
- raise ValueError(
513
- f"Invalid config format in uniqueOverride.{cls.__name__.lower()}, the configuration must be in a format like so:\n{dump(sample_format, default_flow_style=False)}"
514
- )
515
- if primary != cls._unique_fields:
516
- if all(attr in cls.model_fields for attr in primary):
517
- if primary not in cls._unique_fields:
518
- cls._unique_fields.insert(1, primary)
519
- else:
520
- # Move primary to index 1 if it exists
521
- cls._unique_fields.insert(1, cls._unique_fields.pop(cls._unique_fields.index(primary)))
522
- else:
523
- raise ValueError(
524
- f"One or more invalid attribute(s) detected: {primary}, falling back on default unique fields for type: {cls.__name__.lower()}"
525
- )
525
+ cls._process_primary_override(primary)
526
526
  except ValueError as e:
527
527
  logger.warning(e)
528
528
  return cls._unique_fields
529
529
 
530
+ @classmethod
531
+ def _ensure_unique_fields_format(cls) -> None:
532
+ """
533
+ Ensure _unique_fields is in the new format (List[List[str]]).
534
+
535
+ :rtype: None
536
+ """
537
+ # Check if it's still in old format (List[str])
538
+ if cls._unique_fields and isinstance(cls._unique_fields[0], str):
539
+ # Convert old format to new format
540
+ cls._unique_fields = [cls._unique_fields] # type: ignore
541
+
542
+ @classmethod
543
+ def _process_primary_override(cls, primary: List[str]) -> None:
544
+ """
545
+ Process the primary override configuration.
546
+
547
+ :param List[str] primary: The primary override fields
548
+ :raises ValueError: If the primary fields are invalid
549
+ :rtype: None
550
+ """
551
+ cls._validate_primary_format(primary)
552
+
553
+ # Now cls._unique_fields is guaranteed to be List[List[str]]
554
+ # Check if primary is different from any existing unique field set
555
+ if primary not in cls._unique_fields:
556
+ cls._handle_new_primary_fields(primary)
557
+
558
+ @classmethod
559
+ def _validate_primary_format(cls, primary: List[str]) -> None:
560
+ """
561
+ Validate the format of the primary override configuration.
562
+
563
+ :param List[str] primary: The primary override fields
564
+ :raises ValueError: If the primary format is invalid
565
+ :rtype: None
566
+ """
567
+ if not isinstance(primary, list):
568
+ sample_format = {"uniqueOverride": {"asset": ["ipAddress"]}}
569
+ raise ValueError(
570
+ f"Invalid config format in uniqueOverride.{cls.__name__.lower()}, the configuration must be in a format like so:\n{dump(sample_format, default_flow_style=False)}"
571
+ )
572
+
573
+ @classmethod
574
+ def _handle_new_primary_fields(cls, primary: List[str]) -> None:
575
+ """
576
+ Handle new primary fields that are not in existing unique fields.
577
+
578
+ :param List[str] primary: The primary override fields
579
+ :raises ValueError: If any attributes are invalid
580
+ :rtype: None
581
+ """
582
+ if all(attr in cls.model_fields for attr in primary):
583
+ cls._insert_primary_fields(primary)
584
+ else:
585
+ raise ValueError(
586
+ f"One or more invalid attribute(s) detected: {primary}, falling back on default unique fields for type: {cls.__name__.lower()}"
587
+ )
588
+
589
+ @classmethod
590
+ def _insert_primary_fields(cls, primary: List[str]) -> None:
591
+ """
592
+ Insert primary fields into the unique fields list.
593
+
594
+ :param List[str] primary: The primary override fields
595
+ :rtype: None
596
+ """
597
+ # Check if primary already exists in the list
598
+ if primary not in cls._unique_fields:
599
+ cls._unique_fields.insert(1, primary)
600
+ else:
601
+ # Move primary to index 1 if it exists
602
+ cls._unique_fields.insert(1, cls._unique_fields.pop(cls._unique_fields.index(primary)))
603
+
530
604
  @classmethod
531
605
  def _get_endpoints(cls) -> ConfigDict:
532
606
  """
@@ -612,19 +686,37 @@ class RegScaleModel(BaseModel, ABC):
612
686
  raise NotImplementedError(f"_unique_fields not defined for {self.__class__.__name__}")
613
687
 
614
688
  parent_id = getattr(self, parent_id_field or self._parent_id_field, None)
689
+ logger.debug(
690
+ f"find_by_unique for {self.__class__.__name__}: parent_id={parent_id} (type: {type(parent_id).__name__}), "
691
+ f"parent_id_field={parent_id_field or self._parent_id_field}"
692
+ )
615
693
  if parent_id is None:
616
694
  raise ValueError(f"Parent ID not found for {self.__class__.__name__}")
617
695
 
618
696
  parent_module = getattr(self, "parentModule", getattr(self, "parent_module", ""))
697
+ logger.debug(f"find_by_unique for {self.__class__.__name__}: parent_module={parent_module}")
619
698
  cache_key = self._get_cache_key(self)
620
699
 
621
700
  with self._get_lock(cache_key):
622
701
  # Check cache first
623
702
  if cached_object := self.get_cached_object(cache_key):
703
+ logger.debug(f"find_by_unique for {self.__class__.__name__}: Found in cache")
624
704
  return cached_object
625
705
 
626
706
  # Get all instances from parent
707
+ logger.debug(
708
+ f"find_by_unique for {self.__class__.__name__}: Calling get_all_by_parent with "
709
+ f"parent_id={parent_id}, parent_module={parent_module}"
710
+ )
627
711
  instances: List[T] = self.get_all_by_parent(parent_id=parent_id, parent_module=parent_module)
712
+ logger.debug(
713
+ f"find_by_unique for {self.__class__.__name__}: Retrieved {len(instances)} instances from parent_id={parent_id}"
714
+ )
715
+ if instances:
716
+ parent_ids = set(getattr(inst, self._parent_id_field, None) for inst in instances[:10])
717
+ logger.debug(
718
+ f"find_by_unique for {self.__class__.__name__}: Sample parent_ids in results: {parent_ids}"
719
+ )
628
720
 
629
721
  # Try to find matching instance using unique fields
630
722
  for keys in self._unique_fields:
@@ -642,8 +734,14 @@ class RegScaleModel(BaseModel, ABC):
642
734
  None,
643
735
  )
644
736
  if matching_instance:
737
+ matched_parent_id = getattr(matching_instance, self._parent_id_field, None)
738
+ logger.debug(
739
+ f"find_by_unique for {self.__class__.__name__}: Found match using fields {keys}, "
740
+ f"matched instance parent_id={matched_parent_id}, current parent_id={parent_id}"
741
+ )
645
742
  return matching_instance
646
743
 
744
+ logger.debug(f"find_by_unique for {self.__class__.__name__}: No matching instance found")
647
745
  return None
648
746
 
649
747
  def get_or_create_with_status(self: T, bulk: bool = False) -> Tuple[bool, T]:
@@ -665,17 +763,45 @@ class RegScaleModel(BaseModel, ABC):
665
763
  self.cache_object(instance)
666
764
  return False, instance
667
765
  else:
668
- created_instance = self.create(bulk=bulk)
669
- self.cache_object(created_instance)
670
- return True, created_instance
766
+ try:
767
+ created_instance = self.create(bulk=bulk)
768
+ self.cache_object(created_instance)
769
+ return True, created_instance
770
+ except APIInsertionError as e:
771
+ # Check if this is a duplicate error (race condition in threading)
772
+ error_str = str(e).lower()
773
+ if "already exists" in error_str or "mapping already exists" in error_str:
774
+ logger.debug(
775
+ f"Race condition detected for {self.__class__.__name__}, retrying find_by_unique: {e}"
776
+ )
777
+ # Clear the cache to force a fresh lookup
778
+ self.clear_cache()
779
+ # Try to find the instance again - another thread may have created it
780
+ instance = self.find_by_unique()
781
+ if instance:
782
+ self.cache_object(instance)
783
+ logger.debug(
784
+ f"Successfully found existing {self.__class__.__name__} after duplicate creation error, ID: {instance.id}"
785
+ )
786
+ return False, instance
787
+ else:
788
+ # If we still can't find it, log error but don't stop the process
789
+ logger.error(
790
+ f"Failed to find {self.__class__.__name__} after duplicate creation error: {e}"
791
+ )
792
+ # Return None to indicate creation failed but don't raise
793
+ return False, None
794
+ else:
795
+ # Not a duplicate error, re-raise
796
+ logger.error(f"Failed to create object: {self.__class__.__name__} creation error: {e}")
671
797
 
672
- def get_or_create(self: T, bulk: bool = False) -> T:
798
+ def get_or_create(self: T, bulk: bool = False) -> Optional[T]:
673
799
  """
674
800
  Get or create an instance.
675
801
 
676
802
  :param bool bulk: Whether to perform a bulk create operation, defaults to False
677
- :return: The instance
678
- :rtype: T
803
+ :return: The instance or None if creation failed due to race condition
804
+ :rtype: Optional[T]
679
805
  """
680
806
  _, instance = self.get_or_create_with_status(bulk=bulk)
681
807
  return instance
@@ -705,15 +831,15 @@ class RegScaleModel(BaseModel, ABC):
705
831
  bulk_create: bool = False,
706
832
  bulk_update: bool = False,
707
833
  defaults: Optional[Dict[str, Any]] = None,
708
- ) -> Tuple[bool, T]:
834
+ ) -> Tuple[str, T]:
709
835
  """
710
836
  Create or update an instance. Use cache methods to retrieve and store instances based on unique fields.
711
837
 
712
838
  :param bool bulk_create: Whether to perform a bulk create, defaults to False
713
839
  :param bool bulk_update: Whether to perform a bulk update, defaults to False
714
840
  :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply to the instance if it is created, defaults to {}
715
- :return: The instance of the class
716
- :rtype: Tuple[bool, T]
841
+ :return: Tuple of (status, instance) where status is "created", "updated", or "deduped"
842
+ :rtype: Tuple[str, T]
717
843
  """
718
844
  logger.debug(f"Starting create_or_update for {self.__class__.__name__}: #{getattr(self, 'id', '')}")
719
845
 
@@ -727,44 +853,106 @@ class RegScaleModel(BaseModel, ABC):
727
853
  instance = cached_object or self.find_by_unique()
728
854
 
729
855
  if instance:
730
- # An existing instance was found (either in cache or database)
731
- logger.debug(f"Found {'cached' if cached_object else 'existing'} instance of {self.__class__.__name__}")
732
- # Update the current object's ID with the found instance's ID
733
- self.id = instance.id
734
- # If the object has a 'dateCreated' attribute, update it
735
- if hasattr(self, "dateCreated"):
736
- self.dateCreated = instance.dateCreated # noqa
737
-
738
- # Update the _original_data attribute with the instance data
739
- self._original_data = instance.dict(exclude_unset=True)
740
-
741
- # Check if the current object has any changes compared to the found instance
742
- if self.has_changed():
743
- logger.debug(f"Instance of {self.__class__.__name__} has changed, updating")
744
- # Save the changes, potentially in bulk
745
- updated_instance = self.save(bulk=bulk_update)
746
- # Update the cache with the new instance
747
- self.cache_object(updated_instance)
748
- # Return the updated instance, optionally with a flag indicating it wasn't newly created
749
- return False, updated_instance
750
-
751
- # If no changes, return the existing instance
752
- return False, instance
856
+ return self._handle_existing_instance(instance, cached_object, bulk_update)
753
857
 
754
858
  # No existing instance was found, so create a new one
755
- # apply defaults if they are provided
756
- if defaults:
757
- for key, value in defaults.items():
758
- # Handle callable values by executing them
759
- if callable(value):
760
- value = value()
761
- setattr(self, key, value)
762
- logger.debug(f"No existing instance found for {self.__class__.__name__}, creating new")
763
- created_instance = self.create(bulk=bulk_create)
764
- # Cache the newly created instance
765
- self.cache_object(created_instance)
766
- # Return the created instance, optionally with a flag indicating it was newly created
767
- return True, created_instance
859
+ return self._handle_new_instance(defaults, bulk_create)
860
+
861
+ def _handle_existing_instance(self: T, instance: T, cached_object: Optional[T], bulk_update: bool) -> Tuple[str, T]:
862
+ """
863
+ Handle processing of an existing instance found in cache or database.
864
+
865
+ :param T instance: The found instance
866
+ :param Optional[T] cached_object: The cached object if found in cache
867
+ :param bool bulk_update: Whether to perform a bulk update
868
+ :return: Tuple of (status, instance) where status is "updated" or "deduped"
869
+ :rtype: Tuple[str, T]
870
+ """
871
+ # An existing instance was found (either in cache or database)
872
+ logger.debug(f"Found {'cached' if cached_object else 'existing'} instance of {self.__class__.__name__}")
873
+
874
+ # Update current object with instance data
875
+ self._sync_with_existing_instance(instance)
876
+
877
+ # Check if the current object has any changes compared to the found instance
878
+ if self.has_changed():
879
+ return self._update_existing_instance(bulk_update, is_cached=cached_object is not None)
880
+
881
+ # If no changes, determine if this is a dedupe or update
882
+ # Dedupe = found in cache OR was created during this session
883
+ is_dedupe = cached_object is not None or instance.id in self.__class__._session_created_ids
884
+ status = "deduped" if is_dedupe else "updated"
885
+ return status, instance
886
+
887
+ def _sync_with_existing_instance(self: T, instance: T) -> None:
888
+ """
889
+ Synchronize current object with existing instance data.
890
+
891
+ :param T instance: The existing instance to sync with
892
+ :rtype: None
893
+ """
894
+ # Update the current object's ID with the found instance's ID
895
+ self.id = instance.id
896
+ # If the object has a 'dateCreated' attribute, update it
897
+ if hasattr(self, "dateCreated"):
898
+ self.dateCreated = instance.dateCreated # noqa
899
+
900
+ # Update the _original_data attribute with the instance data
901
+ self._original_data = instance.dict(exclude_unset=True)
902
+
903
+ def _update_existing_instance(self: T, bulk_update: bool, is_cached: bool = False) -> Tuple[str, T]:
904
+ """
905
+ Update an existing instance that has changes.
906
+
907
+ :param bool bulk_update: Whether to perform a bulk update
908
+ :param bool is_cached: Whether the instance was found in cache (dedupe) or from API (update)
909
+ :return: Tuple of (status, updated_instance) where status is "updated" or "deduped"
910
+ :rtype: Tuple[str, T]
911
+ """
912
+ logger.debug(f"Instance of {self.__class__.__name__} has changed, updating")
913
+ # Save the changes, potentially in bulk
914
+ updated_instance = self.save(bulk=bulk_update)
915
+ # Update the cache with the new instance
916
+ self.cache_object(updated_instance)
917
+ # Determine if this is a dedupe: found in cache OR was created during this session
918
+ is_dedupe = is_cached or self.id in self.__class__._session_created_ids
919
+ status = "deduped" if is_dedupe else "updated"
920
+ return status, updated_instance
921
+
922
+ def _handle_new_instance(self: T, defaults: Optional[Dict[str, Any]], bulk_create: bool) -> Tuple[str, T]:
923
+ """
924
+ Handle creation of a new instance when none exists.
925
+
926
+ :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply
927
+ :param bool bulk_create: Whether to perform a bulk create
928
+ :return: Tuple of (status, created_instance) where status is "created"
929
+ :rtype: Tuple[str, T]
930
+ """
931
+ # apply defaults if they are provided
932
+ self._apply_defaults(defaults)
933
+
934
+ logger.debug(f"No existing instance found for {self.__class__.__name__}, creating new")
935
+ created_instance = self.create(bulk=bulk_create)
936
+ # Track this ID as created in this session for dedupe detection
937
+ self.__class__._session_created_ids.add(created_instance.id)
938
+ # Cache the newly created instance
939
+ self.cache_object(created_instance)
940
+ # Return the created instance with "created" status
941
+ return "created", created_instance
942
+
943
+ def _apply_defaults(self: T, defaults: Optional[Dict[str, Any]]) -> None:
944
+ """
945
+ Apply default values to the instance.
946
+
947
+ :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply
948
+ :rtype: None
949
+ """
950
+ if defaults:
951
+ for key, value in defaults.items():
952
+ # Handle callable values by executing them
953
+ if callable(value):
954
+ value = value()
955
+ setattr(self, key, value)
768
956
 
769
957
  @classmethod
770
958
  def _handle_list_response(
@@ -1037,24 +1225,33 @@ class RegScaleModel(BaseModel, ABC):
1037
1225
  :return: A list of objects
1038
1226
  :rtype: List[T]
1039
1227
  """
1228
+ logger.debug(
1229
+ f"get_all_by_parent for {cls.__name__}: parent_id={parent_id}, parent_module={parent_module}, "
1230
+ f"search={'provided' if search else 'None'}"
1231
+ )
1040
1232
  cache_key = f"{parent_id}:{cls.__name__}"
1041
1233
 
1042
1234
  with cls._get_lock(cache_key):
1043
1235
  cached_objects = cls._parent_cache.get(cache_key)
1044
1236
  # Check for None and empty list
1045
1237
  if cached_objects is not None and len(cached_objects) > 0:
1238
+ logger.debug(f"get_all_by_parent for {cls.__name__}: Returning {len(cached_objects)} cached objects")
1046
1239
  return cached_objects
1047
1240
 
1048
- if "get_all_by_search" in cls._get_endpoints() and parent_id and parent_module and not search:
1049
- logger.debug("Using get_all_by_search")
1241
+ if "get_all_by_search" in cls._get_endpoints() and parent_id is not None and parent_module and not search:
1242
+ logger.debug(
1243
+ f"get_all_by_search for {cls.__name__}: Creating Search with parentID={parent_id}, module={parent_module}"
1244
+ )
1050
1245
  search = Search(parentID=parent_id, module=parent_module)
1051
1246
  if search:
1247
+ logger.debug(f"get_all_by_parent for {cls.__name__}: Using search endpoint")
1052
1248
  objects: List[T] = cls._handle_looping_response(search)
1053
1249
  else:
1054
1250
  try:
1055
1251
  endpoint = cls.get_endpoint("get_all_by_parent").format(
1056
1252
  intParentID=parent_id, strModule=parent_module
1057
1253
  )
1254
+ logger.debug(f"get_all_by_parent for {cls.__name__}: Using endpoint: {endpoint}")
1058
1255
  objects: List[T] = cls._handle_list_response(
1059
1256
  cls._get_api_handler().get(endpoint=endpoint), parent_id=parent_id, parent_module=parent_module
1060
1257
  )
@@ -1062,6 +1259,11 @@ class RegScaleModel(BaseModel, ABC):
1062
1259
  logger.error(f"Failed to get endpoint: {e}", exc_info=True)
1063
1260
  objects = []
1064
1261
 
1262
+ logger.debug(f"get_all_by_parent for {cls.__name__}: Retrieved {len(objects)} objects from API")
1263
+ if objects:
1264
+ sample_parent_ids = set(getattr(obj, cls._parent_id_field, None) for obj in objects[:10])
1265
+ logger.debug(f"get_all_by_parent for {cls.__name__}: Sample parent_ids in results: {sample_parent_ids}")
1266
+
1065
1267
  cls.cache_list_objects(cache_key=cache_key, objects=objects)
1066
1268
 
1067
1269
  return objects
@@ -1111,17 +1313,18 @@ class RegScaleModel(BaseModel, ABC):
1111
1313
  return ConfigDict()
1112
1314
 
1113
1315
  @classmethod
1114
- def get_endpoint(cls, endpoint_type: str) -> str:
1316
+ def get_endpoint(cls, endpoint_type: str, suppress_error: bool = False) -> str:
1115
1317
  """
1116
1318
  Get the endpoint for a specific type.
1117
1319
 
1118
1320
  :param str endpoint_type: The type of endpoint
1321
+ :param bool suppress_error: Whether to suppress the error if the endpoint is not found, defaults to False
1119
1322
  :raises ValueError: If the endpoint type is not found
1120
1323
  :return: The endpoint
1121
1324
  :rtype: str
1122
1325
  """
1123
1326
  endpoint = cls._get_endpoints().get(endpoint_type, "na") # noqa
1124
- if not endpoint or endpoint == "na":
1327
+ if not endpoint or endpoint == "na" and not suppress_error:
1125
1328
  logger.error(f"{cls.__name__} does not have endpoint {endpoint_type}")
1126
1329
  raise ValueError(f"Endpoint {endpoint_type} not found")
1127
1330
  endpoint = str(endpoint).replace("{model_slug}", cls.get_module_slug())
@@ -1163,7 +1366,11 @@ class RegScaleModel(BaseModel, ABC):
1163
1366
  """
1164
1367
  # Check if the model has change tracking and if there are changes
1165
1368
  has_change_tracking = hasattr(self, "has_changed") and callable(getattr(self, "has_changed", None))
1166
- should_save = not has_change_tracking or self.has_changed()
1369
+
1370
+ if hasattr(self, "_ignore_has_changed") and self._ignore_has_changed:
1371
+ should_save = True
1372
+ else:
1373
+ should_save = not has_change_tracking or self.has_changed()
1167
1374
 
1168
1375
  if should_save:
1169
1376
  if bulk:
@@ -1269,7 +1476,15 @@ class RegScaleModel(BaseModel, ABC):
1269
1476
  endpoint = self.get_endpoint("insert")
1270
1477
  response = self._get_api_handler().post(endpoint=endpoint, data=self.dict(), headers=self._get_headers())
1271
1478
  if response and response.ok:
1272
- obj = self.__class__(**response.json())
1479
+ response_data = response.json()
1480
+
1481
+ # Handle special case for ComponentMapping which may have nested response structure
1482
+ if self.__class__.__name__ == "ComponentMapping" and "componentMapping" in response_data:
1483
+ component_mapping_data = response_data["componentMapping"]
1484
+ obj = self.__class__(**component_mapping_data)
1485
+ else:
1486
+ obj = self.__class__(**response_data)
1487
+
1273
1488
  self.cache_object(obj)
1274
1489
  return obj
1275
1490
  else:
@@ -1407,11 +1622,22 @@ class RegScaleModel(BaseModel, ABC):
1407
1622
  f"[#f68d1f]Updating {total_items} RegScale {cls.__name__}s...",
1408
1623
  total=total_items,
1409
1624
  )
1625
+ endpoint = cls.get_endpoint("batch_update", suppress_error=True)
1626
+ if not endpoint or endpoint == "na":
1627
+ logger.debug(f"No batch_update endpoint found for {cls.__name__}, using save method instead")
1628
+ for item in items:
1629
+ updated_item = item.save()
1630
+ cls.cache_object(updated_item)
1631
+ results.append(updated_item)
1632
+ if progress and update_job is not None:
1633
+ progress.advance(update_job, advance=1)
1634
+ cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
1635
+ return results
1410
1636
  for i in range(0, total_items, batch_size):
1411
1637
  batch = items[i : i + batch_size]
1412
1638
  batch_results = cls._handle_list_response(
1413
1639
  cls._get_api_handler().put(
1414
- endpoint=cls.get_endpoint("batch_update"),
1640
+ endpoint=endpoint,
1415
1641
  data=[item.model_dump() for item in batch if item],
1416
1642
  )
1417
1643
  )
@@ -1424,10 +1650,10 @@ class RegScaleModel(BaseModel, ABC):
1424
1650
  cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
1425
1651
 
1426
1652
  if progress_context:
1427
- process_batch(progress_context)
1653
+ process_batch(progress=progress_context, remove_progress_bar=remove_progress)
1428
1654
  else:
1429
1655
  with create_progress_object() as create_progress:
1430
- process_batch(create_progress)
1656
+ process_batch(progress=create_progress, remove_progress_bar=remove_progress)
1431
1657
 
1432
1658
  return results
1433
1659
 
@@ -1468,6 +1694,17 @@ class RegScaleModel(BaseModel, ABC):
1468
1694
  logger.warning(f"{cls.__name__}: No matching record found for ID: {cls.__name__} {object_id}")
1469
1695
  return None
1470
1696
 
1697
+ @classmethod
1698
+ def get(cls, id: Union[str, int]) -> Optional[T]:
1699
+ """
1700
+ Get a RegScale object by ID. shortcut for get_object.
1701
+
1702
+ :param Union[str, int] id: The ID of the object
1703
+ :return: The object or None if not found
1704
+ :rtype: Optional[T]
1705
+ """
1706
+ return cls.get_object(object_id=id)
1707
+
1471
1708
  @classmethod
1472
1709
  def get_objects_and_attachments_by_parent(
1473
1710
  cls, parent_id: int, parent_module: str
@@ -1480,7 +1717,7 @@ class RegScaleModel(BaseModel, ABC):
1480
1717
  :return: A tuple of a list of objects and a list of attachments
1481
1718
  :rtype: Tuple[List[T], dict[int, List["File"]]]
1482
1719
  """
1483
- from regscale.models import File
1720
+ from regscale.models.regscale_models import File
1484
1721
 
1485
1722
  # get the existing issues for the parent record that are already in RegScale
1486
1723
  logger.info("Fetching full %s list from RegScale %s #%i.", cls.__name__, parent_module, parent_id)
@@ -43,6 +43,7 @@ class SecurityPlan(RegScaleModel):
43
43
  environment: Optional[str] = ""
44
44
  lawsAndRegulations: Optional[str] = ""
45
45
  authorizationBoundary: Optional[str] = ""
46
+ authorizationTerminationDate: Optional[str] = ""
46
47
  networkArchitecture: Optional[str] = ""
47
48
  dataFlow: Optional[str] = ""
48
49
  overallCategorization: Optional[str] = ""
@@ -107,6 +108,7 @@ class SecurityPlan(RegScaleModel):
107
108
  fedrampDateSubmitted: Optional[str] = ""
108
109
  fedrampDateAuthorized: Optional[str] = ""
109
110
  fedrampId: Optional[str] = ""
111
+ complianceSettings: Optional[str] = None
110
112
  complianceSettingsId: Optional[int] = 1
111
113
  tenantsId: int = 1
112
114
 
@@ -119,11 +121,11 @@ class SecurityPlan(RegScaleModel):
119
121
  :return: The ComplianceSettings ID if the RegScale version is compatible, None otherwise
120
122
  :rtype: Optional[int]
121
123
  """
122
- from packaging.version import Version
124
+ from regscale.utils.version import RegscaleVersion
123
125
 
124
126
  regscale_version = cls._get_api_handler().regscale_version
125
127
 
126
- if len(regscale_version) >= 10 or Version(regscale_version) >= Version("6.13.0.0"):
128
+ if len(regscale_version) >= 10 or RegscaleVersion.compare_versions(regscale_version, "6.13.0.0"):
127
129
  return v
128
130
  else:
129
131
  return None
@@ -67,8 +67,8 @@ class Vulnerability(RegScaleModel):
67
67
  lastSeen: Optional[str] = None
68
68
  firstSeen: Optional[str] = None
69
69
  daysOpen: Optional[int] = None
70
- dns: Optional[str] = None
71
- ipAddress: Optional[str] = None
70
+ dns: Optional[str] = ""
71
+ ipAddress: Optional[str] = ""
72
72
  mitigated: Optional[bool] = None
73
73
  operatingSystem: Optional[str] = None
74
74
  port: Optional[Union[str, int]] = None
@@ -83,7 +83,7 @@ class Vulnerability(RegScaleModel):
83
83
  cvsSv3BaseScore: Optional[Union[float, int]] = None
84
84
  description: Optional[str] = None
85
85
  plugInText: Optional[str] = None
86
- tenantsId: int = Field(default=0)
86
+ tenantsId: int = Field(default_factory=RegScaleModel.get_tenant_id)
87
87
  isPublic: bool = Field(default=False)
88
88
  dateClosed: Optional[str] = None
89
89
  status: Optional[Union[str, IssueStatus]] = Field(default_factory=lambda: IssueStatus.Open)