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,89 @@
1
+ """LLM provider configuration for ActShield AI Secura reasoning engine.
2
+
3
+ Reads from environment variables. No hardcoded values.
4
+
5
+ Variables:
6
+ AGENTGUARD_LLM_PROVIDER : 'ollama' or 'local' (default: 'local')
7
+ AGENTGUARD_LLM_MODEL : model name (default: 'qwen2.5:7b')
8
+ AGENTGUARD_OLLAMA_BASE_URL: Ollama server URL (default: 'http://localhost:11434')
9
+ AGENTGUARD_LLM_TIMEOUT : request timeout seconds (default: 30)
10
+ AGENTGUARD_LLM_MAX_TOKENS : max response tokens (default: 2048)
11
+ AGENTGUARD_LLM_TEMPERATURE: sampling temperature (default: 0.1)
12
+ """
13
+
14
+ import os
15
+ from typing import Literal
16
+ from pydantic import BaseModel, Field
17
+
18
+
19
+ class LLMConfig(BaseModel):
20
+ """Runtime configuration for the AI Secura LLM reasoning engine."""
21
+
22
+ provider: Literal["ollama", "local"] = Field(
23
+ default="local",
24
+ description="LLM provider: 'ollama' for real AI reasoning, 'local' for deterministic heuristic adapter",
25
+ )
26
+ model: str = Field(
27
+ default="qwen2.5:7b",
28
+ description="Model identifier (e.g. 'qwen2.5:7b', 'ai-secura')",
29
+ )
30
+ ollama_base_url: str = Field(
31
+ default="http://localhost:11434",
32
+ description="Base URL for the Ollama server",
33
+ )
34
+ timeout_seconds: int = Field(
35
+ default=60,
36
+ ge=5,
37
+ le=300,
38
+ description="HTTP request timeout in seconds for Ollama calls",
39
+ )
40
+ max_tokens: int = Field(
41
+ default=512,
42
+ ge=256,
43
+ le=8192,
44
+ description="Maximum tokens the model may generate per analysis",
45
+ )
46
+ temperature: float = Field(
47
+ default=0.1,
48
+ ge=0.0,
49
+ le=1.0,
50
+ description="Sampling temperature. Low values produce deterministic, consistent security analysis.",
51
+ )
52
+ num_ctx: int = Field(
53
+ default=8192,
54
+ ge=2048,
55
+ description="Ollama context window size in tokens",
56
+ )
57
+ repair_on_invalid_json: bool = Field(
58
+ default=True,
59
+ description="Attempt exactly one JSON repair request if the initial response is malformed",
60
+ )
61
+ fallback_to_local_on_error: bool = Field(
62
+ default=True,
63
+ description="Return an ai_unavailable=True analysis when Ollama is unreachable",
64
+ )
65
+
66
+ @classmethod
67
+ def from_env(cls) -> "LLMConfig":
68
+ return cls(
69
+ provider=os.environ.get("AGENTGUARD_LLM_PROVIDER", "local"), # type: ignore[arg-type]
70
+ model=os.environ.get("AGENTGUARD_LLM_MODEL", "qwen2.5:7b"),
71
+ ollama_base_url=os.environ.get("AGENTGUARD_OLLAMA_BASE_URL", "http://localhost:11434"),
72
+ timeout_seconds=int(os.environ.get("AGENTGUARD_LLM_TIMEOUT", "30")),
73
+ max_tokens=int(os.environ.get("AGENTGUARD_LLM_MAX_TOKENS", "2048")),
74
+ temperature=float(os.environ.get("AGENTGUARD_LLM_TEMPERATURE", "0.1")),
75
+ num_ctx=int(os.environ.get("AGENTGUARD_LLM_NUM_CTX", "8192")),
76
+ )
77
+
78
+ @property
79
+ def ollama_generate_url(self) -> str:
80
+ return f"{self.ollama_base_url.rstrip('/')}/api/generate"
81
+
82
+ @property
83
+ def ollama_tags_url(self) -> str:
84
+ return f"{self.ollama_base_url.rstrip('/')}/api/tags"
85
+
86
+ @property
87
+ def is_ollama(self) -> bool:
88
+ return self.provider == "ollama"
89
+
@@ -0,0 +1,95 @@
1
+ """ActShield Offensive Security Validation Engine."""
2
+
3
+ from actshield.offensive.attack import (
4
+ AttackCase,
5
+ AttackType,
6
+ ExpectedBehavior,
7
+ SafetyClass,
8
+ )
9
+ from actshield.offensive.target import (
10
+ AttackTarget,
11
+ TargetEnvironment,
12
+ TargetRegistry,
13
+ target_registry,
14
+ )
15
+ from actshield.offensive.safety import (
16
+ SafetyValidationResult,
17
+ SafetyValidator,
18
+ safety_validator,
19
+ )
20
+ from actshield.offensive.evidence import SecurityAnalysisEvidence
21
+ from actshield.offensive.results import (
22
+ AttackStatus,
23
+ ExecutionEvidence,
24
+ OffensiveAttackResult,
25
+ )
26
+ from actshield.offensive.evaluator import AttackEvaluator
27
+ from actshield.offensive.mutation import (
28
+ MutationEngine,
29
+ MutationStrategy,
30
+ )
31
+ from actshield.offensive.campaign import (
32
+ AttackCampaign,
33
+ CampaignSummary,
34
+ )
35
+ from actshield.offensive.corpus import (
36
+ AttackCorpus,
37
+ attack_corpus,
38
+ )
39
+ from actshield.offensive.generator import (
40
+ AIAttackGenerator,
41
+ OFFENSIVE_SYSTEM_PROMPT,
42
+ )
43
+ from actshield.offensive.vulnerable_target import VulnerableDemoTarget
44
+ from actshield.offensive.engine import OffensiveEngine
45
+ from actshield.offensive.adaptive import (
46
+ AdaptiveAttackSelector,
47
+ AdaptiveEngine,
48
+ BoundarySearchEngine,
49
+ CampaignMemory,
50
+ DefenseCategory,
51
+ DefenseFeedback,
52
+ FeedbackAnalyzer,
53
+ MutationNode,
54
+ MutationPlanner,
55
+ )
56
+
57
+ __all__ = [
58
+ "AttackCase",
59
+ "AttackType",
60
+ "ExpectedBehavior",
61
+ "SafetyClass",
62
+ "AttackTarget",
63
+ "TargetEnvironment",
64
+ "TargetRegistry",
65
+ "target_registry",
66
+ "SafetyValidationResult",
67
+ "SafetyValidator",
68
+ "safety_validator",
69
+ "SecurityAnalysisEvidence",
70
+ "AttackStatus",
71
+ "ExecutionEvidence",
72
+ "OffensiveAttackResult",
73
+ "AttackEvaluator",
74
+ "MutationEngine",
75
+ "MutationStrategy",
76
+ "AttackCampaign",
77
+ "CampaignSummary",
78
+ "AttackCorpus",
79
+ "attack_corpus",
80
+ "AIAttackGenerator",
81
+ "OFFENSIVE_SYSTEM_PROMPT",
82
+ "VulnerableDemoTarget",
83
+ "OffensiveEngine",
84
+ "AdaptiveAttackSelector",
85
+ "AdaptiveEngine",
86
+ "BoundarySearchEngine",
87
+ "CampaignMemory",
88
+ "DefenseCategory",
89
+ "DefenseFeedback",
90
+ "FeedbackAnalyzer",
91
+ "MutationNode",
92
+ "MutationPlanner",
93
+ ]
94
+
95
+
@@ -0,0 +1,22 @@
1
+ """ActShield Adaptive Offensive Validation Subsystem."""
2
+
3
+ from actshield.offensive.adaptive.campaign_memory import CampaignMemory, MutationNode
4
+ from actshield.offensive.adaptive.feedback import DefenseCategory, DefenseFeedback, FeedbackAnalyzer
5
+ from actshield.offensive.adaptive.mutation_planner import MutationPlanner
6
+ from actshield.offensive.adaptive.attack_selector import AdaptiveAttackSelector
7
+ from actshield.offensive.adaptive.boundary_search import BoundarySearchEngine
8
+ from actshield.offensive.adaptive.adaptive_engine import AdaptiveEngine
9
+
10
+ __all__ = [
11
+ "CampaignMemory",
12
+ "MutationNode",
13
+ "DefenseCategory",
14
+ "DefenseFeedback",
15
+ "FeedbackAnalyzer",
16
+ "MutationPlanner",
17
+ "AdaptiveAttackSelector",
18
+ "BoundarySearchEngine",
19
+ "AdaptiveEngine",
20
+ ]
21
+
22
+
@@ -0,0 +1,165 @@
1
+ """Adaptive Offensive Engine — Master orchestrator for adaptive validation campaigns."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import pathlib
7
+ import time
8
+ from datetime import datetime, timezone
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ from actshield.offensive.attack import AttackCase, AttackType
12
+ from actshield.offensive.campaign import AttackCampaign, CampaignSummary
13
+ from actshield.offensive.corpus import AttackCorpus, attack_corpus
14
+ from actshield.offensive.engine import OffensiveEngine
15
+ from actshield.offensive.results import AttackStatus, OffensiveAttackResult
16
+ from actshield.offensive.safety import SafetyValidator, safety_validator
17
+ from actshield.offensive.target import AttackTarget, TargetRegistry, target_registry
18
+ from actshield.offensive.adaptive.attack_selector import AdaptiveAttackSelector
19
+ from actshield.offensive.adaptive.boundary_search import BoundarySearchEngine
20
+ from actshield.offensive.adaptive.campaign_memory import CampaignMemory, MutationNode
21
+
22
+ logger = logging.getLogger("actshield.offensive.adaptive.engine")
23
+
24
+
25
+ class AdaptiveEngine:
26
+ """Orchestrates adaptive multi-depth offensive campaigns against ActShield defenses."""
27
+
28
+ def __init__(
29
+ self,
30
+ engine: Optional[OffensiveEngine] = None,
31
+ corpus: Optional[AttackCorpus] = None,
32
+ registry: Optional[TargetRegistry] = None,
33
+ validator: Optional[SafetyValidator] = None,
34
+ max_depth: int = 3,
35
+ branch_factor: int = 2,
36
+ seed: Optional[int] = 20260919,
37
+ ) -> None:
38
+ self.engine = engine or OffensiveEngine(corpus=corpus, registry=registry, validator=validator)
39
+ self.corpus = corpus or attack_corpus
40
+ self.registry = registry or target_registry
41
+ self.validator = validator or safety_validator
42
+ self.max_depth = max_depth
43
+ self.branch_factor = branch_factor
44
+ self.seed = seed
45
+ self.selector = AdaptiveAttackSelector(corpus=self.corpus, seed=seed)
46
+
47
+ def run_adaptive_campaign(
48
+ self,
49
+ family: Optional[str] = None,
50
+ attack_id: Optional[str] = None,
51
+ limit_seeds: Optional[int] = None,
52
+ target: Optional[AttackTarget] = None,
53
+ campaign_name: str = "Adaptive Offensive Campaign",
54
+ ) -> Tuple[List[OffensiveAttackResult], CampaignSummary, CampaignMemory]:
55
+ """Execute a full adaptive validation campaign."""
56
+ start_time = time.perf_counter()
57
+ target = target or self.registry.get("ActShield-demo")
58
+ campaign_id = f"camp_adapt_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}"
59
+ memory = CampaignMemory(campaign_id=campaign_id)
60
+
61
+ search_engine = BoundarySearchEngine(
62
+ engine=self.engine,
63
+ memory=memory,
64
+ max_depth=self.max_depth,
65
+ branch_factor=self.branch_factor,
66
+ )
67
+
68
+ seed_attacks = self.selector.select_seeds(family=family, attack_id=attack_id, limit=limit_seeds)
69
+ all_results: List[OffensiveAttackResult] = []
70
+
71
+ campaign = AttackCampaign(
72
+ campaign_id=campaign_id,
73
+ name=campaign_name,
74
+ objective=f"Adaptive exploration of defense boundaries (Max Depth: {self.max_depth})",
75
+ target=target,
76
+ attacks=seed_attacks,
77
+ )
78
+
79
+ for seed_attack in seed_attacks:
80
+ branch_results = search_engine.explore_boundary(
81
+ root_attack=seed_attack,
82
+ target=target,
83
+ campaign_id=campaign_id,
84
+ )
85
+ all_results.extend(branch_results)
86
+
87
+ duration_ms = (time.perf_counter() - start_time) * 1000
88
+
89
+ # Calculate enhanced scorecard
90
+ summary = CampaignSummary.from_results(
91
+ campaign=campaign,
92
+ results=all_results,
93
+ memory=memory,
94
+ seed=self.seed,
95
+ duration_ms=duration_ms,
96
+ )
97
+
98
+ return all_results, summary, memory
99
+
100
+ def export_dashboard_json(
101
+ self,
102
+ summary: CampaignSummary,
103
+ results: List[OffensiveAttackResult],
104
+ memory: CampaignMemory,
105
+ output_dir: Optional[str] = None,
106
+ ) -> Dict[str, str]:
107
+ """Export comprehensive dashboard-ready JSON artifacts."""
108
+ if output_dir:
109
+ out_path = pathlib.Path(output_dir)
110
+ else:
111
+ root = pathlib.Path(__file__).resolve().parent.parent.parent.parent.parent
112
+ out_path = root / "reports" / "phase5"
113
+
114
+ out_path.mkdir(parents=True, exist_ok=True)
115
+
116
+ # 1. Dashboard Master Package
117
+ dashboard_payload = {
118
+ "campaign": {
119
+ "campaign_id": summary.campaign_id,
120
+ "name": summary.name,
121
+ "target_id": summary.target_id,
122
+ "timestamp": summary.timestamp.isoformat(),
123
+ "seed": summary.campaign_seed,
124
+ "duration_ms": summary.duration_ms,
125
+ },
126
+ "coverage": {
127
+ "total_attacks": summary.total_attacks,
128
+ "attack_family_breakdown": summary.coverage_breakdown,
129
+ "unique_attack_variants": summary.unique_attack_variants,
130
+ "max_mutation_depth": summary.max_mutation_depth,
131
+ "avg_mutation_depth": summary.avg_mutation_depth,
132
+ },
133
+ "security": {
134
+ "blocked": summary.blocked,
135
+ "bypassed": summary.bypassed,
136
+ "block_rate_pct": summary.block_rate_pct,
137
+ "sensitive_actions_attempted": summary.sensitive_actions_attempted,
138
+ "sensitive_actions_executed": summary.sensitive_actions_executed,
139
+ "empirical_prevention_rate": summary.empirical_prevention_rate,
140
+ "ai_analysis_availability_pct": summary.ai_analysis_availability_pct,
141
+ "apiris_analysis_availability_pct": summary.apiris_analysis_availability_pct,
142
+ "defense_reasons_breakdown": summary.defense_reasons_breakdown,
143
+ },
144
+ "performance": {
145
+ "mean_latency_ms": summary.mean_latency_ms,
146
+ "p95_latency_ms": summary.p95_latency_ms,
147
+ },
148
+ "mutations": [n.to_dict() for n in memory.get_all_nodes()],
149
+ "bypasses": [n.to_dict() for n in memory.get_bypasses()],
150
+ "regressions": summary.regression_fixtures_generated,
151
+ }
152
+
153
+ dash_file = out_path / "adaptive_dashboard.json"
154
+ dash_file.write_text(json.dumps(dashboard_payload, indent=2), encoding="utf-8")
155
+
156
+ # 2. Markdown Summary
157
+ md_file = out_path / "adaptive_campaign_summary.md"
158
+ md_file.write_text(summary.to_markdown(), encoding="utf-8")
159
+
160
+ return {
161
+ "dashboard_json": str(dash_file),
162
+ "summary_md": str(md_file),
163
+ }
164
+
165
+
@@ -0,0 +1,51 @@
1
+ """Adaptive attack selector providing deterministic seed selection across attack families."""
2
+
3
+ import random
4
+ from typing import List, Optional
5
+ from actshield.offensive.attack import AttackCase, AttackType
6
+ from actshield.offensive.corpus import AttackCorpus, attack_corpus
7
+
8
+
9
+ class AdaptiveAttackSelector:
10
+ """Selects base seed attacks from the corpus for adaptive exploration."""
11
+
12
+ def __init__(self, corpus: Optional[AttackCorpus] = None, seed: Optional[int] = None) -> None:
13
+ self.corpus = corpus or attack_corpus
14
+ self.seed = seed
15
+ if seed is not None:
16
+ random.seed(seed)
17
+
18
+ def select_seeds(
19
+ self,
20
+ family: Optional[str] = None,
21
+ attack_id: Optional[str] = None,
22
+ limit: Optional[int] = None,
23
+ ) -> List[AttackCase]:
24
+ """Select seed attacks matching criteria."""
25
+ all_attacks = self.corpus.get_all_attacks()
26
+
27
+ if attack_id:
28
+ found = self.corpus.get_attack(attack_id)
29
+ return [found] if found else []
30
+
31
+ if family:
32
+ fam_clean = family.lower().replace("-", "_")
33
+ filtered = [
34
+ a for a in all_attacks
35
+ if fam_clean in a.attack_type.value.lower() or fam_clean in a.attack_id.lower()
36
+ ]
37
+ else:
38
+ filtered = all_attacks
39
+
40
+ if self.seed is not None:
41
+ # Deterministic shuffle
42
+ rng = random.Random(self.seed)
43
+ shuffled = list(filtered)
44
+ rng.shuffle(shuffled)
45
+ filtered = shuffled
46
+
47
+ if limit and limit > 0:
48
+ return filtered[:limit]
49
+
50
+ return filtered
51
+
@@ -0,0 +1,99 @@
1
+ """Boundary search engine executing bounded adaptive exploration of defense limits."""
2
+
3
+ from typing import List, Optional, Tuple
4
+ from actshield.offensive.attack import AttackCase
5
+ from actshield.offensive.engine import OffensiveEngine
6
+ from actshield.offensive.results import AttackStatus, OffensiveAttackResult
7
+ from actshield.offensive.target import AttackTarget
8
+ from actshield.offensive.adaptive.campaign_memory import CampaignMemory
9
+ from actshield.offensive.adaptive.feedback import FeedbackAnalyzer
10
+ from actshield.offensive.adaptive.mutation_planner import MutationPlanner
11
+
12
+
13
+ class BoundarySearchEngine:
14
+ """Explores the decision boundary around an attack vector until bypass or max depth."""
15
+
16
+ def __init__(
17
+ self,
18
+ engine: OffensiveEngine,
19
+ memory: CampaignMemory,
20
+ max_depth: int = 3,
21
+ branch_factor: int = 2,
22
+ max_attempts_per_attack: int = 10,
23
+ ) -> None:
24
+ self.engine = engine
25
+ self.memory = memory
26
+ self.max_depth = max_depth
27
+ self.branch_factor = branch_factor
28
+ self.max_attempts_per_attack = max_attempts_per_attack
29
+
30
+ def explore_boundary(
31
+ self,
32
+ root_attack: AttackCase,
33
+ target: Optional[AttackTarget] = None,
34
+ campaign_id: Optional[str] = None,
35
+ ) -> List[OffensiveAttackResult]:
36
+ """Execute bounded adaptive exploration starting from root_attack."""
37
+ results: List[OffensiveAttackResult] = []
38
+ attempts = 0
39
+
40
+ # Queue items: (AttackCase, depth, parent_id, strategy_name)
41
+ queue: List[Tuple[AttackCase, int, Optional[str], str]] = [(root_attack, 0, None, "baseline")]
42
+
43
+ while queue and attempts < self.max_attempts_per_attack:
44
+ current_attack, depth, parent_id, strategy = queue.pop(0)
45
+ attempts += 1
46
+
47
+ # Get lineage so far
48
+ lineage_ids = [n.mutation_id for n in self.memory.get_lineage(parent_id)] if parent_id else []
49
+ lineage_ids.append(current_attack.attack_id)
50
+
51
+ # Execute attack through full security pipeline
52
+ res = self.engine.execute_attack(
53
+ attack=current_attack,
54
+ target=target,
55
+ campaign_id=campaign_id,
56
+ mutation_lineage=lineage_ids,
57
+ )
58
+ results.append(res)
59
+
60
+ # Record in campaign memory
61
+ self.memory.record_node(
62
+ mutation_id=current_attack.attack_id,
63
+ root_attack_id=root_attack.attack_id,
64
+ parent_id=parent_id,
65
+ strategy=strategy,
66
+ depth=depth,
67
+ input_payload=root_attack.payload,
68
+ output_payload=current_attack.payload,
69
+ defense_reason=res.execution_evidence.policy_reason,
70
+ decision=res.actual_decision,
71
+ status=res.status.value,
72
+ sensitive_executions=res.execution_evidence.sensitive_db_calls,
73
+ latency_ms=res.latency_ms,
74
+ )
75
+
76
+ # INVARIANT: If bypass is discovered, STOP exploring this branch immediately!
77
+ if res.bypassed:
78
+ break
79
+
80
+ # If not bypassed and depth < max_depth, generate targeted mutations
81
+ if depth < self.max_depth:
82
+ feedback = FeedbackAnalyzer.analyze(res)
83
+ next_mutations = MutationPlanner.plan_next_mutations(
84
+ parent_attack=current_attack,
85
+ feedback=feedback,
86
+ depth=depth + 1,
87
+ branch_factor=self.branch_factor,
88
+ )
89
+
90
+ for mut_case in next_mutations:
91
+ queue.append((
92
+ mut_case,
93
+ depth + 1,
94
+ current_attack.attack_id,
95
+ mut_case.mutation_strategy or "mutation",
96
+ ))
97
+
98
+ return results
99
+
@@ -0,0 +1,123 @@
1
+ """Campaign Memory and Mutation Lineage Graph for Adaptive Offensive Validation."""
2
+
3
+ import hashlib
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, List, Optional
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class MutationNode(BaseModel):
10
+ """Represents a single node in an adaptive attack mutation lineage tree."""
11
+
12
+ mutation_id: str = Field(..., description="Unique ID for this mutation attempt")
13
+ parent_id: Optional[str] = Field(default=None, description="Parent attack/mutation ID")
14
+ root_attack_id: str = Field(..., description="The original baseline attack ID")
15
+ strategy: str = Field(default="baseline", description="Mutation strategy applied")
16
+ depth: int = Field(default=0, description="Mutation depth from baseline root (0 = root)")
17
+
18
+ input_payload: str = Field(default="", description="Input payload before mutation")
19
+ output_payload: str = Field(default="", description="Output payload after mutation")
20
+ input_hash: str = Field(default="", description="SHA-256 hash of input payload")
21
+ output_hash: str = Field(default="", description="SHA-256 hash of output payload")
22
+
23
+ defense_reason: str = Field(default="", description="Defensive policy reason code rendered")
24
+ decision: str = Field(default="UNKNOWN", description="Policy decision (BLOCK/ALLOW/etc.)")
25
+ status: str = Field(default="PASS", description="Attack status (PASS/BYPASS/ERROR/REFUSED)")
26
+ sensitive_executions: int = Field(default=0, description="Count of unauthorized sensitive sink calls")
27
+ latency_ms: float = Field(default=0.0, description="Execution latency in milliseconds")
28
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
29
+
30
+ @staticmethod
31
+ def compute_hash(text: str) -> str:
32
+ """Compute 8-char truncated SHA-256 hash of text."""
33
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ """Convert node to dictionary."""
37
+ return self.model_dump(mode="json")
38
+
39
+
40
+ class CampaignMemory:
41
+ """Stores the full adaptive exploration graph, lineage trees, and defense responses."""
42
+
43
+ def __init__(self, campaign_id: Optional[str] = None) -> None:
44
+ self.campaign_id = campaign_id or f"camp_mem_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}"
45
+ self.nodes: Dict[str, MutationNode] = {}
46
+ self.children_map: Dict[str, List[str]] = {}
47
+ self.bypasses: List[str] = []
48
+
49
+ def record_node(
50
+ self,
51
+ mutation_id: str,
52
+ root_attack_id: str,
53
+ parent_id: Optional[str] = None,
54
+ strategy: str = "baseline",
55
+ depth: int = 0,
56
+ input_payload: str = "",
57
+ output_payload: str = "",
58
+ defense_reason: str = "",
59
+ decision: str = "UNKNOWN",
60
+ status: str = "PASS",
61
+ sensitive_executions: int = 0,
62
+ latency_ms: float = 0.0,
63
+ ) -> MutationNode:
64
+ """Record an attack attempt into campaign memory."""
65
+ node = MutationNode(
66
+ mutation_id=mutation_id,
67
+ parent_id=parent_id,
68
+ root_attack_id=root_attack_id,
69
+ strategy=strategy,
70
+ depth=depth,
71
+ input_payload=input_payload,
72
+ output_payload=output_payload,
73
+ input_hash=MutationNode.compute_hash(input_payload),
74
+ output_hash=MutationNode.compute_hash(output_payload),
75
+ defense_reason=defense_reason,
76
+ decision=decision,
77
+ status=status,
78
+ sensitive_executions=sensitive_executions,
79
+ latency_ms=latency_ms,
80
+ )
81
+ self.nodes[mutation_id] = node
82
+
83
+ if parent_id:
84
+ if parent_id not in self.children_map:
85
+ self.children_map[parent_id] = []
86
+ self.children_map[parent_id].append(mutation_id)
87
+
88
+ if status == "BYPASS" or sensitive_executions > 0:
89
+ if mutation_id not in self.bypasses:
90
+ self.bypasses.append(mutation_id)
91
+
92
+ return node
93
+
94
+ def get_lineage(self, attack_or_mutation_id: str) -> List[MutationNode]:
95
+ """Return root-to-leaf sequence of MutationNodes for the given attack/mutation ID."""
96
+ lineage: List[MutationNode] = []
97
+ curr_id: Optional[str] = attack_or_mutation_id
98
+
99
+ while curr_id and curr_id in self.nodes:
100
+ node = self.nodes[curr_id]
101
+ lineage.append(node)
102
+ curr_id = node.parent_id
103
+
104
+ lineage.reverse()
105
+ return lineage
106
+
107
+ def get_all_nodes(self) -> List[MutationNode]:
108
+ """Return all recorded mutation nodes."""
109
+ return list(self.nodes.values())
110
+
111
+ def get_bypasses(self) -> List[MutationNode]:
112
+ """Return all nodes where a bypass was discovered."""
113
+ return [self.nodes[bid] for bid in self.bypasses if bid in self.nodes]
114
+
115
+ def to_dict(self) -> Dict[str, Any]:
116
+ """Export campaign memory to dictionary."""
117
+ return {
118
+ "campaign_id": self.campaign_id,
119
+ "total_nodes": len(self.nodes),
120
+ "total_bypasses": len(self.bypasses),
121
+ "nodes": [n.to_dict() for n in self.nodes.values()],
122
+ "bypasses": self.bypasses,
123
+ }