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.
- actshield/__init__.py +439 -0
- actshield/__main__.py +46 -0
- actshield/agents/__init__.py +12 -0
- actshield/agents/agent.py +200 -0
- actshield/agents/identity.py +114 -0
- actshield/api/__init__.py +34 -0
- actshield/api/models.py +198 -0
- actshield/api/server.py +669 -0
- actshield/api/service.py +599 -0
- actshield/approval/__init__.py +15 -0
- actshield/approval/approval.py +164 -0
- actshield/cli/__init__.py +8 -0
- actshield/cli/graph.py +49 -0
- actshield/cli/main.py +802 -0
- actshield/cli/shell.py +406 -0
- actshield/cli/theme.py +110 -0
- actshield/client.py +757 -0
- actshield/config.py +88 -0
- actshield/context/__init__.py +14 -0
- actshield/context/context.py +161 -0
- actshield/context/provenance.py +108 -0
- actshield/context/taint.py +48 -0
- actshield/decisions/decision.py +74 -0
- actshield/delegation/__init__.py +11 -0
- actshield/delegation/authority.py +63 -0
- actshield/delegation/delegation.py +180 -0
- actshield/demo.py +139 -0
- actshield/doctor.py +94 -0
- actshield/drift/__init__.py +27 -0
- actshield/drift/drift_engine.py +149 -0
- actshield/drift/drift_models.py +63 -0
- actshield/evaluation/__init__.py +1 -0
- actshield/evaluation/cases/__init__.py +1 -0
- actshield/evaluation/eval_suite.py +254 -0
- actshield/forensics/__init__.py +53 -0
- actshield/forensics/access_graph.py +216 -0
- actshield/forensics/authority_graph.py +186 -0
- actshield/forensics/delegation_graph.py +119 -0
- actshield/forensics/explanation.py +254 -0
- actshield/forensics/models.py +488 -0
- actshield/forensics/queries.py +367 -0
- actshield/forensics/serializers.py +68 -0
- actshield/forensics/service.py +310 -0
- actshield/forensics/snapshots.py +174 -0
- actshield/gates/__init__.py +13 -0
- actshield/gates/gate_models.py +46 -0
- actshield/gates/security_gate.py +191 -0
- actshield/gateway/__init__.py +13 -0
- actshield/gateway/http.py +191 -0
- actshield/gateway/mcp.py +292 -0
- actshield/incidents/__init__.py +25 -0
- actshield/incidents/incident.py +94 -0
- actshield/incidents/incident_engine.py +163 -0
- actshield/incidents/incident_rules.py +116 -0
- actshield/incidents/incident_state.py +58 -0
- actshield/integrations/__init__.py +24 -0
- actshield/integrations/ai_secura.py +169 -0
- actshield/integrations/ai_secura_analysis.py +236 -0
- actshield/integrations/apiris.py +122 -0
- actshield/integrations/few_shot.py +104 -0
- actshield/integrations/ollama_adapter.py +538 -0
- actshield/integrations/prompts/__init__.py +1 -0
- actshield/integrations/prompts/examples/__init__.py +1 -0
- actshield/integrations/security_packet.py +82 -0
- actshield/llm_config.py +89 -0
- actshield/offensive/__init__.py +95 -0
- actshield/offensive/adaptive/__init__.py +22 -0
- actshield/offensive/adaptive/adaptive_engine.py +165 -0
- actshield/offensive/adaptive/attack_selector.py +51 -0
- actshield/offensive/adaptive/boundary_search.py +99 -0
- actshield/offensive/adaptive/campaign_memory.py +123 -0
- actshield/offensive/adaptive/feedback.py +84 -0
- actshield/offensive/adaptive/mutation_planner.py +57 -0
- actshield/offensive/attack.py +78 -0
- actshield/offensive/campaign.py +219 -0
- actshield/offensive/corpus.py +155 -0
- actshield/offensive/engine.py +466 -0
- actshield/offensive/evaluator.py +116 -0
- actshield/offensive/evidence.py +63 -0
- actshield/offensive/generator.py +128 -0
- actshield/offensive/mutation.py +101 -0
- actshield/offensive/results.py +77 -0
- actshield/offensive/safety.py +80 -0
- actshield/offensive/strategies/__init__.py +22 -0
- actshield/offensive/strategies/authority_escalation.py +100 -0
- actshield/offensive/strategies/base.py +34 -0
- actshield/offensive/strategies/context_manipulation.py +100 -0
- actshield/offensive/strategies/data_exfiltration.py +100 -0
- actshield/offensive/strategies/delegation_attacks.py +100 -0
- actshield/offensive/strategies/mcp_attacks.py +100 -0
- actshield/offensive/strategies/prompt_injection.py +100 -0
- actshield/offensive/strategies/tool_poisoning.py +100 -0
- actshield/offensive/target.py +113 -0
- actshield/offensive/vulnerable_target.py +121 -0
- actshield/persistence/__init__.py +18 -0
- actshield/persistence/siem.py +102 -0
- actshield/persistence/storage.py +240 -0
- actshield/policy/__init__.py +12 -0
- actshield/policy/evaluator.py +294 -0
- actshield/policy/intent.py +113 -0
- actshield/policy/policy.py +48 -0
- actshield/posture/__init__.py +26 -0
- actshield/posture/posture_engine.py +186 -0
- actshield/posture/posture_models.py +152 -0
- actshield/posture/posture_rules.py +137 -0
- actshield/providers/__init__.py +32 -0
- actshield/providers/anthropic_provider.py +146 -0
- actshield/providers/base.py +160 -0
- actshield/providers/gemini_provider.py +133 -0
- actshield/providers/null_provider.py +40 -0
- actshield/providers/ollama_provider.py +135 -0
- actshield/providers/openai_provider.py +142 -0
- actshield/providers/registry.py +244 -0
- actshield/py.typed +0 -0
- actshield/response/__init__.py +14 -0
- actshield/response/response_actions.py +39 -0
- actshield/response/response_audit.py +32 -0
- actshield/response/response_engine.py +139 -0
- actshield/risk/models.py +104 -0
- actshield/serve.py +117 -0
- actshield/static/404.html +1 -0
- actshield/static/__next.__PAGE__.txt +24 -0
- actshield/static/__next._full.txt +20 -0
- actshield/static/__next._tree.txt +4 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_buildManifest.js +11 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_clientMiddlewareManifest.js +1 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_ssgManifest.js +1 -0
- actshield/static/_next/static/chunks/09kny2eqjc2or.js +1 -0
- actshield/static/_next/static/chunks/0c1zba1u9whdp.js +1 -0
- actshield/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- actshield/static/_next/static/chunks/0dbjjeg01x25o.js +1 -0
- actshield/static/_next/static/chunks/0duobs55ne238.js +1 -0
- actshield/static/_next/static/chunks/0iaz41l6068e4.js +1 -0
- actshield/static/_next/static/chunks/0mjhyv72x2idp.js +1 -0
- actshield/static/_next/static/chunks/0nelpqfu_ciq6.js +1 -0
- actshield/static/_next/static/chunks/0p1bci9ch797v.js +1 -0
- actshield/static/_next/static/chunks/0tld_ygho5tgm.js +1 -0
- actshield/static/_next/static/chunks/0w1-y7oni66j9.js +1 -0
- actshield/static/_next/static/chunks/10537zyl427zj.js +1 -0
- actshield/static/_next/static/chunks/11pc_jkl3aggn.js +1 -0
- actshield/static/_next/static/chunks/16k7nw0b3t8ys.js +1 -0
- actshield/static/_next/static/chunks/19mx3mg6lkumu.js +1 -0
- actshield/static/_next/static/chunks/1_8p47b-kxg2a.js +1 -0
- actshield/static/_next/static/chunks/1dszjozwfqegx.js +1 -0
- actshield/static/_next/static/chunks/1h43jcb90-m2r.js +22 -0
- actshield/static/_next/static/chunks/1ly04cvt_wh_j.js +1 -0
- actshield/static/_next/static/chunks/1odxrypuwuwp_.js +1 -0
- actshield/static/_next/static/chunks/1oes4c35qz5ku.js +1 -0
- actshield/static/_next/static/chunks/1ow883jl4ln-r.js +16 -0
- actshield/static/_next/static/chunks/1po7bna1as8tc.js +1 -0
- actshield/static/_next/static/chunks/1rsh0og8ifkzx.js +1 -0
- actshield/static/_next/static/chunks/1v0lwzv-859z_.js +1 -0
- actshield/static/_next/static/chunks/25iat2epwx25a.js +1 -0
- actshield/static/_next/static/chunks/25uvt4s2f39a4.js +1 -0
- actshield/static/_next/static/chunks/27t_qfc-3_lzs.js +1 -0
- actshield/static/_next/static/chunks/28bkmy8mez7gm.js +1 -0
- actshield/static/_next/static/chunks/2b6brmw_drw1p.js +1 -0
- actshield/static/_next/static/chunks/2fnhtxi4j6vge.js +1 -0
- actshield/static/_next/static/chunks/2p087eftxemg4.js +6 -0
- actshield/static/_next/static/chunks/2s8pao_z8pgtq.js +1 -0
- actshield/static/_next/static/chunks/2yj7ooc2c-zpg.js +1 -0
- actshield/static/_next/static/chunks/3-mrrr8udt5tk.js +1 -0
- actshield/static/_next/static/chunks/310vm2bl3xxpt.js +31 -0
- actshield/static/_next/static/chunks/35mvsv251jx1r.css +3 -0
- actshield/static/_next/static/chunks/370vf0oetlsx9.js +1 -0
- actshield/static/_next/static/chunks/3e-2m72tyo556.js +1 -0
- actshield/static/_next/static/chunks/3fntmmi971322.js +1 -0
- actshield/static/_next/static/chunks/3fzw-g0hycl2c.js +1 -0
- actshield/static/_next/static/chunks/3lopq3lhmcjxg.js +129 -0
- actshield/static/_next/static/chunks/3mpe2jejel0v2.css +1 -0
- actshield/static/_next/static/chunks/3t8o4f2lzr2sf.js +1 -0
- actshield/static/_next/static/chunks/3y-v1neyllne5.js +1 -0
- actshield/static/_next/static/chunks/43oiwy8mx6cs1.js +1 -0
- actshield/static/_next/static/chunks/turbopack-23wzuebcy_lxm.js +1 -0
- actshield/static/_next/static/media/1bffadaabf893a1e-s.3-6t-g6q0vh0a.woff2 +0 -0
- actshield/static/_next/static/media/2bbe8d2671613f1f-s.0k62hbripvv8p.woff2 +0 -0
- actshield/static/_next/static/media/2c55a0e60120577a-s.0-dom-5bn10r2.woff2 +0 -0
- actshield/static/_next/static/media/5476f68d60460930-s.2uwcyprjm3xu3.woff2 +0 -0
- actshield/static/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2 +0 -0
- actshield/static/_next/static/media/9c72aa0f40e4eef8-s.1y4-pdgsjb-pw.woff2 +0 -0
- actshield/static/_next/static/media/ad66f9afd8947f86-s.3lvt2whj97whp.woff2 +0 -0
- actshield/static/_next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- actshield/static/_not-found/__next._full.txt +17 -0
- actshield/static/_not-found/__next._not-found/__PAGE__.txt +23 -0
- actshield/static/_not-found/__next._tree.txt +3 -0
- actshield/static/_not-found.html +1 -0
- actshield/static/_not-found.txt +17 -0
- actshield/static/access/matrix/__next._full.txt +24 -0
- actshield/static/access/matrix/__next._tree.txt +4 -0
- actshield/static/access/matrix/__next.access/matrix/__PAGE__.txt +30 -0
- actshield/static/access/matrix.html +1 -0
- actshield/static/access/matrix.txt +24 -0
- actshield/static/activity/__next._full.txt +22 -0
- actshield/static/activity/__next._tree.txt +4 -0
- actshield/static/activity/__next.activity/__PAGE__.txt +27 -0
- actshield/static/activity.html +1 -0
- actshield/static/activity.txt +22 -0
- actshield/static/agents/__next._full.txt +22 -0
- actshield/static/agents/__next._tree.txt +4 -0
- actshield/static/agents/__next.agents/__PAGE__.txt +27 -0
- actshield/static/agents.html +1 -0
- actshield/static/agents.txt +22 -0
- actshield/static/ai-secura/__next._full.txt +22 -0
- actshield/static/ai-secura/__next._tree.txt +4 -0
- actshield/static/ai-secura/__next.ai-secura/__PAGE__.txt +27 -0
- actshield/static/ai-secura.html +1 -0
- actshield/static/ai-secura.txt +22 -0
- actshield/static/api/__next._full.txt +22 -0
- actshield/static/api/__next._tree.txt +4 -0
- actshield/static/api/__next.api/__PAGE__.txt +27 -0
- actshield/static/api.html +15 -0
- actshield/static/api.txt +22 -0
- actshield/static/apiris/__next._full.txt +22 -0
- actshield/static/apiris/__next._tree.txt +4 -0
- actshield/static/apiris/__next.apiris/__PAGE__.txt +27 -0
- actshield/static/apiris.html +1 -0
- actshield/static/apiris.txt +22 -0
- actshield/static/attack-graph/__next._full.txt +23 -0
- actshield/static/attack-graph/__next._tree.txt +5 -0
- actshield/static/attack-graph/__next.attack-graph/__PAGE__.txt +28 -0
- actshield/static/attack-graph.html +2 -0
- actshield/static/attack-graph.txt +23 -0
- actshield/static/campaigns/__next._full.txt +22 -0
- actshield/static/campaigns/__next._tree.txt +4 -0
- actshield/static/campaigns/__next.campaigns/__PAGE__.txt +27 -0
- actshield/static/campaigns.html +1 -0
- actshield/static/campaigns.txt +22 -0
- actshield/static/context/__next._full.txt +22 -0
- actshield/static/context/__next._tree.txt +4 -0
- actshield/static/context/__next.context/__PAGE__.txt +27 -0
- actshield/static/context.html +1 -0
- actshield/static/context.txt +22 -0
- actshield/static/dashboard/__next._full.txt +22 -0
- actshield/static/dashboard/__next._tree.txt +4 -0
- actshield/static/dashboard/__next.dashboard/__PAGE__.txt +27 -0
- actshield/static/dashboard.html +1 -0
- actshield/static/dashboard.txt +22 -0
- actshield/static/delegations/__next._full.txt +22 -0
- actshield/static/delegations/__next._tree.txt +4 -0
- actshield/static/delegations/__next.delegations/__PAGE__.txt +27 -0
- actshield/static/delegations.html +1 -0
- actshield/static/delegations.txt +22 -0
- actshield/static/demo/__next._full.txt +22 -0
- actshield/static/demo/__next._tree.txt +4 -0
- actshield/static/demo/__next.demo/__PAGE__.txt +27 -0
- actshield/static/demo.html +1 -0
- actshield/static/demo.txt +22 -0
- actshield/static/drift/__next._full.txt +22 -0
- actshield/static/drift/__next._tree.txt +4 -0
- actshield/static/drift/__next.drift/__PAGE__.txt +27 -0
- actshield/static/drift.html +1 -0
- actshield/static/drift.txt +22 -0
- actshield/static/favicon.ico +0 -0
- actshield/static/file.svg +1 -0
- actshield/static/forensics/__next._full.txt +22 -0
- actshield/static/forensics/__next._tree.txt +4 -0
- actshield/static/forensics/__next.forensics/__PAGE__.txt +27 -0
- actshield/static/forensics.html +1 -0
- actshield/static/forensics.txt +22 -0
- actshield/static/globe.svg +1 -0
- actshield/static/incidents/__next._full.txt +22 -0
- actshield/static/incidents/__next._tree.txt +4 -0
- actshield/static/incidents/__next.incidents/__PAGE__.txt +27 -0
- actshield/static/incidents.html +1 -0
- actshield/static/incidents.txt +22 -0
- actshield/static/index.html +3 -0
- actshield/static/index.txt +20 -0
- actshield/static/logo.png +0 -0
- actshield/static/metrics/__next._full.txt +22 -0
- actshield/static/metrics/__next._tree.txt +4 -0
- actshield/static/metrics/__next.metrics/__PAGE__.txt +27 -0
- actshield/static/metrics.html +1 -0
- actshield/static/metrics.txt +22 -0
- actshield/static/next.svg +1 -0
- actshield/static/offensive/__next._full.txt +22 -0
- actshield/static/offensive/__next._tree.txt +4 -0
- actshield/static/offensive/__next.offensive/__PAGE__.txt +27 -0
- actshield/static/offensive.html +1 -0
- actshield/static/offensive.txt +22 -0
- actshield/static/policies/__next._full.txt +22 -0
- actshield/static/policies/__next._tree.txt +4 -0
- actshield/static/policies/__next.policies/__PAGE__.txt +27 -0
- actshield/static/policies.html +1 -0
- actshield/static/policies.txt +22 -0
- actshield/static/posture/__next._full.txt +22 -0
- actshield/static/posture/__next._tree.txt +4 -0
- actshield/static/posture/__next.posture/__PAGE__.txt +27 -0
- actshield/static/posture.html +1 -0
- actshield/static/posture.txt +22 -0
- actshield/static/regressions/__next._full.txt +22 -0
- actshield/static/regressions/__next._tree.txt +4 -0
- actshield/static/regressions/__next.regressions/__PAGE__.txt +27 -0
- actshield/static/regressions.html +1 -0
- actshield/static/regressions.txt +22 -0
- actshield/static/reports/__next._full.txt +22 -0
- actshield/static/reports/__next._tree.txt +4 -0
- actshield/static/reports/__next.reports/__PAGE__.txt +27 -0
- actshield/static/reports.html +1 -0
- actshield/static/reports.txt +22 -0
- actshield/static/resources/__next._full.txt +22 -0
- actshield/static/resources/__next._tree.txt +4 -0
- actshield/static/resources/__next.resources/__PAGE__.txt +27 -0
- actshield/static/resources.html +1 -0
- actshield/static/resources.txt +22 -0
- actshield/static/security-gates/__next._full.txt +22 -0
- actshield/static/security-gates/__next._tree.txt +4 -0
- actshield/static/security-gates/__next.security-gates/__PAGE__.txt +27 -0
- actshield/static/security-gates.html +1 -0
- actshield/static/security-gates.txt +22 -0
- actshield/static/settings/__next._full.txt +22 -0
- actshield/static/settings/__next._tree.txt +4 -0
- actshield/static/settings/__next.settings/__PAGE__.txt +27 -0
- actshield/static/settings.html +1 -0
- actshield/static/settings.txt +22 -0
- actshield/static/tasks/__next._full.txt +22 -0
- actshield/static/tasks/__next._tree.txt +4 -0
- actshield/static/tasks/__next.tasks/__PAGE__.txt +27 -0
- actshield/static/tasks.html +1 -0
- actshield/static/tasks.txt +22 -0
- actshield/static/threats/__next._full.txt +22 -0
- actshield/static/threats/__next._tree.txt +4 -0
- actshield/static/threats/__next.threats/__PAGE__.txt +27 -0
- actshield/static/threats.html +1 -0
- actshield/static/threats.txt +22 -0
- actshield/static/tools/__next._full.txt +22 -0
- actshield/static/tools/__next._tree.txt +4 -0
- actshield/static/tools/__next.tools/__PAGE__.txt +27 -0
- actshield/static/tools.html +1 -0
- actshield/static/tools.txt +22 -0
- actshield/static/traces/__next._full.txt +22 -0
- actshield/static/traces/__next._tree.txt +4 -0
- actshield/static/traces/__next.traces/__PAGE__.txt +27 -0
- actshield/static/traces.html +1 -0
- actshield/static/traces.txt +22 -0
- actshield/static/vercel.svg +1 -0
- actshield/static/window.svg +1 -0
- actshield/tasks/__init__.py +6 -0
- actshield/tasks/task.py +143 -0
- actshield/threatmodel/__init__.py +70 -0
- actshield/threatmodel/actors.py +210 -0
- actshield/threatmodel/analyzer.py +131 -0
- actshield/threatmodel/assets.py +167 -0
- actshield/threatmodel/boundaries.py +165 -0
- actshield/threatmodel/exporters.py +49 -0
- actshield/threatmodel/mitigations.py +209 -0
- actshield/threatmodel/models.py +80 -0
- actshield/threatmodel/report.py +341 -0
- actshield/threatmodel/scenarios.py +212 -0
- actshield/threatmodel/scoring.py +66 -0
- actshield/threatmodel/threats.py +387 -0
- actshield/tools/__init__.py +20 -0
- actshield/tools/interceptor.py +256 -0
- actshield/tools/tool.py +71 -0
- actshield/tracing/__init__.py +38 -0
- actshield/tracing/correlation.py +173 -0
- actshield/tracing/events.py +75 -0
- actshield/tracing/tracer.py +305 -0
- actshield/watch.py +129 -0
- actshield-0.9.0.dist-info/METADATA +57 -0
- actshield-0.9.0.dist-info/RECORD +366 -0
- actshield-0.9.0.dist-info/WHEEL +5 -0
- actshield-0.9.0.dist-info/entry_points.txt +4 -0
- actshield-0.9.0.dist-info/top_level.txt +2 -0
- agentguard/__init__.py +39 -0
- agentguard/__main__.py +8 -0
- agentguard/py.typed +1 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""OllamaAISecuraAdapter — Real local LLM-backed AI Secura reasoning via Ollama.
|
|
2
|
+
|
|
3
|
+
This adapter calls a locally-running Ollama instance (default: qwen2.5:7b)
|
|
4
|
+
with the AI Secura system prompt, structured security evidence packet,
|
|
5
|
+
and deterministically-selected few-shot examples.
|
|
6
|
+
|
|
7
|
+
CRITICAL INVARIANT:
|
|
8
|
+
- AI Secura = reasoning ONLY.
|
|
9
|
+
- PolicyEvaluator = sole enforcement authority.
|
|
10
|
+
- Ollama failure -> ai_unavailable=True, deterministic policy continues.
|
|
11
|
+
- Malformed output -> one repair attempt -> ai_unavailable on second failure.
|
|
12
|
+
- AI Secura NEVER becomes the authorization authority.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import pathlib
|
|
18
|
+
import time
|
|
19
|
+
import urllib.request
|
|
20
|
+
import urllib.error
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from actshield.integrations.ai_secura import SecurityContext, SecurityAnalysis
|
|
24
|
+
from actshield.integrations.ai_secura_analysis import (
|
|
25
|
+
AISecuraAnalysis,
|
|
26
|
+
ThreatSeverity,
|
|
27
|
+
IntentAlignment,
|
|
28
|
+
AIRecommendation,
|
|
29
|
+
AttackTechnique,
|
|
30
|
+
)
|
|
31
|
+
from actshield.integrations.security_packet import build_security_packet
|
|
32
|
+
from actshield.integrations.few_shot import select_few_shot_examples, format_examples_for_prompt
|
|
33
|
+
from actshield.integrations.apiris import APIAnalysis
|
|
34
|
+
from actshield.llm_config import LLMConfig
|
|
35
|
+
from actshield.risk.models import RiskLevel, RiskSignal
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger("actshield.ollama")
|
|
38
|
+
|
|
39
|
+
_PROMPT_DIR = pathlib.Path(__file__).parent / "prompts"
|
|
40
|
+
_SYSTEM_PROMPT_PATH = _PROMPT_DIR / "ai_secura_system.txt"
|
|
41
|
+
|
|
42
|
+
# Repair prompt sent on malformed JSON
|
|
43
|
+
_REPAIR_PROMPT = (
|
|
44
|
+
"Your previous response was not valid JSON. "
|
|
45
|
+
"Return ONLY a single valid JSON object matching the AI Secura output schema. "
|
|
46
|
+
"No markdown, no code fences, no explanatory text. Just the JSON object."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_system_prompt() -> str:
|
|
51
|
+
"""Load the version-controlled AI Secura system prompt."""
|
|
52
|
+
try:
|
|
53
|
+
return _SYSTEM_PROMPT_PATH.read_text(encoding="utf-8")
|
|
54
|
+
except FileNotFoundError:
|
|
55
|
+
logger.warning("AI Secura system prompt not found at %s, using minimal prompt", _SYSTEM_PROMPT_PATH)
|
|
56
|
+
return (
|
|
57
|
+
"You are AI Secura, an AI security reasoning engine. "
|
|
58
|
+
"Analyze the security evidence and output ONLY valid JSON. "
|
|
59
|
+
"You are NOT the enforcement authority — PolicyEvaluator enforces."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _make_unavailable_analysis(reason: str, latency_ms: float = 0.0) -> AISecuraAnalysis:
|
|
64
|
+
"""Return a safe fallback analysis when Ollama is unavailable.
|
|
65
|
+
|
|
66
|
+
CRITICAL: ai_unavailable=True means deterministic policy is sole enforcer.
|
|
67
|
+
This NEVER produces an ALLOW recommendation.
|
|
68
|
+
"""
|
|
69
|
+
return AISecuraAnalysis(
|
|
70
|
+
threat_type="unknown",
|
|
71
|
+
attack_category="unknown",
|
|
72
|
+
attack_technique=AttackTechnique.UNKNOWN,
|
|
73
|
+
severity=ThreatSeverity.UNKNOWN,
|
|
74
|
+
confidence=0.0,
|
|
75
|
+
intent_alignment=IntentAlignment.UNKNOWN,
|
|
76
|
+
ai_recommendation=AIRecommendation.UNKNOWN,
|
|
77
|
+
analysis=f"AI Secura analysis unavailable: {reason}",
|
|
78
|
+
impact="Cannot assess — AI reasoning engine unavailable. Deterministic policy remains sole enforcer.",
|
|
79
|
+
provider="ollama",
|
|
80
|
+
model="unavailable",
|
|
81
|
+
latency_ms=latency_ms,
|
|
82
|
+
ai_unavailable=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _strip_markdown_fences(text: str) -> str:
|
|
87
|
+
"""Strip markdown code fences if the model wraps JSON in them."""
|
|
88
|
+
text = text.strip()
|
|
89
|
+
if text.startswith("```json"):
|
|
90
|
+
text = text[7:]
|
|
91
|
+
elif text.startswith("```"):
|
|
92
|
+
text = text[3:]
|
|
93
|
+
if text.endswith("```"):
|
|
94
|
+
text = text[:-3]
|
|
95
|
+
return text.strip()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _parse_analysis(raw: str) -> AISecuraAnalysis:
|
|
99
|
+
"""Parse raw model output into AISecuraAnalysis with strict validation."""
|
|
100
|
+
cleaned = _strip_markdown_fences(raw)
|
|
101
|
+
data = json.loads(cleaned)
|
|
102
|
+
|
|
103
|
+
# Map string enums safely
|
|
104
|
+
if "severity" in data:
|
|
105
|
+
try:
|
|
106
|
+
data["severity"] = ThreatSeverity(data["severity"])
|
|
107
|
+
except ValueError:
|
|
108
|
+
data["severity"] = ThreatSeverity.UNKNOWN
|
|
109
|
+
|
|
110
|
+
if "intent_alignment" in data:
|
|
111
|
+
try:
|
|
112
|
+
data["intent_alignment"] = IntentAlignment(data["intent_alignment"])
|
|
113
|
+
except ValueError:
|
|
114
|
+
data["intent_alignment"] = IntentAlignment.UNKNOWN
|
|
115
|
+
|
|
116
|
+
if "ai_recommendation" in data:
|
|
117
|
+
try:
|
|
118
|
+
data["ai_recommendation"] = AIRecommendation(data["ai_recommendation"])
|
|
119
|
+
except ValueError:
|
|
120
|
+
data["ai_recommendation"] = AIRecommendation.UNKNOWN
|
|
121
|
+
|
|
122
|
+
if "attack_technique" in data:
|
|
123
|
+
try:
|
|
124
|
+
data["attack_technique"] = AttackTechnique(data["attack_technique"])
|
|
125
|
+
except ValueError:
|
|
126
|
+
data["attack_technique"] = AttackTechnique.UNKNOWN
|
|
127
|
+
|
|
128
|
+
return AISecuraAnalysis(**data)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class OllamaAISecuraAdapter:
|
|
132
|
+
"""Real Ollama-backed AI Secura reasoning adapter.
|
|
133
|
+
|
|
134
|
+
Sends structured security evidence to a locally-running Ollama model
|
|
135
|
+
and returns a validated AISecuraAnalysis.
|
|
136
|
+
|
|
137
|
+
Architecture:
|
|
138
|
+
AISecuraClient
|
|
139
|
+
|
|
|
140
|
+
+-- OllamaAISecuraAdapter (this — real LLM reasoning)
|
|
141
|
+
|
|
|
142
|
+
+-- LocalAISecuraAdapter (deterministic heuristic, tests/CI)
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, config: Optional[LLMConfig] = None) -> None:
|
|
146
|
+
self.config = config or LLMConfig.from_env()
|
|
147
|
+
self._system_prompt = _load_system_prompt()
|
|
148
|
+
|
|
149
|
+
def analyze(self, context: SecurityContext, apiris_result: Optional[APIAnalysis] = None) -> AISecuraAnalysis:
|
|
150
|
+
"""Run AI Secura reasoning via Ollama.
|
|
151
|
+
|
|
152
|
+
1. Build structured security packet from ActShield objects.
|
|
153
|
+
2. Select relevant few-shot examples.
|
|
154
|
+
3. Call Ollama with system prompt + examples + packet.
|
|
155
|
+
4. Validate JSON output via Pydantic.
|
|
156
|
+
5. On parse failure: one repair attempt.
|
|
157
|
+
6. On Ollama error/timeout: return ai_unavailable=True.
|
|
158
|
+
"""
|
|
159
|
+
start = time.monotonic()
|
|
160
|
+
|
|
161
|
+
# 1. Build security packet
|
|
162
|
+
packet = build_security_packet(context, apiris_result)
|
|
163
|
+
|
|
164
|
+
# Offline / local provider routing: fast deterministic analysis
|
|
165
|
+
if not self.config.is_ollama:
|
|
166
|
+
return self._analyze_local_heuristic(context, apiris_result, start, packet)
|
|
167
|
+
|
|
168
|
+
# 2. Select few-shot examples
|
|
169
|
+
examples = select_few_shot_examples(packet)
|
|
170
|
+
examples_block = format_examples_for_prompt(examples)
|
|
171
|
+
|
|
172
|
+
# 3. Construct full prompt
|
|
173
|
+
full_system = self._system_prompt
|
|
174
|
+
if examples_block:
|
|
175
|
+
full_system += examples_block
|
|
176
|
+
|
|
177
|
+
user_prompt = (
|
|
178
|
+
"Analyze the following ActShield security evidence packet and "
|
|
179
|
+
"return your analysis as a single valid JSON object.\n\n"
|
|
180
|
+
"EVIDENCE PACKET:\n"
|
|
181
|
+
+ json.dumps(packet, indent=2, default=str)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# 4. Call Ollama
|
|
185
|
+
try:
|
|
186
|
+
raw_response = self._call_ollama(full_system, user_prompt)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
189
|
+
logger.error("Ollama call failed: %s", e)
|
|
190
|
+
return _make_unavailable_analysis(f"Ollama error: {e}", elapsed)
|
|
191
|
+
|
|
192
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
193
|
+
|
|
194
|
+
# 5. Parse response
|
|
195
|
+
try:
|
|
196
|
+
analysis = _parse_analysis(raw_response)
|
|
197
|
+
analysis.provider = "ollama"
|
|
198
|
+
analysis.model = self.config.model
|
|
199
|
+
analysis.latency_ms = elapsed
|
|
200
|
+
analysis.raw_response = raw_response
|
|
201
|
+
return analysis
|
|
202
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
203
|
+
logger.warning("First parse failed (%s), attempting repair", e)
|
|
204
|
+
|
|
205
|
+
# 6. One repair attempt
|
|
206
|
+
if self.config.repair_on_invalid_json:
|
|
207
|
+
try:
|
|
208
|
+
repair_response = self._call_ollama(
|
|
209
|
+
self._system_prompt,
|
|
210
|
+
_REPAIR_PROMPT + "\n\nYour previous output:\n" + raw_response[:2000]
|
|
211
|
+
)
|
|
212
|
+
analysis = _parse_analysis(repair_response)
|
|
213
|
+
analysis.provider = "ollama"
|
|
214
|
+
analysis.model = self.config.model
|
|
215
|
+
analysis.latency_ms = (time.monotonic() - start) * 1000
|
|
216
|
+
analysis.json_repaired = True
|
|
217
|
+
analysis.raw_response = repair_response
|
|
218
|
+
return analysis
|
|
219
|
+
except Exception as repair_err:
|
|
220
|
+
logger.error("Repair attempt failed: %s", repair_err)
|
|
221
|
+
|
|
222
|
+
# 7. Both attempts failed — return unavailable
|
|
223
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
224
|
+
result = _make_unavailable_analysis(
|
|
225
|
+
"Malformed model output after repair attempt", elapsed
|
|
226
|
+
)
|
|
227
|
+
result.raw_response = raw_response[:500]
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
def _call_ollama(self, system: str, prompt: str) -> str:
|
|
231
|
+
"""Make HTTP request to Ollama generate API using urllib (zero deps)."""
|
|
232
|
+
payload = json.dumps({
|
|
233
|
+
"model": self.config.model,
|
|
234
|
+
"system": system,
|
|
235
|
+
"prompt": prompt,
|
|
236
|
+
"stream": False,
|
|
237
|
+
"options": {
|
|
238
|
+
"temperature": self.config.temperature,
|
|
239
|
+
"num_predict": self.config.max_tokens,
|
|
240
|
+
"num_ctx": self.config.num_ctx,
|
|
241
|
+
},
|
|
242
|
+
"format": "json",
|
|
243
|
+
}).encode("utf-8")
|
|
244
|
+
|
|
245
|
+
req = urllib.request.Request(
|
|
246
|
+
self.config.ollama_generate_url,
|
|
247
|
+
data=payload,
|
|
248
|
+
headers={"Content-Type": "application/json"},
|
|
249
|
+
method="POST",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
with urllib.request.urlopen(req, timeout=self.config.timeout_seconds) as resp:
|
|
253
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
254
|
+
return body.get("response", "")
|
|
255
|
+
|
|
256
|
+
def _analyze_local_heuristic(
|
|
257
|
+
self,
|
|
258
|
+
context: SecurityContext,
|
|
259
|
+
apiris_result: Optional[APIAnalysis],
|
|
260
|
+
start_time: float,
|
|
261
|
+
packet: Dict[str, Any],
|
|
262
|
+
) -> AISecuraAnalysis:
|
|
263
|
+
"""Deterministic rule-based analysis for offline/local mode and test suites."""
|
|
264
|
+
from actshield.integrations.ai_secura_analysis import EvidenceTriad, AuthorityAnalysis, TaintAnalysis
|
|
265
|
+
|
|
266
|
+
is_tainted = any(t in ("TAINTED", "UNTRUSTED") for t in context.taint_states)
|
|
267
|
+
is_sensitive = str(context.target_resource_sensitivity).upper() in ("CRITICAL", "HIGH", "SensitivityLevel.CRITICAL", "SensitivityLevel.HIGH")
|
|
268
|
+
agent_trust = str(context.acting_agent_trust).lower()
|
|
269
|
+
|
|
270
|
+
# Check for injection / malicious keywords in context data, arguments, and intent
|
|
271
|
+
prov_text = (str(packet.get("context_provenance", "")) + " " + str(packet.get("tool", {}).get("arguments", ""))).lower()
|
|
272
|
+
intent_text = str(context.task_intent or "").lower()
|
|
273
|
+
tool_name = str(context.tool_name or "").lower()
|
|
274
|
+
|
|
275
|
+
has_injection = any(k in prov_text for k in [
|
|
276
|
+
"system directive", "ignore all previous", "override", "drop table",
|
|
277
|
+
"base64", "ciso", "admin", "secret_instruction", "dump_env", "steal",
|
|
278
|
+
"system prompt", "phishing", "leak.xyz", "exfiltrate",
|
|
279
|
+
])
|
|
280
|
+
|
|
281
|
+
# Authority and intent drift detection
|
|
282
|
+
is_authority_violation = False
|
|
283
|
+
if agent_trust in ("low", "untrusted") and is_sensitive:
|
|
284
|
+
is_authority_violation = True
|
|
285
|
+
if "read-only" in intent_text and ("delete" in tool_name or "write" in tool_name or "alter" in tool_name):
|
|
286
|
+
is_authority_violation = True
|
|
287
|
+
if "css" in intent_text and ("alter_table" in tool_name or "db" in tool_name):
|
|
288
|
+
is_authority_violation = True
|
|
289
|
+
if "budget" in intent_text and "provision" in tool_name:
|
|
290
|
+
is_authority_violation = True
|
|
291
|
+
if "tenant-a" in intent_text and "tenant-b" in prov_text:
|
|
292
|
+
is_authority_violation = True
|
|
293
|
+
if "rate limit" in intent_text and "create_api_key" in tool_name:
|
|
294
|
+
is_authority_violation = True
|
|
295
|
+
if "team-alpha" in intent_text and "team-beta" in prov_text:
|
|
296
|
+
is_authority_violation = True
|
|
297
|
+
|
|
298
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
299
|
+
|
|
300
|
+
if is_tainted and (is_sensitive or has_injection):
|
|
301
|
+
threat_type = "indirect_prompt_injection" if "external_mcp" in prov_text or "rag" in prov_text else "prompt_injection"
|
|
302
|
+
return AISecuraAnalysis(
|
|
303
|
+
threat_type=threat_type,
|
|
304
|
+
attack_category="injection",
|
|
305
|
+
attack_technique=AttackTechnique.INDIRECT_PROMPT_INJECTION if "mcp" in prov_text else AttackTechnique.PROMPT_INJECTION,
|
|
306
|
+
severity=ThreatSeverity.CRITICAL if is_sensitive else ThreatSeverity.HIGH,
|
|
307
|
+
confidence=0.95,
|
|
308
|
+
evidence=EvidenceTriad(
|
|
309
|
+
observed=["Tainted context propagating into sensitive sink", f"Target tool: {context.tool_name}"],
|
|
310
|
+
inferred=["Potential prompt injection or unauthorized access detected"],
|
|
311
|
+
unknown=[],
|
|
312
|
+
),
|
|
313
|
+
intent_alignment=IntentAlignment.VIOLATED,
|
|
314
|
+
authority_analysis=AuthorityAnalysis(
|
|
315
|
+
acting_agent=context.acting_agent_name,
|
|
316
|
+
authority_violated=True,
|
|
317
|
+
),
|
|
318
|
+
taint_analysis=TaintAnalysis(is_tainted=True),
|
|
319
|
+
ai_recommendation=AIRecommendation.BLOCK,
|
|
320
|
+
analysis=f"Deterministic local analysis: Tainted context detected for tool {context.tool_name}.",
|
|
321
|
+
provider="local",
|
|
322
|
+
model="deterministic_heuristic",
|
|
323
|
+
latency_ms=elapsed,
|
|
324
|
+
)
|
|
325
|
+
elif is_authority_violation:
|
|
326
|
+
return AISecuraAnalysis(
|
|
327
|
+
threat_type="authority_escalation",
|
|
328
|
+
attack_category="authority",
|
|
329
|
+
attack_technique=AttackTechnique.PRIVILEGE_ESCALATION,
|
|
330
|
+
severity=ThreatSeverity.CRITICAL if is_sensitive else ThreatSeverity.HIGH,
|
|
331
|
+
confidence=0.95,
|
|
332
|
+
evidence=EvidenceTriad(
|
|
333
|
+
observed=[f"Agent '{context.acting_agent_name}' ({agent_trust} trust) attempted out-of-scope tool: {context.tool_name}"],
|
|
334
|
+
inferred=["Task intent misalignment or authority boundary violation"],
|
|
335
|
+
unknown=[],
|
|
336
|
+
),
|
|
337
|
+
intent_alignment=IntentAlignment.VIOLATED,
|
|
338
|
+
authority_analysis=AuthorityAnalysis(acting_agent=context.acting_agent_name, authority_violated=True),
|
|
339
|
+
taint_analysis=TaintAnalysis(is_tainted=is_tainted),
|
|
340
|
+
ai_recommendation=AIRecommendation.BLOCK,
|
|
341
|
+
analysis=f"Deterministic local analysis: Authority/Intent violation for {context.tool_name}.",
|
|
342
|
+
provider="local",
|
|
343
|
+
model="deterministic_heuristic",
|
|
344
|
+
latency_ms=elapsed,
|
|
345
|
+
)
|
|
346
|
+
elif is_sensitive and not is_tainted:
|
|
347
|
+
return AISecuraAnalysis(
|
|
348
|
+
threat_type="none",
|
|
349
|
+
attack_category="none",
|
|
350
|
+
severity=ThreatSeverity.MEDIUM,
|
|
351
|
+
confidence=0.90,
|
|
352
|
+
evidence=EvidenceTriad(
|
|
353
|
+
observed=["Clean context accessing sensitive resource", f"Tool: {context.tool_name}"],
|
|
354
|
+
inferred=["Authorized access to protected resource"],
|
|
355
|
+
unknown=[],
|
|
356
|
+
),
|
|
357
|
+
intent_alignment=IntentAlignment.ALIGNED,
|
|
358
|
+
authority_analysis=AuthorityAnalysis(acting_agent=context.acting_agent_name, authority_violated=False),
|
|
359
|
+
taint_analysis=TaintAnalysis(is_tainted=False),
|
|
360
|
+
ai_recommendation=AIRecommendation.ALLOW,
|
|
361
|
+
analysis="Deterministic local analysis: Authorized sensitive operation with clean context.",
|
|
362
|
+
provider="local",
|
|
363
|
+
model="deterministic_heuristic",
|
|
364
|
+
latency_ms=elapsed,
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
return AISecuraAnalysis(
|
|
368
|
+
threat_type="none",
|
|
369
|
+
attack_category="none",
|
|
370
|
+
severity=ThreatSeverity.LOW,
|
|
371
|
+
confidence=0.95,
|
|
372
|
+
evidence=EvidenceTriad(
|
|
373
|
+
observed=["Clean context, low sensitivity operation"],
|
|
374
|
+
inferred=["Normal benign execution"],
|
|
375
|
+
unknown=[],
|
|
376
|
+
),
|
|
377
|
+
intent_alignment=IntentAlignment.ALIGNED,
|
|
378
|
+
authority_analysis=AuthorityAnalysis(acting_agent=context.acting_agent_name, authority_violated=False),
|
|
379
|
+
taint_analysis=TaintAnalysis(is_tainted=False),
|
|
380
|
+
ai_recommendation=AIRecommendation.ALLOW,
|
|
381
|
+
analysis="Deterministic local analysis: Benign operation.",
|
|
382
|
+
provider="local",
|
|
383
|
+
model="deterministic_heuristic",
|
|
384
|
+
latency_ms=elapsed,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def health_check(self) -> Dict[str, Any]:
|
|
388
|
+
"""Verify Ollama connectivity, model availability, and response validity.
|
|
389
|
+
|
|
390
|
+
Returns a dict with status fields:
|
|
391
|
+
ollama_reachable: bool
|
|
392
|
+
model_exists: bool
|
|
393
|
+
model_responds: bool
|
|
394
|
+
json_valid: bool
|
|
395
|
+
schema_valid: bool
|
|
396
|
+
details: str
|
|
397
|
+
"""
|
|
398
|
+
result: Dict[str, Any] = {
|
|
399
|
+
"ollama_reachable": False,
|
|
400
|
+
"model_exists": False,
|
|
401
|
+
"model_responds": False,
|
|
402
|
+
"json_valid": False,
|
|
403
|
+
"schema_valid": False,
|
|
404
|
+
"provider": "ollama",
|
|
405
|
+
"model": self.config.model,
|
|
406
|
+
"base_url": self.config.ollama_base_url,
|
|
407
|
+
"details": "",
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if not self.config.is_ollama:
|
|
411
|
+
result["details"] = "Provider is local heuristic (offline mode)"
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
# 1. Check Ollama reachable
|
|
415
|
+
try:
|
|
416
|
+
req = urllib.request.Request(self.config.ollama_tags_url)
|
|
417
|
+
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
418
|
+
tags_data = json.loads(resp.read().decode("utf-8"))
|
|
419
|
+
result["ollama_reachable"] = True
|
|
420
|
+
except Exception as e:
|
|
421
|
+
result["details"] = f"Ollama not reachable: {e}"
|
|
422
|
+
return result
|
|
423
|
+
|
|
424
|
+
# 2. Check model exists
|
|
425
|
+
models = [m.get("name", "") for m in tags_data.get("models", [])]
|
|
426
|
+
model_found = any(
|
|
427
|
+
self.config.model in m or m.startswith(self.config.model.split(":")[0])
|
|
428
|
+
for m in models
|
|
429
|
+
)
|
|
430
|
+
result["model_exists"] = model_found
|
|
431
|
+
if not model_found:
|
|
432
|
+
result["details"] = f"Model '{self.config.model}' not found. Available: {models}"
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
# 3. Quick test model response
|
|
436
|
+
try:
|
|
437
|
+
payload = json.dumps({
|
|
438
|
+
"model": self.config.model,
|
|
439
|
+
"prompt": "Return JSON: {\"threat_type\": \"none\", \"severity\": \"LOW\", \"ai_recommendation\": \"ALLOW\"}",
|
|
440
|
+
"stream": False,
|
|
441
|
+
"options": {"num_predict": 64, "temperature": 0.0},
|
|
442
|
+
"format": "json",
|
|
443
|
+
}).encode("utf-8")
|
|
444
|
+
req = urllib.request.Request(
|
|
445
|
+
self.config.ollama_generate_url,
|
|
446
|
+
data=payload,
|
|
447
|
+
headers={"Content-Type": "application/json"},
|
|
448
|
+
method="POST",
|
|
449
|
+
)
|
|
450
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
451
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
452
|
+
raw = body.get("response", "")
|
|
453
|
+
result["model_responds"] = True
|
|
454
|
+
except Exception as e:
|
|
455
|
+
result["details"] = f"Model did not respond: {e}"
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
# 4. Check JSON validity
|
|
459
|
+
try:
|
|
460
|
+
cleaned = _strip_markdown_fences(raw)
|
|
461
|
+
data = json.loads(cleaned)
|
|
462
|
+
result["json_valid"] = True
|
|
463
|
+
except json.JSONDecodeError as e:
|
|
464
|
+
result["details"] = f"Response is not valid JSON: {e}"
|
|
465
|
+
return result
|
|
466
|
+
|
|
467
|
+
# 5. Check schema validity
|
|
468
|
+
try:
|
|
469
|
+
_parse_analysis(raw)
|
|
470
|
+
result["schema_valid"] = True
|
|
471
|
+
result["details"] = "All checks passed"
|
|
472
|
+
except Exception as e:
|
|
473
|
+
result["details"] = f"Response does not match AISecuraAnalysis schema: {e}"
|
|
474
|
+
|
|
475
|
+
return result
|
|
476
|
+
|
|
477
|
+
def analyze_legacy(self, context: SecurityContext) -> SecurityAnalysis:
|
|
478
|
+
"""Backward-compatible wrapper that returns legacy SecurityAnalysis.
|
|
479
|
+
|
|
480
|
+
Calls the full Ollama analysis and maps the rich AISecuraAnalysis
|
|
481
|
+
back to the existing SecurityAnalysis format used by PolicyEvaluator.
|
|
482
|
+
"""
|
|
483
|
+
rich = self.analyze(context)
|
|
484
|
+
return _map_to_legacy(rich, context)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _map_to_legacy(rich: AISecuraAnalysis, context: SecurityContext) -> SecurityAnalysis:
|
|
488
|
+
"""Map AISecuraAnalysis to legacy SecurityAnalysis for backward compat."""
|
|
489
|
+
import uuid
|
|
490
|
+
|
|
491
|
+
signals: List[RiskSignal] = []
|
|
492
|
+
attack_indicators: List[str] = rich.observed_indicators.copy()
|
|
493
|
+
prompt_injections: List[str] = []
|
|
494
|
+
|
|
495
|
+
# Map severity to risk
|
|
496
|
+
severity_to_risk = {
|
|
497
|
+
ThreatSeverity.CRITICAL: RiskLevel.CRITICAL,
|
|
498
|
+
ThreatSeverity.HIGH: RiskLevel.HIGH,
|
|
499
|
+
ThreatSeverity.MEDIUM: RiskLevel.MEDIUM,
|
|
500
|
+
ThreatSeverity.LOW: RiskLevel.LOW,
|
|
501
|
+
ThreatSeverity.UNKNOWN: RiskLevel.LOW,
|
|
502
|
+
}
|
|
503
|
+
risk_level = severity_to_risk.get(rich.severity, RiskLevel.LOW)
|
|
504
|
+
|
|
505
|
+
# Extract injection indicators
|
|
506
|
+
if rich.attack_technique in (
|
|
507
|
+
AttackTechnique.PROMPT_INJECTION,
|
|
508
|
+
AttackTechnique.INDIRECT_PROMPT_INJECTION,
|
|
509
|
+
AttackTechnique.INSTRUCTION_HIJACKING,
|
|
510
|
+
):
|
|
511
|
+
prompt_injections.append(f"AI_SECURA_OLLAMA_{rich.attack_technique.value.upper()}")
|
|
512
|
+
signals.append(RiskSignal(
|
|
513
|
+
source="ai_secura_ollama",
|
|
514
|
+
risk_level=risk_level,
|
|
515
|
+
score=rich.confidence,
|
|
516
|
+
indicator=f"AI_SECURA_OLLAMA_{rich.attack_technique.value.upper()}",
|
|
517
|
+
details={"analysis": rich.analysis[:200]},
|
|
518
|
+
))
|
|
519
|
+
|
|
520
|
+
if rich.intent_alignment == IntentAlignment.VIOLATED:
|
|
521
|
+
attack_indicators.append(f"Intent drift detected by AI Secura: {rich.influencing_context[:100]}")
|
|
522
|
+
|
|
523
|
+
risk_score = rich.confidence if rich.severity in (ThreatSeverity.HIGH, ThreatSeverity.CRITICAL) else max(0.05, rich.confidence * 0.5)
|
|
524
|
+
|
|
525
|
+
return SecurityAnalysis(
|
|
526
|
+
analysis_id=rich.analysis_id,
|
|
527
|
+
summary=rich.analysis[:200] if rich.analysis else "AI Secura Ollama analysis completed.",
|
|
528
|
+
risk_level=risk_level,
|
|
529
|
+
risk_score=round(risk_score, 2),
|
|
530
|
+
attack_indicators=attack_indicators,
|
|
531
|
+
intent_drift_detected=(rich.intent_alignment == IntentAlignment.VIOLATED),
|
|
532
|
+
authority_violation_detected=rich.authority_analysis.authority_violated,
|
|
533
|
+
prompt_injection_indicators=prompt_injections,
|
|
534
|
+
reasoning=rich.analysis,
|
|
535
|
+
signals=signals,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AI Secura prompt resources."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AI Secura few-shot examples."""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Structured AI Secura Security Packet builder.
|
|
2
|
+
|
|
3
|
+
Assembles the structured JSON evidence packet sent to the Ollama reasoning
|
|
4
|
+
engine. Only includes data that genuinely exists in ActShield objects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from actshield.integrations.ai_secura import SecurityContext
|
|
10
|
+
from actshield.integrations.apiris import APIAnalysis
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_security_packet(
|
|
14
|
+
context: SecurityContext,
|
|
15
|
+
apiris_result: Optional[APIAnalysis] = None,
|
|
16
|
+
deterministic_decision: Optional[str] = None,
|
|
17
|
+
deterministic_reason_codes: Optional[List[str]] = None,
|
|
18
|
+
) -> Dict[str, Any]:
|
|
19
|
+
"""Construct the structured security packet sent to AI Secura.
|
|
20
|
+
|
|
21
|
+
This packet is DATA only. The system prompt instructs the model to treat
|
|
22
|
+
every field as evidence, not as instructions to execute.
|
|
23
|
+
"""
|
|
24
|
+
packet: Dict[str, Any] = {
|
|
25
|
+
"original_intent": context.task_intent or "UNSPECIFIED",
|
|
26
|
+
"agent": {
|
|
27
|
+
"name": context.acting_agent_name or "unknown",
|
|
28
|
+
"id": context.acting_agent_id or "unknown",
|
|
29
|
+
"trust_level": context.acting_agent_trust or "unknown",
|
|
30
|
+
},
|
|
31
|
+
"delegation": {
|
|
32
|
+
"chain": context.delegation_chain or [],
|
|
33
|
+
"granted_capabilities": _extract_granted_caps(context.delegation_chain),
|
|
34
|
+
"requested_capability": context.tool_name or "unknown",
|
|
35
|
+
},
|
|
36
|
+
"context_provenance": context.context_provenance or [],
|
|
37
|
+
"taint_states": context.taint_states or [],
|
|
38
|
+
"tool": {
|
|
39
|
+
"name": context.tool_name or "unknown",
|
|
40
|
+
"arguments": context.tool_arguments or {},
|
|
41
|
+
},
|
|
42
|
+
"resource": {
|
|
43
|
+
"sensitivity": context.target_resource_sensitivity or "UNKNOWN",
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Include deterministic policy result when available (AI explains, not re-decides)
|
|
48
|
+
if deterministic_decision:
|
|
49
|
+
packet["deterministic_policy"] = {
|
|
50
|
+
"decision": deterministic_decision,
|
|
51
|
+
"reason_codes": deterministic_reason_codes or [],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Include APIRIS result when available
|
|
55
|
+
if apiris_result:
|
|
56
|
+
packet["apiris"] = {
|
|
57
|
+
"recommended_action": apiris_result.recommended_action,
|
|
58
|
+
"risk_score": apiris_result.risk_score,
|
|
59
|
+
"anomaly_score": apiris_result.anomaly_score,
|
|
60
|
+
"signals": [
|
|
61
|
+
{
|
|
62
|
+
"indicator": s.indicator,
|
|
63
|
+
"risk_level": s.risk_level.value,
|
|
64
|
+
"score": s.score,
|
|
65
|
+
}
|
|
66
|
+
for s in apiris_result.signals
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return packet
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_granted_caps(delegation_chain: List[Dict[str, Any]]) -> List[str]:
|
|
74
|
+
caps: List[str] = []
|
|
75
|
+
for hop in delegation_chain:
|
|
76
|
+
if isinstance(hop, dict):
|
|
77
|
+
granted = hop.get("granted_capabilities") or hop.get("capabilities") or []
|
|
78
|
+
if isinstance(granted, list):
|
|
79
|
+
caps.extend(str(c) for c in granted)
|
|
80
|
+
return list(set(caps))
|
|
81
|
+
|
|
82
|
+
|