actshield 0.9.0__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 (366) hide show
  1. actshield/__init__.py +439 -0
  2. actshield/__main__.py +46 -0
  3. actshield/agents/__init__.py +12 -0
  4. actshield/agents/agent.py +200 -0
  5. actshield/agents/identity.py +114 -0
  6. actshield/api/__init__.py +34 -0
  7. actshield/api/models.py +198 -0
  8. actshield/api/server.py +669 -0
  9. actshield/api/service.py +599 -0
  10. actshield/approval/__init__.py +15 -0
  11. actshield/approval/approval.py +164 -0
  12. actshield/cli/__init__.py +8 -0
  13. actshield/cli/graph.py +49 -0
  14. actshield/cli/main.py +802 -0
  15. actshield/cli/shell.py +406 -0
  16. actshield/cli/theme.py +110 -0
  17. actshield/client.py +757 -0
  18. actshield/config.py +88 -0
  19. actshield/context/__init__.py +14 -0
  20. actshield/context/context.py +161 -0
  21. actshield/context/provenance.py +108 -0
  22. actshield/context/taint.py +48 -0
  23. actshield/decisions/decision.py +74 -0
  24. actshield/delegation/__init__.py +11 -0
  25. actshield/delegation/authority.py +63 -0
  26. actshield/delegation/delegation.py +180 -0
  27. actshield/demo.py +139 -0
  28. actshield/doctor.py +94 -0
  29. actshield/drift/__init__.py +27 -0
  30. actshield/drift/drift_engine.py +149 -0
  31. actshield/drift/drift_models.py +63 -0
  32. actshield/evaluation/__init__.py +1 -0
  33. actshield/evaluation/cases/__init__.py +1 -0
  34. actshield/evaluation/eval_suite.py +254 -0
  35. actshield/forensics/__init__.py +53 -0
  36. actshield/forensics/access_graph.py +216 -0
  37. actshield/forensics/authority_graph.py +186 -0
  38. actshield/forensics/delegation_graph.py +119 -0
  39. actshield/forensics/explanation.py +254 -0
  40. actshield/forensics/models.py +488 -0
  41. actshield/forensics/queries.py +367 -0
  42. actshield/forensics/serializers.py +68 -0
  43. actshield/forensics/service.py +310 -0
  44. actshield/forensics/snapshots.py +174 -0
  45. actshield/gates/__init__.py +13 -0
  46. actshield/gates/gate_models.py +46 -0
  47. actshield/gates/security_gate.py +191 -0
  48. actshield/gateway/__init__.py +13 -0
  49. actshield/gateway/http.py +191 -0
  50. actshield/gateway/mcp.py +292 -0
  51. actshield/incidents/__init__.py +25 -0
  52. actshield/incidents/incident.py +94 -0
  53. actshield/incidents/incident_engine.py +163 -0
  54. actshield/incidents/incident_rules.py +116 -0
  55. actshield/incidents/incident_state.py +58 -0
  56. actshield/integrations/__init__.py +24 -0
  57. actshield/integrations/ai_secura.py +169 -0
  58. actshield/integrations/ai_secura_analysis.py +236 -0
  59. actshield/integrations/apiris.py +122 -0
  60. actshield/integrations/few_shot.py +104 -0
  61. actshield/integrations/ollama_adapter.py +538 -0
  62. actshield/integrations/prompts/__init__.py +1 -0
  63. actshield/integrations/prompts/examples/__init__.py +1 -0
  64. actshield/integrations/security_packet.py +82 -0
  65. actshield/llm_config.py +89 -0
  66. actshield/offensive/__init__.py +95 -0
  67. actshield/offensive/adaptive/__init__.py +22 -0
  68. actshield/offensive/adaptive/adaptive_engine.py +165 -0
  69. actshield/offensive/adaptive/attack_selector.py +51 -0
  70. actshield/offensive/adaptive/boundary_search.py +99 -0
  71. actshield/offensive/adaptive/campaign_memory.py +123 -0
  72. actshield/offensive/adaptive/feedback.py +84 -0
  73. actshield/offensive/adaptive/mutation_planner.py +57 -0
  74. actshield/offensive/attack.py +78 -0
  75. actshield/offensive/campaign.py +219 -0
  76. actshield/offensive/corpus.py +155 -0
  77. actshield/offensive/engine.py +466 -0
  78. actshield/offensive/evaluator.py +116 -0
  79. actshield/offensive/evidence.py +63 -0
  80. actshield/offensive/generator.py +128 -0
  81. actshield/offensive/mutation.py +101 -0
  82. actshield/offensive/results.py +77 -0
  83. actshield/offensive/safety.py +80 -0
  84. actshield/offensive/strategies/__init__.py +22 -0
  85. actshield/offensive/strategies/authority_escalation.py +100 -0
  86. actshield/offensive/strategies/base.py +34 -0
  87. actshield/offensive/strategies/context_manipulation.py +100 -0
  88. actshield/offensive/strategies/data_exfiltration.py +100 -0
  89. actshield/offensive/strategies/delegation_attacks.py +100 -0
  90. actshield/offensive/strategies/mcp_attacks.py +100 -0
  91. actshield/offensive/strategies/prompt_injection.py +100 -0
  92. actshield/offensive/strategies/tool_poisoning.py +100 -0
  93. actshield/offensive/target.py +113 -0
  94. actshield/offensive/vulnerable_target.py +121 -0
  95. actshield/persistence/__init__.py +18 -0
  96. actshield/persistence/siem.py +102 -0
  97. actshield/persistence/storage.py +240 -0
  98. actshield/policy/__init__.py +12 -0
  99. actshield/policy/evaluator.py +294 -0
  100. actshield/policy/intent.py +113 -0
  101. actshield/policy/policy.py +48 -0
  102. actshield/posture/__init__.py +26 -0
  103. actshield/posture/posture_engine.py +186 -0
  104. actshield/posture/posture_models.py +152 -0
  105. actshield/posture/posture_rules.py +137 -0
  106. actshield/providers/__init__.py +32 -0
  107. actshield/providers/anthropic_provider.py +146 -0
  108. actshield/providers/base.py +160 -0
  109. actshield/providers/gemini_provider.py +133 -0
  110. actshield/providers/null_provider.py +40 -0
  111. actshield/providers/ollama_provider.py +135 -0
  112. actshield/providers/openai_provider.py +142 -0
  113. actshield/providers/registry.py +244 -0
  114. actshield/py.typed +0 -0
  115. actshield/response/__init__.py +14 -0
  116. actshield/response/response_actions.py +39 -0
  117. actshield/response/response_audit.py +32 -0
  118. actshield/response/response_engine.py +139 -0
  119. actshield/risk/models.py +104 -0
  120. actshield/serve.py +117 -0
  121. actshield/static/404.html +1 -0
  122. actshield/static/__next.__PAGE__.txt +24 -0
  123. actshield/static/__next._full.txt +20 -0
  124. actshield/static/__next._tree.txt +4 -0
  125. actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_buildManifest.js +11 -0
  126. actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_clientMiddlewareManifest.js +1 -0
  127. actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_ssgManifest.js +1 -0
  128. actshield/static/_next/static/chunks/09kny2eqjc2or.js +1 -0
  129. actshield/static/_next/static/chunks/0c1zba1u9whdp.js +1 -0
  130. actshield/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  131. actshield/static/_next/static/chunks/0dbjjeg01x25o.js +1 -0
  132. actshield/static/_next/static/chunks/0duobs55ne238.js +1 -0
  133. actshield/static/_next/static/chunks/0iaz41l6068e4.js +1 -0
  134. actshield/static/_next/static/chunks/0mjhyv72x2idp.js +1 -0
  135. actshield/static/_next/static/chunks/0nelpqfu_ciq6.js +1 -0
  136. actshield/static/_next/static/chunks/0p1bci9ch797v.js +1 -0
  137. actshield/static/_next/static/chunks/0tld_ygho5tgm.js +1 -0
  138. actshield/static/_next/static/chunks/0w1-y7oni66j9.js +1 -0
  139. actshield/static/_next/static/chunks/10537zyl427zj.js +1 -0
  140. actshield/static/_next/static/chunks/11pc_jkl3aggn.js +1 -0
  141. actshield/static/_next/static/chunks/16k7nw0b3t8ys.js +1 -0
  142. actshield/static/_next/static/chunks/19mx3mg6lkumu.js +1 -0
  143. actshield/static/_next/static/chunks/1_8p47b-kxg2a.js +1 -0
  144. actshield/static/_next/static/chunks/1dszjozwfqegx.js +1 -0
  145. actshield/static/_next/static/chunks/1h43jcb90-m2r.js +22 -0
  146. actshield/static/_next/static/chunks/1ly04cvt_wh_j.js +1 -0
  147. actshield/static/_next/static/chunks/1odxrypuwuwp_.js +1 -0
  148. actshield/static/_next/static/chunks/1oes4c35qz5ku.js +1 -0
  149. actshield/static/_next/static/chunks/1ow883jl4ln-r.js +16 -0
  150. actshield/static/_next/static/chunks/1po7bna1as8tc.js +1 -0
  151. actshield/static/_next/static/chunks/1rsh0og8ifkzx.js +1 -0
  152. actshield/static/_next/static/chunks/1v0lwzv-859z_.js +1 -0
  153. actshield/static/_next/static/chunks/25iat2epwx25a.js +1 -0
  154. actshield/static/_next/static/chunks/25uvt4s2f39a4.js +1 -0
  155. actshield/static/_next/static/chunks/27t_qfc-3_lzs.js +1 -0
  156. actshield/static/_next/static/chunks/28bkmy8mez7gm.js +1 -0
  157. actshield/static/_next/static/chunks/2b6brmw_drw1p.js +1 -0
  158. actshield/static/_next/static/chunks/2fnhtxi4j6vge.js +1 -0
  159. actshield/static/_next/static/chunks/2p087eftxemg4.js +6 -0
  160. actshield/static/_next/static/chunks/2s8pao_z8pgtq.js +1 -0
  161. actshield/static/_next/static/chunks/2yj7ooc2c-zpg.js +1 -0
  162. actshield/static/_next/static/chunks/3-mrrr8udt5tk.js +1 -0
  163. actshield/static/_next/static/chunks/310vm2bl3xxpt.js +31 -0
  164. actshield/static/_next/static/chunks/35mvsv251jx1r.css +3 -0
  165. actshield/static/_next/static/chunks/370vf0oetlsx9.js +1 -0
  166. actshield/static/_next/static/chunks/3e-2m72tyo556.js +1 -0
  167. actshield/static/_next/static/chunks/3fntmmi971322.js +1 -0
  168. actshield/static/_next/static/chunks/3fzw-g0hycl2c.js +1 -0
  169. actshield/static/_next/static/chunks/3lopq3lhmcjxg.js +129 -0
  170. actshield/static/_next/static/chunks/3mpe2jejel0v2.css +1 -0
  171. actshield/static/_next/static/chunks/3t8o4f2lzr2sf.js +1 -0
  172. actshield/static/_next/static/chunks/3y-v1neyllne5.js +1 -0
  173. actshield/static/_next/static/chunks/43oiwy8mx6cs1.js +1 -0
  174. actshield/static/_next/static/chunks/turbopack-23wzuebcy_lxm.js +1 -0
  175. actshield/static/_next/static/media/1bffadaabf893a1e-s.3-6t-g6q0vh0a.woff2 +0 -0
  176. actshield/static/_next/static/media/2bbe8d2671613f1f-s.0k62hbripvv8p.woff2 +0 -0
  177. actshield/static/_next/static/media/2c55a0e60120577a-s.0-dom-5bn10r2.woff2 +0 -0
  178. actshield/static/_next/static/media/5476f68d60460930-s.2uwcyprjm3xu3.woff2 +0 -0
  179. actshield/static/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2 +0 -0
  180. actshield/static/_next/static/media/9c72aa0f40e4eef8-s.1y4-pdgsjb-pw.woff2 +0 -0
  181. actshield/static/_next/static/media/ad66f9afd8947f86-s.3lvt2whj97whp.woff2 +0 -0
  182. actshield/static/_next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  183. actshield/static/_not-found/__next._full.txt +17 -0
  184. actshield/static/_not-found/__next._not-found/__PAGE__.txt +23 -0
  185. actshield/static/_not-found/__next._tree.txt +3 -0
  186. actshield/static/_not-found.html +1 -0
  187. actshield/static/_not-found.txt +17 -0
  188. actshield/static/access/matrix/__next._full.txt +24 -0
  189. actshield/static/access/matrix/__next._tree.txt +4 -0
  190. actshield/static/access/matrix/__next.access/matrix/__PAGE__.txt +30 -0
  191. actshield/static/access/matrix.html +1 -0
  192. actshield/static/access/matrix.txt +24 -0
  193. actshield/static/activity/__next._full.txt +22 -0
  194. actshield/static/activity/__next._tree.txt +4 -0
  195. actshield/static/activity/__next.activity/__PAGE__.txt +27 -0
  196. actshield/static/activity.html +1 -0
  197. actshield/static/activity.txt +22 -0
  198. actshield/static/agents/__next._full.txt +22 -0
  199. actshield/static/agents/__next._tree.txt +4 -0
  200. actshield/static/agents/__next.agents/__PAGE__.txt +27 -0
  201. actshield/static/agents.html +1 -0
  202. actshield/static/agents.txt +22 -0
  203. actshield/static/ai-secura/__next._full.txt +22 -0
  204. actshield/static/ai-secura/__next._tree.txt +4 -0
  205. actshield/static/ai-secura/__next.ai-secura/__PAGE__.txt +27 -0
  206. actshield/static/ai-secura.html +1 -0
  207. actshield/static/ai-secura.txt +22 -0
  208. actshield/static/api/__next._full.txt +22 -0
  209. actshield/static/api/__next._tree.txt +4 -0
  210. actshield/static/api/__next.api/__PAGE__.txt +27 -0
  211. actshield/static/api.html +15 -0
  212. actshield/static/api.txt +22 -0
  213. actshield/static/apiris/__next._full.txt +22 -0
  214. actshield/static/apiris/__next._tree.txt +4 -0
  215. actshield/static/apiris/__next.apiris/__PAGE__.txt +27 -0
  216. actshield/static/apiris.html +1 -0
  217. actshield/static/apiris.txt +22 -0
  218. actshield/static/attack-graph/__next._full.txt +23 -0
  219. actshield/static/attack-graph/__next._tree.txt +5 -0
  220. actshield/static/attack-graph/__next.attack-graph/__PAGE__.txt +28 -0
  221. actshield/static/attack-graph.html +2 -0
  222. actshield/static/attack-graph.txt +23 -0
  223. actshield/static/campaigns/__next._full.txt +22 -0
  224. actshield/static/campaigns/__next._tree.txt +4 -0
  225. actshield/static/campaigns/__next.campaigns/__PAGE__.txt +27 -0
  226. actshield/static/campaigns.html +1 -0
  227. actshield/static/campaigns.txt +22 -0
  228. actshield/static/context/__next._full.txt +22 -0
  229. actshield/static/context/__next._tree.txt +4 -0
  230. actshield/static/context/__next.context/__PAGE__.txt +27 -0
  231. actshield/static/context.html +1 -0
  232. actshield/static/context.txt +22 -0
  233. actshield/static/dashboard/__next._full.txt +22 -0
  234. actshield/static/dashboard/__next._tree.txt +4 -0
  235. actshield/static/dashboard/__next.dashboard/__PAGE__.txt +27 -0
  236. actshield/static/dashboard.html +1 -0
  237. actshield/static/dashboard.txt +22 -0
  238. actshield/static/delegations/__next._full.txt +22 -0
  239. actshield/static/delegations/__next._tree.txt +4 -0
  240. actshield/static/delegations/__next.delegations/__PAGE__.txt +27 -0
  241. actshield/static/delegations.html +1 -0
  242. actshield/static/delegations.txt +22 -0
  243. actshield/static/demo/__next._full.txt +22 -0
  244. actshield/static/demo/__next._tree.txt +4 -0
  245. actshield/static/demo/__next.demo/__PAGE__.txt +27 -0
  246. actshield/static/demo.html +1 -0
  247. actshield/static/demo.txt +22 -0
  248. actshield/static/drift/__next._full.txt +22 -0
  249. actshield/static/drift/__next._tree.txt +4 -0
  250. actshield/static/drift/__next.drift/__PAGE__.txt +27 -0
  251. actshield/static/drift.html +1 -0
  252. actshield/static/drift.txt +22 -0
  253. actshield/static/favicon.ico +0 -0
  254. actshield/static/file.svg +1 -0
  255. actshield/static/forensics/__next._full.txt +22 -0
  256. actshield/static/forensics/__next._tree.txt +4 -0
  257. actshield/static/forensics/__next.forensics/__PAGE__.txt +27 -0
  258. actshield/static/forensics.html +1 -0
  259. actshield/static/forensics.txt +22 -0
  260. actshield/static/globe.svg +1 -0
  261. actshield/static/incidents/__next._full.txt +22 -0
  262. actshield/static/incidents/__next._tree.txt +4 -0
  263. actshield/static/incidents/__next.incidents/__PAGE__.txt +27 -0
  264. actshield/static/incidents.html +1 -0
  265. actshield/static/incidents.txt +22 -0
  266. actshield/static/index.html +3 -0
  267. actshield/static/index.txt +20 -0
  268. actshield/static/logo.png +0 -0
  269. actshield/static/metrics/__next._full.txt +22 -0
  270. actshield/static/metrics/__next._tree.txt +4 -0
  271. actshield/static/metrics/__next.metrics/__PAGE__.txt +27 -0
  272. actshield/static/metrics.html +1 -0
  273. actshield/static/metrics.txt +22 -0
  274. actshield/static/next.svg +1 -0
  275. actshield/static/offensive/__next._full.txt +22 -0
  276. actshield/static/offensive/__next._tree.txt +4 -0
  277. actshield/static/offensive/__next.offensive/__PAGE__.txt +27 -0
  278. actshield/static/offensive.html +1 -0
  279. actshield/static/offensive.txt +22 -0
  280. actshield/static/policies/__next._full.txt +22 -0
  281. actshield/static/policies/__next._tree.txt +4 -0
  282. actshield/static/policies/__next.policies/__PAGE__.txt +27 -0
  283. actshield/static/policies.html +1 -0
  284. actshield/static/policies.txt +22 -0
  285. actshield/static/posture/__next._full.txt +22 -0
  286. actshield/static/posture/__next._tree.txt +4 -0
  287. actshield/static/posture/__next.posture/__PAGE__.txt +27 -0
  288. actshield/static/posture.html +1 -0
  289. actshield/static/posture.txt +22 -0
  290. actshield/static/regressions/__next._full.txt +22 -0
  291. actshield/static/regressions/__next._tree.txt +4 -0
  292. actshield/static/regressions/__next.regressions/__PAGE__.txt +27 -0
  293. actshield/static/regressions.html +1 -0
  294. actshield/static/regressions.txt +22 -0
  295. actshield/static/reports/__next._full.txt +22 -0
  296. actshield/static/reports/__next._tree.txt +4 -0
  297. actshield/static/reports/__next.reports/__PAGE__.txt +27 -0
  298. actshield/static/reports.html +1 -0
  299. actshield/static/reports.txt +22 -0
  300. actshield/static/resources/__next._full.txt +22 -0
  301. actshield/static/resources/__next._tree.txt +4 -0
  302. actshield/static/resources/__next.resources/__PAGE__.txt +27 -0
  303. actshield/static/resources.html +1 -0
  304. actshield/static/resources.txt +22 -0
  305. actshield/static/security-gates/__next._full.txt +22 -0
  306. actshield/static/security-gates/__next._tree.txt +4 -0
  307. actshield/static/security-gates/__next.security-gates/__PAGE__.txt +27 -0
  308. actshield/static/security-gates.html +1 -0
  309. actshield/static/security-gates.txt +22 -0
  310. actshield/static/settings/__next._full.txt +22 -0
  311. actshield/static/settings/__next._tree.txt +4 -0
  312. actshield/static/settings/__next.settings/__PAGE__.txt +27 -0
  313. actshield/static/settings.html +1 -0
  314. actshield/static/settings.txt +22 -0
  315. actshield/static/tasks/__next._full.txt +22 -0
  316. actshield/static/tasks/__next._tree.txt +4 -0
  317. actshield/static/tasks/__next.tasks/__PAGE__.txt +27 -0
  318. actshield/static/tasks.html +1 -0
  319. actshield/static/tasks.txt +22 -0
  320. actshield/static/threats/__next._full.txt +22 -0
  321. actshield/static/threats/__next._tree.txt +4 -0
  322. actshield/static/threats/__next.threats/__PAGE__.txt +27 -0
  323. actshield/static/threats.html +1 -0
  324. actshield/static/threats.txt +22 -0
  325. actshield/static/tools/__next._full.txt +22 -0
  326. actshield/static/tools/__next._tree.txt +4 -0
  327. actshield/static/tools/__next.tools/__PAGE__.txt +27 -0
  328. actshield/static/tools.html +1 -0
  329. actshield/static/tools.txt +22 -0
  330. actshield/static/traces/__next._full.txt +22 -0
  331. actshield/static/traces/__next._tree.txt +4 -0
  332. actshield/static/traces/__next.traces/__PAGE__.txt +27 -0
  333. actshield/static/traces.html +1 -0
  334. actshield/static/traces.txt +22 -0
  335. actshield/static/vercel.svg +1 -0
  336. actshield/static/window.svg +1 -0
  337. actshield/tasks/__init__.py +6 -0
  338. actshield/tasks/task.py +143 -0
  339. actshield/threatmodel/__init__.py +70 -0
  340. actshield/threatmodel/actors.py +210 -0
  341. actshield/threatmodel/analyzer.py +131 -0
  342. actshield/threatmodel/assets.py +167 -0
  343. actshield/threatmodel/boundaries.py +165 -0
  344. actshield/threatmodel/exporters.py +49 -0
  345. actshield/threatmodel/mitigations.py +209 -0
  346. actshield/threatmodel/models.py +80 -0
  347. actshield/threatmodel/report.py +341 -0
  348. actshield/threatmodel/scenarios.py +212 -0
  349. actshield/threatmodel/scoring.py +66 -0
  350. actshield/threatmodel/threats.py +387 -0
  351. actshield/tools/__init__.py +20 -0
  352. actshield/tools/interceptor.py +256 -0
  353. actshield/tools/tool.py +71 -0
  354. actshield/tracing/__init__.py +38 -0
  355. actshield/tracing/correlation.py +173 -0
  356. actshield/tracing/events.py +75 -0
  357. actshield/tracing/tracer.py +305 -0
  358. actshield/watch.py +129 -0
  359. actshield-0.9.0.dist-info/METADATA +57 -0
  360. actshield-0.9.0.dist-info/RECORD +366 -0
  361. actshield-0.9.0.dist-info/WHEEL +5 -0
  362. actshield-0.9.0.dist-info/entry_points.txt +4 -0
  363. actshield-0.9.0.dist-info/top_level.txt +2 -0
  364. agentguard/__init__.py +39 -0
  365. agentguard/__main__.py +8 -0
  366. agentguard/py.typed +1 -0
@@ -0,0 +1,367 @@
1
+ """Forensic query engine — answers structured forensic questions.
2
+
3
+ Implements all named queries from the spec:
4
+ get_agent_access, get_resource_access, get_agent_delegation_chain,
5
+ get_resource_authorized_agents, get_agent_attempts, get_agent_actual_access,
6
+ get_resource_access_history, get_reachable_resources, get_reachable_agents,
7
+ get_access_snapshot, compare_access_snapshots, get_influencing_context,
8
+ get_causal_path, get_attack_forensics, get_decision_explanation,
9
+ get_regression_forensics
10
+
11
+ All queries are read-only. None can modify security authority.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
17
+
18
+ from actshield.forensics.models import (
19
+ AccessDiff,
20
+ AccessMatrix,
21
+ AccessMatrixEntry,
22
+ AccessSnapshot,
23
+ AgentAccessProfile,
24
+ AttackForensicReport,
25
+ ForensicExplanation,
26
+ ForensicIncident,
27
+ ResourceAccessProfile,
28
+ )
29
+ from actshield.forensics.snapshots import compare_snapshots
30
+
31
+ if TYPE_CHECKING:
32
+ from actshield.forensics.service import ForensicService
33
+
34
+
35
+ class ForensicQueryEngine:
36
+ """Central query interface for the forensic layer.
37
+
38
+ Every method is read-only with respect to the security runtime.
39
+ Queries never grant capabilities, modify taint, or bypass policy.
40
+ """
41
+
42
+ def __init__(self, service: "ForensicService") -> None:
43
+ self._svc = service
44
+
45
+ # ------------------------------------------------------------------
46
+ # Agent queries
47
+ # ------------------------------------------------------------------
48
+
49
+ def get_agent_access(self, agent_id: str) -> Optional[AgentAccessProfile]:
50
+ """Return full forensic access profile for an agent."""
51
+ agent = self._svc._guard.agent_registry.get(agent_id)
52
+ if agent is None:
53
+ return None
54
+
55
+ calc = self._svc._calculator
56
+ declared = calc.get_declared_capabilities(agent_id) if calc else []
57
+ delegated = calc.get_delegated_capabilities(agent_id) if calc else []
58
+ effective = calc.get_effective_capabilities(agent_id) if calc else []
59
+ reachable_tools = calc.get_reachable_tools(agent_id) if calc else []
60
+ reachable_resources = calc.get_reachable_resources(agent_id) if calc else []
61
+
62
+ # Authority graph
63
+ auth_graph = self._svc._auth_graph
64
+ delegation_chain = auth_graph.get_delegation_chain(agent_id) if auth_graph else []
65
+
66
+ # Access graph
67
+ acc_graph = self._svc.access_graph
68
+ attempts = acc_graph.get_agent_attempts(agent_id)
69
+ actual = acc_graph.get_agent_actual_access(agent_id)
70
+
71
+ # Never count blocked attempts as actual access
72
+ total_blocked = sum(1 for a in attempts if a.was_blocked)
73
+ total_allowed = sum(1 for a in attempts if a.was_allowed)
74
+ total_executions = sum(a.execution_count for a in actual)
75
+
76
+ return AgentAccessProfile(
77
+ agent_id=agent_id,
78
+ agent_name=agent.name,
79
+ trust_level=agent.trust_level.value,
80
+ declared_capabilities=declared,
81
+ delegated_capabilities=delegated,
82
+ effective_capabilities=effective,
83
+ restricted_capabilities=[],
84
+ reachable_tools=reachable_tools,
85
+ reachable_resources=reachable_resources,
86
+ recent_attempts=attempts[-20:],
87
+ recent_actual_access=actual[-20:],
88
+ delegation_chain=delegation_chain,
89
+ total_attempts=len(attempts),
90
+ total_blocked=total_blocked,
91
+ total_allowed=total_allowed,
92
+ total_executions=total_executions,
93
+ )
94
+
95
+ def get_all_agents(self) -> List[str]:
96
+ return sorted(self._svc._guard.agent_registry.keys())
97
+
98
+ def get_agent_delegation_chain(self, agent_id: str) -> List[Dict[str, Any]]:
99
+ """Return the full delegation chain leading to agent_id."""
100
+ if not self._svc._auth_graph:
101
+ return []
102
+ chain = self._svc._auth_graph.get_delegation_chain(agent_id)
103
+ return [c.model_dump(mode="json") for c in chain]
104
+
105
+ def get_agent_attempts(self, agent_id: str) -> List[Dict[str, Any]]:
106
+ return [
107
+ a.model_dump(mode="json")
108
+ for a in self._svc.access_graph.get_agent_attempts(agent_id)
109
+ ]
110
+
111
+ def get_agent_actual_access(self, agent_id: str) -> List[Dict[str, Any]]:
112
+ return [
113
+ a.model_dump(mode="json")
114
+ for a in self._svc.access_graph.get_agent_actual_access(agent_id)
115
+ ]
116
+
117
+ def get_reachable_resources(self, agent_id: str) -> List[str]:
118
+ """Deterministic reachable-resource analysis using authority state."""
119
+ if not self._svc._calculator:
120
+ return []
121
+ return self._svc._calculator.get_reachable_resources(agent_id)
122
+
123
+ def get_reachable_tools(self, agent_id: str) -> List[str]:
124
+ if not self._svc._calculator:
125
+ return []
126
+ return self._svc._calculator.get_reachable_tools(agent_id)
127
+
128
+ # ------------------------------------------------------------------
129
+ # Resource queries
130
+ # ------------------------------------------------------------------
131
+
132
+ def get_resource_access(
133
+ self, resource_name: str
134
+ ) -> Optional[ResourceAccessProfile]:
135
+ return self._svc.access_graph.get_resource_profile(resource_name)
136
+
137
+ def get_all_resources(self) -> List[str]:
138
+ return self._svc.access_graph.get_all_resources()
139
+
140
+ def get_resource_authorized_agents(
141
+ self, resource_name: str
142
+ ) -> List[str]:
143
+ return self._svc.access_graph.get_reachable_agents(resource_name)
144
+
145
+ def get_resource_access_history(
146
+ self, resource_name: str
147
+ ) -> List[Dict[str, Any]]:
148
+ return [
149
+ a.model_dump(mode="json")
150
+ for a in self._svc.access_graph.get_resource_history(resource_name)
151
+ ]
152
+
153
+ def get_reachable_agents(self, resource_name: str) -> List[str]:
154
+ return self._svc.access_graph.get_reachable_agents(resource_name)
155
+
156
+ # ------------------------------------------------------------------
157
+ # Access matrix
158
+ # ------------------------------------------------------------------
159
+
160
+ def get_access_matrix(self) -> AccessMatrix:
161
+ """Build the full agent × resource access matrix."""
162
+ agent_ids = self.get_all_agents()
163
+ resource_names = self.get_all_resources()
164
+
165
+ # Also include resources from tool registry
166
+ for tool_def in self._svc._guard.tool_registry.values():
167
+ for resource in tool_def.target_resources:
168
+ if resource.name not in resource_names:
169
+ resource_names.append(resource.name)
170
+ resource_names = sorted(set(resource_names))
171
+
172
+ matrix = AccessMatrix(agents=agent_ids, resources=resource_names)
173
+ calc = self._svc._calculator
174
+
175
+ for agent_id in agent_ids:
176
+ matrix.entries[agent_id] = {}
177
+ eff_perm = set(calc.get_reachable_resources(agent_id)) if calc else set()
178
+ attempts = self._svc.access_graph.get_agent_attempts(agent_id)
179
+ actual = self._svc.access_graph.get_agent_actual_access(agent_id)
180
+
181
+ for resource_name in resource_names:
182
+ attempted = any(
183
+ a.resource_name == resource_name or a.tool_name == resource_name
184
+ for a in attempts
185
+ )
186
+ blocked = any(
187
+ (a.resource_name == resource_name or a.tool_name == resource_name)
188
+ and a.was_blocked
189
+ for a in attempts
190
+ )
191
+ historically_accessed = any(
192
+ a.resource_name == resource_name or a.tool_name == resource_name
193
+ for a in actual
194
+ )
195
+ matrix.entries[agent_id][resource_name] = AccessMatrixEntry(
196
+ agent_id=agent_id,
197
+ resource_name=resource_name,
198
+ effective_permission=resource_name in eff_perm,
199
+ historically_accessed=historically_accessed,
200
+ attempted=attempted,
201
+ blocked=blocked,
202
+ )
203
+
204
+ return matrix
205
+
206
+ # ------------------------------------------------------------------
207
+ # Snapshot queries
208
+ # ------------------------------------------------------------------
209
+
210
+ def get_access_snapshot(
211
+ self, snapshot_id: str
212
+ ) -> Optional[AccessSnapshot]:
213
+ return self._svc._snapshots.get(snapshot_id)
214
+
215
+ def list_snapshots(self) -> List[Dict[str, Any]]:
216
+ return [
217
+ {"snapshot_id": s.snapshot_id, "label": s.label, "timestamp": s.timestamp.isoformat()}
218
+ for s in self._svc._snapshots.list_all()
219
+ ]
220
+
221
+ def compare_access_snapshots(
222
+ self, before_id: str, after_id: str
223
+ ) -> Optional[AccessDiff]:
224
+ before = self._svc._snapshots.get(before_id)
225
+ after = self._svc._snapshots.get(after_id)
226
+ if not before or not after:
227
+ return None
228
+ return compare_snapshots(before, after)
229
+
230
+ # ------------------------------------------------------------------
231
+ # Trace / causal queries
232
+ # ------------------------------------------------------------------
233
+
234
+ def get_causal_path(self, trace_id: str) -> Optional[str]:
235
+ """Return the causal graph JSON for a trace (from storage)."""
236
+ if self._svc._guard.storage:
237
+ return self._svc._guard.storage.get_trace_graph(trace_id)
238
+ return None
239
+
240
+ def get_influencing_context(
241
+ self, trace_id: str
242
+ ) -> List[Dict[str, Any]]:
243
+ """Return context records influencing a trace."""
244
+ events = self._svc._guard.storage.get_events(trace_id=trace_id)
245
+ contexts = []
246
+ for event in events:
247
+ if event.event_type.value in (
248
+ "context.received", "context.propagated", "context.generated"
249
+ ):
250
+ contexts.append({
251
+ "event_id": event.event_id,
252
+ "event_type": event.event_type.value,
253
+ "context_id": event.context_id,
254
+ "agent_id": event.agent_id,
255
+ "timestamp": event.timestamp.isoformat(),
256
+ "payload": event.payload,
257
+ })
258
+ return contexts
259
+
260
+ # ------------------------------------------------------------------
261
+ # Attack / campaign queries
262
+ # ------------------------------------------------------------------
263
+
264
+ def get_attack_forensics(
265
+ self, attack_id: str
266
+ ) -> Optional[AttackForensicReport]:
267
+ """Return the full forensic report for an attack."""
268
+ return self._svc._explainer.explain_attack(attack_id)
269
+
270
+ def get_regression_forensics(
271
+ self, regression_id: str
272
+ ) -> Optional[AttackForensicReport]:
273
+ """Return forensic report for a regression attack (same as attack)."""
274
+ return self.get_attack_forensics(regression_id)
275
+
276
+ def get_all_attack_ids(self) -> List[str]:
277
+ return list(self._svc._attack_results.keys())
278
+
279
+ def get_all_bypass_attacks(self) -> List[str]:
280
+ return [
281
+ aid for aid, r in self._svc._attack_results.items()
282
+ if r.bypassed
283
+ ]
284
+
285
+ # ------------------------------------------------------------------
286
+ # Decision explanation
287
+ # ------------------------------------------------------------------
288
+
289
+ def get_decision_explanation(
290
+ self,
291
+ event_id: Optional[str] = None,
292
+ trace_id: Optional[str] = None,
293
+ agent_id: Optional[str] = None,
294
+ tool_name: Optional[str] = None,
295
+ ) -> ForensicExplanation:
296
+ """Return a deterministic explanation for a security decision."""
297
+ return self._svc._explainer.explain_decision(
298
+ event_id=event_id,
299
+ trace_id=trace_id,
300
+ agent_id=agent_id,
301
+ tool_name=tool_name,
302
+ )
303
+
304
+ # ------------------------------------------------------------------
305
+ # Incidents
306
+ # ------------------------------------------------------------------
307
+
308
+ def get_all_incidents(self) -> List[ForensicIncident]:
309
+ return list(self._svc._incidents.values())
310
+
311
+ def get_incident(self, incident_id: str) -> Optional[ForensicIncident]:
312
+ return self._svc._incidents.get(incident_id)
313
+
314
+ # ------------------------------------------------------------------
315
+ # Overview metrics
316
+ # ------------------------------------------------------------------
317
+
318
+ def get_overview(self) -> Dict[str, Any]:
319
+ """Return aggregate dashboard-ready metrics."""
320
+ guard = self._svc._guard
321
+ attempts = self._svc.access_graph.get_all_attempts()
322
+ actual = self._svc.access_graph.get_all_actual_accesses()
323
+ bypass_count = len(self.get_all_bypass_attacks())
324
+ incidents = list(self._svc._incidents.values())
325
+
326
+ # Count today's attacks (all attacks from attack_results)
327
+ attack_count = len(self._svc._attack_results)
328
+ blocked_count = sum(1 for a in attempts if a.was_blocked)
329
+ hitl_count = sum(
330
+ 1 for a in attempts if a.decision.value == "HITL"
331
+ )
332
+ sensitive_attempts = sum(
333
+ 1 for a in attempts
334
+ if a.resource_sensitivity in ("HIGH", "CRITICAL")
335
+ )
336
+ sensitive_prevented = sum(
337
+ 1 for a in attempts
338
+ if a.resource_sensitivity in ("HIGH", "CRITICAL") and a.was_blocked
339
+ )
340
+ sensitive_executed = sum(
341
+ a.sensitive_db_calls for a in actual
342
+ )
343
+
344
+ return {
345
+ "agents": len(guard.agent_registry),
346
+ "active_tasks": None, # TaskManager not directly accessible
347
+ "protected_tools": len(guard.tool_registry),
348
+ "mcp_servers": None,
349
+ "attacks_today": attack_count,
350
+ "blocked": blocked_count,
351
+ "hitl": hitl_count,
352
+ "bypasses": bypass_count,
353
+ "sensitive_attempts": sensitive_attempts,
354
+ "sensitive_prevented": sensitive_prevented,
355
+ "sensitive_executed": sensitive_executed,
356
+ "campaigns": len({
357
+ r.campaign_id for r in self._svc._attack_results.values()
358
+ if r.campaign_id
359
+ }),
360
+ "regressions": bypass_count,
361
+ "incidents": len(incidents),
362
+ "availability": {
363
+ "active_tasks": False,
364
+ "mcp_servers": False,
365
+ },
366
+ }
367
+
@@ -0,0 +1,68 @@
1
+ """JSON serializers for forensic exports.
2
+
3
+ All forensic data exported to reports/forensics/ uses stable schemas.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ from datetime import datetime
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ def _default_serializer(obj: Any) -> Any:
15
+ if isinstance(obj, datetime):
16
+ return obj.isoformat()
17
+ if hasattr(obj, "model_dump"):
18
+ return obj.model_dump(mode="json")
19
+ if hasattr(obj, "value"):
20
+ return obj.value
21
+ return str(obj)
22
+
23
+
24
+ def to_json(data: Any, indent: int = 2) -> str:
25
+ """Serialize forensic data to JSON string."""
26
+ if hasattr(data, "model_dump"):
27
+ data = data.model_dump(mode="json")
28
+ return json.dumps(data, indent=indent, default=_default_serializer)
29
+
30
+
31
+ def export_json(
32
+ data: Any,
33
+ filepath: str,
34
+ indent: int = 2,
35
+ ) -> str:
36
+ """Export forensic data to a JSON file. Creates directory if needed."""
37
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
38
+ content = to_json(data, indent=indent)
39
+ with open(filepath, "w", encoding="utf-8") as f:
40
+ f.write(content)
41
+ return filepath
42
+
43
+
44
+ def sanitize_for_export(data: Dict[str, Any]) -> Dict[str, Any]:
45
+ """Remove sensitive fields before export.
46
+
47
+ Strips: credentials, tokens, raw passwords, internal paths.
48
+ """
49
+ SENSITIVE_KEYS = {
50
+ "password", "secret", "token", "api_key", "credential",
51
+ "authorization", "bearer", "private_key",
52
+ }
53
+ if not isinstance(data, dict):
54
+ return data
55
+ result = {}
56
+ for key, value in data.items():
57
+ if any(s in key.lower() for s in SENSITIVE_KEYS):
58
+ result[key] = "[REDACTED]"
59
+ elif isinstance(value, dict):
60
+ result[key] = sanitize_for_export(value)
61
+ elif isinstance(value, list):
62
+ result[key] = [
63
+ sanitize_for_export(item) if isinstance(item, dict) else item
64
+ for item in value
65
+ ]
66
+ else:
67
+ result[key] = value
68
+ return result