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,186 @@
1
+ """Authority graph — builds and queries the agent authority/delegation graph.
2
+
3
+ Read-only. Does not grant or modify any authority.
4
+ Uses data already present in delegation_registry and agent_registry.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Dict, List, Optional, Set
10
+ from actshield.forensics.models import AuthorityRelationship, DelegationRelationship
11
+
12
+ if TYPE_CHECKING:
13
+ from actshield.client import ActShield
14
+
15
+
16
+ class AuthorityGraph:
17
+ """Directed authority graph built from the ActShield delegation registry.
18
+
19
+ Nodes = agent_ids
20
+ Edges = delegation records (delegator → delegate, labelled with capabilities)
21
+
22
+ Never grants authority — read-only projection of existing security state.
23
+ """
24
+
25
+ def __init__(self) -> None:
26
+ # adjacency: delegator_id → list of DelegationRelationship
27
+ self._outgoing: Dict[str, List[DelegationRelationship]] = {}
28
+ # reverse: delegate_id → list of DelegationRelationship
29
+ self._incoming: Dict[str, List[DelegationRelationship]] = {}
30
+
31
+ # ------------------------------------------------------------------
32
+ # Build
33
+ # ------------------------------------------------------------------
34
+
35
+ @classmethod
36
+ def from_guard(cls, guard: "ActShield") -> "AuthorityGraph":
37
+ """Construct graph from an ActShield instance's delegation registry."""
38
+ g = cls()
39
+ for dlg in guard.delegation_registry.values():
40
+ rel = DelegationRelationship(
41
+ delegation_id=dlg.delegation_id,
42
+ delegator_agent_id=dlg.delegator_agent_id,
43
+ delegate_agent_id=dlg.delegate_agent_id,
44
+ granted_capabilities=list(dlg.authority_grant.granted_capabilities),
45
+ depth=dlg.depth,
46
+ parent_delegation_id=dlg.parent_delegation_id,
47
+ task_id=dlg.task_id,
48
+ trace_id=dlg.trace_id,
49
+ status=dlg.status,
50
+ timestamp=dlg.created_at,
51
+ )
52
+ g._add_edge(rel)
53
+ return g
54
+
55
+ def add_delegation(self, rel: DelegationRelationship) -> None:
56
+ """Add a delegation relationship to the graph."""
57
+ self._add_edge(rel)
58
+
59
+ def _add_edge(self, rel: DelegationRelationship) -> None:
60
+ src = rel.delegator_agent_id
61
+ dst = rel.delegate_agent_id
62
+ self._outgoing.setdefault(src, []).append(rel)
63
+ self._incoming.setdefault(dst, []).append(rel)
64
+
65
+ # ------------------------------------------------------------------
66
+ # Queries
67
+ # ------------------------------------------------------------------
68
+
69
+ def get_delegations_from(self, agent_id: str) -> List[DelegationRelationship]:
70
+ """Return all delegations initiated by agent_id."""
71
+ return list(self._outgoing.get(agent_id, []))
72
+
73
+ def get_delegations_to(self, agent_id: str) -> List[DelegationRelationship]:
74
+ """Return all delegations received by agent_id."""
75
+ return list(self._incoming.get(agent_id, []))
76
+
77
+ def get_authority_relationships(
78
+ self, agent_id: str
79
+ ) -> List[AuthorityRelationship]:
80
+ """Return AuthorityRelationship objects where agent_id is delegator or delegate."""
81
+ rels: List[AuthorityRelationship] = []
82
+ for dlg in self.get_delegations_from(agent_id):
83
+ rels.append(
84
+ AuthorityRelationship(
85
+ source_agent_id=dlg.delegator_agent_id,
86
+ target_agent_id=dlg.delegate_agent_id,
87
+ capabilities=dlg.granted_capabilities,
88
+ delegation_id=dlg.delegation_id,
89
+ task_id=dlg.task_id,
90
+ trace_id=dlg.trace_id,
91
+ depth=dlg.depth,
92
+ timestamp=dlg.timestamp,
93
+ status=dlg.status,
94
+ )
95
+ )
96
+ for dlg in self.get_delegations_to(agent_id):
97
+ rels.append(
98
+ AuthorityRelationship(
99
+ source_agent_id=dlg.delegator_agent_id,
100
+ target_agent_id=dlg.delegate_agent_id,
101
+ capabilities=dlg.granted_capabilities,
102
+ delegation_id=dlg.delegation_id,
103
+ task_id=dlg.task_id,
104
+ trace_id=dlg.trace_id,
105
+ depth=dlg.depth,
106
+ timestamp=dlg.timestamp,
107
+ status=dlg.status,
108
+ )
109
+ )
110
+ return rels
111
+
112
+ def get_delegation_chain(self, agent_id: str) -> List[DelegationRelationship]:
113
+ """Walk the delegation chain from root to agent_id (recursive)."""
114
+ chain: List[DelegationRelationship] = []
115
+ visited: Set[str] = set()
116
+ self._walk_up(agent_id, chain, visited)
117
+ chain.sort(key=lambda r: r.depth)
118
+ return chain
119
+
120
+ def _walk_up(
121
+ self,
122
+ agent_id: str,
123
+ chain: List[DelegationRelationship],
124
+ visited: Set[str],
125
+ ) -> None:
126
+ if agent_id in visited:
127
+ return
128
+ visited.add(agent_id)
129
+ for rel in self.get_delegations_to(agent_id):
130
+ chain.append(rel)
131
+ self._walk_up(rel.delegator_agent_id, chain, visited)
132
+
133
+ def get_all_delegatees(self, agent_id: str) -> List[str]:
134
+ """Return all agents that agent_id has (directly or transitively) delegated to."""
135
+ result: List[str] = []
136
+ visited: Set[str] = set()
137
+ self._walk_down(agent_id, result, visited)
138
+ return result
139
+
140
+ def _walk_down(
141
+ self, agent_id: str, result: List[str], visited: Set[str]
142
+ ) -> None:
143
+ if agent_id in visited:
144
+ return
145
+ visited.add(agent_id)
146
+ for rel in self.get_delegations_from(agent_id):
147
+ dst = rel.delegate_agent_id
148
+ if dst not in result:
149
+ result.append(dst)
150
+ self._walk_down(dst, result, visited)
151
+
152
+ def get_delegated_capabilities(self, agent_id: str) -> List[str]:
153
+ """Return all capabilities delegated TO agent_id (most recent active delegation)."""
154
+ delegations = self.get_delegations_to(agent_id)
155
+ if not delegations:
156
+ return []
157
+ # Prefer active ones, fall back to completed
158
+ active = [d for d in delegations if d.status == "active"]
159
+ source = active if active else delegations
160
+ caps: Set[str] = set()
161
+ for d in source:
162
+ caps.update(d.granted_capabilities)
163
+ return sorted(caps)
164
+
165
+ def get_all_agents(self) -> List[str]:
166
+ """Return all agent IDs present in the graph."""
167
+ agents: Set[str] = set()
168
+ agents.update(self._outgoing.keys())
169
+ agents.update(self._incoming.keys())
170
+ return sorted(agents)
171
+
172
+ def to_dict(self) -> dict:
173
+ """Serializable representation of the authority graph."""
174
+ edges = []
175
+ seen: Set[str] = set()
176
+ for rels in self._outgoing.values():
177
+ for rel in rels:
178
+ if rel.delegation_id not in seen:
179
+ seen.add(rel.delegation_id)
180
+ edges.append(rel.model_dump(mode="json"))
181
+ return {
182
+ "nodes": self.get_all_agents(),
183
+ "edges": edges,
184
+ }
185
+
186
+
@@ -0,0 +1,119 @@
1
+ """Effective access calculator — deterministic, never uses AI.
2
+
3
+ Calculates:
4
+ effective_access = declared_capabilities
5
+ + valid_delegated_capabilities
6
+ - explicit_restrictions
7
+
8
+ Uses existing ActShield:
9
+ - agent_registry (declared capabilities)
10
+ - delegation_registry (delegated capabilities)
11
+ - tool_registry (required capabilities per tool)
12
+ - authority containment logic
13
+
14
+ Does NOT use AI Secura or APIRIS for access determination.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING, Dict, List, Set
20
+
21
+ if TYPE_CHECKING:
22
+ from actshield.client import ActShield
23
+
24
+
25
+ class EffectiveAccessCalculator:
26
+ """Deterministic calculation of effective capabilities and reachable resources.
27
+
28
+ effective_access = declared_capabilities
29
+ + valid_delegated_capabilities
30
+ - explicit_restrictions
31
+
32
+ Monotonic authority containment is preserved — a delegated agent can
33
+ never receive more authority than the delegator holds.
34
+ """
35
+
36
+ def __init__(self, guard: "ActShield") -> None:
37
+ self._guard = guard
38
+
39
+ def get_effective_capabilities(self, agent_id: str) -> List[str]:
40
+ """Return the union of declared + delegated capabilities for agent_id."""
41
+ agent = self._guard.agent_registry.get(agent_id)
42
+ declared: Set[str] = set(agent.capability_names()) if agent else set()
43
+
44
+ delegated: Set[str] = set()
45
+ for dlg in self._guard.delegation_registry.values():
46
+ if dlg.delegate_agent_id == agent_id and dlg.status in ("active", "completed"):
47
+ delegated.update(dlg.authority_grant.granted_capabilities)
48
+
49
+ return sorted(declared | delegated)
50
+
51
+ def get_declared_capabilities(self, agent_id: str) -> List[str]:
52
+ agent = self._guard.agent_registry.get(agent_id)
53
+ return agent.capability_names() if agent else []
54
+
55
+ def get_delegated_capabilities(self, agent_id: str) -> List[str]:
56
+ caps: Set[str] = set()
57
+ for dlg in self._guard.delegation_registry.values():
58
+ if dlg.delegate_agent_id == agent_id and dlg.status in ("active", "completed"):
59
+ caps.update(dlg.authority_grant.granted_capabilities)
60
+ return sorted(caps)
61
+
62
+ def get_reachable_tools(self, agent_id: str) -> List[str]:
63
+ """Return tools this agent can invoke based on effective capabilities."""
64
+ effective = set(self.get_effective_capabilities(agent_id))
65
+ reachable = []
66
+ for tool_def in self._guard.tool_registry.values():
67
+ required = set(tool_def.required_capabilities)
68
+ if not required or required.issubset(effective):
69
+ reachable.append(tool_def.name)
70
+ return sorted(reachable)
71
+
72
+ def get_reachable_resources(self, agent_id: str) -> List[str]:
73
+ """Return resources this agent can reach via effective capabilities."""
74
+ reachable_tools = set(self.get_reachable_tools(agent_id))
75
+ resources: List[str] = []
76
+ seen: Set[str] = set()
77
+ for tool_def in self._guard.tool_registry.values():
78
+ if tool_def.name in reachable_tools:
79
+ for resource in tool_def.target_resources:
80
+ if resource.name not in seen:
81
+ seen.add(resource.name)
82
+ resources.append(resource.name)
83
+ return sorted(resources)
84
+
85
+ def can_access_tool(self, agent_id: str, tool_name: str) -> bool:
86
+ """Check if agent has effective permission to invoke a named tool."""
87
+ tool_def = self._get_tool(tool_name)
88
+ if tool_def is None:
89
+ return False
90
+ effective = set(self.get_effective_capabilities(agent_id))
91
+ required = set(tool_def.required_capabilities)
92
+ return not required or required.issubset(effective)
93
+
94
+ def can_access_resource(self, agent_id: str, resource_name: str) -> bool:
95
+ """Check if agent has effective permission to reach a named resource."""
96
+ return resource_name in self.get_reachable_resources(agent_id)
97
+
98
+ def build_access_matrix(
99
+ self,
100
+ agent_ids: List[str],
101
+ resource_names: List[str],
102
+ ) -> Dict[str, Dict[str, bool]]:
103
+ """Build effective-permission matrix: agent × resource → bool."""
104
+ matrix: Dict[str, Dict[str, bool]] = {}
105
+ for agent_id in agent_ids:
106
+ matrix[agent_id] = {}
107
+ for resource_name in resource_names:
108
+ matrix[agent_id][resource_name] = self.can_access_resource(
109
+ agent_id, resource_name
110
+ )
111
+ return matrix
112
+
113
+ def _get_tool(self, tool_name: str):
114
+ for tool_def in self._guard.tool_registry.values():
115
+ if tool_def.name == tool_name:
116
+ return tool_def
117
+ return None
118
+
119
+
@@ -0,0 +1,254 @@
1
+ """Deterministic forensic explainer — assembles evidence-backed explanations.
2
+
3
+ The explanation is assembled from ACTUAL recorded evidence.
4
+ No LLM is called. No AI invents the explanation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
10
+
11
+ from actshield.forensics.models import (
12
+ AccessDecision,
13
+ AttackForensicReport,
14
+ ForensicExplanation,
15
+ ForensicIncident,
16
+ IncidentSeverity,
17
+ IncidentStatus,
18
+ IncidentType,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from actshield.forensics.service import ForensicService
23
+
24
+
25
+ class ForensicExplainer:
26
+ """Builds deterministic forensic explanations from recorded evidence.
27
+
28
+ Source of truth chain:
29
+ SecurityDecision → structured_explanation → authority_containment
30
+ → intent_alignment → risk_breakdown + SecurityAnalysisEvidence
31
+ """
32
+
33
+ def __init__(self, service: "ForensicService") -> None:
34
+ self._svc = service
35
+
36
+ def explain_decision(
37
+ self,
38
+ event_id: Optional[str] = None,
39
+ trace_id: Optional[str] = None,
40
+ agent_id: Optional[str] = None,
41
+ tool_name: Optional[str] = None,
42
+ decision_dict: Optional[Dict[str, Any]] = None,
43
+ ) -> ForensicExplanation:
44
+ """Build a forensic explanation from a stored security decision.
45
+
46
+ Falls back to the decision_dict if no storage record is found.
47
+ """
48
+ resolved_decision: Dict[str, Any] = {}
49
+
50
+ # Try to pull from storage by trace_id
51
+ if trace_id and self._svc._guard.storage:
52
+ stored = self._svc._guard.storage.get_decisions(trace_id=trace_id, limit=1)
53
+ if stored:
54
+ resolved_decision = stored[0]
55
+
56
+ # Merge with provided dict
57
+ if decision_dict:
58
+ resolved_decision.update(decision_dict)
59
+
60
+ structured = resolved_decision.get("details_json") or {}
61
+ if isinstance(structured, str):
62
+ import json
63
+ try:
64
+ structured = json.loads(structured)
65
+ except Exception:
66
+ structured = {}
67
+
68
+ se = structured.get("structured_explanation", {})
69
+ auth = structured.get("authority_containment", {})
70
+ intent = structured.get("intent_alignment", {})
71
+
72
+ # Map decision action to AccessDecision
73
+ action_str = resolved_decision.get("action", "UNKNOWN").upper()
74
+ try:
75
+ policy_decision = AccessDecision(action_str)
76
+ except ValueError:
77
+ policy_decision = AccessDecision.UNKNOWN
78
+
79
+ # Influencing context — from the access graph
80
+ attempts = self._svc.access_graph.get_agent_attempts(agent_id or "")
81
+ related = [a for a in attempts if a.tool_name == (tool_name or "")]
82
+ context_sources = []
83
+ context_ids = []
84
+ taint_state = "CLEAN"
85
+ if related:
86
+ latest = related[-1]
87
+ context_ids = latest.context_ids
88
+ taint_state = latest.taint_state
89
+
90
+ # Context sources from structured explanation
91
+ context_sources = se.get("context_sources", [])
92
+
93
+ # Delegated authority
94
+ delegated_caps = se.get("delegated_capabilities", [])
95
+ if isinstance(delegated_caps, str):
96
+ delegated_caps = [delegated_caps] if delegated_caps else []
97
+
98
+ effective_authority = "GRANTED" if auth.get("contained", False) else "DENIED"
99
+
100
+ # AI/APIRIS summaries
101
+ ai_summary = None
102
+ apiris_summary = None
103
+ if agent_id:
104
+ for attempt in self._svc.access_graph.get_agent_attempts(agent_id):
105
+ if attempt.tool_name == (tool_name or ""):
106
+ # Check access graph for security evidence references
107
+ pass
108
+
109
+ return ForensicExplanation(
110
+ event_id=event_id,
111
+ trace_id=trace_id or resolved_decision.get("trace_id"),
112
+ agent_id=agent_id or se.get("agent", ""),
113
+ tool_name=tool_name or se.get("requested_tool", ""),
114
+ resource_name=se.get("sensitive_sink"),
115
+ resource_sensitivity=se.get("sink_sensitivity", "UNKNOWN"),
116
+ original_intent=intent.get("original_intent", "Unspecified"),
117
+ influencing_context_sources=context_sources,
118
+ influencing_context_ids=context_ids,
119
+ context_trust="UNTRUSTED" if taint_state == "TAINTED" else "TRUSTED",
120
+ taint_state=taint_state,
121
+ delegated_authority=delegated_caps,
122
+ requested_capability=(
123
+ se.get("required_capabilities", [None])[0]
124
+ if isinstance(se.get("required_capabilities"), list)
125
+ else se.get("required_capabilities")
126
+ ),
127
+ effective_authority=effective_authority,
128
+ policy_decision=policy_decision,
129
+ policy_reason_code=resolved_decision.get("reason_code", ""),
130
+ policy_explanation=resolved_decision.get("explanation", ""),
131
+ ai_secura_summary=ai_summary,
132
+ apiris_summary=apiris_summary,
133
+ tool_executed=False,
134
+ execution_count=0,
135
+ )
136
+
137
+ def explain_attack(
138
+ self, attack_id: str
139
+ ) -> Optional[AttackForensicReport]:
140
+ """Build an AttackForensicReport from Phase 5 evidence."""
141
+ result = self._svc._attack_results.get(attack_id)
142
+ if result is None:
143
+ return None
144
+
145
+ ev = result.security_evidence
146
+ lineage = result.mutation_lineage
147
+
148
+ # Reachable resources for acting agent
149
+ agent_id = ev.acting_agent if ev else ""
150
+ reachable = []
151
+ if agent_id and self._svc._calculator:
152
+ reachable = self._svc._calculator.get_reachable_resources(agent_id)
153
+
154
+ ai_summary = None
155
+ apiris_summary = None
156
+ if ev:
157
+ if ev.ai_analysis and not ev.ai_unavailable:
158
+ ai_summary = str(
159
+ ev.ai_analysis.get("threat_classification") or
160
+ ev.ai_analysis.get("security_level") or ""
161
+ )
162
+ if ev.apiris_analysis and not ev.apiris_unavailable:
163
+ apiris_summary = str(
164
+ ev.apiris_analysis.get("risk_level") or
165
+ ev.apiris_analysis.get("classification") or ""
166
+ )
167
+
168
+ try:
169
+ policy_dec = AccessDecision(result.actual_decision.upper())
170
+ except ValueError:
171
+ policy_dec = AccessDecision.UNKNOWN
172
+
173
+ return AttackForensicReport(
174
+ attack_id=attack_id,
175
+ campaign_id=result.campaign_id,
176
+ attack_type=result.attack_type.value,
177
+ target=result.target,
178
+ entry_point=result.entry_point,
179
+ agent_id=agent_id,
180
+ trace_id=result.trace_id,
181
+ original_intent=ev.task_intent if ev else "",
182
+ mutation_lineage=lineage,
183
+ adaptive_iteration=getattr(result, "adaptive_iteration", 0),
184
+ influencing_contexts=ev.context_ids if ev else [],
185
+ context_trust="UNTRUSTED" if (ev and ev.taint_state == "TAINTED") else "TRUSTED",
186
+ taint_state=ev.taint_state if ev else "UNKNOWN",
187
+ delegated_capabilities=ev.delegated_capabilities if ev else [],
188
+ requested_capability=ev.requested_capability if ev else None,
189
+ effective_capabilities=ev.delegated_capabilities if ev else [],
190
+ authority_contained=ev.authority_contained if ev else True,
191
+ resource_name=None,
192
+ apiris_summary=apiris_summary,
193
+ ai_secura_summary=ai_summary,
194
+ risk_score=ev.risk_score if ev else 0.0,
195
+ policy_decision=policy_dec,
196
+ policy_reason_code=ev.policy_reason_code if ev else "",
197
+ policy_explanation=ev.policy_explanation if ev else "",
198
+ tool_executed=result.actual_execution,
199
+ execution_count=result.execution_evidence.execution_count,
200
+ sensitive_db_calls=result.execution_evidence.sensitive_db_calls,
201
+ actual_access=result.actual_execution,
202
+ reachable_resources=reachable,
203
+ bypassed=result.bypassed,
204
+ expected_decision=result.expected_decision,
205
+ actual_decision=result.actual_decision,
206
+ regression_status=(
207
+ "REGRESSION_CREATED" if result.bypassed else "NONE"
208
+ ),
209
+ )
210
+
211
+ def classify_incident(
212
+ self, attempt: "ForensicExplanation"
213
+ ) -> IncidentType:
214
+ """Classify incident type deterministically from the explanation."""
215
+ if attempt.bypass_detected:
216
+ return IncidentType.BYPASS_DETECTED
217
+ reason = attempt.policy_reason_code
218
+ if "TAINTED" in reason:
219
+ return IncidentType.TAINT_PROPAGATION
220
+ if "AUTHORITY" in reason or "DELEGATION" in reason:
221
+ return IncidentType.AUTHORITY_ESCALATION
222
+ if "CAPABILITY" in reason or "MISSING" in reason:
223
+ return IncidentType.CAPABILITY_VIOLATION
224
+ if "INTENT" in reason:
225
+ return IncidentType.INTENT_VIOLATION
226
+ if "UNTRUSTED" in reason:
227
+ return IncidentType.UNTRUSTED_SENSITIVE_ACCESS
228
+ return IncidentType.GENERIC
229
+
230
+ def build_incident(
231
+ self, explanation: ForensicExplanation
232
+ ) -> ForensicIncident:
233
+ """Build a ForensicIncident from an explanation (uses IDs, no duplication)."""
234
+ severity = (
235
+ IncidentSeverity.CRITICAL
236
+ if explanation.resource_sensitivity == "CRITICAL"
237
+ else IncidentSeverity.HIGH
238
+ if explanation.policy_decision == AccessDecision.BLOCK
239
+ else IncidentSeverity.MEDIUM
240
+ )
241
+ return ForensicIncident(
242
+ incident_type=self.classify_incident(explanation),
243
+ severity=severity,
244
+ agent_id=explanation.agent_id,
245
+ resource_name=explanation.resource_name,
246
+ tool_name=explanation.tool_name,
247
+ decision=explanation.policy_decision,
248
+ executed=explanation.tool_executed,
249
+ trace_id=explanation.trace_id,
250
+ attack_id=explanation.bypass_attack_id,
251
+ status=IncidentStatus.CONTAINED,
252
+ description=explanation.policy_explanation,
253
+ )
254
+