trustops-security-data-lake 0.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 (329) hide show
  1. security_lakehouse/__init__.py +3 -0
  2. security_lakehouse/agents/__init__.py +21 -0
  3. security_lakehouse/agents/budgets.py +143 -0
  4. security_lakehouse/agents/checkpoints.py +54 -0
  5. security_lakehouse/agents/evaluations.py +201 -0
  6. security_lakehouse/agents/graphs.py +199 -0
  7. security_lakehouse/agents/model_client.py +245 -0
  8. security_lakehouse/agents/model_contract.py +226 -0
  9. security_lakehouse/agents/providers.py +132 -0
  10. security_lakehouse/agents/soc.py +168 -0
  11. security_lakehouse/agents/state.py +42 -0
  12. security_lakehouse/agents/tools.py +191 -0
  13. security_lakehouse/ai_governance.py +317 -0
  14. security_lakehouse/aibom.py +242 -0
  15. security_lakehouse/api_legacy.py +430 -0
  16. security_lakehouse/api_v1.py +1668 -0
  17. security_lakehouse/assessment.py +655 -0
  18. security_lakehouse/audit_log.py +258 -0
  19. security_lakehouse/audit_readiness.py +399 -0
  20. security_lakehouse/auth/__init__.py +14 -0
  21. security_lakehouse/auth/api_key_session.py +50 -0
  22. security_lakehouse/auth/dependencies.py +114 -0
  23. security_lakehouse/auth/idp_roles.py +77 -0
  24. security_lakehouse/auth/oidc.py +139 -0
  25. security_lakehouse/auth/presentation.py +237 -0
  26. security_lakehouse/auth/rate_limit.py +117 -0
  27. security_lakehouse/auth/rate_limit_redis.py +110 -0
  28. security_lakehouse/auth/rbac.py +44 -0
  29. security_lakehouse/auth/request_audit.py +55 -0
  30. security_lakehouse/auth/saml.py +230 -0
  31. security_lakehouse/auth/sessions.py +80 -0
  32. security_lakehouse/auth/tokens.py +35 -0
  33. security_lakehouse/auth/users_admin.py +74 -0
  34. security_lakehouse/brand_assets.py +85 -0
  35. security_lakehouse/catalog.py +222 -0
  36. security_lakehouse/catalog_versions.py +415 -0
  37. security_lakehouse/cli.py +1877 -0
  38. security_lakehouse/cloud_linking.py +444 -0
  39. security_lakehouse/commercial/__init__.py +1 -0
  40. security_lakehouse/commercial/email.py +50 -0
  41. security_lakehouse/commercial/invites.py +152 -0
  42. security_lakehouse/commercial/limits.py +91 -0
  43. security_lakehouse/commercial/pricing.py +158 -0
  44. security_lakehouse/commercial/scim.py +38 -0
  45. security_lakehouse/commercial/scim_provision.py +175 -0
  46. security_lakehouse/commercial/signup.py +99 -0
  47. security_lakehouse/connector_health.py +98 -0
  48. security_lakehouse/connector_runner.py +1028 -0
  49. security_lakehouse/connector_state.py +1376 -0
  50. security_lakehouse/connectors.py +116 -0
  51. security_lakehouse/connectors_aws.py +550 -0
  52. security_lakehouse/connectors_azure.py +501 -0
  53. security_lakehouse/connectors_clickhouse.py +308 -0
  54. security_lakehouse/connectors_gcp.py +384 -0
  55. security_lakehouse/connectors_google_workspace.py +363 -0
  56. security_lakehouse/connectors_jira.py +453 -0
  57. security_lakehouse/connectors_okta.py +464 -0
  58. security_lakehouse/connectors_runtime.py +304 -0
  59. security_lakehouse/connectors_s3.py +409 -0
  60. security_lakehouse/connectors_scaffold.py +119 -0
  61. security_lakehouse/connectors_siem.py +304 -0
  62. security_lakehouse/connectors_snowflake.py +772 -0
  63. security_lakehouse/controls.py +45 -0
  64. security_lakehouse/dashboard.py +184 -0
  65. security_lakehouse/data_policy.py +67 -0
  66. security_lakehouse/db/__init__.py +19 -0
  67. security_lakehouse/db/access_reviews.py +289 -0
  68. security_lakehouse/db/agent_runs.py +278 -0
  69. security_lakehouse/db/base.py +97 -0
  70. security_lakehouse/db/metrics.py +323 -0
  71. security_lakehouse/db/migrate.py +45 -0
  72. security_lakehouse/db/models.py +638 -0
  73. security_lakehouse/db/poam.py +161 -0
  74. security_lakehouse/db/policy_acknowledgments.py +113 -0
  75. security_lakehouse/db/policy_documents.py +178 -0
  76. security_lakehouse/db/remediation.py +320 -0
  77. security_lakehouse/db/repository.py +189 -0
  78. security_lakehouse/db/risks.py +174 -0
  79. security_lakehouse/db/tags.py +321 -0
  80. security_lakehouse/db/vendor_assessments.py +138 -0
  81. security_lakehouse/demo_links.py +218 -0
  82. security_lakehouse/evidence_freshness.py +324 -0
  83. security_lakehouse/evidence_freshness_workflows.py +152 -0
  84. security_lakehouse/evidence_hints.py +136 -0
  85. security_lakehouse/evidence_types.py +34 -0
  86. security_lakehouse/fixtures.py +78 -0
  87. security_lakehouse/framework_coverage.py +191 -0
  88. security_lakehouse/framework_detail.py +170 -0
  89. security_lakehouse/framework_enrich.py +137 -0
  90. security_lakehouse/framework_packs.py +755 -0
  91. security_lakehouse/framework_provenance.py +101 -0
  92. security_lakehouse/framework_sync.py +167 -0
  93. security_lakehouse/golden_fixture.py +129 -0
  94. security_lakehouse/graph.py +626 -0
  95. security_lakehouse/identity.py +58 -0
  96. security_lakehouse/ingestion/__init__.py +6 -0
  97. security_lakehouse/ingestion/backoff.py +90 -0
  98. security_lakehouse/ingestion/merge.py +36 -0
  99. security_lakehouse/ingestion/paginate.py +37 -0
  100. security_lakehouse/ingestion/strategy.py +117 -0
  101. security_lakehouse/ingestion/watermark.py +41 -0
  102. security_lakehouse/ingestion_metrics.py +77 -0
  103. security_lakehouse/ingestion_status.py +397 -0
  104. security_lakehouse/io.py +231 -0
  105. security_lakehouse/lake_eval.py +151 -0
  106. security_lakehouse/lake_scale.py +163 -0
  107. security_lakehouse/ledger.py +87 -0
  108. security_lakehouse/limited_packs.py +208 -0
  109. security_lakehouse/mappings.py +218 -0
  110. security_lakehouse/mcp_server.py +1272 -0
  111. security_lakehouse/migrations/env.py +53 -0
  112. security_lakehouse/migrations/script.py.mako +26 -0
  113. security_lakehouse/migrations/versions/0001_baseline.py +57 -0
  114. security_lakehouse/migrations/versions/0002_api_keys.py +74 -0
  115. security_lakehouse/migrations/versions/0003_user_sessions.py +53 -0
  116. security_lakehouse/migrations/versions/0004_remediation.py +99 -0
  117. security_lakehouse/migrations/versions/0005_tags.py +82 -0
  118. security_lakehouse/migrations/versions/0006_metrics.py +61 -0
  119. security_lakehouse/migrations/versions/0007_risks.py +61 -0
  120. security_lakehouse/migrations/versions/0008_agent_runs.py +62 -0
  121. security_lakehouse/migrations/versions/0009_access_reviews.py +81 -0
  122. security_lakehouse/migrations/versions/0010_vendor_assessments.py +59 -0
  123. security_lakehouse/migrations/versions/0011_policy_documents.py +56 -0
  124. security_lakehouse/migrations/versions/0012_tenant_invites.py +52 -0
  125. security_lakehouse/migrations/versions/0013_tenant_plan_tier.py +35 -0
  126. security_lakehouse/migrations/versions/0014_policy_acknowledgments.py +57 -0
  127. security_lakehouse/migrations/versions/0015_poam_items.py +66 -0
  128. security_lakehouse/models.py +79 -0
  129. security_lakehouse/netguard.py +150 -0
  130. security_lakehouse/pack_data.py +231 -0
  131. security_lakehouse/pack_spec.py +22 -0
  132. security_lakehouse/pipeline.py +1222 -0
  133. security_lakehouse/platform_jobs.py +134 -0
  134. security_lakehouse/policy.py +201 -0
  135. security_lakehouse/policy_templates.py +69 -0
  136. security_lakehouse/programs.py +262 -0
  137. security_lakehouse/public_url.py +33 -0
  138. security_lakehouse/readiness.py +104 -0
  139. security_lakehouse/remediation_guidance.py +47 -0
  140. security_lakehouse/repo_audit.py +394 -0
  141. security_lakehouse/repo_governance.py +585 -0
  142. security_lakehouse/reporting/__init__.py +5 -0
  143. security_lakehouse/reporting/executive_pdf.py +200 -0
  144. security_lakehouse/safeguards.py +186 -0
  145. security_lakehouse/scale_synthesis.py +211 -0
  146. security_lakehouse/scenarios.py +498 -0
  147. security_lakehouse/scheduler.py +416 -0
  148. security_lakehouse/sdk.py +542 -0
  149. security_lakehouse/server.py +287 -0
  150. security_lakehouse/server_app.py +3181 -0
  151. security_lakehouse/server_routes/__init__.py +1 -0
  152. security_lakehouse/server_routes/deps.py +56 -0
  153. security_lakehouse/server_routes/routers/__init__.py +1 -0
  154. security_lakehouse/server_routes/routers/gov_compliance.py +117 -0
  155. security_lakehouse/server_routes/schemas/__init__.py +1 -0
  156. security_lakehouse/server_routes/schemas/base.py +11 -0
  157. security_lakehouse/server_routes/schemas/poam.py +28 -0
  158. security_lakehouse/services/__init__.py +29 -0
  159. security_lakehouse/services/access_reviews.py +269 -0
  160. security_lakehouse/services/grc.py +252 -0
  161. security_lakehouse/services/poam.py +181 -0
  162. security_lakehouse/services/policy_documents.py +183 -0
  163. security_lakehouse/services/vendor_risk.py +134 -0
  164. security_lakehouse/sinks/__init__.py +48 -0
  165. security_lakehouse/sinks/clickhouse_sink.py +241 -0
  166. security_lakehouse/sinks/duckdb_sink.py +263 -0
  167. security_lakehouse/sinks/snowflake_sink.py +316 -0
  168. security_lakehouse/sprs.py +101 -0
  169. security_lakehouse/tenancy.py +61 -0
  170. security_lakehouse/tracking.py +95 -0
  171. security_lakehouse/trust_share.py +224 -0
  172. security_lakehouse/validation.py +54 -0
  173. security_lakehouse/vendor_questionnaires.py +93 -0
  174. security_lakehouse/verification.py +169 -0
  175. security_lakehouse/web/__init__.py +22 -0
  176. security_lakehouse/web/dist/404/index.html +1 -0
  177. security_lakehouse/web/dist/404.html +1 -0
  178. security_lakehouse/web/dist/_next/static/Ar_yy_KUlNa749xK_jip_/_buildManifest.js +1 -0
  179. security_lakehouse/web/dist/_next/static/Ar_yy_KUlNa749xK_jip_/_ssgManifest.js +1 -0
  180. security_lakehouse/web/dist/_next/static/chunks/1223-14899aff00021144.js +1 -0
  181. security_lakehouse/web/dist/_next/static/chunks/1255-c6b05b4c36a5bddd.js +1 -0
  182. security_lakehouse/web/dist/_next/static/chunks/16-5cc0ce99b5028664.js +1 -0
  183. security_lakehouse/web/dist/_next/static/chunks/1a258343-51490711a941b76f.js +1 -0
  184. security_lakehouse/web/dist/_next/static/chunks/2162-c2369dc3fa45bd47.js +1 -0
  185. security_lakehouse/web/dist/_next/static/chunks/2706-eafc31a0af87064e.js +1 -0
  186. security_lakehouse/web/dist/_next/static/chunks/2738-16012512b31ffb0d.js +1 -0
  187. security_lakehouse/web/dist/_next/static/chunks/3348-ab818b21353b7409.js +1 -0
  188. security_lakehouse/web/dist/_next/static/chunks/3792-c80d84ca8a63e61a.js +1 -0
  189. security_lakehouse/web/dist/_next/static/chunks/3939-b3496ee4946c7b56.js +1 -0
  190. security_lakehouse/web/dist/_next/static/chunks/464-cafe99efc4721d8f.js +1 -0
  191. security_lakehouse/web/dist/_next/static/chunks/4bd1b696-100b9d70ed4e49c1.js +1 -0
  192. security_lakehouse/web/dist/_next/static/chunks/6718-14478b20f381320d.js +1 -0
  193. security_lakehouse/web/dist/_next/static/chunks/6792-943d405c942df658.js +1 -0
  194. security_lakehouse/web/dist/_next/static/chunks/6801-5ecf34952e155c23.js +1 -0
  195. security_lakehouse/web/dist/_next/static/chunks/705-8496719e77eed0d0.js +1 -0
  196. security_lakehouse/web/dist/_next/static/chunks/7319-75e71da3729a234c.js +1 -0
  197. security_lakehouse/web/dist/_next/static/chunks/7349-0bfff027f44b3aee.js +1 -0
  198. security_lakehouse/web/dist/_next/static/chunks/7802-f93238751149e6db.js +1 -0
  199. security_lakehouse/web/dist/_next/static/chunks/8166-894aaa63ecfd823b.js +1 -0
  200. security_lakehouse/web/dist/_next/static/chunks/9036-146f5b60690b7a4a.js +1 -0
  201. security_lakehouse/web/dist/_next/static/chunks/9602-b450c27d5c2e2d05.js +1 -0
  202. security_lakehouse/web/dist/_next/static/chunks/9870-621f3324f99f9e7e.js +1 -0
  203. security_lakehouse/web/dist/_next/static/chunks/9983-fb6d5ee55bc6a0e7.js +1 -0
  204. security_lakehouse/web/dist/_next/static/chunks/app/_not-found/page-c8d07af36791f7cf.js +1 -0
  205. security_lakehouse/web/dist/_next/static/chunks/app/access-reviews/page-86ce9210581f6d9e.js +1 -0
  206. security_lakehouse/web/dist/_next/static/chunks/app/agents/page-d4930bd870efdaa5.js +1 -0
  207. security_lakehouse/web/dist/_next/static/chunks/app/ai-governance/page-71fc21f12408f67f.js +1 -0
  208. security_lakehouse/web/dist/_next/static/chunks/app/audit-log/page-978922d4f24dfdd3.js +1 -0
  209. security_lakehouse/web/dist/_next/static/chunks/app/audit-room/page-1bb746a22da5f0d6.js +1 -0
  210. security_lakehouse/web/dist/_next/static/chunks/app/auth/page-bb0ae8e9ef69667b.js +1 -0
  211. security_lakehouse/web/dist/_next/static/chunks/app/automation/page-7fa3d7690bcdf17e.js +1 -0
  212. security_lakehouse/web/dist/_next/static/chunks/app/connectors/page-5085fc6aa4061c61.js +1 -0
  213. security_lakehouse/web/dist/_next/static/chunks/app/controls/page-32b9cdc3b0329038.js +1 -0
  214. security_lakehouse/web/dist/_next/static/chunks/app/crosswalk/page-d3ef2494036946ed.js +1 -0
  215. security_lakehouse/web/dist/_next/static/chunks/app/dashboard/page-ed2510351637d0d8.js +1 -0
  216. security_lakehouse/web/dist/_next/static/chunks/app/demo/page-f540fcda72f1e836.js +1 -0
  217. security_lakehouse/web/dist/_next/static/chunks/app/deploy/page-dec8b11158a91c5a.js +1 -0
  218. security_lakehouse/web/dist/_next/static/chunks/app/evidence/page-70229c0493c7745b.js +1 -0
  219. security_lakehouse/web/dist/_next/static/chunks/app/frameworks/page-c1ef5e354486be46.js +1 -0
  220. security_lakehouse/web/dist/_next/static/chunks/app/graph/page-b3efa662cce319c0.js +1 -0
  221. security_lakehouse/web/dist/_next/static/chunks/app/insights/page-9a7e9df0aa2ec1e0.js +1 -0
  222. security_lakehouse/web/dist/_next/static/chunks/app/invite/page-5f43c0a9760f97fb.js +1 -0
  223. security_lakehouse/web/dist/_next/static/chunks/app/layout-562b65e36e81fea1.js +1 -0
  224. security_lakehouse/web/dist/_next/static/chunks/app/login/page-9114f4988ef2dd71.js +1 -0
  225. security_lakehouse/web/dist/_next/static/chunks/app/onboarding/page-f72a218063562882.js +1 -0
  226. security_lakehouse/web/dist/_next/static/chunks/app/page-159da0cc7a804e5c.js +1 -0
  227. security_lakehouse/web/dist/_next/static/chunks/app/poc/page-2212a992d2369005.js +1 -0
  228. security_lakehouse/web/dist/_next/static/chunks/app/policies/page-194ff3236807cb7e.js +1 -0
  229. security_lakehouse/web/dist/_next/static/chunks/app/pricing/page-159da0cc7a804e5c.js +1 -0
  230. security_lakehouse/web/dist/_next/static/chunks/app/remediation/page-47fcca10ac4ada2e.js +1 -0
  231. security_lakehouse/web/dist/_next/static/chunks/app/risks/page-b00b0a5c7231fa58.js +1 -0
  232. security_lakehouse/web/dist/_next/static/chunks/app/signup/page-159da0cc7a804e5c.js +1 -0
  233. security_lakehouse/web/dist/_next/static/chunks/app/trust/[token]/page-c418f0c0197e1d3c.js +1 -0
  234. security_lakehouse/web/dist/_next/static/chunks/app/trust-center/page-bc8890526d7d5e7a.js +1 -0
  235. security_lakehouse/web/dist/_next/static/chunks/app/vendor-risk/page-416724d1b3e8c623.js +1 -0
  236. security_lakehouse/web/dist/_next/static/chunks/app/violations/page-31474c1349685079.js +1 -0
  237. security_lakehouse/web/dist/_next/static/chunks/framework-d063bb259975c6cd.js +1 -0
  238. security_lakehouse/web/dist/_next/static/chunks/main-a6eedcab8b0e1044.js +1 -0
  239. security_lakehouse/web/dist/_next/static/chunks/main-app-4624b51e6155d7fd.js +1 -0
  240. security_lakehouse/web/dist/_next/static/chunks/pages/_app-4b3fb5e477a0267f.js +1 -0
  241. security_lakehouse/web/dist/_next/static/chunks/pages/_error-c970d8b55ace1b48.js +1 -0
  242. security_lakehouse/web/dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  243. security_lakehouse/web/dist/_next/static/chunks/webpack-f182010c21e2e6e3.js +1 -0
  244. security_lakehouse/web/dist/_next/static/css/8453b8ba6645dc0d.css +3 -0
  245. security_lakehouse/web/dist/_next/static/css/e07f2bf4fa519943.css +31 -0
  246. security_lakehouse/web/dist/access-reviews/index.html +1 -0
  247. security_lakehouse/web/dist/access-reviews/index.txt +23 -0
  248. security_lakehouse/web/dist/agents/index.html +1 -0
  249. security_lakehouse/web/dist/agents/index.txt +23 -0
  250. security_lakehouse/web/dist/ai-governance/index.html +1 -0
  251. security_lakehouse/web/dist/ai-governance/index.txt +23 -0
  252. security_lakehouse/web/dist/audit-log/index.html +1 -0
  253. security_lakehouse/web/dist/audit-log/index.txt +23 -0
  254. security_lakehouse/web/dist/audit-room/index.html +1 -0
  255. security_lakehouse/web/dist/audit-room/index.txt +23 -0
  256. security_lakehouse/web/dist/auth/index.html +1 -0
  257. security_lakehouse/web/dist/auth/index.txt +23 -0
  258. security_lakehouse/web/dist/automation/index.html +2 -0
  259. security_lakehouse/web/dist/automation/index.txt +24 -0
  260. security_lakehouse/web/dist/connectors/index.html +1 -0
  261. security_lakehouse/web/dist/connectors/index.txt +23 -0
  262. security_lakehouse/web/dist/controls/index.html +1 -0
  263. security_lakehouse/web/dist/controls/index.txt +23 -0
  264. security_lakehouse/web/dist/crosswalk/index.html +1 -0
  265. security_lakehouse/web/dist/crosswalk/index.txt +23 -0
  266. security_lakehouse/web/dist/dashboard/index.html +1 -0
  267. security_lakehouse/web/dist/dashboard/index.txt +23 -0
  268. security_lakehouse/web/dist/demo/index.html +1 -0
  269. security_lakehouse/web/dist/demo/index.txt +23 -0
  270. security_lakehouse/web/dist/deploy/index.html +1 -0
  271. security_lakehouse/web/dist/deploy/index.txt +23 -0
  272. security_lakehouse/web/dist/evidence/index.html +1 -0
  273. security_lakehouse/web/dist/evidence/index.txt +23 -0
  274. security_lakehouse/web/dist/frameworks/index.html +1 -0
  275. security_lakehouse/web/dist/frameworks/index.txt +23 -0
  276. security_lakehouse/web/dist/frameworks/nist-ai-rmf.png +0 -0
  277. security_lakehouse/web/dist/frameworks/nist-csf-2.0.png +0 -0
  278. security_lakehouse/web/dist/graph/index.html +1 -0
  279. security_lakehouse/web/dist/graph/index.txt +24 -0
  280. security_lakehouse/web/dist/icon.svg +27 -0
  281. security_lakehouse/web/dist/index.html +1 -0
  282. security_lakehouse/web/dist/index.txt +20 -0
  283. security_lakehouse/web/dist/insights/index.html +1 -0
  284. security_lakehouse/web/dist/insights/index.txt +23 -0
  285. security_lakehouse/web/dist/invite/index.html +1 -0
  286. security_lakehouse/web/dist/invite/index.txt +23 -0
  287. security_lakehouse/web/dist/login/index.html +1 -0
  288. security_lakehouse/web/dist/login/index.txt +23 -0
  289. security_lakehouse/web/dist/og/trustops-share.svg +71 -0
  290. security_lakehouse/web/dist/onboarding/index.html +1 -0
  291. security_lakehouse/web/dist/onboarding/index.txt +23 -0
  292. security_lakehouse/web/dist/poc/index.html +1 -0
  293. security_lakehouse/web/dist/poc/index.txt +23 -0
  294. security_lakehouse/web/dist/policies/index.html +1 -0
  295. security_lakehouse/web/dist/policies/index.txt +23 -0
  296. security_lakehouse/web/dist/pricing/index.html +1 -0
  297. security_lakehouse/web/dist/pricing/index.txt +20 -0
  298. security_lakehouse/web/dist/remediation/index.html +1 -0
  299. security_lakehouse/web/dist/remediation/index.txt +23 -0
  300. security_lakehouse/web/dist/risks/index.html +1 -0
  301. security_lakehouse/web/dist/risks/index.txt +23 -0
  302. security_lakehouse/web/dist/signup/index.html +1 -0
  303. security_lakehouse/web/dist/signup/index.txt +20 -0
  304. security_lakehouse/web/dist/trust/share/index.html +1 -0
  305. security_lakehouse/web/dist/trust/share/index.txt +20 -0
  306. security_lakehouse/web/dist/trust-center/index.html +1 -0
  307. security_lakehouse/web/dist/trust-center/index.txt +23 -0
  308. security_lakehouse/web/dist/vendor-risk/index.html +1 -0
  309. security_lakehouse/web/dist/vendor-risk/index.txt +23 -0
  310. security_lakehouse/web/dist/violations/index.html +1 -0
  311. security_lakehouse/web/dist/violations/index.txt +23 -0
  312. security_lakehouse/workflows.py +1551 -0
  313. trustops_security_data_lake-0.2.1.data/data/connectors/catalog.json +422 -0
  314. trustops_security_data_lake-0.2.1.data/data/controls/bundle.lock.json +6716 -0
  315. trustops_security_data_lake-0.2.1.data/data/controls/catalog.json +27515 -0
  316. trustops_security_data_lake-0.2.1.data/data/frameworks/registry.json +214 -0
  317. trustops_security_data_lake-0.2.1.data/data/frameworks/verified_article_ids.json +974 -0
  318. trustops_security_data_lake-0.2.1.data/data/mappings/control_articles.json +13195 -0
  319. trustops_security_data_lake-0.2.1.data/data/mappings/control_map.json +6444 -0
  320. trustops_security_data_lake-0.2.1.data/data/mappings/remediation_guidance.json +70 -0
  321. trustops_security_data_lake-0.2.1.data/data/policy_templates/catalog.json +106 -0
  322. trustops_security_data_lake-0.2.1.data/data/programs/catalog.json +90 -0
  323. trustops_security_data_lake-0.2.1.data/data/programs/vendor_questionnaires.json +101 -0
  324. trustops_security_data_lake-0.2.1.dist-info/METADATA +237 -0
  325. trustops_security_data_lake-0.2.1.dist-info/RECORD +329 -0
  326. trustops_security_data_lake-0.2.1.dist-info/WHEEL +5 -0
  327. trustops_security_data_lake-0.2.1.dist-info/entry_points.txt +3 -0
  328. trustops_security_data_lake-0.2.1.dist-info/licenses/LICENSE +202 -0
  329. trustops_security_data_lake-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """TrustOps Security Data Lake package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,21 @@
1
+ """Optional agent harness for TrustOps.
2
+
3
+ The harness is allowed to orchestrate TrustOps APIs and propose actions. It is
4
+ not the source of truth for evidence, controls, RBAC, redaction, or compliance
5
+ evaluation.
6
+ """
7
+
8
+ from security_lakehouse.agents.budgets import AgentBudgetPolicy
9
+ from security_lakehouse.agents.graphs import build_posture_review_graph, build_soc_triage_graph, run_posture_review
10
+ from security_lakehouse.agents.soc import run_soc_triage
11
+ from security_lakehouse.agents.state import AgentDecision, AgentRunState
12
+
13
+ __all__ = [
14
+ "AgentBudgetPolicy",
15
+ "AgentDecision",
16
+ "AgentRunState",
17
+ "build_posture_review_graph",
18
+ "build_soc_triage_graph",
19
+ "run_posture_review",
20
+ "run_soc_triage",
21
+ ]
@@ -0,0 +1,143 @@
1
+ """Token and context budgets for optional model-assisted harness runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from copy import deepcopy
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+
12
+ def _env_int(name: str, default: int, *, minimum: int, maximum: int) -> int:
13
+ raw = os.environ.get(name)
14
+ if raw is None:
15
+ return default
16
+ try:
17
+ value = int(raw)
18
+ except ValueError:
19
+ return default
20
+ return max(minimum, min(value, maximum))
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AgentBudgetPolicy:
25
+ """Bound how much context an optional model can receive."""
26
+
27
+ max_context_chars: int = 12_000
28
+ max_fact_items: int = 20
29
+ max_output_tokens: int = 600
30
+ max_string_chars: int = 1_000
31
+
32
+ def __post_init__(self) -> None:
33
+ object.__setattr__(self, "max_context_chars", max(256, min(int(self.max_context_chars), 200_000)))
34
+ object.__setattr__(self, "max_fact_items", max(1, min(int(self.max_fact_items), 250)))
35
+ object.__setattr__(self, "max_output_tokens", max(64, min(int(self.max_output_tokens), 8_000)))
36
+ object.__setattr__(self, "max_string_chars", max(128, min(int(self.max_string_chars), 16_000)))
37
+
38
+ @classmethod
39
+ def from_env(cls) -> AgentBudgetPolicy:
40
+ """Load bounded budget knobs from environment."""
41
+ return cls(
42
+ max_context_chars=_env_int("TRUSTOPS_AGENT_MAX_CONTEXT_CHARS", 12_000, minimum=1_000, maximum=200_000),
43
+ max_fact_items=_env_int("TRUSTOPS_AGENT_MAX_FACT_ITEMS", 20, minimum=1, maximum=250),
44
+ max_output_tokens=_env_int("TRUSTOPS_AGENT_MAX_OUTPUT_TOKENS", 600, minimum=64, maximum=8_000),
45
+ max_string_chars=_env_int("TRUSTOPS_AGENT_MAX_STRING_CHARS", 1_000, minimum=128, maximum=16_000),
46
+ )
47
+
48
+ def public_dict(self) -> dict[str, int | str]:
49
+ """Return non-secret budget policy metadata for audit output."""
50
+ return {
51
+ "profile": "small_context_default",
52
+ "max_context_chars": self.max_context_chars,
53
+ "max_fact_items": self.max_fact_items,
54
+ "max_output_tokens": self.max_output_tokens,
55
+ "max_string_chars": self.max_string_chars,
56
+ }
57
+
58
+
59
+ def estimate_tokens(payload: Any) -> int:
60
+ """Estimate model input tokens without provider-specific tokenizers."""
61
+ chars = len(json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str))
62
+ return max(1, (chars + 3) // 4)
63
+
64
+
65
+ def apply_budget(context: dict[str, Any], policy: AgentBudgetPolicy) -> dict[str, Any]:
66
+ """Return a compacted model context with budget telemetry attached."""
67
+ original_chars = _context_chars(context)
68
+ original_tokens = estimate_tokens(context)
69
+ for item_limit in _candidate_limits(policy.max_fact_items):
70
+ candidate = deepcopy(context)
71
+ omitted: dict[str, int] = {}
72
+ _limit_fact_lists(candidate, item_limit=item_limit, omitted=omitted)
73
+ _truncate_strings(candidate, max_chars=policy.max_string_chars, omitted=omitted)
74
+ candidate["budget"] = {
75
+ **policy.public_dict(),
76
+ "status": "checking",
77
+ "estimated_context_chars_before": original_chars,
78
+ "estimated_context_tokens_before": original_tokens,
79
+ "estimated_context_chars": 0,
80
+ "estimated_context_tokens": 0,
81
+ "fact_item_limit_applied": item_limit,
82
+ "truncated": bool(omitted),
83
+ "omitted": omitted,
84
+ }
85
+ compacted_chars = _context_chars(candidate)
86
+ if compacted_chars <= policy.max_context_chars or item_limit == 1:
87
+ status = "within_budget" if compacted_chars <= policy.max_context_chars else "over_budget"
88
+ candidate["budget"] = {
89
+ **policy.public_dict(),
90
+ "status": status,
91
+ "estimated_context_chars_before": original_chars,
92
+ "estimated_context_tokens_before": original_tokens,
93
+ "estimated_context_chars": compacted_chars,
94
+ "estimated_context_tokens": estimate_tokens(candidate),
95
+ "fact_item_limit_applied": item_limit,
96
+ "truncated": bool(omitted),
97
+ "omitted": omitted,
98
+ }
99
+ return candidate
100
+ raise AssertionError("candidate limits always return at least one policy")
101
+
102
+
103
+ def _candidate_limits(max_items: int) -> list[int]:
104
+ limits: list[int] = []
105
+ current = max(1, max_items)
106
+ while current not in limits:
107
+ limits.append(current)
108
+ if current == 1:
109
+ break
110
+ current = max(1, current // 2)
111
+ return limits
112
+
113
+
114
+ def _context_chars(payload: Any) -> int:
115
+ return len(json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str))
116
+
117
+
118
+ def _limit_fact_lists(context: dict[str, Any], *, item_limit: int, omitted: dict[str, int]) -> None:
119
+ facts = context.get("facts")
120
+ if not isinstance(facts, dict):
121
+ return
122
+ for key in ("evidence_gaps", "alerts", "deterministic_decisions"):
123
+ value = facts.get(key)
124
+ if not isinstance(value, list) or len(value) <= item_limit:
125
+ continue
126
+ facts[key] = value[:item_limit]
127
+ omitted[f"facts.{key}"] = len(value) - item_limit
128
+
129
+
130
+ def _truncate_strings(value: Any, *, max_chars: int, omitted: dict[str, int], path: str = "") -> Any:
131
+ if isinstance(value, dict):
132
+ for key, child in list(value.items()):
133
+ child_path = f"{path}.{key}" if path else str(key)
134
+ value[key] = _truncate_strings(child, max_chars=max_chars, omitted=omitted, path=child_path)
135
+ return value
136
+ if isinstance(value, list):
137
+ for index, child in enumerate(list(value)):
138
+ value[index] = _truncate_strings(child, max_chars=max_chars, omitted=omitted, path=f"{path}[{index}]")
139
+ return value
140
+ if isinstance(value, str) and len(value) > max_chars:
141
+ omitted[path or "string"] = len(value) - max_chars
142
+ return value[:max_chars] + "...[truncated]"
143
+ return value
@@ -0,0 +1,54 @@
1
+ """LangGraph checkpoint helpers for resumable harness orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from typing import Any
7
+
8
+
9
+ def langgraph_available() -> bool:
10
+ return importlib.util.find_spec("langgraph") is not None
11
+
12
+
13
+ def memory_checkpointer() -> Any:
14
+ """Return a process-local MemorySaver checkpointer."""
15
+ try:
16
+ from langgraph.checkpoint.memory import MemorySaver
17
+ except ImportError as exc: # pragma: no cover - optional extra
18
+ raise RuntimeError("install trustops-security-data-lake[agents] to use LangGraph checkpoints") from exc
19
+ return MemorySaver()
20
+
21
+
22
+ def run_config(thread_id: str) -> dict[str, Any]:
23
+ return {"configurable": {"thread_id": thread_id}}
24
+
25
+
26
+ def checkpoint_state(compiled: Any, thread_id: str) -> dict[str, Any] | None:
27
+ """Return the latest checkpointed harness state for ``thread_id``, if any."""
28
+ snapshot = compiled.get_state(run_config(thread_id))
29
+ values = getattr(snapshot, "values", None)
30
+ if isinstance(values, dict) and values:
31
+ return dict(values)
32
+ return None
33
+
34
+
35
+ def invoke_with_checkpoint(
36
+ compiled: Any,
37
+ state: dict[str, Any],
38
+ *,
39
+ thread_id: str | None = None,
40
+ resume: bool = False,
41
+ ) -> dict[str, Any]:
42
+ """Invoke a compiled LangGraph, optionally checkpointing by ``thread_id``.
43
+
44
+ The optional model step stays outside the graph; checkpoints only cover
45
+ deterministic TrustOps tool nodes.
46
+ """
47
+ if not thread_id:
48
+ return dict(compiled.invoke(state))
49
+ config = run_config(thread_id)
50
+ if resume and checkpoint_state(compiled, thread_id) is not None:
51
+ result = compiled.invoke(None, config)
52
+ return dict(result) if isinstance(result, dict) else dict(state)
53
+ result = compiled.invoke(state, config)
54
+ return dict(result)
@@ -0,0 +1,201 @@
1
+ """Deterministic guardrail evaluations for agent harnesses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from security_lakehouse.agents.model_contract import POSTURE_REVIEW_TOOL_CALLS, SOC_TRIAGE_TOOL_CALLS
8
+
9
+
10
+ def _decision_items(state: dict[str, Any]) -> list[Any]:
11
+ return list(state.get("decisions") or [])
12
+
13
+
14
+ def _decision_action(item: Any) -> str:
15
+ return str(getattr(item, "action", "") or (item.get("action") if isinstance(item, dict) else ""))
16
+
17
+
18
+ def _decision_requires_approval(item: Any) -> bool:
19
+ if hasattr(item, "requires_approval"):
20
+ return bool(item.requires_approval)
21
+ if isinstance(item, dict):
22
+ return bool(item.get("requires_approval"))
23
+ return False
24
+
25
+
26
+ def _decision_status(item: Any) -> str:
27
+ return str(getattr(item, "status", "") or (item.get("status") if isinstance(item, dict) else ""))
28
+
29
+
30
+ def _check(check: str, passed: bool, detail: dict[str, Any] | None = None, *, weight: int = 1) -> dict[str, Any]:
31
+ return {"check": check, "passed": passed, "detail": detail or {}, "weight": weight}
32
+
33
+
34
+ def _budget_status(state: dict[str, Any]) -> str:
35
+ context = state.get("model_context") if isinstance(state.get("model_context"), dict) else {}
36
+ budget = context.get("budget") if isinstance(context.get("budget"), dict) else {}
37
+ return str(budget.get("status") or "not_applicable")
38
+
39
+
40
+ def _rejected_tool_calls(state: dict[str, Any]) -> list[Any]:
41
+ model_output = state.get("model_output") if isinstance(state.get("model_output"), dict) else {}
42
+ rejected = model_output.get("rejected_tool_calls", []) if isinstance(model_output, dict) else []
43
+ return rejected if isinstance(rejected, list) else []
44
+
45
+
46
+ def _coverage(state: dict[str, Any], *, use_case: str) -> dict[str, Any]:
47
+ decisions = _decision_items(state)
48
+ return {
49
+ "use_case": use_case,
50
+ "mode": state.get("mode", "rules_only"),
51
+ "decisions_total": len(decisions),
52
+ "approval_gated_decisions": sum(1 for item in decisions if _decision_requires_approval(item)),
53
+ "proposed_decisions": sum(1 for item in decisions if _decision_status(item) == "proposed"),
54
+ "model_rejected_tool_calls": len(_rejected_tool_calls(state)),
55
+ "model_budget_status": _budget_status(state),
56
+ "errors": len(state.get("errors") or []),
57
+ }
58
+
59
+
60
+ def _result(checks: list[dict[str, Any]], coverage: dict[str, Any]) -> dict[str, Any]:
61
+ total_weight = sum(int(item.get("weight") or 1) for item in checks) or 1
62
+ passed_weight = sum(int(item.get("weight") or 1) for item in checks if item.get("passed"))
63
+ score = round((passed_weight / total_weight) * 100)
64
+ failures = [item for item in checks if not item.get("passed")]
65
+ rejected_count = int(coverage.get("model_rejected_tool_calls", 0) or 0)
66
+ if failures:
67
+ confidence = "low" if score < 70 else "medium"
68
+ elif rejected_count:
69
+ confidence = "medium"
70
+ elif score >= 90:
71
+ confidence = "high"
72
+ elif score >= 70:
73
+ confidence = "medium"
74
+ else:
75
+ confidence = "low"
76
+ risk_level = "critical" if any(item["check"] == "writes_are_approval_gated" for item in failures) else "high"
77
+ if not failures:
78
+ risk_level = "low" if rejected_count == 0 else "medium"
79
+ return {
80
+ "ok": not failures,
81
+ "score": score,
82
+ "confidence": confidence,
83
+ "risk_level": risk_level,
84
+ "checks": checks,
85
+ "failures": failures,
86
+ "coverage": coverage,
87
+ }
88
+
89
+
90
+ def evaluate_agent_run(state: dict[str, Any], *, use_case: str) -> dict[str, Any]:
91
+ """Evaluate generic harness safety invariants and deterministic confidence."""
92
+ allowed = SOC_TRIAGE_TOOL_CALLS if use_case == "soc_triage" else POSTURE_REVIEW_TOOL_CALLS
93
+ checks: list[dict[str, Any]] = []
94
+ decisions = _decision_items(state)
95
+ unsupported = sorted({_decision_action(item) for item in decisions if _decision_action(item) not in allowed})
96
+ checks.append(_check("allowed_actions_only", not unsupported, {"unsupported_actions": unsupported}, weight=3))
97
+ unsafe_writes = [
98
+ _decision_action(item)
99
+ for item in decisions
100
+ if _decision_action(item) in allowed
101
+ and (not _decision_requires_approval(item) or _decision_status(item) == "executed")
102
+ ]
103
+ checks.append(_check("writes_are_approval_gated", not unsafe_writes, {"unsafe_actions": unsafe_writes}, weight=4))
104
+ model_output = state.get("model_output") if isinstance(state.get("model_output"), dict) else {}
105
+ rejected = model_output.get("rejected_tool_calls", []) if isinstance(model_output, dict) else []
106
+ checks.append(
107
+ _check(
108
+ "model_rejections_recorded",
109
+ isinstance(rejected, list),
110
+ {"rejected_tool_calls": rejected if isinstance(rejected, list) else "invalid"},
111
+ weight=1,
112
+ )
113
+ )
114
+ budget_status = _budget_status(state)
115
+ errors = {str(item) for item in state.get("errors") or []}
116
+ checks.append(
117
+ _check(
118
+ "context_budget_enforced",
119
+ budget_status != "over_budget" or "model_skipped: context_budget_exceeded" in errors,
120
+ {"budget_status": budget_status},
121
+ weight=2,
122
+ )
123
+ )
124
+ coverage = _coverage(state, use_case=use_case)
125
+ if use_case == "posture_review":
126
+ evidence_gaps = [row for row in state.get("evidence_gaps", []) if isinstance(row, dict)]
127
+ gap_control_ids = {str(row.get("control_id") or "") for row in evidence_gaps if row.get("control_id")}
128
+ covered_control_ids = set()
129
+ for item in decisions:
130
+ payload = (
131
+ getattr(item, "payload", None)
132
+ if hasattr(item, "payload")
133
+ else item.get("payload")
134
+ if isinstance(item, dict)
135
+ else {}
136
+ )
137
+ if isinstance(payload, dict) and payload.get("control_id"):
138
+ covered_control_ids.add(str(payload["control_id"]))
139
+ missing = sorted(control_id for control_id in gap_control_ids if control_id not in covered_control_ids)
140
+ checks.append(
141
+ _check(
142
+ "evidence_gaps_have_actions",
143
+ not missing,
144
+ {"missing_control_ids": missing},
145
+ weight=2,
146
+ )
147
+ )
148
+ coverage = {
149
+ **coverage,
150
+ "evidence_gaps_total": len(evidence_gaps),
151
+ "covered_evidence_gap_controls": len(gap_control_ids) - len(missing),
152
+ "evidence_gap_coverage": 1.0
153
+ if not gap_control_ids
154
+ else round((len(gap_control_ids) - len(missing)) / len(gap_control_ids), 4),
155
+ }
156
+ return _result(checks, coverage)
157
+
158
+
159
+ def evaluate_posture_review(state: dict[str, Any]) -> dict[str, Any]:
160
+ """Evaluate posture-review guardrails and deterministic confidence."""
161
+ return evaluate_agent_run(state, use_case="posture_review")
162
+
163
+
164
+ def evaluate_soc_triage(state: dict[str, Any]) -> dict[str, Any]:
165
+ """Evaluate SOC triage-specific deterministic guardrails."""
166
+ base = evaluate_agent_run(state, use_case="soc_triage")
167
+ alerts = [row for row in state.get("alerts", []) if isinstance(row, dict)]
168
+ high_priority = {
169
+ str(row.get("event_id") or "")
170
+ for row in alerts
171
+ if str(row.get("status") or "").lower() in {"open", "failed", "blocked", "noncompliant"}
172
+ and str(row.get("severity") or "").lower() in {"critical", "high"}
173
+ }
174
+ covered = set()
175
+ for item in _decision_items(state):
176
+ payload = (
177
+ getattr(item, "payload", None)
178
+ if hasattr(item, "payload")
179
+ else item.get("payload")
180
+ if isinstance(item, dict)
181
+ else {}
182
+ )
183
+ if isinstance(payload, dict):
184
+ covered.add(str(payload.get("event_id") or ""))
185
+ missing = sorted(event_id for event_id in high_priority if event_id and event_id not in covered)
186
+ check = _check(
187
+ "high_priority_alerts_have_actions",
188
+ not missing,
189
+ {"missing_event_ids": missing},
190
+ weight=3,
191
+ )
192
+ coverage = {
193
+ **base["coverage"],
194
+ "alerts_total": len(alerts),
195
+ "high_priority_alerts": len([event_id for event_id in high_priority if event_id]),
196
+ "covered_high_priority_alerts": len([event_id for event_id in high_priority if event_id]) - len(missing),
197
+ "high_priority_coverage": 1.0
198
+ if not high_priority
199
+ else round((len([event_id for event_id in high_priority if event_id]) - len(missing)) / len(high_priority), 4),
200
+ }
201
+ return _result([*base["checks"], check], coverage)
@@ -0,0 +1,199 @@
1
+ """Optional LangGraph workflows around TrustOps facts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from security_lakehouse.agents.budgets import AgentBudgetPolicy
10
+ from security_lakehouse.agents.checkpoints import invoke_with_checkpoint, memory_checkpointer
11
+ from security_lakehouse.agents.evaluations import evaluate_posture_review
12
+ from security_lakehouse.agents.model_client import ModelClientError, call_model_json
13
+ from security_lakehouse.agents.model_contract import (
14
+ POSTURE_REVIEW_TOOL_CALLS,
15
+ build_model_context,
16
+ validate_model_output,
17
+ )
18
+ from security_lakehouse.agents.providers import ModelProviderConfig, provider_from_env
19
+ from security_lakehouse.agents.state import AgentDecision, AgentOrchestrator, AgentRunState
20
+ from security_lakehouse.agents.tools import (
21
+ assess_data_readiness,
22
+ load_evidence_gaps,
23
+ load_redacted_posture,
24
+ propose_evidence_gap_actions,
25
+ )
26
+
27
+ ModelClient = Callable[[dict[str, Any], ModelProviderConfig], dict[str, Any]]
28
+
29
+
30
+ def _initial_posture_state(
31
+ lake_dir: str | Path,
32
+ *,
33
+ role: str,
34
+ objective: str,
35
+ provider: ModelProviderConfig,
36
+ budget: AgentBudgetPolicy,
37
+ orchestrator: AgentOrchestrator,
38
+ ) -> AgentRunState:
39
+ return {
40
+ "lake_dir": str(lake_dir),
41
+ "role": role,
42
+ "objective": objective,
43
+ "mode": "rules_only",
44
+ "orchestrator": orchestrator,
45
+ "model_provider": provider.public_dict(),
46
+ "agent_budget": budget.public_dict(),
47
+ "data_readiness": assess_data_readiness(lake_dir, role=role, harness="posture_review"),
48
+ "errors": [],
49
+ }
50
+
51
+
52
+ def _load_posture_node(state: AgentRunState) -> AgentRunState:
53
+ return {
54
+ **state,
55
+ "posture": load_redacted_posture(state["lake_dir"], role=state.get("role", "read_only")),
56
+ }
57
+
58
+
59
+ def _load_gaps_node(state: AgentRunState) -> AgentRunState:
60
+ return {
61
+ **state,
62
+ "evidence_gaps": load_evidence_gaps(state["lake_dir"], role=state.get("role", "read_only")),
63
+ }
64
+
65
+
66
+ def _propose_actions_node(state: AgentRunState) -> AgentRunState:
67
+ decisions = [
68
+ AgentDecision(
69
+ action=str(item["action"]),
70
+ reason=str(item["reason"]),
71
+ requires_approval=bool(item["requires_approval"]),
72
+ payload={"control_id": item["control_id"]},
73
+ status="proposed",
74
+ )
75
+ for item in propose_evidence_gap_actions(state.get("evidence_gaps", []))
76
+ ]
77
+ return {**state, "decisions": decisions}
78
+
79
+
80
+ def _finalize_no_gaps_node(state: AgentRunState) -> AgentRunState:
81
+ return {**state, "decisions": []}
82
+
83
+
84
+ def run_posture_review(
85
+ lake_dir: str | Path,
86
+ *,
87
+ role: str = "read_only",
88
+ objective: str = "Review posture and propose evidence-gap actions.",
89
+ provider: ModelProviderConfig | None = None,
90
+ budget: AgentBudgetPolicy | None = None,
91
+ model_client: ModelClient | None = None,
92
+ orchestrator: AgentOrchestrator = "sequential",
93
+ checkpoint_thread_id: str | None = None,
94
+ resume: bool = False,
95
+ ) -> AgentRunState:
96
+ """Run the posture-review harness without requiring LangGraph or an LLM."""
97
+ provider = provider or provider_from_env()
98
+ budget = budget or AgentBudgetPolicy.from_env()
99
+ if orchestrator not in {"sequential", "langgraph"}:
100
+ raise ValueError("posture_review orchestrator must be 'sequential' or 'langgraph'")
101
+ state = _initial_posture_state(
102
+ lake_dir,
103
+ role=role,
104
+ objective=objective,
105
+ provider=provider,
106
+ budget=budget,
107
+ orchestrator=orchestrator,
108
+ )
109
+ if orchestrator == "langgraph":
110
+ state = dict(
111
+ invoke_with_checkpoint(
112
+ build_posture_review_graph(checkpointer=memory_checkpointer() if checkpoint_thread_id else None),
113
+ state,
114
+ thread_id=checkpoint_thread_id,
115
+ resume=resume,
116
+ )
117
+ )
118
+ state["mode"] = "langgraph"
119
+ state["orchestrator"] = "langgraph"
120
+ else:
121
+ state = _load_posture_node(state)
122
+ state = _load_gaps_node(state)
123
+ state = _propose_actions_node(state)
124
+ if provider.enabled:
125
+ context = build_model_context(dict(state), provider, budget=budget)
126
+ state["model_context"] = context
127
+ if context.get("budget", {}).get("status") == "over_budget":
128
+ state["errors"] = [*state.get("errors", []), "model_skipped: context_budget_exceeded"]
129
+ elif provider.should_call_model:
130
+ client = model_client or call_model_json
131
+ try:
132
+ raw_output = client(context, provider)
133
+ state["model_output"] = validate_model_output(raw_output, allowed_tool_calls=POSTURE_REVIEW_TOOL_CALLS)
134
+ state["mode"] = "model_assisted"
135
+ except ModelClientError as exc:
136
+ state["errors"] = [*state.get("errors", []), f"model_error: {exc}"]
137
+ state["evaluation"] = evaluate_posture_review(dict(state))
138
+ return state
139
+
140
+
141
+ def build_posture_review_graph(*, checkpointer: Any | None = None) -> Any:
142
+ """Build a LangGraph StateGraph when the optional extra is installed."""
143
+ try:
144
+ from langgraph.graph import END, StateGraph
145
+ except ImportError as exc: # pragma: no cover - exercised without optional extra
146
+ raise RuntimeError("install trustops-security-data-lake[agents] to use LangGraph orchestration") from exc
147
+
148
+ graph = StateGraph(AgentRunState)
149
+
150
+ def _route_after_gaps(state: AgentRunState) -> str:
151
+ gaps = state.get("evidence_gaps") or []
152
+ return "propose_actions" if gaps else "finalize_no_gaps"
153
+
154
+ graph.add_node("load_posture", _load_posture_node)
155
+ graph.add_node("load_evidence_gaps", _load_gaps_node)
156
+ graph.add_node("propose_actions", _propose_actions_node)
157
+ graph.add_node("finalize_no_gaps", _finalize_no_gaps_node)
158
+ graph.set_entry_point("load_posture")
159
+ graph.add_edge("load_posture", "load_evidence_gaps")
160
+ graph.add_conditional_edges(
161
+ "load_evidence_gaps",
162
+ _route_after_gaps,
163
+ {"propose_actions": "propose_actions", "finalize_no_gaps": "finalize_no_gaps"},
164
+ )
165
+ graph.add_edge("propose_actions", END)
166
+ graph.add_edge("finalize_no_gaps", END)
167
+ return graph.compile(checkpointer=checkpointer) if checkpointer is not None else graph.compile()
168
+
169
+
170
+ def _load_alerts_node(state: AgentRunState) -> AgentRunState:
171
+ from security_lakehouse.agents.soc import load_soc_alerts
172
+
173
+ return {
174
+ **state,
175
+ "alerts": load_soc_alerts(state["lake_dir"], role=state.get("role", "read_only")),
176
+ }
177
+
178
+
179
+ def _propose_soc_actions_node(state: AgentRunState) -> AgentRunState:
180
+ from security_lakehouse.agents.soc import propose_soc_actions
181
+
182
+ decisions = propose_soc_actions(state.get("alerts") or [])
183
+ return {**state, "decisions": decisions}
184
+
185
+
186
+ def build_soc_triage_graph(*, checkpointer: Any | None = None) -> Any:
187
+ """Build the SOC triage LangGraph when the optional extra is installed."""
188
+ try:
189
+ from langgraph.graph import END, StateGraph
190
+ except ImportError as exc: # pragma: no cover
191
+ raise RuntimeError("install trustops-security-data-lake[agents] to use LangGraph orchestration") from exc
192
+
193
+ graph = StateGraph(AgentRunState)
194
+ graph.add_node("load_alerts", _load_alerts_node)
195
+ graph.add_node("propose_actions", _propose_soc_actions_node)
196
+ graph.set_entry_point("load_alerts")
197
+ graph.add_edge("load_alerts", "propose_actions")
198
+ graph.add_edge("propose_actions", END)
199
+ return graph.compile(checkpointer=checkpointer) if checkpointer is not None else graph.compile()