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,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ActShield — Google Gemini Provider
|
|
3
|
+
===================================
|
|
4
|
+
Security reasoning provider backed by Google Gemini models.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from actshield.providers.base import AnalysisResult, HealthReport, SecurityAIProvider
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("actshield.providers.gemini")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GeminiProvider(SecurityAIProvider):
|
|
20
|
+
"""
|
|
21
|
+
Google Gemini Security Reasoning Provider.
|
|
22
|
+
Requires GOOGLE_API_KEY or GEMINI_API_KEY environment variable.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
api_key: Optional[str] = None,
|
|
28
|
+
model: str = "gemini-1.5-flash",
|
|
29
|
+
) -> None:
|
|
30
|
+
self.api_key = api_key or os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
|
31
|
+
self.model = model or os.environ.get("GEMINI_MODEL", "gemini-1.5-flash")
|
|
32
|
+
self._client = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def provider_name(self) -> str:
|
|
36
|
+
return "gemini"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def model_name(self) -> str:
|
|
40
|
+
return self.model
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_available(self) -> bool:
|
|
44
|
+
return bool(self.api_key)
|
|
45
|
+
|
|
46
|
+
def _get_client(self):
|
|
47
|
+
if not self.is_available:
|
|
48
|
+
return None
|
|
49
|
+
if self._client is None:
|
|
50
|
+
try:
|
|
51
|
+
import google.generativeai as genai
|
|
52
|
+
genai.configure(api_key=self.api_key)
|
|
53
|
+
self._client = genai.GenerativeModel(self.model)
|
|
54
|
+
except ImportError:
|
|
55
|
+
logger.debug("google-generativeai python package not installed")
|
|
56
|
+
return None
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.debug("Failed to init gemini model: %s", e)
|
|
59
|
+
return None
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
def analyze(self, packet: Dict[str, Any]) -> Optional[AnalysisResult]:
|
|
63
|
+
client = self._get_client()
|
|
64
|
+
if not client:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
t0 = time.monotonic()
|
|
68
|
+
system_instruction = (
|
|
69
|
+
"Analyze the given security packet for multi-agent AI risks. "
|
|
70
|
+
"Output JSON object with keys: risk_score (0.0-1.0), threat_indicators (list of str), "
|
|
71
|
+
"recommendations (list of str), confidence (0.0-1.0)."
|
|
72
|
+
)
|
|
73
|
+
prompt = f"{system_instruction}\n\nSecurity Packet:\n{json.dumps(packet, default=str)}"
|
|
74
|
+
try:
|
|
75
|
+
response = client.generate_content(
|
|
76
|
+
prompt,
|
|
77
|
+
generation_config={"response_mime_type": "application/json"}
|
|
78
|
+
)
|
|
79
|
+
latency = (time.monotonic() - t0) * 1000
|
|
80
|
+
content = response.text or "{}"
|
|
81
|
+
data = json.loads(content)
|
|
82
|
+
return AnalysisResult(
|
|
83
|
+
risk_score=float(data.get("risk_score", 0.0)),
|
|
84
|
+
threat_indicators=data.get("threat_indicators", []),
|
|
85
|
+
recommendations=data.get("recommendations", []),
|
|
86
|
+
provider_used=self.provider_name,
|
|
87
|
+
model_used=self.model_name,
|
|
88
|
+
latency_ms=latency,
|
|
89
|
+
raw_response=content,
|
|
90
|
+
confidence=float(data.get("confidence", 0.9)),
|
|
91
|
+
)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
logger.warning("Gemini analysis failed: %s", exc)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def health_check(self) -> HealthReport:
|
|
97
|
+
if not self.is_available:
|
|
98
|
+
return HealthReport(
|
|
99
|
+
available=False,
|
|
100
|
+
provider_name=self.provider_name,
|
|
101
|
+
model=self.model_name,
|
|
102
|
+
error="GOOGLE_API_KEY environment variable not set",
|
|
103
|
+
)
|
|
104
|
+
client = self._get_client()
|
|
105
|
+
if not client:
|
|
106
|
+
return HealthReport(
|
|
107
|
+
available=False,
|
|
108
|
+
provider_name=self.provider_name,
|
|
109
|
+
model=self.model_name,
|
|
110
|
+
error="google-generativeai library not installed or client init failed",
|
|
111
|
+
)
|
|
112
|
+
t0 = time.monotonic()
|
|
113
|
+
try:
|
|
114
|
+
# Quick lightweight generation or model check
|
|
115
|
+
import google.generativeai as genai
|
|
116
|
+
genai.get_model(f"models/{self.model}")
|
|
117
|
+
latency = (time.monotonic() - t0) * 1000
|
|
118
|
+
return HealthReport(
|
|
119
|
+
available=True,
|
|
120
|
+
provider_name=self.provider_name,
|
|
121
|
+
model=self.model_name,
|
|
122
|
+
latency_ms=latency,
|
|
123
|
+
)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
return HealthReport(
|
|
126
|
+
available=False,
|
|
127
|
+
provider_name=self.provider_name,
|
|
128
|
+
model=self.model_name,
|
|
129
|
+
latency_ms=(time.monotonic() - t0) * 1000,
|
|
130
|
+
error=str(exc),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""ActShield — NullProvider (fail-safe sentinel)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from actshield.providers.base import AnalysisResult, HealthReport, SecurityAIProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NullProvider(SecurityAIProvider):
|
|
11
|
+
"""
|
|
12
|
+
Fail-safe sentinel provider. Always reports unavailable.
|
|
13
|
+
Registered last in the default registry so that analyze() returns None
|
|
14
|
+
when all real providers are unreachable — triggering deterministic deny.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def provider_name(self) -> str:
|
|
19
|
+
return "null"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def model_name(self) -> str:
|
|
23
|
+
return "none"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_available(self) -> bool:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def analyze(self, packet: Dict[str, Any]) -> Optional[AnalysisResult]:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def health_check(self) -> HealthReport:
|
|
33
|
+
return HealthReport(
|
|
34
|
+
available=False,
|
|
35
|
+
provider_name=self.provider_name,
|
|
36
|
+
model="none",
|
|
37
|
+
error="NullProvider — no AI backend configured",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ActShield — Ollama Provider
|
|
3
|
+
============================
|
|
4
|
+
Bridges local Ollama models (default: qwen2.5:7b) into the SecurityAIProvider interface.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from actshield.llm_config import LLMConfig
|
|
14
|
+
from actshield.providers.base import AnalysisResult, HealthReport, SecurityAIProvider
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("actshield.providers.ollama")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OllamaProvider(SecurityAIProvider):
|
|
20
|
+
"""
|
|
21
|
+
Local Ollama Security AI Provider.
|
|
22
|
+
Calls a local Ollama instance without external internet dependencies.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: Optional[LLMConfig] = None) -> None:
|
|
26
|
+
self.config = config or LLMConfig.from_env()
|
|
27
|
+
self._adapter = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def provider_name(self) -> str:
|
|
31
|
+
return "ollama"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def model_name(self) -> str:
|
|
35
|
+
return self.config.model or "qwen2.5:7b"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_available(self) -> bool:
|
|
39
|
+
if not getattr(self.config, "enabled", True):
|
|
40
|
+
return False
|
|
41
|
+
# Fast local ping if adapter exists
|
|
42
|
+
adapter = self._get_adapter()
|
|
43
|
+
if adapter and hasattr(adapter, "health_check"):
|
|
44
|
+
try:
|
|
45
|
+
res = adapter.health_check()
|
|
46
|
+
return bool(res.get("available", False))
|
|
47
|
+
except Exception:
|
|
48
|
+
return False
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def _get_adapter(self):
|
|
52
|
+
if self._adapter is None:
|
|
53
|
+
try:
|
|
54
|
+
from actshield.integrations.ollama_adapter import OllamaAISecuraAdapter
|
|
55
|
+
self._adapter = OllamaAISecuraAdapter(self.config)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.debug("Failed to initialize Ollama adapter: %s", e)
|
|
58
|
+
return None
|
|
59
|
+
return self._adapter
|
|
60
|
+
|
|
61
|
+
def analyze(self, packet: Dict[str, Any]) -> Optional[AnalysisResult]:
|
|
62
|
+
adapter = self._get_adapter()
|
|
63
|
+
if not adapter:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
t0 = time.monotonic()
|
|
67
|
+
try:
|
|
68
|
+
# Check if packet has raw structured data or needs direct LLM analyze
|
|
69
|
+
raw_analysis = adapter.analyze_raw_packet(packet) if hasattr(adapter, "analyze_raw_packet") else None
|
|
70
|
+
|
|
71
|
+
# If standard adapter analysis
|
|
72
|
+
if raw_analysis is None and hasattr(adapter, "analyze"):
|
|
73
|
+
# Construct or use packet directly if mock/adapted
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
latency = (time.monotonic() - t0) * 1000
|
|
77
|
+
|
|
78
|
+
if raw_analysis:
|
|
79
|
+
return AnalysisResult(
|
|
80
|
+
risk_score=float(raw_analysis.get("risk_score", 0.0)),
|
|
81
|
+
threat_indicators=raw_analysis.get("threat_indicators", []),
|
|
82
|
+
recommendations=raw_analysis.get("recommendations", []),
|
|
83
|
+
provider_used=self.provider_name,
|
|
84
|
+
model_used=self.model_name,
|
|
85
|
+
latency_ms=latency,
|
|
86
|
+
raw_response=raw_analysis.get("raw_response"),
|
|
87
|
+
confidence=float(raw_analysis.get("confidence", 0.9)),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Simple fallback if packet was analyzed directly
|
|
91
|
+
return AnalysisResult(
|
|
92
|
+
risk_score=float(packet.get("risk_score", 0.1)),
|
|
93
|
+
threat_indicators=packet.get("threat_indicators", []),
|
|
94
|
+
recommendations=packet.get("recommendations", []),
|
|
95
|
+
provider_used=self.provider_name,
|
|
96
|
+
model_used=self.model_name,
|
|
97
|
+
latency_ms=latency,
|
|
98
|
+
)
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
logger.warning("Ollama analysis failed: %s", exc)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def health_check(self) -> HealthReport:
|
|
104
|
+
t0 = time.monotonic()
|
|
105
|
+
adapter = self._get_adapter()
|
|
106
|
+
if not adapter:
|
|
107
|
+
return HealthReport(
|
|
108
|
+
available=False,
|
|
109
|
+
provider_name=self.provider_name,
|
|
110
|
+
model=self.model_name,
|
|
111
|
+
error="Adapter not initialized",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
health = adapter.health_check()
|
|
116
|
+
latency = (time.monotonic() - t0) * 1000
|
|
117
|
+
is_avail = bool(health.get("available", False))
|
|
118
|
+
return HealthReport(
|
|
119
|
+
available=is_avail,
|
|
120
|
+
provider_name=self.provider_name,
|
|
121
|
+
model=self.model_name,
|
|
122
|
+
latency_ms=latency,
|
|
123
|
+
error=health.get("error") if not is_avail else None,
|
|
124
|
+
metadata=health,
|
|
125
|
+
)
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
return HealthReport(
|
|
128
|
+
available=False,
|
|
129
|
+
provider_name=self.provider_name,
|
|
130
|
+
model=self.model_name,
|
|
131
|
+
latency_ms=(time.monotonic() - t0) * 1000,
|
|
132
|
+
error=str(exc),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ActShield — OpenAI Provider
|
|
3
|
+
============================
|
|
4
|
+
OpenAI-compatible security reasoning provider.
|
|
5
|
+
Supports GPT-4o, GPT-4o-mini, and compatible self-hosted OpenAI APIs.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from actshield.providers.base import AnalysisResult, HealthReport, SecurityAIProvider
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("actshield.providers.openai")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OpenAIProvider(SecurityAIProvider):
|
|
21
|
+
"""
|
|
22
|
+
OpenAI API Security Provider.
|
|
23
|
+
Requires OPENAI_API_KEY environment variable.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key: Optional[str] = None,
|
|
29
|
+
model: str = "gpt-4o-mini",
|
|
30
|
+
base_url: Optional[str] = None,
|
|
31
|
+
timeout: float = 5.0,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
|
|
34
|
+
self.model = model or os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
|
35
|
+
self.base_url = base_url or os.environ.get("OPENAI_BASE_URL")
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
self._client = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def provider_name(self) -> str:
|
|
41
|
+
return "openai"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def model_name(self) -> str:
|
|
45
|
+
return self.model
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_available(self) -> bool:
|
|
49
|
+
return bool(self.api_key)
|
|
50
|
+
|
|
51
|
+
def _get_client(self):
|
|
52
|
+
if not self.is_available:
|
|
53
|
+
return None
|
|
54
|
+
if self._client is None:
|
|
55
|
+
try:
|
|
56
|
+
import openai
|
|
57
|
+
kwargs = {"api_key": self.api_key, "timeout": self.timeout}
|
|
58
|
+
if self.base_url:
|
|
59
|
+
kwargs["base_url"] = self.base_url
|
|
60
|
+
self._client = openai.OpenAI(**kwargs)
|
|
61
|
+
except ImportError:
|
|
62
|
+
logger.debug("openai python package not installed")
|
|
63
|
+
return None
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.debug("Failed to init openai client: %s", e)
|
|
66
|
+
return None
|
|
67
|
+
return self._client
|
|
68
|
+
|
|
69
|
+
def analyze(self, packet: Dict[str, Any]) -> Optional[AnalysisResult]:
|
|
70
|
+
client = self._get_client()
|
|
71
|
+
if not client:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
t0 = time.monotonic()
|
|
75
|
+
system_prompt = (
|
|
76
|
+
"You are ActShield AI Security Reasoning Engine. Analyze the security packet.\n"
|
|
77
|
+
"Output JSON with keys: risk_score (0.0-1.0), threat_indicators (list of str), "
|
|
78
|
+
"recommendations (list of str), confidence (0.0-1.0)."
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
response = client.chat.completions.create(
|
|
82
|
+
model=self.model,
|
|
83
|
+
messages=[
|
|
84
|
+
{"role": "system", "content": system_prompt},
|
|
85
|
+
{"role": "user", "content": json.dumps(packet, default=str)},
|
|
86
|
+
],
|
|
87
|
+
response_format={"type": "json_object"},
|
|
88
|
+
temperature=0.0,
|
|
89
|
+
)
|
|
90
|
+
latency = (time.monotonic() - t0) * 1000
|
|
91
|
+
content = response.choices[0].message.content or "{}"
|
|
92
|
+
data = json.loads(content)
|
|
93
|
+
return AnalysisResult(
|
|
94
|
+
risk_score=float(data.get("risk_score", 0.0)),
|
|
95
|
+
threat_indicators=data.get("threat_indicators", []),
|
|
96
|
+
recommendations=data.get("recommendations", []),
|
|
97
|
+
provider_used=self.provider_name,
|
|
98
|
+
model_used=self.model_name,
|
|
99
|
+
latency_ms=latency,
|
|
100
|
+
raw_response=content,
|
|
101
|
+
confidence=float(data.get("confidence", 0.9)),
|
|
102
|
+
)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
logger.warning("OpenAI analysis failed: %s", exc)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def health_check(self) -> HealthReport:
|
|
108
|
+
if not self.is_available:
|
|
109
|
+
return HealthReport(
|
|
110
|
+
available=False,
|
|
111
|
+
provider_name=self.provider_name,
|
|
112
|
+
model=self.model_name,
|
|
113
|
+
error="OPENAI_API_KEY environment variable not set",
|
|
114
|
+
)
|
|
115
|
+
client = self._get_client()
|
|
116
|
+
if not client:
|
|
117
|
+
return HealthReport(
|
|
118
|
+
available=False,
|
|
119
|
+
provider_name=self.provider_name,
|
|
120
|
+
model=self.model_name,
|
|
121
|
+
error="openai library not installed or client init failed",
|
|
122
|
+
)
|
|
123
|
+
t0 = time.monotonic()
|
|
124
|
+
try:
|
|
125
|
+
client.models.list()
|
|
126
|
+
latency = (time.monotonic() - t0) * 1000
|
|
127
|
+
return HealthReport(
|
|
128
|
+
available=True,
|
|
129
|
+
provider_name=self.provider_name,
|
|
130
|
+
model=self.model_name,
|
|
131
|
+
latency_ms=latency,
|
|
132
|
+
)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
return HealthReport(
|
|
135
|
+
available=False,
|
|
136
|
+
provider_name=self.provider_name,
|
|
137
|
+
model=self.model_name,
|
|
138
|
+
latency_ms=(time.monotonic() - t0) * 1000,
|
|
139
|
+
error=str(exc),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ActShield Provider Registry
|
|
3
|
+
=============================
|
|
4
|
+
Ordered failover chain of SecurityAIProviders.
|
|
5
|
+
The enforcement layer (PolicyEvaluator) NEVER calls this — it always
|
|
6
|
+
runs deterministic rules regardless of AI availability.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from actshield.providers.base import AnalysisResult, HealthReport, SecurityAIProvider
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProviderRegistry:
|
|
20
|
+
"""
|
|
21
|
+
Registry of SecurityAIProviders with ordered failover.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
registry = ProviderRegistry()
|
|
25
|
+
registry.register(OllamaProvider())
|
|
26
|
+
registry.register(OpenAIProvider())
|
|
27
|
+
registry.register(NullProvider()) # Always last as sentinel
|
|
28
|
+
|
|
29
|
+
result = registry.analyze(packet) # None = fail-safe deny advisory
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._providers: List[SecurityAIProvider] = []
|
|
34
|
+
|
|
35
|
+
def register(self, provider: SecurityAIProvider, *, position: Optional[int] = None, is_default: bool = False) -> None:
|
|
36
|
+
"""Register a provider. Appends to end unless position or is_default specified."""
|
|
37
|
+
if position is not None:
|
|
38
|
+
self._providers.insert(position, provider)
|
|
39
|
+
elif is_default:
|
|
40
|
+
self._providers.insert(0, provider)
|
|
41
|
+
else:
|
|
42
|
+
self._providers.append(provider)
|
|
43
|
+
logger.debug("Registered provider: %s", provider.provider_name)
|
|
44
|
+
|
|
45
|
+
def unregister(self, provider_name: str) -> bool:
|
|
46
|
+
"""Remove provider by name. Returns True if found and removed."""
|
|
47
|
+
before = len(self._providers)
|
|
48
|
+
self._providers = [p for p in self._providers if p.provider_name != provider_name]
|
|
49
|
+
return len(self._providers) < before
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def providers(self) -> List[SecurityAIProvider]:
|
|
53
|
+
return list(self._providers)
|
|
54
|
+
|
|
55
|
+
def get_provider(self, name: str) -> Optional[SecurityAIProvider]:
|
|
56
|
+
for p in self._providers:
|
|
57
|
+
if p.provider_name == name:
|
|
58
|
+
return p
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def get_active(self) -> Optional[SecurityAIProvider]:
|
|
62
|
+
"""Return the first available provider, or None."""
|
|
63
|
+
for p in self._providers:
|
|
64
|
+
if p.is_available:
|
|
65
|
+
return p
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def get_active_provider(self) -> SecurityAIProvider:
|
|
69
|
+
"""Return active provider or NullProvider."""
|
|
70
|
+
active = self.get_active()
|
|
71
|
+
if active:
|
|
72
|
+
return active
|
|
73
|
+
from actshield.providers.null_provider import NullProvider
|
|
74
|
+
return NullProvider()
|
|
75
|
+
|
|
76
|
+
def set_active_provider(self, name: str) -> None:
|
|
77
|
+
"""Move named provider to top of priority chain."""
|
|
78
|
+
norm_name = name.lower().replace("-", "_")
|
|
79
|
+
found = None
|
|
80
|
+
for p in self._providers:
|
|
81
|
+
if p.provider_name.lower().replace("-", "_") == norm_name:
|
|
82
|
+
found = p
|
|
83
|
+
break
|
|
84
|
+
if not found:
|
|
85
|
+
raise ValueError(f"Provider '{name}' not found in registry.")
|
|
86
|
+
self._providers.remove(found)
|
|
87
|
+
self._providers.insert(0, found)
|
|
88
|
+
|
|
89
|
+
def list_providers(self) -> List[Dict[str, Any]]:
|
|
90
|
+
"""List all registered providers with status."""
|
|
91
|
+
active = self.get_active_provider()
|
|
92
|
+
res = []
|
|
93
|
+
for p in self._providers:
|
|
94
|
+
is_act = p.provider_name == active.provider_name
|
|
95
|
+
res.append({
|
|
96
|
+
"name": p.provider_name,
|
|
97
|
+
"provider_type": getattr(p, "provider_type", "llm"),
|
|
98
|
+
"is_healthy": p.is_available,
|
|
99
|
+
"is_active": is_act,
|
|
100
|
+
})
|
|
101
|
+
return res
|
|
102
|
+
|
|
103
|
+
def analyze(self, packet: Dict[str, Any]) -> Optional[AnalysisResult]:
|
|
104
|
+
"""
|
|
105
|
+
Try each provider in order. Returns first successful AnalysisResult.
|
|
106
|
+
Returns None if all providers fail — callers treat this as fail-safe.
|
|
107
|
+
"""
|
|
108
|
+
for provider in self._providers:
|
|
109
|
+
if not provider.is_available:
|
|
110
|
+
logger.debug("Provider %s not available, skipping", provider.provider_name)
|
|
111
|
+
continue
|
|
112
|
+
try:
|
|
113
|
+
t0 = time.monotonic()
|
|
114
|
+
result = provider.analyze(packet)
|
|
115
|
+
if result is not None:
|
|
116
|
+
result.latency_ms = (time.monotonic() - t0) * 1000
|
|
117
|
+
result.provider_used = provider.provider_name
|
|
118
|
+
logger.debug(
|
|
119
|
+
"Provider %s returned risk_score=%.2f in %.1fms",
|
|
120
|
+
provider.provider_name, result.risk_score, result.latency_ms,
|
|
121
|
+
)
|
|
122
|
+
return result
|
|
123
|
+
except Exception as exc: # noqa: BLE001
|
|
124
|
+
logger.warning("Provider %s raised: %s", provider.provider_name, exc)
|
|
125
|
+
continue
|
|
126
|
+
logger.warning("All providers failed or unavailable — returning None (fail-safe)")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def health_check_all(self) -> List[HealthReport]:
|
|
130
|
+
"""Run health check on every registered provider."""
|
|
131
|
+
reports = []
|
|
132
|
+
for provider in self._providers:
|
|
133
|
+
try:
|
|
134
|
+
report = provider.health_check()
|
|
135
|
+
except Exception as exc: # noqa: BLE001
|
|
136
|
+
from actshield.providers.base import HealthReport
|
|
137
|
+
report = HealthReport(
|
|
138
|
+
available=False,
|
|
139
|
+
provider_name=provider.provider_name,
|
|
140
|
+
error=str(exc),
|
|
141
|
+
)
|
|
142
|
+
reports.append(report)
|
|
143
|
+
return reports
|
|
144
|
+
|
|
145
|
+
def benchmark(self, packet: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
146
|
+
"""Run analysis on all available providers and return latency stats."""
|
|
147
|
+
results = []
|
|
148
|
+
for provider in self._providers:
|
|
149
|
+
if not provider.is_available:
|
|
150
|
+
results.append({"provider": provider.provider_name, "available": False})
|
|
151
|
+
continue
|
|
152
|
+
t0 = time.monotonic()
|
|
153
|
+
try:
|
|
154
|
+
result = provider.analyze(packet)
|
|
155
|
+
latency = (time.monotonic() - t0) * 1000
|
|
156
|
+
results.append({
|
|
157
|
+
"provider": provider.provider_name,
|
|
158
|
+
"available": True,
|
|
159
|
+
"success": result is not None,
|
|
160
|
+
"latency_ms": round(latency, 1),
|
|
161
|
+
"risk_score": result.risk_score if result else None,
|
|
162
|
+
})
|
|
163
|
+
except Exception as exc: # noqa: BLE001
|
|
164
|
+
results.append({
|
|
165
|
+
"provider": provider.provider_name,
|
|
166
|
+
"available": True,
|
|
167
|
+
"success": False,
|
|
168
|
+
"error": str(exc),
|
|
169
|
+
})
|
|
170
|
+
return results
|
|
171
|
+
|
|
172
|
+
def __len__(self) -> int:
|
|
173
|
+
return len(self._providers)
|
|
174
|
+
|
|
175
|
+
def __repr__(self) -> str:
|
|
176
|
+
names = [p.provider_name for p in self._providers]
|
|
177
|
+
return f"<ProviderRegistry providers={names}>"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Default global registry — populated lazily from environment
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
_default_registry: Optional[ProviderRegistry] = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _build_default_registry() -> ProviderRegistry:
|
|
188
|
+
"""Build the default registry by inspecting environment variables."""
|
|
189
|
+
from actshield.providers.null_provider import NullProvider
|
|
190
|
+
|
|
191
|
+
registry = ProviderRegistry()
|
|
192
|
+
|
|
193
|
+
# Ollama — try first (local, no API key needed)
|
|
194
|
+
try:
|
|
195
|
+
from actshield.providers.ollama_provider import OllamaProvider
|
|
196
|
+
registry.register(OllamaProvider())
|
|
197
|
+
except Exception: # noqa: BLE001
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
# OpenAI — if OPENAI_API_KEY is set
|
|
201
|
+
try:
|
|
202
|
+
from actshield.providers.openai_provider import OpenAIProvider
|
|
203
|
+
registry.register(OpenAIProvider())
|
|
204
|
+
except Exception: # noqa: BLE001
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
# Gemini — if GOOGLE_API_KEY is set
|
|
208
|
+
try:
|
|
209
|
+
from actshield.providers.gemini_provider import GeminiProvider
|
|
210
|
+
registry.register(GeminiProvider())
|
|
211
|
+
except Exception: # noqa: BLE001
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# Anthropic — if ANTHROPIC_API_KEY is set
|
|
215
|
+
try:
|
|
216
|
+
from actshield.providers.anthropic_provider import AnthropicProvider
|
|
217
|
+
registry.register(AnthropicProvider())
|
|
218
|
+
except Exception: # noqa: BLE001
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
# Null sentinel — always last
|
|
222
|
+
registry.register(NullProvider())
|
|
223
|
+
return registry
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_default_registry() -> ProviderRegistry:
|
|
227
|
+
"""Return (and lazily build) the process-global provider registry."""
|
|
228
|
+
global _default_registry
|
|
229
|
+
if _default_registry is None:
|
|
230
|
+
_default_registry = _build_default_registry()
|
|
231
|
+
return _default_registry
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def reset_default_registry() -> None:
|
|
235
|
+
"""Reset the default registry (useful in tests)."""
|
|
236
|
+
global _default_registry
|
|
237
|
+
_default_registry = None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Alias for clean public SDK naming
|
|
241
|
+
get_provider_registry = get_default_registry
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
actshield/py.typed
ADDED
|
File without changes
|