regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/api.py +5 -2
  4. regscale/core/app/application.py +36 -6
  5. regscale/core/app/internal/control_editor.py +73 -21
  6. regscale/core/app/internal/evidence.py +727 -204
  7. regscale/core/app/internal/login.py +4 -2
  8. regscale/core/app/internal/model_editor.py +219 -64
  9. regscale/core/app/utils/app_utils.py +86 -12
  10. regscale/core/app/utils/catalog_utils/common.py +1 -1
  11. regscale/core/login.py +21 -4
  12. regscale/core/utils/async_graphql_client.py +363 -0
  13. regscale/core/utils/date.py +77 -1
  14. regscale/dev/cli.py +26 -0
  15. regscale/dev/code_gen.py +109 -24
  16. regscale/dev/version.py +72 -0
  17. regscale/integrations/commercial/__init__.py +30 -2
  18. regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
  19. regscale/integrations/commercial/aws/cli.py +3107 -54
  20. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  21. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  22. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  23. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  24. regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
  25. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  26. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  27. regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
  28. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  29. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  30. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  31. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  32. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  33. regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
  34. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  35. regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
  36. regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
  37. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  38. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  39. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  40. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  41. regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
  42. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  43. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  44. regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
  45. regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
  46. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  47. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  48. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  49. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  50. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  51. regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
  52. regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
  53. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  54. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  55. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  56. regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
  57. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  58. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  59. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  60. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  61. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  62. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  63. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  64. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  65. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  66. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  67. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  68. regscale/integrations/commercial/aws/scanner.py +1072 -205
  69. regscale/integrations/commercial/aws/security_hub.py +319 -0
  70. regscale/integrations/commercial/aws/session_manager.py +282 -0
  71. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  72. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  73. regscale/integrations/commercial/jira.py +489 -153
  74. regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
  75. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  76. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  77. regscale/integrations/commercial/qualys/__init__.py +167 -68
  78. regscale/integrations/commercial/qualys/scanner.py +305 -39
  79. regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
  80. regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
  81. regscale/integrations/commercial/sicura/api.py +79 -42
  82. regscale/integrations/commercial/sicura/commands.py +8 -2
  83. regscale/integrations/commercial/sicura/scanner.py +83 -44
  84. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  85. regscale/integrations/commercial/synqly/assets.py +133 -16
  86. regscale/integrations/commercial/synqly/edr.py +2 -8
  87. regscale/integrations/commercial/synqly/query_builder.py +536 -0
  88. regscale/integrations/commercial/synqly/ticketing.py +27 -0
  89. regscale/integrations/commercial/synqly/vulnerabilities.py +165 -28
  90. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  91. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  92. regscale/integrations/commercial/tenablev2/commands.py +146 -5
  93. regscale/integrations/commercial/tenablev2/scanner.py +1 -3
  94. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  95. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  96. regscale/integrations/commercial/wizv2/click.py +191 -76
  97. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  98. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  99. regscale/integrations/commercial/wizv2/compliance_report.py +1592 -0
  100. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  101. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
  102. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
  103. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  104. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  105. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +66 -9
  106. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  107. regscale/integrations/commercial/wizv2/issue.py +776 -28
  108. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  109. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  110. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  111. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  112. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  113. regscale/integrations/commercial/wizv2/reports.py +243 -0
  114. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  115. regscale/integrations/commercial/wizv2/scanner.py +1031 -441
  116. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  117. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  118. regscale/integrations/commercial/wizv2/variables.py +89 -3
  119. regscale/integrations/compliance_integration.py +1036 -151
  120. regscale/integrations/control_matcher.py +432 -0
  121. regscale/integrations/due_date_handler.py +333 -0
  122. regscale/integrations/milestone_manager.py +291 -0
  123. regscale/integrations/public/__init__.py +14 -0
  124. regscale/integrations/public/cci_importer.py +834 -0
  125. regscale/integrations/public/csam/__init__.py +0 -0
  126. regscale/integrations/public/csam/csam.py +938 -0
  127. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  128. regscale/integrations/public/csam/csam_common.py +154 -0
  129. regscale/integrations/public/csam/csam_controls.py +432 -0
  130. regscale/integrations/public/csam/csam_poam.py +124 -0
  131. regscale/integrations/public/fedramp/click.py +77 -6
  132. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  133. regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
  134. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  135. regscale/integrations/public/fedramp/poam/scanner.py +75 -7
  136. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  137. regscale/integrations/scanner_integration.py +1961 -430
  138. regscale/models/integration_models/CCI_List.xml +1 -0
  139. regscale/models/integration_models/aqua.py +2 -2
  140. regscale/models/integration_models/cisa_kev_data.json +805 -11
  141. regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
  142. regscale/models/integration_models/nexpose.py +36 -10
  143. regscale/models/integration_models/qualys.py +3 -4
  144. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  145. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +87 -18
  146. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  147. regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
  148. regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
  149. regscale/models/locking.py +12 -8
  150. regscale/models/platform.py +4 -2
  151. regscale/models/regscale_models/__init__.py +7 -0
  152. regscale/models/regscale_models/assessment.py +2 -1
  153. regscale/models/regscale_models/catalog.py +1 -1
  154. regscale/models/regscale_models/compliance_settings.py +251 -1
  155. regscale/models/regscale_models/component.py +1 -0
  156. regscale/models/regscale_models/control_implementation.py +236 -41
  157. regscale/models/regscale_models/control_objective.py +74 -5
  158. regscale/models/regscale_models/file.py +2 -0
  159. regscale/models/regscale_models/form_field_value.py +5 -3
  160. regscale/models/regscale_models/inheritance.py +44 -0
  161. regscale/models/regscale_models/issue.py +301 -102
  162. regscale/models/regscale_models/milestone.py +33 -14
  163. regscale/models/regscale_models/organization.py +3 -0
  164. regscale/models/regscale_models/regscale_model.py +310 -73
  165. regscale/models/regscale_models/security_plan.py +4 -2
  166. regscale/models/regscale_models/vulnerability.py +3 -3
  167. regscale/regscale.py +25 -4
  168. regscale/templates/__init__.py +0 -0
  169. regscale/utils/threading/threadhandler.py +20 -15
  170. regscale/validation/record.py +23 -1
  171. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
  172. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
  173. tests/core/__init__.py +0 -0
  174. tests/core/utils/__init__.py +0 -0
  175. tests/core/utils/test_async_graphql_client.py +472 -0
  176. tests/fixtures/test_fixture.py +13 -8
  177. tests/regscale/core/test_login.py +171 -4
  178. tests/regscale/integrations/commercial/__init__.py +0 -0
  179. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  180. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  181. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  182. tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
  183. tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
  184. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  185. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  186. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  187. tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
  188. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  189. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  190. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  191. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  192. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  193. tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
  194. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  195. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  196. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  197. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  198. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  199. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  200. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  201. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  202. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  203. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  204. tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
  205. tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
  206. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  207. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  208. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  209. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  210. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  211. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  212. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  213. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  214. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  215. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  216. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  217. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  218. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  219. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  220. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  221. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  222. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  223. tests/regscale/integrations/commercial/conftest.py +28 -0
  224. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  225. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  226. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  227. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  228. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  229. tests/regscale/integrations/commercial/test_aws.py +3742 -0
  230. tests/regscale/integrations/commercial/test_burp.py +48 -0
  231. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  232. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  233. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  234. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  235. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  236. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  237. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  238. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  239. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  240. tests/regscale/integrations/commercial/test_sicura.py +349 -0
  241. tests/regscale/integrations/commercial/test_snow.py +423 -0
  242. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  243. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  244. tests/regscale/integrations/commercial/test_stig.py +33 -0
  245. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  246. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  247. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  248. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  249. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  250. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  251. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  252. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  253. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  254. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  255. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  256. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  257. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  258. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  259. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  260. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  261. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  262. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  263. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  264. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  265. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  266. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  267. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  268. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  269. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  270. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  271. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  272. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  273. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  274. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  275. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  276. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  277. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  278. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  279. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  280. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1218 -0
  281. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  282. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  283. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  284. tests/regscale/integrations/public/__init__.py +0 -0
  285. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  286. tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
  287. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  288. tests/regscale/integrations/public/test_alienvault.py +220 -0
  289. tests/regscale/integrations/public/test_cci.py +1053 -0
  290. tests/regscale/integrations/public/test_cisa.py +1021 -0
  291. tests/regscale/integrations/public/test_emass.py +518 -0
  292. tests/regscale/integrations/public/test_fedramp.py +1152 -0
  293. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  294. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  295. tests/regscale/integrations/public/test_oscal.py +453 -0
  296. tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
  297. tests/regscale/integrations/test_control_matcher.py +1421 -0
  298. tests/regscale/integrations/test_control_matching.py +155 -0
  299. tests/regscale/integrations/test_milestone_manager.py +408 -0
  300. tests/regscale/models/test_control_implementation.py +118 -3
  301. tests/regscale/models/test_form_field_value_integration.py +304 -0
  302. tests/regscale/models/test_issue.py +378 -1
  303. tests/regscale/models/test_module_integration.py +582 -0
  304. tests/regscale/models/test_tenable_integrations.py +811 -105
  305. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
  306. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
  307. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
  308. regscale/integrations/public/fedramp/parts_mapper.py +0 -107
  309. /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
  310. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  311. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
  312. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
  313. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
  314. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,879 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """AWS KMS Evidence Integration for RegScale CLI."""
4
+
5
+ import gzip
6
+ import json
7
+ import logging
8
+ import os
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
12
+ from io import BytesIO
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ import boto3
16
+ from botocore.exceptions import ClientError
17
+
18
+ from regscale.core.app.api import Api
19
+ from regscale.core.app.utils.app_utils import get_current_datetime
20
+ from regscale.integrations.commercial.aws.kms_control_mappings import KMSControlMapper
21
+ from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
22
+ from regscale.models import regscale_models
23
+ from regscale.models.regscale_models.evidence import Evidence
24
+ from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
25
+ from regscale.models.regscale_models.file import File
26
+
27
+ logger = logging.getLogger("regscale")
28
+
29
+ # Constants for file paths and cache TTL
30
+ KMS_CACHE_FILE = os.path.join("artifacts", "aws", "kms_data.json")
31
+ CACHE_TTL_SECONDS = 4 * 60 * 60 # 4 hours in seconds
32
+
33
+ # HTML tag constants to avoid duplication
34
+ HTML_STRONG_OPEN = "<strong>"
35
+ HTML_STRONG_CLOSE = "</strong>"
36
+ HTML_P_OPEN = "<p>"
37
+ HTML_P_CLOSE = "</p>"
38
+ HTML_UL_OPEN = "<ul>"
39
+ HTML_UL_CLOSE = "</ul>"
40
+ HTML_LI_OPEN = "<li>"
41
+ HTML_LI_CLOSE = "</li>"
42
+ HTML_H2_OPEN = "<h2>"
43
+ HTML_H2_CLOSE = "</h2>"
44
+ HTML_H3_OPEN = "<h3>"
45
+ HTML_H3_CLOSE = "</h3>"
46
+ HTML_BR = "<br>"
47
+
48
+
49
+ @dataclass
50
+ class KMSEvidenceConfig:
51
+ """Configuration for AWS KMS evidence collection."""
52
+
53
+ plan_id: int
54
+ region: str = "us-east-1"
55
+ framework: str = "NIST800-53R5"
56
+ create_issues: bool = True
57
+ update_control_status: bool = True
58
+ create_poams: bool = False
59
+ parent_module: str = "securityplans"
60
+ collect_evidence: bool = False
61
+ evidence_as_attachments: bool = True
62
+ evidence_control_ids: Optional[List[str]] = None
63
+ evidence_frequency: int = 30
64
+ force_refresh: bool = False
65
+ account_id: Optional[str] = None
66
+ tags: Optional[Dict[str, str]] = None
67
+ profile: Optional[str] = None
68
+ aws_access_key_id: Optional[str] = None
69
+ aws_secret_access_key: Optional[str] = None
70
+ aws_session_token: Optional[str] = None
71
+
72
+
73
+ class KMSComplianceItem(ComplianceItem):
74
+ """
75
+ Compliance item representing a single KMS key assessment.
76
+
77
+ Maps KMS key attributes to compliance control requirements.
78
+ """
79
+
80
+ def __init__(self, key_data: Dict[str, Any], control_mapper: KMSControlMapper):
81
+ """
82
+ Initialize KMS compliance item from key data.
83
+
84
+ :param Dict[str, Any] key_data: KMS key metadata and attributes
85
+ :param KMSControlMapper control_mapper: Control mapper for compliance assessment
86
+ """
87
+ self.key_data = key_data
88
+ self.control_mapper = control_mapper
89
+
90
+ # Extract key attributes
91
+ self._key_id = key_data.get("KeyId", "")
92
+ self._key_arn = key_data.get("Arn", "")
93
+ self._key_state = key_data.get("KeyState", "Unknown")
94
+ self._rotation_enabled = key_data.get("RotationEnabled", False)
95
+ self._key_manager = key_data.get("KeyManager", "CUSTOMER")
96
+ self._description = key_data.get("Description", "")
97
+ self._tags = key_data.get("Tags", [])
98
+
99
+ # Assess compliance for all mapped controls
100
+ self._compliance_results = control_mapper.assess_key_compliance(key_data)
101
+
102
+ # Extract region and account from ARN
103
+ self._region = self._extract_region_from_arn(self._key_arn)
104
+ self._account_id = self._extract_account_from_arn(self._key_arn)
105
+
106
+ @property
107
+ def resource_id(self) -> str:
108
+ """Unique identifier for the KMS key."""
109
+ return self._key_id
110
+
111
+ @property
112
+ def resource_name(self) -> str:
113
+ """Human-readable name of the KMS key."""
114
+ # Try to get alias from tags or description
115
+ for tag in self._tags:
116
+ if tag.get("TagKey") == "Name":
117
+ return f"{tag.get('TagValue')} ({self._key_id[:8]}...)"
118
+
119
+ if self._description:
120
+ return f"{self._description[:50]} ({self._key_id[:8]}...)"
121
+
122
+ return f"KMS Key {self._key_id[:12]}..."
123
+
124
+ @property
125
+ def control_id(self) -> str:
126
+ """
127
+ Primary control identifier for this key assessment.
128
+
129
+ Returns the first failing control, or first passing control if all pass.
130
+ """
131
+ # Return first failing control for issue creation
132
+ for control_id, result in self._compliance_results.items():
133
+ if result == "FAIL":
134
+ return control_id
135
+
136
+ # If all pass, return first control
137
+ return list(self._compliance_results.keys())[0] if self._compliance_results else "SC-12"
138
+
139
+ @property
140
+ def compliance_result(self) -> str:
141
+ """
142
+ Overall compliance result for this key.
143
+
144
+ Returns FAIL if any control fails, PASS if all pass.
145
+ """
146
+ if not self._compliance_results:
147
+ return "PASS"
148
+
149
+ # If ANY control fails, the key fails overall
150
+ if "FAIL" in self._compliance_results.values():
151
+ return "FAIL"
152
+
153
+ return "PASS"
154
+
155
+ @property
156
+ def severity(self) -> Optional[str]:
157
+ """Severity level based on which controls are failing."""
158
+ if self.compliance_result == "PASS":
159
+ return None
160
+
161
+ # SC-12 failures (rotation) are HIGH severity
162
+ if self._compliance_results.get("SC-12") == "FAIL":
163
+ return "HIGH"
164
+
165
+ # SC-13 failures (crypto protection) are MEDIUM severity
166
+ if self._compliance_results.get("SC-13") == "FAIL":
167
+ return "MEDIUM"
168
+
169
+ # SC-28 failures (data at rest) are MEDIUM severity
170
+ if self._compliance_results.get("SC-28") == "FAIL":
171
+ return "MEDIUM"
172
+
173
+ return "MEDIUM"
174
+
175
+ @property
176
+ def description(self) -> str:
177
+ """Detailed description of the KMS key compliance assessment."""
178
+ desc_parts = self._build_key_details()
179
+
180
+ if self._description:
181
+ desc_parts.extend(self._build_description_section())
182
+
183
+ desc_parts.extend(self._build_compliance_results_section())
184
+
185
+ if self.compliance_result == "FAIL":
186
+ desc_parts.extend(self._build_remediation_section())
187
+
188
+ return "\n".join(desc_parts)
189
+
190
+ def _build_key_details(self) -> List[str]:
191
+ """Build the key details section of the description."""
192
+ rotation_status = "Yes" if self._rotation_enabled else "No"
193
+ return [
194
+ f"{HTML_H3_OPEN}AWS KMS Key Compliance Assessment{HTML_H3_CLOSE}",
195
+ HTML_P_OPEN,
196
+ f"{HTML_STRONG_OPEN}Key ID:{HTML_STRONG_CLOSE} {self._key_id}{HTML_BR}",
197
+ f"{HTML_STRONG_OPEN}Key ARN:{HTML_STRONG_CLOSE} {self._key_arn}{HTML_BR}",
198
+ f"{HTML_STRONG_OPEN}Key State:{HTML_STRONG_CLOSE} {self._key_state}{HTML_BR}",
199
+ f"{HTML_STRONG_OPEN}Key Manager:{HTML_STRONG_CLOSE} {self._key_manager}{HTML_BR}",
200
+ f"{HTML_STRONG_OPEN}Rotation Enabled:{HTML_STRONG_CLOSE} {rotation_status}{HTML_BR}",
201
+ f"{HTML_STRONG_OPEN}Region:{HTML_STRONG_CLOSE} {self._region}",
202
+ HTML_P_CLOSE,
203
+ ]
204
+
205
+ def _build_description_section(self) -> List[str]:
206
+ """Build the optional description section."""
207
+ return [
208
+ HTML_P_OPEN,
209
+ f"{HTML_STRONG_OPEN}Description:{HTML_STRONG_CLOSE} {self._description}",
210
+ HTML_P_CLOSE,
211
+ ]
212
+
213
+ def _build_compliance_results_section(self) -> List[str]:
214
+ """Build the compliance results section."""
215
+ section_parts = [
216
+ f"{HTML_H3_OPEN}Control Compliance Results{HTML_H3_CLOSE}",
217
+ HTML_UL_OPEN,
218
+ ]
219
+
220
+ for control_id, result in self._compliance_results.items():
221
+ result_item = self._format_compliance_result(control_id, result)
222
+ section_parts.append(result_item)
223
+
224
+ section_parts.append(HTML_UL_CLOSE)
225
+ return section_parts
226
+
227
+ def _format_compliance_result(self, control_id: str, result: str) -> str:
228
+ """Format a single compliance result item."""
229
+ result_color = "#d32f2f" if result == "FAIL" else "#2e7d32"
230
+ control_desc = self.control_mapper.get_control_description(control_id)
231
+ return (
232
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
233
+ f"<span style='color: {result_color};'>{result}</span> - {control_desc}{HTML_LI_CLOSE}"
234
+ )
235
+
236
+ def _build_remediation_section(self) -> List[str]:
237
+ """Build remediation guidance for failed controls."""
238
+ section_parts = [
239
+ f"{HTML_H3_OPEN}Remediation Guidance{HTML_H3_CLOSE}",
240
+ HTML_UL_OPEN,
241
+ ]
242
+
243
+ section_parts.extend(self._get_sc12_remediation())
244
+ section_parts.extend(self._get_sc13_remediation())
245
+ section_parts.extend(self._get_sc28_remediation())
246
+
247
+ section_parts.append(HTML_UL_CLOSE)
248
+ return section_parts
249
+
250
+ def _get_sc12_remediation(self) -> List[str]:
251
+ """Get remediation steps for SC-12 control failures."""
252
+ items = []
253
+ if self._compliance_results.get("SC-12") == "FAIL":
254
+ if not self._rotation_enabled and self._key_manager == "CUSTOMER":
255
+ items.append(
256
+ f"{HTML_LI_OPEN}Enable automatic key rotation for this customer-managed key{HTML_LI_CLOSE}"
257
+ )
258
+ if self._key_state in ["PendingDeletion", "Disabled"]:
259
+ items.append(f"{HTML_LI_OPEN}Key is {self._key_state} - review key lifecycle{HTML_LI_CLOSE}")
260
+ return items
261
+
262
+ def _get_sc13_remediation(self) -> List[str]:
263
+ """Get remediation steps for SC-13 control failures."""
264
+ items = []
265
+ if self._compliance_results.get("SC-13") == "FAIL":
266
+ key_spec = self.key_data.get("KeySpec", "Unknown")
267
+ items.append(
268
+ f"{HTML_LI_OPEN}Review key specification ({key_spec}) - ensure FIPS-validated "
269
+ f"algorithms are used{HTML_LI_CLOSE}"
270
+ )
271
+ return items
272
+
273
+ def _get_sc28_remediation(self) -> List[str]:
274
+ """Get remediation steps for SC-28 control failures."""
275
+ items = []
276
+ if self._compliance_results.get("SC-28") == "FAIL":
277
+ items.append(
278
+ f"{HTML_LI_OPEN}Ensure key is enabled and available for data-at-rest encryption{HTML_LI_CLOSE}"
279
+ )
280
+ return items
281
+
282
+ @property
283
+ def framework(self) -> str:
284
+ """Compliance framework used for assessment."""
285
+ return self.control_mapper.framework
286
+
287
+ @staticmethod
288
+ def _extract_region_from_arn(arn: str) -> str:
289
+ """Extract AWS region from KMS key ARN."""
290
+ try:
291
+ # ARN format: arn:aws:kms:region:account:key/key-id
292
+ return arn.split(":")[3]
293
+ except (IndexError, AttributeError):
294
+ return "unknown"
295
+
296
+ @staticmethod
297
+ def _extract_account_from_arn(arn: str) -> str:
298
+ """Extract AWS account ID from KMS key ARN."""
299
+ try:
300
+ # ARN format: arn:aws:kms:region:account:key/key-id
301
+ return arn.split(":")[4]
302
+ except (IndexError, AttributeError):
303
+ return "unknown"
304
+
305
+
306
+ class AWSKMSEvidenceIntegration(ComplianceIntegration):
307
+ """Process AWS KMS key data and create evidence/compliance records in RegScale."""
308
+
309
+ def __init__(self, config: KMSEvidenceConfig):
310
+ """
311
+ Initialize AWS KMS evidence integration.
312
+
313
+ :param KMSEvidenceConfig config: Configuration object containing all parameters
314
+ """
315
+ super().__init__(
316
+ plan_id=config.plan_id,
317
+ framework=config.framework,
318
+ create_issues=config.create_issues,
319
+ update_control_status=config.update_control_status,
320
+ create_poams=config.create_poams,
321
+ parent_module=config.parent_module,
322
+ )
323
+
324
+ # Initialize API for file operations
325
+ self.api = Api()
326
+
327
+ self.region = config.region
328
+ self.title = "AWS KMS"
329
+ self.framework = config.framework
330
+
331
+ # Evidence collection parameters
332
+ self.collect_evidence = config.collect_evidence
333
+ self.evidence_as_attachments = config.evidence_as_attachments
334
+ self.evidence_control_ids = config.evidence_control_ids
335
+ self.evidence_frequency = config.evidence_frequency
336
+
337
+ # Cache control
338
+ self.force_refresh = config.force_refresh
339
+
340
+ # Filtering parameters
341
+ self.account_id = config.account_id
342
+ self.tags = config.tags or {}
343
+
344
+ # Initialize control mapper
345
+ self.control_mapper = KMSControlMapper(framework=config.framework)
346
+
347
+ # Extract AWS credentials from config
348
+ profile = config.profile
349
+ aws_access_key_id = config.aws_access_key_id
350
+ aws_secret_access_key = config.aws_secret_access_key
351
+ aws_session_token = config.aws_session_token
352
+
353
+ # INFO-level logging for credential resolution
354
+ if aws_access_key_id and aws_secret_access_key:
355
+ logger.info("Initializing AWS KMS client with explicit credentials")
356
+ self.session = boto3.Session(
357
+ region_name=config.region,
358
+ aws_access_key_id=aws_access_key_id,
359
+ aws_secret_access_key=aws_secret_access_key,
360
+ aws_session_token=aws_session_token,
361
+ )
362
+ else:
363
+ logger.info(f"Initializing AWS KMS client with profile: {profile if profile else 'default'}")
364
+ self.session = boto3.Session(profile_name=profile, region_name=config.region)
365
+
366
+ try:
367
+ self.client = self.session.client("kms")
368
+ logger.info("Successfully created AWS KMS client")
369
+ except Exception as e:
370
+ logger.error(f"Failed to create AWS KMS client: {e}")
371
+ raise
372
+
373
+ # Store raw KMS data for evidence generation
374
+ self.raw_kms_data: List[Dict[str, Any]] = []
375
+
376
+ def _is_cache_valid(self) -> bool:
377
+ """
378
+ Check if the cache file exists and is within the TTL.
379
+
380
+ :return: True if cache is valid, False otherwise
381
+ :rtype: bool
382
+ """
383
+ if not os.path.exists(KMS_CACHE_FILE):
384
+ logger.debug("Cache file does not exist")
385
+ return False
386
+
387
+ file_age = time.time() - os.path.getmtime(KMS_CACHE_FILE)
388
+ is_valid = file_age < CACHE_TTL_SECONDS
389
+
390
+ if is_valid:
391
+ hours_old = file_age / 3600
392
+ logger.info(f"Using cached KMS data (age: {hours_old:.1f} hours)")
393
+ else:
394
+ hours_old = file_age / 3600
395
+ logger.debug(f"Cache expired (age: {hours_old:.1f} hours, TTL: {CACHE_TTL_SECONDS / 3600} hours)")
396
+
397
+ return is_valid
398
+
399
+ def _load_cached_data(self) -> List[Dict[str, Any]]:
400
+ """
401
+ Load KMS data from cache file.
402
+
403
+ :return: List of raw KMS key data from cache
404
+ :rtype: List[Dict[str, Any]]
405
+ """
406
+ try:
407
+ with open(KMS_CACHE_FILE, encoding="utf-8") as file:
408
+ cached_data = json.load(file)
409
+
410
+ # Validate cache format - must be a list
411
+ if not isinstance(cached_data, list):
412
+ logger.warning("Invalid cache format detected (not a list). Invalidating cache.")
413
+ return []
414
+
415
+ # Check if items are dicts
416
+ if cached_data and not isinstance(cached_data[0], dict):
417
+ logger.warning("Invalid cache format detected (items not dicts). Invalidating cache.")
418
+ return []
419
+
420
+ logger.info(f"Loaded {len(cached_data)} KMS keys from cache")
421
+ return cached_data
422
+ except (json.JSONDecodeError, IOError) as e:
423
+ logger.warning(f"Error reading cache file: {e}. Fetching fresh data.")
424
+ return []
425
+
426
+ def _save_to_cache(self, kms_data: List[Dict[str, Any]]) -> None:
427
+ """
428
+ Save KMS data to cache file.
429
+
430
+ :param List[Dict[str, Any]] kms_data: Data to cache
431
+ :return: None
432
+ :rtype: None
433
+ """
434
+ try:
435
+ # Ensure the artifacts directory exists
436
+ os.makedirs(os.path.dirname(KMS_CACHE_FILE), exist_ok=True)
437
+
438
+ with open(KMS_CACHE_FILE, "w", encoding="utf-8") as file:
439
+ json.dump(kms_data, file, indent=2, default=str)
440
+
441
+ logger.info(f"Cached {len(kms_data)} KMS keys to {KMS_CACHE_FILE}")
442
+ except IOError as e:
443
+ logger.warning(f"Error writing to cache file: {e}")
444
+
445
+ def _fetch_fresh_kms_data(self) -> List[Dict[str, Any]]:
446
+ """
447
+ Fetch fresh KMS data from AWS.
448
+
449
+ :return: List of KMS key data
450
+ :rtype: List[Dict[str, Any]]
451
+ """
452
+ logger.info("Fetching KMS data from AWS...")
453
+
454
+ # Log filtering parameters
455
+ if self.account_id:
456
+ logger.info(f"Filtering KMS keys by account ID: {self.account_id}")
457
+ if self.tags:
458
+ logger.info(f"Filtering KMS keys by tags: {self.tags}")
459
+
460
+ # Use inventory collector for consistency
461
+ from regscale.integrations.commercial.aws.inventory.resources.kms import KMSCollector
462
+
463
+ collector = KMSCollector(session=self.session, region=self.region, account_id=self.account_id, tags=self.tags)
464
+
465
+ inventory = collector.collect()
466
+ keys = inventory.get("Keys", [])
467
+
468
+ logger.info(f"Fetched {len(keys)} KMS keys from AWS (after filtering)")
469
+ return keys
470
+
471
+ def fetch_compliance_data(self) -> List[Dict[str, Any]]:
472
+ """
473
+ Fetch raw KMS data from AWS.
474
+
475
+ Uses cached data if available and not expired (4-hour TTL), unless force_refresh is True.
476
+
477
+ :return: List of raw KMS key data
478
+ :rtype: List[Dict[str, Any]]
479
+ """
480
+ # Check if we should use cached data
481
+ if not self.force_refresh and self._is_cache_valid():
482
+ cached_data = self._load_cached_data()
483
+ if cached_data:
484
+ self.raw_kms_data = cached_data
485
+ return cached_data
486
+
487
+ # Force refresh requested or no valid cache, fetch fresh data from AWS
488
+ if self.force_refresh:
489
+ logger.info("Force refresh requested, bypassing cache and fetching fresh data from AWS KMS...")
490
+
491
+ try:
492
+ kms_data = self._fetch_fresh_kms_data()
493
+ self.raw_kms_data = kms_data
494
+ self._save_to_cache(kms_data)
495
+ return kms_data
496
+ except ClientError as e:
497
+ logger.error(f"Error fetching KMS data from AWS: {e}")
498
+ return []
499
+
500
+ def create_compliance_item(self, raw_data: Dict[str, Any]) -> ComplianceItem:
501
+ """
502
+ Create a ComplianceItem from raw KMS key data.
503
+
504
+ :param Dict[str, Any] raw_data: Raw KMS key data
505
+ :return: KMSComplianceItem instance
506
+ :rtype: ComplianceItem
507
+ """
508
+ return KMSComplianceItem(raw_data, self.control_mapper)
509
+
510
+ def _map_resource_type_to_asset_type(self, compliance_item: ComplianceItem) -> str:
511
+ """
512
+ Map KMS key to RegScale asset type.
513
+
514
+ :param ComplianceItem compliance_item: Compliance item
515
+ :return: Asset type string
516
+ :rtype: str
517
+ """
518
+ return "AWS KMS Key"
519
+
520
+ def sync_compliance(self) -> None:
521
+ """
522
+ Main method to sync KMS compliance data.
523
+
524
+ Extends base sync_compliance to add evidence collection support.
525
+
526
+ :return: None
527
+ :rtype: None
528
+ """
529
+ # Call the base class sync_compliance to handle control assessments and issues
530
+ super().sync_compliance()
531
+
532
+ # If evidence collection is enabled, collect evidence after compliance sync
533
+ if self.collect_evidence:
534
+ logger.info("Evidence collection enabled, starting evidence collection...")
535
+ self._collect_kms_evidence()
536
+
537
+ def _collect_kms_evidence(self) -> None:
538
+ """
539
+ Collect KMS evidence and create Evidence records or SSP attachments.
540
+
541
+ :return: None
542
+ :rtype: None
543
+ """
544
+ if not self.raw_kms_data:
545
+ logger.warning("No KMS data available for evidence collection")
546
+ return
547
+
548
+ scan_date = get_current_datetime(dt_format="%Y-%m-%d")
549
+
550
+ if self.evidence_as_attachments:
551
+ logger.info("Creating SSP file attachment with KMS evidence...")
552
+ self._create_ssp_attachment(scan_date)
553
+ else:
554
+ logger.info("Creating Evidence record with KMS evidence...")
555
+ self._create_evidence_record(scan_date)
556
+
557
+ def _create_ssp_attachment(self, scan_date: str) -> None:
558
+ """
559
+ Create SSP file attachment with KMS evidence data.
560
+
561
+ :param str scan_date: Scan date string
562
+ :return: None
563
+ :rtype: None
564
+ """
565
+ try:
566
+ # Check for existing evidence to avoid duplicates
567
+ date_str = datetime.now().strftime("%Y%m%d")
568
+ account_suffix = f"_{self.account_id}" if self.account_id else ""
569
+ file_name_pattern = f"kms_evidence{account_suffix}_{date_str}"
570
+
571
+ if self.check_for_existing_evidence(file_name_pattern):
572
+ logger.info("Evidence file for KMS already exists for today. Skipping upload to avoid duplicates.")
573
+ return
574
+
575
+ # Add timestamp to make filename unique if run multiple times per day
576
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
577
+ file_name = f"kms_evidence{account_suffix}_{timestamp}.jsonl.gz"
578
+
579
+ # Prepare JSONL content with compliance results
580
+ jsonl_lines = []
581
+ for key_data in self.raw_kms_data:
582
+ compliance_item = self.create_compliance_item(key_data)
583
+ evidence_entry = {
584
+ **key_data,
585
+ "compliance_assessment": {
586
+ "overall_result": compliance_item.compliance_result,
587
+ "control_results": compliance_item._compliance_results,
588
+ "assessed_controls": list(compliance_item._compliance_results.keys()),
589
+ "assessment_date": scan_date,
590
+ },
591
+ }
592
+ jsonl_lines.append(json.dumps(evidence_entry, default=str))
593
+
594
+ jsonl_content = "\n".join(jsonl_lines)
595
+
596
+ # Compress the JSONL content
597
+ compressed_buffer = BytesIO()
598
+ with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
599
+ gz_file.write(jsonl_content)
600
+
601
+ compressed_data = compressed_buffer.getvalue()
602
+ compressed_size_mb = len(compressed_data) / (1024 * 1024)
603
+ uncompressed_size_mb = len(jsonl_content.encode("utf-8")) / (1024 * 1024)
604
+ compression_ratio = (1 - (len(compressed_data) / len(jsonl_content.encode("utf-8")))) * 100
605
+
606
+ logger.info(
607
+ "Compressed KMS evidence: %.2f MB -> %.2f MB (%.1f%% reduction)",
608
+ uncompressed_size_mb,
609
+ compressed_size_mb,
610
+ compression_ratio,
611
+ )
612
+
613
+ # Upload to SSP
614
+ api = Api()
615
+ success = File.upload_file_to_regscale(
616
+ file_name=file_name,
617
+ parent_id=self.plan_id,
618
+ parent_module=self.parent_module,
619
+ api=api,
620
+ file_data=compressed_data,
621
+ tags="aws,kms,encryption,automated",
622
+ )
623
+
624
+ if success:
625
+ logger.info(f"Successfully uploaded KMS evidence file to SSP {self.plan_id}: {file_name}")
626
+ else:
627
+ logger.error(f"Failed to upload KMS evidence file to SSP {self.plan_id}")
628
+
629
+ except Exception as e:
630
+ logger.error(f"Error creating SSP attachment for KMS evidence: {e}", exc_info=True)
631
+
632
+ def _create_evidence_record(self, scan_date: str) -> None:
633
+ """
634
+ Create Evidence record with KMS evidence data.
635
+
636
+ :param str scan_date: Scan date string
637
+ :return: None
638
+ :rtype: None
639
+ """
640
+ try:
641
+ # Build evidence title and description
642
+ title = f"AWS KMS Evidence - {scan_date}"
643
+ description = self._build_evidence_description(scan_date)
644
+
645
+ # Calculate due date
646
+ due_date = (datetime.now() + timedelta(days=self.evidence_frequency)).isoformat()
647
+
648
+ # Create Evidence record
649
+ evidence = Evidence(
650
+ title=title,
651
+ description=description,
652
+ status="Collected",
653
+ updateFrequency=self.evidence_frequency,
654
+ dueDate=due_date,
655
+ )
656
+
657
+ created_evidence = evidence.create()
658
+ if not created_evidence or not created_evidence.id:
659
+ logger.error("Failed to create evidence record")
660
+ return
661
+
662
+ logger.info(f"Created evidence record {created_evidence.id}: {title}")
663
+
664
+ # Upload compressed evidence file
665
+ self._upload_evidence_file(created_evidence.id, scan_date)
666
+
667
+ # Link evidence to SSP
668
+ self._link_evidence_to_ssp(created_evidence.id)
669
+
670
+ # Link to controls if specified
671
+ if self.evidence_control_ids:
672
+ self._link_evidence_to_controls(created_evidence.id, is_attachment=False)
673
+
674
+ except Exception as e:
675
+ logger.error(f"Error creating evidence record for KMS: {e}", exc_info=True)
676
+
677
+ def _build_evidence_description(self, scan_date: str) -> str:
678
+ """
679
+ Build HTML-formatted evidence description.
680
+
681
+ :param str scan_date: Scan date string
682
+ :return: HTML description
683
+ :rtype: str
684
+ """
685
+ # Gather statistics
686
+ kms_stats = self._calculate_kms_statistics()
687
+ control_stats = self._calculate_control_compliance_stats()
688
+
689
+ # Build description
690
+ desc_parts = self._build_evidence_header(scan_date)
691
+ desc_parts.extend(self._build_filter_info())
692
+ desc_parts.extend(self._build_kms_summary(kms_stats))
693
+ desc_parts.extend(self._build_control_compliance_summary(control_stats))
694
+
695
+ return "\n".join(desc_parts)
696
+
697
+ def _calculate_kms_statistics(self) -> Dict[str, Any]:
698
+ """Calculate KMS key statistics."""
699
+ total_keys = len(self.raw_kms_data)
700
+ rotation_enabled_count = sum(1 for k in self.raw_kms_data if k.get("RotationEnabled", False))
701
+ customer_managed_count = sum(1 for k in self.raw_kms_data if k.get("KeyManager") == "CUSTOMER")
702
+
703
+ rotation_pct = rotation_enabled_count / max(total_keys, 1) * 100
704
+
705
+ return {
706
+ "total": total_keys,
707
+ "rotation_enabled": rotation_enabled_count,
708
+ "rotation_pct": rotation_pct,
709
+ "customer_managed": customer_managed_count,
710
+ }
711
+
712
+ def _calculate_control_compliance_stats(self) -> Dict[str, Dict[str, int]]:
713
+ """Calculate compliance statistics by control."""
714
+ control_stats = {control_id: {"pass": 0, "fail": 0} for control_id in self.control_mapper.get_mapped_controls()}
715
+
716
+ for key_data in self.raw_kms_data:
717
+ compliance_item = self.create_compliance_item(key_data)
718
+ self._update_control_stats(control_stats, compliance_item._compliance_results)
719
+
720
+ return control_stats
721
+
722
+ def _update_control_stats(
723
+ self, control_stats: Dict[str, Dict[str, int]], compliance_results: Dict[str, str]
724
+ ) -> None:
725
+ """Update control statistics with compliance results."""
726
+ for control_id, result in compliance_results.items():
727
+ if result == "PASS":
728
+ control_stats[control_id]["pass"] += 1
729
+ else:
730
+ control_stats[control_id]["fail"] += 1
731
+
732
+ def _build_evidence_header(self, scan_date: str) -> List[str]:
733
+ """Build the evidence header section."""
734
+ return [
735
+ "<h1>AWS KMS Evidence</h1>",
736
+ f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Assessment Date:{HTML_STRONG_CLOSE} {scan_date}{HTML_P_CLOSE}",
737
+ f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Region:{HTML_STRONG_CLOSE} {self.region}{HTML_P_CLOSE}",
738
+ ]
739
+
740
+ def _build_filter_info(self) -> List[str]:
741
+ """Build filter information section."""
742
+ filter_parts = []
743
+
744
+ if self.account_id:
745
+ filter_parts.append(
746
+ f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Filtered by Account ID:{HTML_STRONG_CLOSE} {self.account_id}{HTML_P_CLOSE}"
747
+ )
748
+
749
+ if self.tags:
750
+ tags_str = ", ".join([f"{k}={v}" for k, v in self.tags.items()])
751
+ filter_parts.append(
752
+ f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Filtered by Tags:{HTML_STRONG_CLOSE} {tags_str}{HTML_P_CLOSE}"
753
+ )
754
+
755
+ return filter_parts
756
+
757
+ def _build_kms_summary(self, kms_stats: Dict[str, Any]) -> List[str]:
758
+ """Build KMS summary section."""
759
+ return [
760
+ f"{HTML_H2_OPEN}Summary{HTML_H2_CLOSE}",
761
+ HTML_UL_OPEN,
762
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Total Keys:{HTML_STRONG_CLOSE} {kms_stats['total']}{HTML_LI_CLOSE}",
763
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Customer-Managed Keys:{HTML_STRONG_CLOSE} "
764
+ f"{kms_stats['customer_managed']}{HTML_LI_CLOSE}",
765
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Rotation Enabled:{HTML_STRONG_CLOSE} {kms_stats['rotation_enabled']} "
766
+ f"({kms_stats['rotation_pct']:.1f}%){HTML_LI_CLOSE}",
767
+ HTML_UL_CLOSE,
768
+ ]
769
+
770
+ def _build_control_compliance_summary(self, control_stats: Dict[str, Dict[str, int]]) -> List[str]:
771
+ """Build control compliance summary section."""
772
+ section_parts = [
773
+ f"{HTML_H2_OPEN}Control Compliance Results{HTML_H2_CLOSE}",
774
+ HTML_UL_OPEN,
775
+ ]
776
+
777
+ for control_id in sorted(control_stats.keys()):
778
+ control_line = self._format_control_stats(control_id, control_stats[control_id])
779
+ section_parts.append(control_line)
780
+
781
+ section_parts.append(HTML_UL_CLOSE)
782
+ return section_parts
783
+
784
+ def _format_control_stats(self, control_id: str, stats: Dict[str, int]) -> str:
785
+ """Format control statistics for display."""
786
+ total = stats["pass"] + stats["fail"]
787
+ pass_pct = stats["pass"] / max(total, 1) * 100
788
+ control_desc = self.control_mapper.get_control_description(control_id)
789
+
790
+ return (
791
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
792
+ f"{stats['pass']} PASS / {stats['fail']} FAIL ({pass_pct:.1f}% compliant) - {control_desc}{HTML_LI_CLOSE}"
793
+ )
794
+
795
+ def _upload_evidence_file(self, evidence_id: int, scan_date: str) -> None:
796
+ """
797
+ Upload compressed JSONL evidence file to Evidence record.
798
+
799
+ :param int evidence_id: Evidence record ID
800
+ :param str scan_date: Scan date string
801
+ :return: None
802
+ :rtype: None
803
+ """
804
+ try:
805
+ # Prepare JSONL content
806
+ jsonl_lines = []
807
+ for key_data in self.raw_kms_data:
808
+ compliance_item = self.create_compliance_item(key_data)
809
+ evidence_entry = {
810
+ **key_data,
811
+ "compliance_assessment": {
812
+ "overall_result": compliance_item.compliance_result,
813
+ "control_results": compliance_item._compliance_results,
814
+ "assessed_controls": list(compliance_item._compliance_results.keys()),
815
+ "assessment_date": scan_date,
816
+ },
817
+ }
818
+ jsonl_lines.append(json.dumps(evidence_entry, default=str))
819
+
820
+ jsonl_content = "\n".join(jsonl_lines)
821
+
822
+ # Compress
823
+ compressed_buffer = BytesIO()
824
+ with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
825
+ gz_file.write(jsonl_content)
826
+
827
+ compressed_data = compressed_buffer.getvalue()
828
+
829
+ # Upload
830
+ file_name = f"kms_evidence_{scan_date}.jsonl.gz"
831
+ api = Api()
832
+ success = File.upload_file_to_regscale(
833
+ file_name=file_name,
834
+ parent_id=evidence_id,
835
+ parent_module="evidence",
836
+ api=api,
837
+ file_data=compressed_data,
838
+ tags="aws,kms,encryption",
839
+ )
840
+
841
+ if success:
842
+ logger.info(f"Uploaded KMS evidence file to Evidence {evidence_id}")
843
+ else:
844
+ logger.warning(f"Failed to upload KMS evidence file to Evidence {evidence_id}")
845
+
846
+ except Exception as e:
847
+ logger.error(f"Error uploading evidence file: {e}", exc_info=True)
848
+
849
+ def _link_evidence_to_ssp(self, evidence_id: int) -> None:
850
+ """
851
+ Link evidence to Security Plan.
852
+
853
+ :param int evidence_id: Evidence record ID
854
+ :return: None
855
+ :rtype: None
856
+ """
857
+ try:
858
+ mapping = EvidenceMapping(evidenceID=evidence_id, mappedID=self.plan_id, mappingType=self.parent_module)
859
+ mapping.create()
860
+ logger.info(f"Linked evidence {evidence_id} to SSP {self.plan_id}")
861
+ except Exception as ex:
862
+ logger.warning(f"Failed to link evidence to SSP: {ex}")
863
+
864
+ def _link_evidence_to_controls(self, evidence_id: int, is_attachment: bool = False) -> None:
865
+ """
866
+ Link evidence to specified control IDs.
867
+
868
+ :param int evidence_id: Evidence or attachment ID
869
+ :param bool is_attachment: True if linking attachment, False for evidence record
870
+ """
871
+ try:
872
+ for control_id in self.evidence_control_ids:
873
+ if is_attachment:
874
+ self.api.link_ssp_attachment_to_control(self.plan_id, evidence_id, control_id)
875
+ else:
876
+ self.api.link_evidence_to_control(evidence_id, control_id)
877
+ logger.info(f"Linked evidence {evidence_id} to control {control_id}")
878
+ except Exception as e:
879
+ logger.error(f"Failed to link evidence to controls: {e}", exc_info=True)