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,599 @@
1
+ """ApiService — programmatic API layer over ForensicService.
2
+
3
+ This is the backend contract the dashboard consumes.
4
+ Returns clean Pydantic models that serialize to JSON.
5
+
6
+ Authentication/Authorization:
7
+ SecurityPrincipal interface provided for future extension.
8
+ Development mode allows unauthenticated access.
9
+
10
+ Observability:
11
+ All responses carry request_id for correlation:
12
+ Dashboard request → API request_id → Forensic query → trace_id
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import uuid
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from actshield.api.models import (
21
+ AccessMatrixResponse,
22
+ AgentAccessResponse,
23
+ AgentSummary,
24
+ AttackForensicsResponse,
25
+ AttackSummary,
26
+ CampaignSummary,
27
+ DiffResponse,
28
+ ErrorResponse,
29
+ HealthResponse,
30
+ IncidentResponse,
31
+ OverviewResponse,
32
+ PolicyEvaluateRequest,
33
+ PolicyEvaluateResponse,
34
+ ResourceAccessResponse,
35
+ SnapshotSummary,
36
+ )
37
+ from actshield.forensics.service import ForensicService
38
+
39
+
40
+ def _new_request_id() -> str:
41
+ return f"req_{uuid.uuid4().hex[:12]}"
42
+
43
+
44
+ class SecurityPrincipal:
45
+ """Extensible auth principal. Development mode: all principals are ADMIN."""
46
+
47
+ class Permission:
48
+ READ_RUNTIME = "READ_RUNTIME"
49
+ READ_FORENSICS = "READ_FORENSICS"
50
+ READ_ATTACKS = "READ_ATTACKS"
51
+ READ_POLICIES = "READ_POLICIES"
52
+ ADMIN = "ADMIN"
53
+
54
+ def __init__(self, permissions: Optional[List[str]] = None, dev_mode: bool = True) -> None:
55
+ self.permissions = permissions or [self.Permission.ADMIN]
56
+ self.dev_mode = dev_mode
57
+
58
+ def can(self, permission: str) -> bool:
59
+ if self.dev_mode:
60
+ return True
61
+ return permission in self.permissions or self.Permission.ADMIN in self.permissions
62
+
63
+
64
+ class ApiService:
65
+ """Unified product API service.
66
+
67
+ Wraps ForensicService and exposes clean endpoint-equivalent methods.
68
+ Each method mirrors a REST endpoint conceptually.
69
+ """
70
+
71
+ VERSION = "1.0.0"
72
+
73
+ def __init__(
74
+ self,
75
+ forensic_service: ForensicService,
76
+ dev_mode: bool = True,
77
+ ) -> None:
78
+ self._fs = forensic_service
79
+ self._principal = SecurityPrincipal(dev_mode=dev_mode)
80
+
81
+ # ------------------------------------------------------------------
82
+ # Health
83
+ # ------------------------------------------------------------------
84
+
85
+ def get_health(self) -> HealthResponse:
86
+ """GET /api/v1/health"""
87
+ return HealthResponse(
88
+ status="ok",
89
+ version=self.VERSION,
90
+ components={
91
+ "forensic_service": "ok",
92
+ "authority_graph": "ok",
93
+ "access_graph": "ok",
94
+ "storage": "ok" if self._fs._guard.storage else "unavailable",
95
+ },
96
+ )
97
+
98
+ def get_ready(self) -> Dict[str, Any]:
99
+ """GET /api/v1/ready"""
100
+ return {"ready": True, "version": self.VERSION}
101
+
102
+ # ------------------------------------------------------------------
103
+ # Overview
104
+ # ------------------------------------------------------------------
105
+
106
+ def get_overview(self) -> OverviewResponse:
107
+ """GET /api/v1/overview"""
108
+ data = self._fs.query.get_overview()
109
+ return OverviewResponse(**{k: data.get(k) for k in OverviewResponse.model_fields})
110
+
111
+ # ------------------------------------------------------------------
112
+ # Agents
113
+ # ------------------------------------------------------------------
114
+
115
+ def list_agents(self) -> List[AgentSummary]:
116
+ """GET /api/v1/agents"""
117
+ summaries = []
118
+ for agent_id in self._fs.query.get_all_agents():
119
+ profile = self._fs.query.get_agent_access(agent_id)
120
+ if profile:
121
+ summaries.append(AgentSummary(
122
+ agent_id=profile.agent_id,
123
+ agent_name=profile.agent_name,
124
+ trust_level=profile.trust_level,
125
+ effective_capabilities=profile.effective_capabilities,
126
+ total_attempts=profile.total_attempts,
127
+ total_blocked=profile.total_blocked,
128
+ ))
129
+ return summaries
130
+
131
+ def get_agent(self, agent_id: str) -> AgentSummary | ErrorResponse:
132
+ """GET /api/v1/agents/{agent_id}"""
133
+ rid = _new_request_id()
134
+ profile = self._fs.query.get_agent_access(agent_id)
135
+ if not profile:
136
+ return ErrorResponse.make("AGENT_NOT_FOUND", f"Agent '{agent_id}' was not found", rid)
137
+ return AgentSummary(
138
+ agent_id=profile.agent_id,
139
+ agent_name=profile.agent_name,
140
+ trust_level=profile.trust_level,
141
+ effective_capabilities=profile.effective_capabilities,
142
+ total_attempts=profile.total_attempts,
143
+ total_blocked=profile.total_blocked,
144
+ )
145
+
146
+ def get_agent_access(self, agent_id: str) -> AgentAccessResponse | ErrorResponse:
147
+ """GET /api/v1/agents/{agent_id}/access"""
148
+ rid = _new_request_id()
149
+ profile = self._fs.query.get_agent_access(agent_id)
150
+ if not profile:
151
+ return ErrorResponse.make("AGENT_NOT_FOUND", f"Agent '{agent_id}' was not found", rid)
152
+ return AgentAccessResponse(
153
+ agent_id=profile.agent_id,
154
+ agent_name=profile.agent_name,
155
+ trust_level=profile.trust_level,
156
+ declared=profile.declared_capabilities,
157
+ delegated=profile.delegated_capabilities,
158
+ effective=profile.effective_capabilities,
159
+ restricted=profile.restricted_capabilities,
160
+ reachable_tools=profile.reachable_tools,
161
+ reachable_resources=profile.reachable_resources,
162
+ total_attempts=profile.total_attempts,
163
+ total_blocked=profile.total_blocked,
164
+ total_allowed=profile.total_allowed,
165
+ total_executions=profile.total_executions,
166
+ )
167
+
168
+ def get_agent_delegation(self, agent_id: str) -> List[Dict[str, Any]] | ErrorResponse:
169
+ """GET /api/v1/agents/{agent_id}/delegation"""
170
+ rid = _new_request_id()
171
+ if agent_id not in self._fs._guard.agent_registry:
172
+ return ErrorResponse.make("AGENT_NOT_FOUND", f"Agent '{agent_id}' was not found", rid)
173
+ return self._fs.query.get_agent_delegation_chain(agent_id)
174
+
175
+ def get_agent_attempts(self, agent_id: str) -> List[Dict[str, Any]] | ErrorResponse:
176
+ """GET /api/v1/agents/{agent_id}/attempts"""
177
+ rid = _new_request_id()
178
+ if agent_id not in self._fs._guard.agent_registry:
179
+ return ErrorResponse.make("AGENT_NOT_FOUND", f"Agent '{agent_id}' was not found", rid)
180
+ return self._fs.query.get_agent_attempts(agent_id)
181
+
182
+ def get_agent_actual_access(
183
+ self, agent_id: str
184
+ ) -> List[Dict[str, Any]] | ErrorResponse:
185
+ """GET /api/v1/agents/{agent_id}/actual-access"""
186
+ rid = _new_request_id()
187
+ if agent_id not in self._fs._guard.agent_registry:
188
+ return ErrorResponse.make("AGENT_NOT_FOUND", f"Agent '{agent_id}' was not found", rid)
189
+ return self._fs.query.get_agent_actual_access(agent_id)
190
+
191
+ # ------------------------------------------------------------------
192
+ # Resources
193
+ # ------------------------------------------------------------------
194
+
195
+ def list_resources(self) -> List[str]:
196
+ """GET /api/v1/resources"""
197
+ return self._fs.query.get_all_resources()
198
+
199
+ def get_resource(
200
+ self, resource_name: str
201
+ ) -> ResourceAccessResponse | ErrorResponse:
202
+ """GET /api/v1/resources/{resource_id}"""
203
+ rid = _new_request_id()
204
+ profile = self._fs.query.get_resource_access(resource_name)
205
+ if not profile:
206
+ return ErrorResponse.make(
207
+ "RESOURCE_NOT_FOUND", f"Resource '{resource_name}' was not found", rid
208
+ )
209
+ return ResourceAccessResponse(
210
+ resource_id=profile.resource_id,
211
+ resource_name=profile.resource_name,
212
+ resource_type=profile.resource_type,
213
+ sensitivity=profile.sensitivity,
214
+ exposing_tools=profile.exposing_tools,
215
+ authorized_agents=profile.authorized_agents,
216
+ attempted_agents=profile.attempted_agents,
217
+ actual_access_agents=profile.actual_access_agents,
218
+ blocked_agents=profile.blocked_agents,
219
+ total_attempts=profile.total_attempts,
220
+ total_blocked=profile.total_blocked,
221
+ total_executions=profile.total_executions,
222
+ )
223
+
224
+ def get_resource_history(
225
+ self, resource_name: str
226
+ ) -> List[Dict[str, Any]] | ErrorResponse:
227
+ """GET /api/v1/resources/{resource_id}/history"""
228
+ rid = _new_request_id()
229
+ resources = self._fs.query.get_all_resources()
230
+ if resource_name not in resources:
231
+ return ErrorResponse.make(
232
+ "RESOURCE_NOT_FOUND", f"Resource '{resource_name}' was not found", rid
233
+ )
234
+ return self._fs.query.get_resource_access_history(resource_name)
235
+
236
+ # ------------------------------------------------------------------
237
+ # Access matrix & snapshots
238
+ # ------------------------------------------------------------------
239
+
240
+ def get_access_matrix(self) -> AccessMatrixResponse:
241
+ """GET /api/v1/access/matrix"""
242
+ matrix = self._fs.query.get_access_matrix()
243
+ table = matrix.to_table()
244
+ eff_perms: Dict[str, Dict[str, bool]] = {}
245
+ for agent_id, resources in matrix.entries.items():
246
+ eff_perms[agent_id] = {
247
+ r: e.effective_permission for r, e in resources.items()
248
+ }
249
+ return AccessMatrixResponse(
250
+ matrix_id=matrix.matrix_id,
251
+ agents=matrix.agents,
252
+ resources=matrix.resources,
253
+ table=table,
254
+ effective_permissions=eff_perms,
255
+ )
256
+
257
+ def list_snapshots(self) -> List[SnapshotSummary]:
258
+ """GET /api/v1/access/snapshots"""
259
+ return [
260
+ SnapshotSummary(**s)
261
+ for s in self._fs.query.list_snapshots()
262
+ ]
263
+
264
+ def get_snapshot(
265
+ self, snapshot_id: str
266
+ ) -> Dict[str, Any] | ErrorResponse:
267
+ """GET /api/v1/access/snapshots/{snapshot_id}"""
268
+ rid = _new_request_id()
269
+ snap = self._fs.query.get_access_snapshot(snapshot_id)
270
+ if not snap:
271
+ return ErrorResponse.make(
272
+ "SNAPSHOT_NOT_FOUND", f"Snapshot '{snapshot_id}' was not found", rid
273
+ )
274
+ return snap.model_dump(mode="json")
275
+
276
+ def get_access_diff(
277
+ self, before_id: str, after_id: str
278
+ ) -> DiffResponse | ErrorResponse:
279
+ """GET /api/v1/access/diff?before=...&after=..."""
280
+ rid = _new_request_id()
281
+ diff = self._fs.query.compare_access_snapshots(before_id, after_id)
282
+ if not diff:
283
+ return ErrorResponse.make(
284
+ "SNAPSHOT_NOT_FOUND",
285
+ f"Could not compare snapshots '{before_id}' and '{after_id}'",
286
+ rid,
287
+ )
288
+ return DiffResponse(
289
+ diff_id=diff.diff_id,
290
+ before_snapshot_id=diff.before_snapshot_id,
291
+ after_snapshot_id=diff.after_snapshot_id,
292
+ authority_changed=diff.authority_changed,
293
+ new_capabilities=diff.new_capabilities,
294
+ lost_capabilities=diff.lost_capabilities,
295
+ attack_detected=diff.attack_detected,
296
+ taint_changed=diff.taint_changed,
297
+ context_changed=diff.context_changed,
298
+ summary=diff.summary,
299
+ )
300
+
301
+ # ------------------------------------------------------------------
302
+ # Traces
303
+ # ------------------------------------------------------------------
304
+
305
+ def get_trace(self, trace_id: str) -> Dict[str, Any] | ErrorResponse:
306
+ """GET /api/v1/traces/{trace_id}"""
307
+ rid = _new_request_id()
308
+ graph_json = self._fs.query.get_causal_path(trace_id)
309
+ if graph_json is None:
310
+ return ErrorResponse.make(
311
+ "TRACE_NOT_FOUND", f"Trace '{trace_id}' was not found", rid
312
+ )
313
+ import json
314
+ try:
315
+ return json.loads(graph_json)
316
+ except Exception:
317
+ return {"trace_id": trace_id, "graph": graph_json}
318
+
319
+ # ------------------------------------------------------------------
320
+ # Attacks
321
+ # ------------------------------------------------------------------
322
+
323
+ def list_attacks(self) -> List[AttackSummary]:
324
+ """GET /api/v1/attacks"""
325
+ summaries = []
326
+ for attack_id, result in self._fs._attack_results.items():
327
+ summaries.append(AttackSummary(
328
+ attack_id=attack_id,
329
+ campaign_id=result.campaign_id,
330
+ attack_type=result.attack_type.value,
331
+ target=result.target,
332
+ entry_point=result.entry_point,
333
+ expected_decision=result.expected_decision,
334
+ actual_decision=result.actual_decision,
335
+ bypassed=result.bypassed,
336
+ tool_executed=result.actual_execution,
337
+ execution_count=result.execution_evidence.execution_count,
338
+ ))
339
+ return summaries
340
+
341
+ def get_attack(self, attack_id: str) -> AttackSummary | ErrorResponse:
342
+ """GET /api/v1/attacks/{attack_id}"""
343
+ rid = _new_request_id()
344
+ result = self._fs._attack_results.get(attack_id)
345
+ if not result:
346
+ return ErrorResponse.make(
347
+ "ATTACK_NOT_FOUND", f"Attack '{attack_id}' was not found", rid
348
+ )
349
+ return AttackSummary(
350
+ attack_id=attack_id,
351
+ campaign_id=result.campaign_id,
352
+ attack_type=result.attack_type.value,
353
+ target=result.target,
354
+ entry_point=result.entry_point,
355
+ expected_decision=result.expected_decision,
356
+ actual_decision=result.actual_decision,
357
+ bypassed=result.bypassed,
358
+ tool_executed=result.actual_execution,
359
+ execution_count=result.execution_evidence.execution_count,
360
+ )
361
+
362
+ def get_attack_lineage(
363
+ self, attack_id: str
364
+ ) -> Dict[str, Any] | ErrorResponse:
365
+ """GET /api/v1/attacks/{attack_id}/lineage"""
366
+ rid = _new_request_id()
367
+ result = self._fs._attack_results.get(attack_id)
368
+ if not result:
369
+ return ErrorResponse.make(
370
+ "ATTACK_NOT_FOUND", f"Attack '{attack_id}' was not found", rid
371
+ )
372
+ return {
373
+ "attack_id": attack_id,
374
+ "mutation_lineage": result.mutation_lineage,
375
+ "adaptive_iteration": getattr(result, "adaptive_iteration", 0),
376
+ "parent_attack_id": None,
377
+ }
378
+
379
+ def get_attack_forensics(
380
+ self, attack_id: str
381
+ ) -> AttackForensicsResponse | ErrorResponse:
382
+ """GET /api/v1/attacks/{attack_id}/forensics"""
383
+ rid = _new_request_id()
384
+ report = self._fs.query.get_attack_forensics(attack_id)
385
+ if not report:
386
+ return ErrorResponse.make(
387
+ "ATTACK_NOT_FOUND", f"Attack '{attack_id}' forensics not found", rid
388
+ )
389
+ return AttackForensicsResponse(
390
+ report_id=report.report_id,
391
+ attack_id=report.attack_id,
392
+ campaign_id=report.campaign_id,
393
+ attack_type=report.attack_type,
394
+ target=report.target,
395
+ agent_id=report.agent_id,
396
+ original_intent=report.original_intent,
397
+ mutation_lineage=report.mutation_lineage,
398
+ taint_state=report.taint_state,
399
+ policy_decision=report.policy_decision.value,
400
+ policy_reason_code=report.policy_reason_code,
401
+ tool_executed=report.tool_executed,
402
+ execution_count=report.execution_count,
403
+ sensitive_db_calls=report.sensitive_db_calls,
404
+ bypassed=report.bypassed,
405
+ reachable_resources=report.reachable_resources,
406
+ regression_status=report.regression_status,
407
+ )
408
+
409
+ # ------------------------------------------------------------------
410
+ # Campaigns
411
+ # ------------------------------------------------------------------
412
+
413
+ def list_campaigns(self) -> List[CampaignSummary]:
414
+ """GET /api/v1/campaigns"""
415
+ campaigns: Dict[str, Dict[str, Any]] = {}
416
+ for result in self._fs._attack_results.values():
417
+ cid = result.campaign_id or "unknown"
418
+ if cid not in campaigns:
419
+ campaigns[cid] = {"total": 0, "bypasses": 0, "blocks": 0}
420
+ campaigns[cid]["total"] += 1
421
+ if result.bypassed:
422
+ campaigns[cid]["bypasses"] += 1
423
+ if result.blocked:
424
+ campaigns[cid]["blocks"] += 1
425
+ return [
426
+ CampaignSummary(
427
+ campaign_id=cid,
428
+ total_attacks=data["total"],
429
+ bypasses=data["bypasses"],
430
+ blocks=data["blocks"],
431
+ bypass_rate=data["bypasses"] / data["total"] if data["total"] else 0.0,
432
+ )
433
+ for cid, data in campaigns.items()
434
+ ]
435
+
436
+ # ------------------------------------------------------------------
437
+ # Incidents
438
+ # ------------------------------------------------------------------
439
+
440
+ def list_incidents(self) -> List[IncidentResponse]:
441
+ """GET /api/v1/incidents"""
442
+ return [
443
+ IncidentResponse(
444
+ incident_id=inc.incident_id,
445
+ incident_type=inc.incident_type.value,
446
+ severity=inc.severity.value,
447
+ agent_id=inc.agent_id,
448
+ resource_name=inc.resource_name,
449
+ tool_name=inc.tool_name,
450
+ decision=inc.decision.value,
451
+ executed=inc.executed,
452
+ trace_id=inc.trace_id,
453
+ attack_id=inc.attack_id,
454
+ status=inc.status.value,
455
+ description=inc.description,
456
+ timestamp=inc.timestamp.isoformat(),
457
+ )
458
+ for inc in self._fs.query.get_all_incidents()
459
+ ]
460
+
461
+ # ------------------------------------------------------------------
462
+ # Regressions
463
+ # ------------------------------------------------------------------
464
+
465
+ def list_regressions(self) -> List[AttackSummary]:
466
+ """GET /api/v1/regressions — bypass attacks are regressions."""
467
+ return [
468
+ s for s in self.list_attacks() if s.bypassed
469
+ ]
470
+
471
+ def get_regression(
472
+ self, regression_id: str
473
+ ) -> AttackForensicsResponse | ErrorResponse:
474
+ """GET /api/v1/regressions/{regression_id}"""
475
+ return self.get_attack_forensics(regression_id)
476
+
477
+ # ------------------------------------------------------------------
478
+ # Policies
479
+ # ------------------------------------------------------------------
480
+
481
+ def list_policies(self) -> List[Dict[str, Any]]:
482
+ """GET /api/v1/policies — returns active policy configuration."""
483
+ cfg = self._fs._guard.config
484
+ return [
485
+ {
486
+ "policy_id": "p_tainted_sink",
487
+ "name": "Block tainted context into sensitive sink",
488
+ "enabled": cfg.block_tainted_sink_access,
489
+ "action": "BLOCK",
490
+ },
491
+ {
492
+ "policy_id": "p_monotonic_delegation",
493
+ "name": "Enforce monotonic authority containment",
494
+ "enabled": cfg.enforce_monotonic_delegation,
495
+ "action": "BLOCK",
496
+ },
497
+ ]
498
+
499
+ def evaluate_policy(
500
+ self, request: PolicyEvaluateRequest
501
+ ) -> PolicyEvaluateResponse:
502
+ """POST /api/v1/policies/evaluate — simulate a policy check (READ-ONLY).
503
+
504
+ This does NOT modify any security state.
505
+ This does NOT grant any capabilities.
506
+ """
507
+ from actshield.tools.tool import ToolDefinition, ToolRequest, SensitivityLevel
508
+ from actshield.context.context import Context
509
+ from actshield.context.taint import TaintState
510
+ from actshield.context.provenance import ContextSource, Provenance
511
+
512
+ # Look up the tool definition
513
+ tool_def = None
514
+ for td in self._fs._guard.tool_registry.values():
515
+ if td.name == request.tool_name:
516
+ tool_def = td
517
+ break
518
+
519
+ if tool_def is None:
520
+ # Build a synthetic tool for evaluation
521
+ tool_def = ToolDefinition(
522
+ name=request.tool_name,
523
+ sensitivity=SensitivityLevel.HIGH,
524
+ required_capabilities=request.capabilities,
525
+ )
526
+
527
+ tool_req = ToolRequest(
528
+ tool_id=tool_def.tool_id,
529
+ tool_name=tool_def.name,
530
+ agent_id=request.agent_id,
531
+ )
532
+
533
+ contexts = []
534
+ if request.tainted_context:
535
+ from actshield.tracing.correlation import generate_id
536
+ prov = Provenance(
537
+ source=ContextSource.EXTERNAL_MCP,
538
+ trust_level="untrusted",
539
+ )
540
+ ctx = Context(
541
+ context_id=generate_id("ctx"),
542
+ source=ContextSource.EXTERNAL_MCP,
543
+ taint_state=TaintState.TAINTED,
544
+ provenance=prov,
545
+ )
546
+ contexts.append(ctx)
547
+
548
+ from actshield.tracing.correlation import CorrelationContext, generate_id as gid
549
+ corr = CorrelationContext(
550
+ trace_id=gid("trc"),
551
+ span_id=gid("spn"),
552
+ task_id=gid("tsk"),
553
+ agent_id=request.agent_id,
554
+ metadata={"intent": request.intent},
555
+ )
556
+
557
+ decision = self._fs._guard.policy_evaluator.evaluate(
558
+ tool_def=tool_def,
559
+ request=tool_req,
560
+ ctx=corr,
561
+ active_contexts=contexts,
562
+ )
563
+
564
+ return PolicyEvaluateResponse(
565
+ tool_name=request.tool_name,
566
+ agent_id=request.agent_id,
567
+ decision=decision.action.value,
568
+ reason_code=decision.reason_code,
569
+ explanation=decision.explanation,
570
+ risk_score=decision.risk_score,
571
+ )
572
+
573
+ # ------------------------------------------------------------------
574
+ # Metrics
575
+ # ------------------------------------------------------------------
576
+
577
+ def get_metrics(self) -> Dict[str, Any]:
578
+ """GET /api/v1/metrics"""
579
+ overview = self._fs.query.get_overview()
580
+ return {
581
+ "total_agents": overview.get("agents", 0),
582
+ "total_attacks": overview.get("attacks_today", 0),
583
+ "bypass_rate": (
584
+ overview["bypasses"] / overview["attacks_today"]
585
+ if overview.get("attacks_today")
586
+ else 0.0
587
+ ),
588
+ "block_rate": (
589
+ overview["blocked"] / overview["attacks_today"]
590
+ if overview.get("attacks_today")
591
+ else 0.0
592
+ ),
593
+ "sensitive_prevention_rate": (
594
+ overview["sensitive_prevented"] / overview["sensitive_attempts"]
595
+ if overview.get("sensitive_attempts")
596
+ else 1.0
597
+ ),
598
+ }
599
+
@@ -0,0 +1,15 @@
1
+ """ActShield Human-in-the-Loop (HITL) Approval Workflow."""
2
+
3
+ from actshield.approval.approval import (
4
+ ApprovalManager,
5
+ ApprovalRequest,
6
+ ApprovalStatus,
7
+ )
8
+
9
+ __all__ = [
10
+ "ApprovalManager",
11
+ "ApprovalRequest",
12
+ "ApprovalStatus",
13
+ ]
14
+
15
+