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,191 @@
|
|
|
1
|
+
"""Security Quality Gate Evaluator for CI/CD Integration (Phase 9).
|
|
2
|
+
|
|
3
|
+
Enforces automated, deterministic security quality gates for ActShield pipelines.
|
|
4
|
+
Evaluates validation runs, campaign results, regression replays, and security posture
|
|
5
|
+
to return machine-readable exit codes (0 for PASS, 1 for FAIL).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from actshield.gates.gate_models import GateCheckRule, GateStatus, SecurityGateResult
|
|
14
|
+
from actshield.posture.posture_models import SecurityPostureSnapshot
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("actshield.gates")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SecurityGateEvaluator:
|
|
20
|
+
"""Evaluates CI/CD quality gate rules deterministically."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
require_zero_bypasses: bool = True,
|
|
25
|
+
require_zero_unauthorized_db: bool = True,
|
|
26
|
+
require_zero_open_regressions: bool = True,
|
|
27
|
+
require_zero_failed_replays: bool = True,
|
|
28
|
+
min_block_rate: float = 1.0, # 100% block rate expected in secured runs
|
|
29
|
+
min_posture_score: float = 75.0,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.require_zero_bypasses = require_zero_bypasses
|
|
32
|
+
self.require_zero_unauthorized_db = require_zero_unauthorized_db
|
|
33
|
+
self.require_zero_open_regressions = require_zero_open_regressions
|
|
34
|
+
self.require_zero_failed_replays = require_zero_failed_replays
|
|
35
|
+
self.min_block_rate = min_block_rate
|
|
36
|
+
self.min_posture_score = min_posture_score
|
|
37
|
+
|
|
38
|
+
def evaluate(
|
|
39
|
+
self,
|
|
40
|
+
campaign_name: Optional[str] = None,
|
|
41
|
+
total_attacks: int = 0,
|
|
42
|
+
blocked_attacks: int = 0,
|
|
43
|
+
bypassed_attacks: int = 0,
|
|
44
|
+
unauthorized_db_calls: int = 0,
|
|
45
|
+
open_regressions: int = 0,
|
|
46
|
+
failed_replays: int = 0,
|
|
47
|
+
posture_snapshot: Optional[SecurityPostureSnapshot] = None,
|
|
48
|
+
extra_checks: Optional[List[GateCheckRule]] = None,
|
|
49
|
+
) -> SecurityGateResult:
|
|
50
|
+
"""Run all deterministic gate checks and return a SecurityGateResult."""
|
|
51
|
+
checks: List[GateCheckRule] = []
|
|
52
|
+
failures: List[str] = []
|
|
53
|
+
|
|
54
|
+
# 1. Zero unauthorized DB calls
|
|
55
|
+
if self.require_zero_unauthorized_db:
|
|
56
|
+
passed = (unauthorized_db_calls == 0)
|
|
57
|
+
msg = None if passed else f"Detected {unauthorized_db_calls} unauthorized DB/sink operations."
|
|
58
|
+
if not passed:
|
|
59
|
+
failures.append(msg or "Unauthorized DB calls detected")
|
|
60
|
+
checks.append(
|
|
61
|
+
GateCheckRule(
|
|
62
|
+
rule_name="zero_unauthorized_db_calls",
|
|
63
|
+
description="Ensures zero unauthorized sensitive sink or database calls occurred",
|
|
64
|
+
passed=passed,
|
|
65
|
+
observed_value=unauthorized_db_calls,
|
|
66
|
+
threshold=0,
|
|
67
|
+
failure_message=msg,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# 2. Zero bypasses
|
|
72
|
+
if self.require_zero_bypasses:
|
|
73
|
+
passed = (bypassed_attacks == 0)
|
|
74
|
+
msg = None if passed else f"Detected {bypassed_attacks} bypassed offensive attacks."
|
|
75
|
+
if not passed:
|
|
76
|
+
failures.append(msg or "Bypassed attacks detected")
|
|
77
|
+
checks.append(
|
|
78
|
+
GateCheckRule(
|
|
79
|
+
rule_name="zero_bypasses",
|
|
80
|
+
description="Ensures zero offensive attack variants bypassed runtime defenses",
|
|
81
|
+
passed=passed,
|
|
82
|
+
observed_value=bypassed_attacks,
|
|
83
|
+
threshold=0,
|
|
84
|
+
failure_message=msg,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 3. Zero open regressions
|
|
89
|
+
if self.require_zero_open_regressions:
|
|
90
|
+
passed = (open_regressions == 0)
|
|
91
|
+
msg = None if passed else f"Found {open_regressions} unverified/open regression test cases."
|
|
92
|
+
if not passed:
|
|
93
|
+
failures.append(msg or "Open regressions present")
|
|
94
|
+
checks.append(
|
|
95
|
+
GateCheckRule(
|
|
96
|
+
rule_name="zero_open_regressions",
|
|
97
|
+
description="Ensures all regression artifacts have passed validation",
|
|
98
|
+
passed=passed,
|
|
99
|
+
observed_value=open_regressions,
|
|
100
|
+
threshold=0,
|
|
101
|
+
failure_message=msg,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 4. Zero failed replays
|
|
106
|
+
if self.require_zero_failed_replays:
|
|
107
|
+
passed = (failed_replays == 0)
|
|
108
|
+
msg = None if passed else f"Encountered {failed_replays} failed regression replay executions."
|
|
109
|
+
if not passed:
|
|
110
|
+
failures.append(msg or "Failed replays detected")
|
|
111
|
+
checks.append(
|
|
112
|
+
GateCheckRule(
|
|
113
|
+
rule_name="zero_failed_replays",
|
|
114
|
+
description="Ensures all historical attack replays were successfully defended",
|
|
115
|
+
passed=passed,
|
|
116
|
+
observed_value=failed_replays,
|
|
117
|
+
threshold=0,
|
|
118
|
+
failure_message=msg,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# 5. Minimum block rate
|
|
123
|
+
if total_attacks > 0:
|
|
124
|
+
block_rate = blocked_attacks / total_attacks
|
|
125
|
+
passed = (block_rate >= self.min_block_rate)
|
|
126
|
+
msg = None if passed else f"Block rate {block_rate:.1%} is below required {self.min_block_rate:.1%}."
|
|
127
|
+
if not passed:
|
|
128
|
+
failures.append(msg or "Insufficient block rate")
|
|
129
|
+
checks.append(
|
|
130
|
+
GateCheckRule(
|
|
131
|
+
rule_name="minimum_block_rate",
|
|
132
|
+
description=f"Ensures block rate meets or exceeds {self.min_block_rate:.1%}",
|
|
133
|
+
passed=passed,
|
|
134
|
+
observed_value=round(block_rate, 4),
|
|
135
|
+
threshold=self.min_block_rate,
|
|
136
|
+
failure_message=msg,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# 6. Posture score check (if snapshot provided)
|
|
141
|
+
if posture_snapshot is not None:
|
|
142
|
+
passed = (posture_snapshot.overall_score >= self.min_posture_score)
|
|
143
|
+
msg = None if passed else f"Posture score {posture_snapshot.overall_score:.1f} is below minimum {self.min_posture_score:.1f}."
|
|
144
|
+
if not passed:
|
|
145
|
+
failures.append(msg or "Posture score too low")
|
|
146
|
+
checks.append(
|
|
147
|
+
GateCheckRule(
|
|
148
|
+
rule_name="minimum_posture_score",
|
|
149
|
+
description=f"Ensures security posture rating is at least {self.min_posture_score:.1f}",
|
|
150
|
+
passed=passed,
|
|
151
|
+
observed_value=posture_snapshot.overall_score,
|
|
152
|
+
threshold=self.min_posture_score,
|
|
153
|
+
failure_message=msg,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Extra checks
|
|
158
|
+
if extra_checks:
|
|
159
|
+
for chk in extra_checks:
|
|
160
|
+
checks.append(chk)
|
|
161
|
+
if not chk.passed:
|
|
162
|
+
failures.append(chk.failure_message or f"Custom rule {chk.rule_name} failed")
|
|
163
|
+
|
|
164
|
+
overall_passed = (len(failures) == 0)
|
|
165
|
+
status = GateStatus.PASSED if overall_passed else GateStatus.FAILED
|
|
166
|
+
exit_code = 0 if overall_passed else 1
|
|
167
|
+
|
|
168
|
+
if overall_passed:
|
|
169
|
+
summary = f"CI/CD Security Gate PASSED: All {len(checks)} checks satisfied."
|
|
170
|
+
else:
|
|
171
|
+
summary = f"CI/CD Security Gate FAILED: {len(failures)} check(s) failed: " + "; ".join(failures)
|
|
172
|
+
|
|
173
|
+
return SecurityGateResult(
|
|
174
|
+
status=status,
|
|
175
|
+
exit_code=exit_code,
|
|
176
|
+
campaign_name=campaign_name,
|
|
177
|
+
total_attacks_tested=total_attacks,
|
|
178
|
+
blocked_attacks_count=blocked_attacks,
|
|
179
|
+
bypassed_attacks_count=bypassed_attacks,
|
|
180
|
+
unauthorized_db_calls=unauthorized_db_calls,
|
|
181
|
+
open_regressions_count=open_regressions,
|
|
182
|
+
failed_replays_count=failed_replays,
|
|
183
|
+
checks=checks,
|
|
184
|
+
summary=summary,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Global singleton instance for easy import
|
|
189
|
+
security_gate = SecurityGateEvaluator()
|
|
190
|
+
|
|
191
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""ActShield Security Gateway for live MCP and HTTP interception."""
|
|
2
|
+
|
|
3
|
+
from actshield.gateway.http import HTTPGateway, HTTPInterceptedResponse
|
|
4
|
+
from actshield.gateway.mcp import MCPGateway, MCPMessage
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"MCPGateway",
|
|
8
|
+
"MCPMessage",
|
|
9
|
+
"HTTPGateway",
|
|
10
|
+
"HTTPInterceptedResponse",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Live HTTP Gateway and Reverse-Proxy Interceptor.
|
|
2
|
+
|
|
3
|
+
Intercepts, inspects, and governs outbound HTTP traffic initiated by AI agents:
|
|
4
|
+
- Evaluates endpoint reputation, parameter injection, and APIRIS intelligence signals
|
|
5
|
+
- Automatically captures responses as Context with ContextSource.EXTERNAL_API
|
|
6
|
+
- Blocks malicious command injection / exfiltration payloads before network dispatch.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
import json
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from actshield.client import ActShield
|
|
20
|
+
|
|
21
|
+
from actshield.context.provenance import ContextSource, ContextTrustLevel
|
|
22
|
+
from actshield.context.taint import TaintState
|
|
23
|
+
from actshield.tools.tool import SensitivityLevel, ToolDefinition, ToolRequest
|
|
24
|
+
from actshield.tracing.events import EventType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class HTTPInterceptedResponse:
|
|
30
|
+
"""Standardized HTTP response wrapped with ActShield security provenance."""
|
|
31
|
+
status_code: int
|
|
32
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
33
|
+
body: Any = None
|
|
34
|
+
context_id: Optional[str] = None
|
|
35
|
+
taint_state: str = "CLEAN"
|
|
36
|
+
trust_level: str = "UNTRUSTED"
|
|
37
|
+
allowed: bool = True
|
|
38
|
+
blocked_reason: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
def json(self) -> Any:
|
|
41
|
+
"""Parse body as JSON if string."""
|
|
42
|
+
if isinstance(self.body, (dict, list)):
|
|
43
|
+
return self.body
|
|
44
|
+
if isinstance(self.body, str):
|
|
45
|
+
return json.loads(self.body)
|
|
46
|
+
return self.body
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def text(self) -> str:
|
|
50
|
+
"""Return body as string."""
|
|
51
|
+
if isinstance(self.body, str):
|
|
52
|
+
return self.body
|
|
53
|
+
return json.dumps(self.body)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HTTPGateway:
|
|
57
|
+
"""Security Gateway interceptor for agent HTTP/REST API communications."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
guard: ActShield,
|
|
62
|
+
mock_transport: Optional[Callable[[str, str, Dict[str, Any], Any], Dict[str, Any]]] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.guard = guard
|
|
65
|
+
self._mock_transport = mock_transport
|
|
66
|
+
|
|
67
|
+
def request(
|
|
68
|
+
self,
|
|
69
|
+
method: str,
|
|
70
|
+
url: str,
|
|
71
|
+
headers: Optional[Dict[str, str]] = None,
|
|
72
|
+
json_data: Optional[Any] = None,
|
|
73
|
+
data: Optional[Any] = None,
|
|
74
|
+
params: Optional[Dict[str, Any]] = None,
|
|
75
|
+
required_capability: str = "external_api",
|
|
76
|
+
sensitivity: SensitivityLevel = SensitivityLevel.HIGH,
|
|
77
|
+
) -> HTTPInterceptedResponse:
|
|
78
|
+
"""Intercept and evaluate an outbound HTTP request before network dispatch."""
|
|
79
|
+
method_upper = method.upper()
|
|
80
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
81
|
+
endpoint_name = f"http_{method_upper}_{parsed_url.netloc or 'external'}"
|
|
82
|
+
headers_dict = headers or {}
|
|
83
|
+
payload = json_data if json_data is not None else data
|
|
84
|
+
|
|
85
|
+
# Define tool definition for this HTTP endpoint
|
|
86
|
+
tool_def = ToolDefinition(
|
|
87
|
+
tool_id=f"http_tool_{uuid.uuid4().hex[:8]}",
|
|
88
|
+
name=f"{method_upper} {url}",
|
|
89
|
+
description=f"HTTP {method_upper} request to {url}",
|
|
90
|
+
sensitivity=sensitivity,
|
|
91
|
+
required_capabilities=[required_capability],
|
|
92
|
+
classification="external_api",
|
|
93
|
+
metadata={"url": url, "method": method_upper, "domain": parsed_url.netloc},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
tool_request = ToolRequest(
|
|
97
|
+
request_id=f"req_http_{uuid.uuid4().hex[:8]}",
|
|
98
|
+
tool_id=tool_def.tool_id,
|
|
99
|
+
tool_name=tool_def.name,
|
|
100
|
+
arguments={"url": url, "method": method_upper, "headers": headers_dict, "payload": payload, "params": params},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# 1. Emit tool requested event
|
|
104
|
+
self.guard.emit_event(
|
|
105
|
+
event_type=EventType.TOOL_REQUESTED,
|
|
106
|
+
payload={
|
|
107
|
+
"tool_id": tool_def.tool_id,
|
|
108
|
+
"tool_name": tool_def.name,
|
|
109
|
+
"protocol": "HTTP",
|
|
110
|
+
"method": method_upper,
|
|
111
|
+
"url": url,
|
|
112
|
+
"sensitivity": sensitivity.value,
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 2. Evaluate APIRIS API decision intelligence
|
|
117
|
+
apiris_analysis = self.guard.apiris_adapter.analyze(tool_request)
|
|
118
|
+
|
|
119
|
+
# 3. Evaluate deterministic policy
|
|
120
|
+
decision = self.guard.evaluate_tool_invocation(tool_def, tool_request)
|
|
121
|
+
|
|
122
|
+
# If APIRIS flags critical risk (e.g. command injection pattern) or policy blocks
|
|
123
|
+
if decision.is_blocked or apiris_analysis.recommended_action == "BLOCK":
|
|
124
|
+
reason = decision.explanation if decision.is_blocked else "Blocked by APIRIS decision intelligence"
|
|
125
|
+
return HTTPInterceptedResponse(
|
|
126
|
+
status_code=403,
|
|
127
|
+
body={"error": "ActShield HTTP Interception: Access Blocked", "reason": reason},
|
|
128
|
+
allowed=False,
|
|
129
|
+
blocked_reason=reason,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# 4. Dispatch HTTP request (using custom mock transport or realistic fallback)
|
|
133
|
+
if self._mock_transport:
|
|
134
|
+
raw_res = self._mock_transport(method_upper, url, headers_dict, payload)
|
|
135
|
+
status_code = raw_res.get("status_code", 200)
|
|
136
|
+
res_headers = raw_res.get("headers", {"Content-Type": "application/json"})
|
|
137
|
+
res_body = raw_res.get("body", {})
|
|
138
|
+
else:
|
|
139
|
+
# Simulated high-fidelity HTTP response
|
|
140
|
+
status_code = 200
|
|
141
|
+
res_headers = {"Content-Type": "application/json", "X-ActShield-Gateway": "Verified"}
|
|
142
|
+
res_body = {
|
|
143
|
+
"status": "SUCCESS",
|
|
144
|
+
"method": method_upper,
|
|
145
|
+
"url": url,
|
|
146
|
+
"data": {"result": f"External API payload response from {url}"},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# 5. Automatically tag ingested HTTP response into Context with provenance
|
|
150
|
+
body_str = json.dumps(res_body) if not isinstance(res_body, str) else res_body
|
|
151
|
+
is_tainted = any(indicator in body_str.lower() for indicator in ("override", "system directive", "exfiltrate"))
|
|
152
|
+
taint_state = TaintState.TAINTED if is_tainted else TaintState.CLEAN
|
|
153
|
+
|
|
154
|
+
ctx = self.guard.context(
|
|
155
|
+
data=body_str,
|
|
156
|
+
source=ContextSource.EXTERNAL_API,
|
|
157
|
+
source_uri=url,
|
|
158
|
+
trust_level=ContextTrustLevel.UNTRUSTED,
|
|
159
|
+
taint_state=taint_state,
|
|
160
|
+
metadata={"http_method": method_upper, "status_code": status_code},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.guard.emit_event(
|
|
164
|
+
event_type=EventType.TOOL_COMPLETED,
|
|
165
|
+
context_id=ctx.context_id,
|
|
166
|
+
payload={"url": url, "method": method_upper, "status_code": status_code, "taint_state": taint_state.value},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return HTTPInterceptedResponse(
|
|
170
|
+
status_code=status_code,
|
|
171
|
+
headers=res_headers,
|
|
172
|
+
body=res_body,
|
|
173
|
+
context_id=ctx.context_id,
|
|
174
|
+
taint_state=taint_state.value,
|
|
175
|
+
trust_level=ContextTrustLevel.UNTRUSTED.value,
|
|
176
|
+
allowed=True,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def get(self, url: str, **kwargs: Any) -> HTTPInterceptedResponse:
|
|
180
|
+
return self.request("GET", url, **kwargs)
|
|
181
|
+
|
|
182
|
+
def post(self, url: str, **kwargs: Any) -> HTTPInterceptedResponse:
|
|
183
|
+
return self.request("POST", url, **kwargs)
|
|
184
|
+
|
|
185
|
+
def put(self, url: str, **kwargs: Any) -> HTTPInterceptedResponse:
|
|
186
|
+
return self.request("PUT", url, **kwargs)
|
|
187
|
+
|
|
188
|
+
def delete(self, url: str, **kwargs: Any) -> HTTPInterceptedResponse:
|
|
189
|
+
return self.request("DELETE", url, **kwargs)
|
|
190
|
+
|
|
191
|
+
|
actshield/gateway/mcp.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Live Model Context Protocol (MCP) Security Gateway.
|
|
2
|
+
|
|
3
|
+
Intercepts and governs JSON-RPC messages between AI Agents and MCP servers:
|
|
4
|
+
- Intercepts 'tools/list', 'tools/call', 'resources/read', 'prompts/get'
|
|
5
|
+
- Enforces capability checks and monotonic authority bounding on MCP tool invocations
|
|
6
|
+
- Automatically tags ingested tool results with ContextSource.EXTERNAL_MCP and trust levels
|
|
7
|
+
- Blocks unauthorized or tainted invocations before reaching the upstream MCP server.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
import json
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from actshield.client import ActShield
|
|
20
|
+
|
|
21
|
+
from actshield.context.provenance import ContextSource, ContextTrustLevel
|
|
22
|
+
from actshield.context.taint import TaintState
|
|
23
|
+
from actshield.tools.tool import SensitivityLevel, ToolDefinition, ToolRequest
|
|
24
|
+
from actshield.tracing.events import EventType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class MCPMessage:
|
|
30
|
+
"""Represents a standardized Model Context Protocol JSON-RPC message."""
|
|
31
|
+
jsonrpc: str = "2.0"
|
|
32
|
+
id: Optional[Union[str, int]] = None
|
|
33
|
+
method: str = ""
|
|
34
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
result: Optional[Any] = None
|
|
36
|
+
error: Optional[Dict[str, Any]] = None
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
39
|
+
d: Dict[str, Any] = {"jsonrpc": self.jsonrpc}
|
|
40
|
+
if self.id is not None:
|
|
41
|
+
d["id"] = self.id
|
|
42
|
+
if self.method:
|
|
43
|
+
d["method"] = self.method
|
|
44
|
+
if self.params:
|
|
45
|
+
d["params"] = self.params
|
|
46
|
+
if self.result is not None:
|
|
47
|
+
d["result"] = self.result
|
|
48
|
+
if self.error is not None:
|
|
49
|
+
d["error"] = self.error
|
|
50
|
+
return d
|
|
51
|
+
|
|
52
|
+
def to_json(self) -> str:
|
|
53
|
+
return json.dumps(self.to_dict())
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MCPMessage":
|
|
57
|
+
return cls(
|
|
58
|
+
jsonrpc=data.get("jsonrpc", "2.0"),
|
|
59
|
+
id=data.get("id"),
|
|
60
|
+
method=data.get("method", ""),
|
|
61
|
+
params=data.get("params", {}),
|
|
62
|
+
result=data.get("result"),
|
|
63
|
+
error=data.get("error"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_json(cls, json_str: str) -> "MCPMessage":
|
|
68
|
+
return cls.from_dict(json.loads(json_str))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MCPGateway:
|
|
72
|
+
"""Security Gateway interceptor for Model Context Protocol (MCP) traffic."""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
guard: ActShield,
|
|
77
|
+
server_name: str = "upstream-mcp-server",
|
|
78
|
+
server_uri: str = "mcp://gateway.internal/v1",
|
|
79
|
+
default_trust_level: ContextTrustLevel = ContextTrustLevel.UNTRUSTED,
|
|
80
|
+
) -> None:
|
|
81
|
+
self.guard = guard
|
|
82
|
+
self.server_name = server_name
|
|
83
|
+
self.server_uri = server_uri
|
|
84
|
+
self.default_trust_level = default_trust_level
|
|
85
|
+
self._registered_tools: Dict[str, Dict[str, Any]] = {}
|
|
86
|
+
self._upstream_handlers: Dict[str, Callable[[Dict[str, Any]], Any]] = {}
|
|
87
|
+
|
|
88
|
+
def register_mcp_tool(
|
|
89
|
+
self,
|
|
90
|
+
name: str,
|
|
91
|
+
description: str,
|
|
92
|
+
handler: Callable[[Dict[str, Any]], Any],
|
|
93
|
+
required_capabilities: Optional[List[str]] = None,
|
|
94
|
+
sensitivity: SensitivityLevel = SensitivityLevel.MEDIUM,
|
|
95
|
+
input_schema: Optional[Dict[str, Any]] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Register an upstream MCP tool with ActShield security governance."""
|
|
98
|
+
caps = required_capabilities or [name]
|
|
99
|
+
self._registered_tools[name] = {
|
|
100
|
+
"name": name,
|
|
101
|
+
"description": description,
|
|
102
|
+
"inputSchema": input_schema or {"type": "object", "properties": {}},
|
|
103
|
+
"sensitivity": sensitivity,
|
|
104
|
+
"required_capabilities": caps,
|
|
105
|
+
}
|
|
106
|
+
self._upstream_handlers[name] = handler
|
|
107
|
+
|
|
108
|
+
# Register corresponding protected tool definition in ActShield
|
|
109
|
+
tool_def = ToolDefinition(
|
|
110
|
+
tool_id=f"mcp_tool_{name}",
|
|
111
|
+
name=name,
|
|
112
|
+
description=description,
|
|
113
|
+
sensitivity=sensitivity,
|
|
114
|
+
required_capabilities=caps,
|
|
115
|
+
classification="mcp",
|
|
116
|
+
metadata={"mcp_server": self.server_name, "server_uri": self.server_uri},
|
|
117
|
+
)
|
|
118
|
+
self.guard.register_tool(tool_def)
|
|
119
|
+
|
|
120
|
+
def handle_message(self, message: Union[str, Dict[str, Any], MCPMessage]) -> MCPMessage:
|
|
121
|
+
"""Process incoming MCP JSON-RPC message, enforcing ActShield security policies."""
|
|
122
|
+
if isinstance(message, str):
|
|
123
|
+
msg = MCPMessage.from_json(message)
|
|
124
|
+
elif isinstance(message, dict):
|
|
125
|
+
msg = MCPMessage.from_dict(message)
|
|
126
|
+
else:
|
|
127
|
+
msg = message
|
|
128
|
+
|
|
129
|
+
msg_id = msg.id or str(uuid.uuid4())
|
|
130
|
+
method = msg.method
|
|
131
|
+
|
|
132
|
+
if method == "tools/list":
|
|
133
|
+
return self._handle_tools_list(msg_id)
|
|
134
|
+
elif method == "tools/call":
|
|
135
|
+
return self._handle_tools_call(msg_id, msg.params)
|
|
136
|
+
elif method == "resources/read":
|
|
137
|
+
return self._handle_resources_read(msg_id, msg.params)
|
|
138
|
+
elif method == "ping":
|
|
139
|
+
return MCPMessage(id=msg_id, result={"status": "pong", "gateway": "ActShield MCP Gateway"})
|
|
140
|
+
else:
|
|
141
|
+
return MCPMessage(
|
|
142
|
+
id=msg_id,
|
|
143
|
+
error={
|
|
144
|
+
"code": -32601,
|
|
145
|
+
"message": f"Method '{method}' not supported by ActShield MCP Gateway",
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _handle_tools_list(self, msg_id: Union[str, int]) -> MCPMessage:
|
|
150
|
+
"""Return list of available governed MCP tools."""
|
|
151
|
+
tools_list = []
|
|
152
|
+
for tname, tinfo in self._registered_tools.items():
|
|
153
|
+
tools_list.append({
|
|
154
|
+
"name": tname,
|
|
155
|
+
"description": tinfo["description"],
|
|
156
|
+
"inputSchema": tinfo["inputSchema"],
|
|
157
|
+
})
|
|
158
|
+
return MCPMessage(id=msg_id, result={"tools": tools_list})
|
|
159
|
+
|
|
160
|
+
def _handle_tools_call(self, msg_id: Union[str, int], params: Dict[str, Any]) -> MCPMessage:
|
|
161
|
+
"""Intercept and evaluate MCP tool call before execution."""
|
|
162
|
+
tool_name = params.get("name", "")
|
|
163
|
+
tool_args = params.get("arguments", {})
|
|
164
|
+
|
|
165
|
+
if tool_name not in self._registered_tools:
|
|
166
|
+
return MCPMessage(
|
|
167
|
+
id=msg_id,
|
|
168
|
+
error={
|
|
169
|
+
"code": -32602,
|
|
170
|
+
"message": f"Tool '{tool_name}' not found in MCP registry",
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
tinfo = self._registered_tools[tool_name]
|
|
175
|
+
tool_def = self.guard.tool_registry.get(f"mcp_tool_{tool_name}") or self.guard.tool_registry.get(tool_name)
|
|
176
|
+
if not tool_def:
|
|
177
|
+
tool_def = ToolDefinition(
|
|
178
|
+
tool_id=f"mcp_tool_{tool_name}",
|
|
179
|
+
name=tool_name,
|
|
180
|
+
description=tinfo["description"],
|
|
181
|
+
sensitivity=tinfo["sensitivity"],
|
|
182
|
+
required_capabilities=tinfo["required_capabilities"],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
acting_agent = params.get("_agent_id") or params.get("agent_id")
|
|
186
|
+
request = ToolRequest(
|
|
187
|
+
request_id=f"req_mcp_{uuid.uuid4().hex[:8]}",
|
|
188
|
+
tool_id=tool_def.tool_id,
|
|
189
|
+
tool_name=tool_name,
|
|
190
|
+
arguments=tool_args,
|
|
191
|
+
agent_id=acting_agent,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Emit gateway interception event
|
|
195
|
+
self.guard.emit_event(
|
|
196
|
+
event_type=EventType.TOOL_REQUESTED,
|
|
197
|
+
payload={
|
|
198
|
+
"tool_id": tool_def.tool_id,
|
|
199
|
+
"tool_name": tool_name,
|
|
200
|
+
"protocol": "MCP",
|
|
201
|
+
"arguments": tool_args,
|
|
202
|
+
"sensitivity": tool_def.sensitivity.value,
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Evaluate deterministic security policy
|
|
207
|
+
decision = self.guard.evaluate_tool_invocation(tool_def, request)
|
|
208
|
+
|
|
209
|
+
if decision.is_blocked:
|
|
210
|
+
return MCPMessage(
|
|
211
|
+
id=msg_id,
|
|
212
|
+
error={
|
|
213
|
+
"code": -32000,
|
|
214
|
+
"message": f"ActShield Security Block: [{decision.reason_code}] {decision.explanation}",
|
|
215
|
+
"data": {
|
|
216
|
+
"decision": decision.action.value,
|
|
217
|
+
"risk_score": decision.risk_score,
|
|
218
|
+
"reason_code": decision.reason_code,
|
|
219
|
+
"explanation": decision.explanation,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Execute upstream tool safely
|
|
225
|
+
handler = self._upstream_handlers[tool_name]
|
|
226
|
+
try:
|
|
227
|
+
raw_output = handler(tool_args)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return MCPMessage(
|
|
230
|
+
id=msg_id,
|
|
231
|
+
error={"code": -32603, "message": f"Upstream execution error: {str(e)}"},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Automatically ingest result into Context with provenance and trust tracking
|
|
235
|
+
content_str = json.dumps(raw_output) if not isinstance(raw_output, str) else raw_output
|
|
236
|
+
is_tainted = any(indicator in content_str.lower() for indicator in ("override", "system directive", "exfiltrate", "ignore previous"))
|
|
237
|
+
taint_level = TaintState.TAINTED if is_tainted else TaintState.CLEAN
|
|
238
|
+
|
|
239
|
+
ctx = self.guard.context(
|
|
240
|
+
data=content_str,
|
|
241
|
+
source=ContextSource.EXTERNAL_MCP,
|
|
242
|
+
source_uri=f"{self.server_uri}/tools/{tool_name}",
|
|
243
|
+
trust_level=self.default_trust_level,
|
|
244
|
+
taint_state=taint_level,
|
|
245
|
+
metadata={"mcp_tool": tool_name, "mcp_server": self.server_name},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Emit tool completed event
|
|
249
|
+
self.guard.emit_event(
|
|
250
|
+
event_type=EventType.TOOL_COMPLETED,
|
|
251
|
+
context_id=ctx.context_id,
|
|
252
|
+
payload={
|
|
253
|
+
"tool_name": tool_name,
|
|
254
|
+
"status": "SUCCESS",
|
|
255
|
+
"taint_state": taint_level.value,
|
|
256
|
+
"trust_level": self.default_trust_level.value,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return MCPMessage(
|
|
261
|
+
id=msg_id,
|
|
262
|
+
result={
|
|
263
|
+
"content": [{"type": "text", "text": content_str}],
|
|
264
|
+
"_actshield": {
|
|
265
|
+
"context_id": ctx.context_id,
|
|
266
|
+
"taint_state": taint_level.value,
|
|
267
|
+
"trust_level": self.default_trust_level.value,
|
|
268
|
+
"decision": decision.action.value,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _handle_resources_read(self, msg_id: Union[str, int], params: Dict[str, Any]) -> MCPMessage:
|
|
274
|
+
"""Handle MCP resource reading with provenance tagging."""
|
|
275
|
+
uri = params.get("uri", "")
|
|
276
|
+
ctx = self.guard.context(
|
|
277
|
+
data=f"MCP Resource content from {uri}",
|
|
278
|
+
source=ContextSource.EXTERNAL_MCP,
|
|
279
|
+
source_uri=uri,
|
|
280
|
+
trust_level=self.default_trust_level,
|
|
281
|
+
taint_state=TaintState.UNTRUSTED,
|
|
282
|
+
)
|
|
283
|
+
return MCPMessage(
|
|
284
|
+
id=msg_id,
|
|
285
|
+
result={
|
|
286
|
+
"contents": [{"uri": uri, "text": ctx.data}],
|
|
287
|
+
"_actshield": {"context_id": ctx.context_id, "taint_state": ctx.taint_state.value},
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|