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,632 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """AWS S3 Evidence Integration for RegScale Compliance."""
4
+
5
+ import gzip
6
+ import json
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ import boto3
16
+
17
+ from regscale.core.app.api import Api
18
+ from regscale.integrations.commercial.aws.inventory.resources.s3 import S3Collector
19
+ from regscale.integrations.commercial.aws.s3_control_mappings import S3ControlMapper
20
+ from regscale.integrations.compliance_integration import ComplianceIntegration
21
+ from regscale.models.regscale_models.evidence import Evidence
22
+ from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
23
+ from regscale.models.regscale_models.file import File
24
+
25
+ logger = logging.getLogger("regscale")
26
+
27
+ # HTML formatting constants
28
+ HTML_UL_CLOSE = "</ul>"
29
+
30
+
31
+ @dataclass
32
+ class S3EvidenceConfig:
33
+ """Configuration for AWS S3 evidence collection."""
34
+
35
+ plan_id: int
36
+ region: str = "us-east-1"
37
+ framework: str = "NIST800-53R5"
38
+ create_issues: bool = False
39
+ update_control_status: bool = True
40
+ create_poams: bool = False
41
+ parent_module: str = "securityplans"
42
+ account_id: Optional[str] = None
43
+ tags: Optional[Dict[str, str]] = None
44
+ bucket_name_filter: Optional[str] = None
45
+ create_evidence: bool = False
46
+ create_ssp_attachment: bool = True
47
+ evidence_control_ids: Optional[List[str]] = None
48
+ force_refresh: bool = False
49
+ aws_profile: Optional[str] = None
50
+ aws_access_key_id: Optional[str] = None
51
+ aws_secret_access_key: Optional[str] = None
52
+ aws_session_token: Optional[str] = None
53
+
54
+
55
+ class S3ComplianceItem:
56
+ """Represents S3 bucket configuration for compliance assessment."""
57
+
58
+ def __init__(self, bucket_data: Dict[str, Any]):
59
+ """
60
+ Initialize S3 compliance item from bucket data.
61
+
62
+ :param Dict bucket_data: Bucket configuration data from S3Collector
63
+ """
64
+ self.bucket_name = bucket_data.get("Name", "")
65
+ self.region = bucket_data.get("Region", "")
66
+ self.creation_date = bucket_data.get("CreationDate", "")
67
+ self.encryption = bucket_data.get("Encryption", {})
68
+ self.versioning = bucket_data.get("Versioning", {})
69
+ self.public_access_block = bucket_data.get("PublicAccessBlock", {})
70
+ self.policy_status = bucket_data.get("PolicyStatus", {})
71
+ self.acl = bucket_data.get("ACL", {})
72
+ self.logging = bucket_data.get("Logging", {})
73
+ self.tags = bucket_data.get("Tags", [])
74
+ self.raw_data = bucket_data
75
+
76
+ def to_dict(self) -> Dict[str, Any]:
77
+ """Convert to dictionary representation."""
78
+ return self.raw_data
79
+
80
+
81
+ class AWSS3EvidenceIntegration(ComplianceIntegration):
82
+ """AWS S3 evidence integration for compliance data collection."""
83
+
84
+ def __init__(self, config: S3EvidenceConfig):
85
+ """
86
+ Initialize AWS S3 evidence integration.
87
+
88
+ :param S3EvidenceConfig config: Configuration object containing all parameters
89
+ """
90
+ super().__init__(
91
+ plan_id=config.plan_id,
92
+ framework=config.framework,
93
+ create_issues=config.create_issues,
94
+ update_control_status=config.update_control_status,
95
+ create_poams=config.create_poams,
96
+ parent_module=config.parent_module,
97
+ )
98
+
99
+ self.plan_id = config.plan_id
100
+ self.region = config.region
101
+ self.title = "AWS S3"
102
+ self.account_id = config.account_id
103
+ self.tags = config.tags or {}
104
+ self.bucket_name_filter = config.bucket_name_filter
105
+ self.create_evidence = config.create_evidence
106
+ self.create_ssp_attachment = config.create_ssp_attachment
107
+ self.evidence_control_ids = config.evidence_control_ids or []
108
+ self.force_refresh = config.force_refresh
109
+
110
+ # Initialize control mapper
111
+ self.control_mapper = S3ControlMapper(framework=config.framework)
112
+
113
+ # AWS credentials
114
+ self.aws_profile = config.aws_profile
115
+ self.aws_access_key_id = config.aws_access_key_id
116
+ self.aws_secret_access_key = config.aws_secret_access_key
117
+ self.aws_session_token = config.aws_session_token
118
+
119
+ # Initialize components
120
+ self.api = Api()
121
+ self.session = None
122
+ self.collector = None
123
+
124
+ # Cache configuration
125
+ self.cache_ttl_hours = 4
126
+ self.cache_dir = Path(tempfile.gettempdir()) / "regscale" / "aws_s3_cache"
127
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
128
+
129
+ # Data storage
130
+ self.raw_s3_data: Dict[str, Any] = {}
131
+ self.buckets: List[S3ComplianceItem] = []
132
+
133
+ def _get_cache_file_path(self) -> Path:
134
+ """Get cache file path for S3 data."""
135
+ cache_key = f"{self.region}_{self.account_id or 'default'}"
136
+ return self.cache_dir / f"s3_buckets_{cache_key}.json"
137
+
138
+ def _is_cache_valid(self) -> bool:
139
+ """Check if cache is valid and not expired."""
140
+ cache_file = self._get_cache_file_path()
141
+ if not cache_file.exists():
142
+ return False
143
+
144
+ cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
145
+ return cache_age < timedelta(hours=self.cache_ttl_hours)
146
+
147
+ def _save_cache(self, data: Dict[str, Any]) -> None:
148
+ """Save S3 data to cache."""
149
+ cache_file = self._get_cache_file_path()
150
+ try:
151
+ with open(cache_file, "w", encoding="utf-8") as f:
152
+ json.dump(data, f, default=str)
153
+ logger.debug(f"Saved S3 data to cache: {cache_file}")
154
+ except Exception as e:
155
+ logger.warning(f"Failed to save cache: {e}")
156
+
157
+ def _load_cached_data(self) -> Optional[List[Dict[str, Any]]]:
158
+ """Load S3 data from cache."""
159
+ cache_file = self._get_cache_file_path()
160
+ try:
161
+ with open(cache_file, encoding="utf-8") as f:
162
+ data = json.load(f)
163
+
164
+ # Validate cache format - must be a list of dicts
165
+ if not isinstance(data, list):
166
+ logger.warning("Invalid cache format detected (not a list). Invalidating cache.")
167
+ return None
168
+
169
+ # Check if first item is a dict (bucket configuration)
170
+ if data and not isinstance(data[0], dict):
171
+ logger.warning("Invalid cache format detected (items not dicts). Invalidating cache.")
172
+ return None
173
+
174
+ logger.info(f"Loaded S3 data from cache (age: {self._get_cache_age_hours():.1f} hours)")
175
+ return data
176
+ except Exception as e:
177
+ logger.warning(f"Failed to load cache: {e}")
178
+ return None
179
+
180
+ def _get_cache_age_hours(self) -> float:
181
+ """Get cache age in hours."""
182
+ cache_file = self._get_cache_file_path()
183
+ if not cache_file.exists():
184
+ return float("inf")
185
+ cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
186
+ return cache_age.total_seconds() / 3600
187
+
188
+ def _initialize_aws_session(self) -> None:
189
+ """Initialize AWS session using provided credentials."""
190
+ if self.aws_access_key_id and self.aws_secret_access_key:
191
+ self.session = boto3.Session(
192
+ aws_access_key_id=self.aws_access_key_id,
193
+ aws_secret_access_key=self.aws_secret_access_key,
194
+ aws_session_token=self.aws_session_token,
195
+ region_name=self.region,
196
+ )
197
+ elif self.aws_profile:
198
+ self.session = boto3.Session(profile_name=self.aws_profile, region_name=self.region)
199
+ else:
200
+ self.session = boto3.Session(region_name=self.region)
201
+ logger.info(f"Initialized AWS session for region: {self.region}")
202
+
203
+ def fetch_compliance_data(self) -> List[Dict[str, Any]]:
204
+ """
205
+ Fetch S3 bucket configuration data from AWS.
206
+
207
+ :return: List of bucket configurations
208
+ :rtype: List[Dict[str, Any]]
209
+ """
210
+ # Check cache first
211
+ if not self.force_refresh and self._is_cache_valid():
212
+ cached_data = self._load_cached_data()
213
+ if cached_data:
214
+ return cached_data
215
+
216
+ # Fetch fresh data
217
+ return self._fetch_fresh_s3_data()
218
+
219
+ def _fetch_fresh_s3_data(self) -> List[Dict[str, Any]]:
220
+ """
221
+ Fetch fresh S3 bucket data from AWS API.
222
+
223
+ :return: List of bucket configurations
224
+ :rtype: List[Dict[str, Any]]
225
+ """
226
+ logger.info(f"Fetching S3 bucket configurations from AWS region: {self.region}")
227
+
228
+ # Initialize AWS session
229
+ if not self.session:
230
+ self._initialize_aws_session()
231
+
232
+ # Create S3 collector
233
+ self.collector = S3Collector(
234
+ session=self.session, region=self.region, account_id=self.account_id, tags=self.tags
235
+ )
236
+
237
+ # Collect S3 data
238
+ self.raw_s3_data = self.collector.collect()
239
+ buckets = self.raw_s3_data.get("Buckets", [])
240
+
241
+ # Apply bucket name filter if specified
242
+ if self.bucket_name_filter:
243
+ buckets = [b for b in buckets if self.bucket_name_filter in b.get("Name", "")]
244
+ logger.info(f"Applied bucket name filter '{self.bucket_name_filter}': {len(buckets)} buckets match")
245
+
246
+ logger.info(f"Collected {len(buckets)} S3 bucket(s) from region {self.region}")
247
+
248
+ # Save to cache
249
+ self._save_cache(buckets)
250
+
251
+ return buckets
252
+
253
+ def sync_compliance_data(self) -> None:
254
+ """Sync S3 compliance data to RegScale."""
255
+ logger.info("Starting AWS S3 compliance data sync to RegScale")
256
+
257
+ # Fetch bucket data
258
+ bucket_data = self.fetch_compliance_data()
259
+ if not bucket_data:
260
+ logger.warning("No S3 bucket data to sync")
261
+ return
262
+
263
+ # Convert to compliance items
264
+ self.buckets = [S3ComplianceItem(bucket) for bucket in bucket_data]
265
+ logger.info(f"Processing {len(self.buckets)} S3 bucket(s) for compliance assessment")
266
+
267
+ # Assess compliance
268
+ compliance_results = self._assess_compliance()
269
+
270
+ # Populate control dictionaries for assessment creation
271
+ if self.update_control_status:
272
+ self._populate_control_results(compliance_results["overall"])
273
+ # Create control assessments and update implementation statuses
274
+ self._process_control_assessments()
275
+
276
+ # Create evidence artifacts
277
+ if self.create_evidence or self.create_ssp_attachment:
278
+ self._create_evidence_artifacts(compliance_results)
279
+
280
+ logger.info("AWS S3 compliance sync completed successfully")
281
+
282
+ def create_compliance_item(self, raw_data: Dict[str, Any]):
283
+ """
284
+ Create a ComplianceItem from raw S3 bucket data.
285
+
286
+ :param Dict[str, Any] raw_data: Raw S3 bucket data
287
+ :return: S3ComplianceItem instance
288
+ :rtype: S3ComplianceItem
289
+ """
290
+ return S3ComplianceItem(raw_data)
291
+
292
+ def _assess_compliance(self) -> Dict[str, Any]:
293
+ """
294
+ Assess S3 compliance against NIST controls.
295
+
296
+ :return: Compliance assessment results
297
+ :rtype: Dict[str, Any]
298
+ """
299
+ logger.info("Assessing S3 compliance against NIST 800-53 R5 controls")
300
+
301
+ # Assess each bucket individually
302
+ bucket_assessments = []
303
+ for bucket_item in self.buckets:
304
+ bucket_result = self.control_mapper.assess_bucket_compliance(bucket_item.to_dict())
305
+ bucket_assessments.append({"bucket_name": bucket_item.bucket_name, "controls": bucket_result})
306
+
307
+ # Get overall compliance results
308
+ bucket_dicts = [b.to_dict() for b in self.buckets]
309
+ overall_results = self.control_mapper.assess_all_buckets_compliance(bucket_dicts)
310
+
311
+ # Log summary
312
+ passed_controls = [ctrl for ctrl, result in overall_results.items() if result == "PASS"]
313
+ failed_controls = [ctrl for ctrl, result in overall_results.items() if result == "FAIL"]
314
+
315
+ logger.info("S3 Compliance Assessment Summary:")
316
+ logger.info(f" Total Buckets: {len(self.buckets)}")
317
+ logger.info(f" Controls Passed: {len(passed_controls)} - {', '.join(passed_controls)}")
318
+ logger.info(f" Controls Failed: {len(failed_controls)} - {', '.join(failed_controls)}")
319
+
320
+ return {"overall": overall_results, "buckets": bucket_assessments}
321
+
322
+ def _populate_control_results(self, control_results: Dict[str, str]) -> None:
323
+ """
324
+ Populate passing_controls and failing_controls dictionaries from assessment results.
325
+
326
+ This method converts the control-level assessment results into the format expected
327
+ by the base class _process_control_assessments() method.
328
+
329
+ :param Dict[str, str] control_results: Control assessment results (e.g., {"AC-2": "PASS", "AC-3": "FAIL"})
330
+ :return: None
331
+ :rtype: None
332
+ """
333
+ for control_id, result in control_results.items():
334
+ # Normalize control ID to lowercase for consistent lookup
335
+ control_key = control_id.lower()
336
+
337
+ # Use first bucket as placeholder for the base class
338
+ placeholder_item = self.buckets[0] if self.buckets else None
339
+ if not placeholder_item:
340
+ continue
341
+
342
+ # Create a simple compliance item placeholder for the base class
343
+ if result in self.PASS_STATUSES:
344
+ self.passing_controls[control_key] = placeholder_item
345
+ elif result in self.FAIL_STATUSES:
346
+ self.failing_controls[control_key] = placeholder_item
347
+
348
+ logger.debug(
349
+ f"Populated control results: {len(self.passing_controls)} passing, {len(self.failing_controls)} failing"
350
+ )
351
+
352
+ def _create_evidence_artifacts(self, compliance_results: Dict[str, Any]) -> None:
353
+ """
354
+ Create evidence artifacts in RegScale.
355
+
356
+ :param Dict compliance_results: Compliance assessment results
357
+ """
358
+ logger.info("Creating S3 evidence artifacts in RegScale")
359
+
360
+ # Create comprehensive evidence file
361
+ evidence_file_path = self._create_evidence_file(compliance_results)
362
+
363
+ if self.create_ssp_attachment:
364
+ self._create_ssp_attachment_with_evidence(evidence_file_path)
365
+
366
+ if self.create_evidence:
367
+ self._create_evidence_records(evidence_file_path, compliance_results)
368
+
369
+ # Clean up temporary file
370
+ if os.path.exists(evidence_file_path):
371
+ os.remove(evidence_file_path)
372
+ logger.debug(f"Cleaned up temporary evidence file: {evidence_file_path}")
373
+
374
+ def _create_evidence_file(self, compliance_results: Dict[str, Any]) -> str:
375
+ """
376
+ Create JSONL.GZ evidence file with S3 configuration data.
377
+
378
+ :param Dict compliance_results: Compliance assessment results
379
+ :return: Path to created evidence file
380
+ :rtype: str
381
+ """
382
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
383
+ evidence_file = os.path.join(tempfile.gettempdir(), f"s3_evidence_{self.region}_{timestamp}.jsonl.gz")
384
+
385
+ try:
386
+ with gzip.open(evidence_file, "wt", encoding="utf-8") as f:
387
+ # Write metadata
388
+ metadata = {
389
+ "type": "metadata",
390
+ "timestamp": datetime.now().isoformat(),
391
+ "region": self.region,
392
+ "account_id": self.account_id,
393
+ "bucket_count": len(self.buckets),
394
+ "compliance_framework": "NIST800-53R5",
395
+ }
396
+ f.write(json.dumps(metadata) + "\n")
397
+
398
+ # Write compliance summary
399
+ summary = {"type": "compliance_summary", "results": compliance_results["overall"]}
400
+ f.write(json.dumps(summary) + "\n")
401
+
402
+ # Write bucket configurations
403
+ for bucket_item in self.buckets:
404
+ bucket_record = {
405
+ "type": "bucket_configuration",
406
+ "bucket_name": bucket_item.bucket_name,
407
+ "region": bucket_item.region,
408
+ "encryption": bucket_item.encryption,
409
+ "versioning": bucket_item.versioning,
410
+ "public_access_block": bucket_item.public_access_block,
411
+ "policy_status": bucket_item.policy_status,
412
+ "acl": bucket_item.acl,
413
+ "logging": bucket_item.logging,
414
+ "tags": bucket_item.tags,
415
+ }
416
+ f.write(json.dumps(bucket_record, default=str) + "\n")
417
+
418
+ logger.info(f"Created evidence file: {evidence_file}")
419
+ return evidence_file
420
+
421
+ except Exception as e:
422
+ logger.error(f"Failed to create evidence file: {e}", exc_info=True)
423
+ raise
424
+
425
+ def _create_ssp_attachment_with_evidence(self, evidence_file_path: str) -> None:
426
+ """
427
+ Create SSP attachment with S3 evidence.
428
+
429
+ :param str evidence_file_path: Path to evidence file
430
+ """
431
+ try:
432
+ date_str = datetime.now().strftime("%Y%m%d")
433
+ file_name_pattern = f"s3_evidence_{self.region}_{date_str}"
434
+
435
+ # Check if evidence for today already exists using base class method
436
+ if self.check_for_existing_evidence(file_name_pattern):
437
+ logger.info(
438
+ f"Evidence file for S3 in region {self.region} already exists for today. "
439
+ "Skipping upload to avoid duplicates."
440
+ )
441
+ return
442
+
443
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
444
+ file_name = f"s3_evidence_{self.region}_{timestamp}.jsonl.gz"
445
+
446
+ # Read the compressed file
447
+ with open(evidence_file_path, "rb") as f:
448
+ file_data = f.read()
449
+
450
+ # Upload file to RegScale
451
+ success = File.upload_file_to_regscale(
452
+ file_name=file_name,
453
+ parent_id=self.plan_id,
454
+ parent_module="securityplans",
455
+ api=self.api,
456
+ file_data=file_data,
457
+ tags="aws,s3,storage,compliance,automated",
458
+ )
459
+
460
+ if success:
461
+ logger.info(f"Successfully uploaded S3 evidence file: {file_name}")
462
+ else:
463
+ logger.error("Failed to upload S3 evidence file")
464
+
465
+ except Exception as e:
466
+ logger.error(f"Failed to create SSP attachment: {e}", exc_info=True)
467
+
468
+ def _create_evidence_records(self, evidence_file_path: str, compliance_results: Dict[str, Any]) -> None:
469
+ """
470
+ Create evidence records in RegScale.
471
+
472
+ :param str evidence_file_path: Path to evidence file
473
+ :param Dict compliance_results: Compliance assessment results
474
+ """
475
+ try:
476
+ description = self._build_evidence_description(compliance_results)
477
+
478
+ # Calculate due date (30 days from now as default)
479
+ due_date = (datetime.now() + timedelta(days=30)).isoformat()
480
+
481
+ # Create evidence record using Evidence model directly
482
+ evidence = Evidence(
483
+ title=f"AWS S3 Compliance Evidence - {self.region}",
484
+ description=description,
485
+ status="Collected",
486
+ updateFrequency=30,
487
+ dueDate=due_date,
488
+ )
489
+
490
+ created_evidence = evidence.create()
491
+ if not created_evidence or not created_evidence.id:
492
+ logger.error("Failed to create evidence record")
493
+ return
494
+
495
+ logger.info(f"Created evidence record: {created_evidence.id}")
496
+
497
+ # Upload evidence file
498
+ self._upload_evidence_file(created_evidence.id, evidence_file_path)
499
+
500
+ # Link evidence to SSP
501
+ self._link_evidence_to_ssp(created_evidence.id)
502
+
503
+ # Link to controls if specified
504
+ if self.evidence_control_ids:
505
+ self._link_evidence_to_controls(created_evidence.id, is_attachment=False)
506
+
507
+ except Exception as e:
508
+ logger.error(f"Failed to create evidence record: {e}", exc_info=True)
509
+
510
+ def _upload_evidence_file(self, evidence_id: int, file_path: str) -> None:
511
+ """
512
+ Upload evidence file to RegScale.
513
+
514
+ :param int evidence_id: Evidence record ID
515
+ :param str file_path: Path to evidence file
516
+ """
517
+ try:
518
+ # Read the compressed file
519
+ with open(file_path, "rb") as f:
520
+ file_data = f.read()
521
+
522
+ # Generate filename from the path
523
+ file_name = os.path.basename(file_path)
524
+
525
+ # Upload file to Evidence record
526
+ success = File.upload_file_to_regscale(
527
+ file_name=file_name,
528
+ parent_id=evidence_id,
529
+ parent_module="evidence",
530
+ api=self.api,
531
+ file_data=file_data,
532
+ tags="aws,s3,storage,compliance,automated",
533
+ )
534
+
535
+ if success:
536
+ logger.info(f"Uploaded evidence file for evidence ID: {evidence_id}")
537
+ else:
538
+ logger.warning(f"Failed to upload evidence file for evidence ID: {evidence_id}")
539
+ except Exception as e:
540
+ logger.error(f"Failed to upload evidence file: {e}", exc_info=True)
541
+
542
+ def _link_evidence_to_ssp(self, evidence_id: int) -> None:
543
+ """
544
+ Link evidence to Security Plan.
545
+
546
+ :param int evidence_id: Evidence record ID
547
+ :return: None
548
+ :rtype: None
549
+ """
550
+ try:
551
+ mapping = EvidenceMapping(evidenceID=evidence_id, mappedID=self.plan_id, mappingType=self.parent_module)
552
+ mapping.create()
553
+ logger.info(f"Linked evidence {evidence_id} to SSP {self.plan_id}")
554
+ except Exception as ex:
555
+ logger.warning(f"Failed to link evidence to SSP: {ex}")
556
+
557
+ def _link_evidence_to_controls(self, evidence_id: int, is_attachment: bool = False) -> None:
558
+ """
559
+ Link evidence to specified control IDs.
560
+
561
+ :param int evidence_id: Evidence or attachment ID
562
+ :param bool is_attachment: True if linking attachment, False for evidence record
563
+ """
564
+ try:
565
+ for control_id in self.evidence_control_ids:
566
+ if is_attachment:
567
+ self.api.link_ssp_attachment_to_control(self.plan_id, evidence_id, control_id)
568
+ else:
569
+ self.api.link_evidence_to_control(evidence_id, control_id)
570
+ logger.info(f"Linked evidence {evidence_id} to control {control_id}")
571
+ except Exception as e:
572
+ logger.error(f"Failed to link evidence to controls: {e}", exc_info=True)
573
+
574
+ def _build_evidence_description(self, compliance_results: Dict[str, Any]) -> str:
575
+ """
576
+ Build HTML-formatted evidence description.
577
+
578
+ :param Dict compliance_results: Compliance assessment results
579
+ :return: HTML description
580
+ :rtype: str
581
+ """
582
+ overall_results = compliance_results.get("overall", {})
583
+ passed_controls = [ctrl for ctrl, result in overall_results.items() if result == "PASS"]
584
+ failed_controls = [ctrl for ctrl, result in overall_results.items() if result == "FAIL"]
585
+
586
+ desc_parts = [
587
+ "<h3>AWS S3 Storage Configuration Evidence</h3>",
588
+ f"<p><strong>Region:</strong> {self.region}</p>",
589
+ f"<p><strong>Account ID:</strong> {self.account_id or 'N/A'}</p>",
590
+ f"<p><strong>Collection Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>",
591
+ f"<p><strong>Total Buckets:</strong> {len(self.buckets)}</p>",
592
+ "<h4>Compliance Summary</h4>",
593
+ f"<p><strong>Controls Passed:</strong> {len(passed_controls)}</p>",
594
+ "<ul>",
595
+ ]
596
+
597
+ for control in passed_controls:
598
+ control_desc = self.control_mapper.get_control_description(control)
599
+ desc_parts.append(f"<li>{control}: {control_desc}</li>")
600
+
601
+ desc_parts.append(HTML_UL_CLOSE)
602
+
603
+ if failed_controls:
604
+ desc_parts.append(f"<p><strong>Controls Failed:</strong> {len(failed_controls)}</p>")
605
+ desc_parts.append("<ul>")
606
+ for control in failed_controls:
607
+ control_desc = self.control_mapper.get_control_description(control)
608
+ desc_parts.append(f"<li>{control}: {control_desc}</li>")
609
+ desc_parts.append(HTML_UL_CLOSE)
610
+
611
+ desc_parts.extend(
612
+ [
613
+ "<h4>Bucket Configurations</h4>",
614
+ "<ul>",
615
+ ]
616
+ )
617
+
618
+ for bucket_item in self.buckets[:10]: # Limit to first 10 for description
619
+ encryption_status = "Enabled" if bucket_item.encryption.get("Enabled") else "Disabled"
620
+ versioning_status = bucket_item.versioning.get("Status", "Disabled")
621
+ desc_parts.append(
622
+ f"<li><strong>{bucket_item.bucket_name}</strong>: "
623
+ f"Encryption={encryption_status}, Versioning={versioning_status}</li>"
624
+ )
625
+
626
+ if len(self.buckets) > 10:
627
+ desc_parts.append(f"<li><em>... and {len(self.buckets) - 10} more buckets</em></li>")
628
+
629
+ desc_parts.append(HTML_UL_CLOSE)
630
+ desc_parts.append("<p><em>Complete configuration data available in attached evidence file.</em></p>")
631
+
632
+ return "".join(desc_parts)