agent_os_kernel 3.1.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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""HMAC signing and replay protection for MCP messages."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import logging
|
|
11
|
+
import secrets
|
|
12
|
+
import threading
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime, timedelta, timezone
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
from agent_os.mcp_protocols import InMemoryNonceStore, MCPNonceStore
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class MCPSignedEnvelope:
|
|
25
|
+
"""A signed MCP message envelope."""
|
|
26
|
+
|
|
27
|
+
payload: str
|
|
28
|
+
nonce: str
|
|
29
|
+
timestamp: datetime
|
|
30
|
+
signature: str
|
|
31
|
+
sender_id: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class MCPVerificationResult:
|
|
36
|
+
"""The result of verifying a signed MCP envelope."""
|
|
37
|
+
|
|
38
|
+
is_valid: bool
|
|
39
|
+
payload: str | None = None
|
|
40
|
+
sender_id: str | None = None
|
|
41
|
+
failure_reason: str | None = None
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def success(cls, payload: str, sender_id: str | None) -> "MCPVerificationResult":
|
|
45
|
+
return cls(is_valid=True, payload=payload, sender_id=sender_id)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def failed(cls, reason: str) -> "MCPVerificationResult":
|
|
49
|
+
return cls(is_valid=False, failure_reason=reason)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _utcnow() -> datetime:
|
|
53
|
+
return datetime.now(timezone.utc)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MCPMessageSigner:
|
|
57
|
+
"""Sign and verify MCP messages with replay protection.
|
|
58
|
+
|
|
59
|
+
The signer produces tamper-evident envelopes and tracks nonces inside a
|
|
60
|
+
replay window so previously accepted messages cannot be replayed
|
|
61
|
+
indefinitely. Persistence and nonce generation are injectable to support
|
|
62
|
+
deterministic tests and external storage backends.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
signing_key: bytes,
|
|
68
|
+
*,
|
|
69
|
+
replay_window: timedelta = timedelta(minutes=5),
|
|
70
|
+
nonce_cache_cleanup_interval: timedelta = timedelta(minutes=10),
|
|
71
|
+
max_nonce_cache_size: int = 10_000,
|
|
72
|
+
nonce_store: MCPNonceStore | None = None,
|
|
73
|
+
nonce_generator: Callable[[], str] | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Initialize the signer and replay-protection settings.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
signing_key: Shared secret used to compute HMAC signatures.
|
|
79
|
+
replay_window: Maximum permitted clock skew and replay horizon.
|
|
80
|
+
nonce_cache_cleanup_interval: Minimum interval between automatic
|
|
81
|
+
nonce cleanup passes.
|
|
82
|
+
max_nonce_cache_size: Maximum number of nonces retained by the
|
|
83
|
+
default in-memory nonce store.
|
|
84
|
+
nonce_store: Optional nonce persistence backend. Defaults to the
|
|
85
|
+
in-memory LRU nonce store.
|
|
86
|
+
nonce_generator: Optional nonce factory for deterministic testing
|
|
87
|
+
or custom nonce generation strategies.
|
|
88
|
+
"""
|
|
89
|
+
if signing_key is None:
|
|
90
|
+
raise ValueError("signing_key must not be None")
|
|
91
|
+
if len(signing_key) < 32:
|
|
92
|
+
raise ValueError("signing_key must be at least 32 bytes")
|
|
93
|
+
if replay_window <= timedelta(0):
|
|
94
|
+
raise ValueError("replay_window must be positive")
|
|
95
|
+
if nonce_cache_cleanup_interval <= timedelta(0):
|
|
96
|
+
raise ValueError("nonce_cache_cleanup_interval must be positive")
|
|
97
|
+
if max_nonce_cache_size <= 0:
|
|
98
|
+
raise ValueError("max_nonce_cache_size must be positive")
|
|
99
|
+
|
|
100
|
+
self._signing_key = signing_key
|
|
101
|
+
self.replay_window = replay_window
|
|
102
|
+
self.nonce_cache_cleanup_interval = nonce_cache_cleanup_interval
|
|
103
|
+
self.max_nonce_cache_size = max_nonce_cache_size
|
|
104
|
+
self._lock = threading.Lock()
|
|
105
|
+
self._nonce_store = nonce_store or InMemoryNonceStore(
|
|
106
|
+
max_entries=max_nonce_cache_size,
|
|
107
|
+
)
|
|
108
|
+
self._nonce_generator = nonce_generator or (lambda: uuid.uuid4().hex)
|
|
109
|
+
self._last_cleanup = _utcnow()
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_base64_key(cls, base64_key: str) -> "MCPMessageSigner":
|
|
113
|
+
"""Build a signer from a base64-encoded shared secret.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
base64_key: Shared secret encoded as ASCII base64 text.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A configured ``MCPMessageSigner`` instance using the decoded key
|
|
120
|
+
bytes.
|
|
121
|
+
"""
|
|
122
|
+
if not base64_key or not base64_key.strip():
|
|
123
|
+
raise ValueError("base64_key must not be empty")
|
|
124
|
+
decoded = base64.b64decode(base64_key.encode("ascii"), validate=True)
|
|
125
|
+
return cls(decoded)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def generate_key() -> bytes:
|
|
129
|
+
"""Return a new 256-bit signing key.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
A cryptographically random 32-byte signing key.
|
|
133
|
+
"""
|
|
134
|
+
return secrets.token_bytes(32)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def cached_nonce_count(self) -> int:
|
|
138
|
+
"""Return the number of tracked nonces.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The number of replay-protection nonces currently retained by the
|
|
142
|
+
backing store when the store exposes a ``count`` helper.
|
|
143
|
+
"""
|
|
144
|
+
with self._lock:
|
|
145
|
+
count = getattr(self._nonce_store, "count", None)
|
|
146
|
+
return int(count()) if callable(count) else 0
|
|
147
|
+
|
|
148
|
+
def sign_message(self, payload: str, sender_id: str | None = None) -> MCPSignedEnvelope:
|
|
149
|
+
"""Create a signed message envelope.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
payload: Serialized MCP payload to sign.
|
|
153
|
+
sender_id: Optional sender identifier included in the signature.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A signed envelope containing the payload, nonce, timestamp, and
|
|
157
|
+
computed signature.
|
|
158
|
+
"""
|
|
159
|
+
if payload is None:
|
|
160
|
+
raise ValueError("payload must not be None")
|
|
161
|
+
if not payload.strip():
|
|
162
|
+
raise ValueError("payload must not be empty")
|
|
163
|
+
|
|
164
|
+
timestamp = _utcnow()
|
|
165
|
+
nonce = self._nonce_generator()
|
|
166
|
+
signature = self._compute_signature(
|
|
167
|
+
nonce=nonce,
|
|
168
|
+
timestamp=timestamp,
|
|
169
|
+
sender_id=sender_id,
|
|
170
|
+
payload=payload,
|
|
171
|
+
)
|
|
172
|
+
return MCPSignedEnvelope(
|
|
173
|
+
payload=payload,
|
|
174
|
+
nonce=nonce,
|
|
175
|
+
timestamp=timestamp,
|
|
176
|
+
sender_id=sender_id,
|
|
177
|
+
signature=signature,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def verify_message(self, envelope: MCPSignedEnvelope) -> MCPVerificationResult:
|
|
181
|
+
"""Verify an envelope's signature and replay metadata.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
envelope: Signed message envelope to validate.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
``MCPVerificationResult`` describing whether the envelope is valid
|
|
188
|
+
and, on success, exposing the verified payload and sender identity.
|
|
189
|
+
Invalid signatures, replayed nonces, or unexpected errors fail
|
|
190
|
+
closed.
|
|
191
|
+
"""
|
|
192
|
+
if envelope is None:
|
|
193
|
+
raise ValueError("envelope must not be None")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
now = _utcnow()
|
|
197
|
+
age = now - envelope.timestamp
|
|
198
|
+
if age > self.replay_window or age < -self.replay_window:
|
|
199
|
+
return MCPVerificationResult.failed("Message timestamp outside replay window.")
|
|
200
|
+
|
|
201
|
+
with self._lock:
|
|
202
|
+
self._maybe_cleanup_locked(now)
|
|
203
|
+
if self._nonce_store.has(envelope.nonce):
|
|
204
|
+
logger.warning("Duplicate MCP nonce detected: %s", envelope.nonce)
|
|
205
|
+
return MCPVerificationResult.failed("Duplicate nonce (replay detected).")
|
|
206
|
+
self._nonce_store.add(
|
|
207
|
+
envelope.nonce,
|
|
208
|
+
envelope.timestamp + self.replay_window,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
expected_signature = self._compute_signature(
|
|
212
|
+
nonce=envelope.nonce,
|
|
213
|
+
timestamp=envelope.timestamp,
|
|
214
|
+
sender_id=envelope.sender_id,
|
|
215
|
+
payload=envelope.payload,
|
|
216
|
+
)
|
|
217
|
+
if not hmac.compare_digest(expected_signature, envelope.signature):
|
|
218
|
+
return MCPVerificationResult.failed("Invalid signature.")
|
|
219
|
+
|
|
220
|
+
return MCPVerificationResult.success(envelope.payload, envelope.sender_id)
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
logger.error("MCP signature verification failed closed", exc_info=True)
|
|
223
|
+
return MCPVerificationResult.failed(f"Verification error (fail-closed): {exc}")
|
|
224
|
+
|
|
225
|
+
def cleanup_nonce_cache(self) -> int:
|
|
226
|
+
"""Remove expired nonces and return the number removed.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
The number of expired nonces removed from the backing store.
|
|
230
|
+
"""
|
|
231
|
+
with self._lock:
|
|
232
|
+
return self._cleanup_nonce_cache_locked(_utcnow())
|
|
233
|
+
|
|
234
|
+
def _compute_signature(
|
|
235
|
+
self,
|
|
236
|
+
*,
|
|
237
|
+
nonce: str,
|
|
238
|
+
timestamp: datetime,
|
|
239
|
+
sender_id: str | None,
|
|
240
|
+
payload: str,
|
|
241
|
+
) -> str:
|
|
242
|
+
canonical = self._build_canonical_string(
|
|
243
|
+
nonce=nonce,
|
|
244
|
+
timestamp=timestamp,
|
|
245
|
+
sender_id=sender_id,
|
|
246
|
+
payload=payload,
|
|
247
|
+
)
|
|
248
|
+
digest = hmac.new(
|
|
249
|
+
self._signing_key,
|
|
250
|
+
canonical.encode("utf-8"),
|
|
251
|
+
hashlib.sha256,
|
|
252
|
+
).digest()
|
|
253
|
+
return base64.b64encode(digest).decode("ascii")
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def _build_canonical_string(
|
|
257
|
+
*,
|
|
258
|
+
nonce: str,
|
|
259
|
+
timestamp: datetime,
|
|
260
|
+
sender_id: str | None,
|
|
261
|
+
payload: str,
|
|
262
|
+
) -> str:
|
|
263
|
+
timestamp_ms = int(timestamp.timestamp() * 1000)
|
|
264
|
+
return f"{nonce}|{timestamp_ms}|{sender_id or ''}|{payload}"
|
|
265
|
+
|
|
266
|
+
def _maybe_cleanup_locked(self, now: datetime) -> None:
|
|
267
|
+
if now - self._last_cleanup >= self.nonce_cache_cleanup_interval:
|
|
268
|
+
self._cleanup_nonce_cache_locked(now)
|
|
269
|
+
|
|
270
|
+
def _cleanup_nonce_cache_locked(self, now: datetime) -> int:
|
|
271
|
+
expired = self._nonce_store.cleanup()
|
|
272
|
+
self._last_cleanup = now
|
|
273
|
+
return expired
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Persistence protocols and in-memory defaults for MCP governance components."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
from collections import OrderedDict
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any, Callable, Protocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _utcnow() -> datetime:
|
|
14
|
+
return datetime.now(timezone.utc)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCPSessionStore(Protocol):
|
|
18
|
+
"""Persistence contract for authenticated MCP sessions."""
|
|
19
|
+
|
|
20
|
+
def get(self, session_id: str) -> Any | None:
|
|
21
|
+
"""Return the stored session for *session_id*, if present."""
|
|
22
|
+
|
|
23
|
+
def set(self, session: Any) -> None:
|
|
24
|
+
"""Persist *session* keyed by its session identifier."""
|
|
25
|
+
|
|
26
|
+
def delete(self, session_id: str) -> bool:
|
|
27
|
+
"""Delete *session_id* and return ``True`` when it existed."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MCPNonceStore(Protocol):
|
|
31
|
+
"""Persistence contract for replay-protection nonces."""
|
|
32
|
+
|
|
33
|
+
def has(self, nonce: str) -> bool:
|
|
34
|
+
"""Return ``True`` when *nonce* is still tracked."""
|
|
35
|
+
|
|
36
|
+
def add(self, nonce: str, expires_at: datetime) -> None:
|
|
37
|
+
"""Track *nonce* until *expires_at*."""
|
|
38
|
+
|
|
39
|
+
def cleanup(self) -> int:
|
|
40
|
+
"""Remove expired entries and return the number removed."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MCPRateLimitStore(Protocol):
|
|
44
|
+
"""Persistence contract for per-agent rate-limit buckets."""
|
|
45
|
+
|
|
46
|
+
def get_bucket(self, agent_id: str) -> Any | None:
|
|
47
|
+
"""Return the bucket state stored for *agent_id*, if present."""
|
|
48
|
+
|
|
49
|
+
def set_bucket(self, agent_id: str, bucket: Any) -> None:
|
|
50
|
+
"""Persist *bucket* for *agent_id*."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MCPAuditSink(Protocol):
|
|
54
|
+
"""Persistence contract for structured audit records."""
|
|
55
|
+
|
|
56
|
+
def record(self, entry: dict[str, Any]) -> None:
|
|
57
|
+
"""Persist a structured audit *entry*."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class InMemorySessionStore:
|
|
61
|
+
"""Thread-safe in-memory session storage."""
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self._sessions: dict[str, Any] = {}
|
|
65
|
+
self._lock = threading.Lock()
|
|
66
|
+
|
|
67
|
+
def get(self, session_id: str) -> Any | None:
|
|
68
|
+
with self._lock:
|
|
69
|
+
return self._sessions.get(session_id)
|
|
70
|
+
|
|
71
|
+
def set(self, session: Any) -> None:
|
|
72
|
+
session_id = getattr(session, "token", None)
|
|
73
|
+
if not isinstance(session_id, str) or not session_id:
|
|
74
|
+
raise ValueError("session must provide a non-empty string token")
|
|
75
|
+
with self._lock:
|
|
76
|
+
self._sessions[session_id] = session
|
|
77
|
+
|
|
78
|
+
def delete(self, session_id: str) -> bool:
|
|
79
|
+
with self._lock:
|
|
80
|
+
return self._sessions.pop(session_id, None) is not None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InMemoryNonceStore:
|
|
84
|
+
"""Thread-safe in-memory nonce storage with TTL cleanup and eviction."""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
clock: Callable[[], datetime] = _utcnow,
|
|
90
|
+
max_entries: int = 10_000,
|
|
91
|
+
) -> None:
|
|
92
|
+
if max_entries <= 0:
|
|
93
|
+
raise ValueError("max_entries must be positive")
|
|
94
|
+
self._clock = clock
|
|
95
|
+
self._max_entries = max_entries
|
|
96
|
+
self._nonces: OrderedDict[str, datetime] = OrderedDict()
|
|
97
|
+
self._lock = threading.Lock()
|
|
98
|
+
|
|
99
|
+
def has(self, nonce: str) -> bool:
|
|
100
|
+
with self._lock:
|
|
101
|
+
expires_at = self._nonces.get(nonce)
|
|
102
|
+
if expires_at is None:
|
|
103
|
+
return False
|
|
104
|
+
if expires_at <= self._clock():
|
|
105
|
+
self._nonces.pop(nonce, None)
|
|
106
|
+
return False
|
|
107
|
+
self._nonces.move_to_end(nonce)
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def add(self, nonce: str, expires_at: datetime) -> None:
|
|
111
|
+
with self._lock:
|
|
112
|
+
self._nonces[nonce] = expires_at
|
|
113
|
+
self._nonces.move_to_end(nonce)
|
|
114
|
+
while len(self._nonces) > self._max_entries:
|
|
115
|
+
self._nonces.popitem(last=False)
|
|
116
|
+
|
|
117
|
+
def cleanup(self) -> int:
|
|
118
|
+
removed = 0
|
|
119
|
+
now = self._clock()
|
|
120
|
+
with self._lock:
|
|
121
|
+
expired = [nonce for nonce, expires_at in self._nonces.items() if expires_at <= now]
|
|
122
|
+
for nonce in expired:
|
|
123
|
+
self._nonces.pop(nonce, None)
|
|
124
|
+
removed = len(expired)
|
|
125
|
+
return removed
|
|
126
|
+
|
|
127
|
+
def count(self) -> int:
|
|
128
|
+
with self._lock:
|
|
129
|
+
return len(self._nonces)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class InMemoryRateLimitStore:
|
|
133
|
+
"""Thread-safe in-memory rate-limit bucket storage."""
|
|
134
|
+
|
|
135
|
+
def __init__(self) -> None:
|
|
136
|
+
self._buckets: dict[str, Any] = {}
|
|
137
|
+
self._lock = threading.Lock()
|
|
138
|
+
|
|
139
|
+
def get_bucket(self, agent_id: str) -> Any | None:
|
|
140
|
+
with self._lock:
|
|
141
|
+
return self._buckets.get(agent_id)
|
|
142
|
+
|
|
143
|
+
def set_bucket(self, agent_id: str, bucket: Any) -> None:
|
|
144
|
+
with self._lock:
|
|
145
|
+
self._buckets[agent_id] = bucket
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class InMemoryAuditSink:
|
|
149
|
+
"""Thread-safe in-memory audit sink for structured records."""
|
|
150
|
+
|
|
151
|
+
def __init__(self) -> None:
|
|
152
|
+
self._entries: list[dict[str, Any]] = []
|
|
153
|
+
self._lock = threading.Lock()
|
|
154
|
+
|
|
155
|
+
def record(self, entry: dict[str, Any]) -> None:
|
|
156
|
+
with self._lock:
|
|
157
|
+
self._entries.append(dict(entry))
|
|
158
|
+
|
|
159
|
+
def entries(self) -> list[dict[str, Any]]:
|
|
160
|
+
with self._lock:
|
|
161
|
+
return [dict(entry) for entry in self._entries]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Scan MCP tool responses for prompt injection and data leakage."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from agent_os.credential_redactor import CredentialRedactor
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_INSTRUCTION_TAG_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
16
|
+
re.compile(
|
|
17
|
+
r"<(?:important|system|instruction|instructions|hidden|inject|admin|override|prompt|context|role)\b[^>]*>",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
),
|
|
20
|
+
re.compile(r"\[(?:system|admin|instructions?)\]", re.IGNORECASE),
|
|
21
|
+
)
|
|
22
|
+
_IMPERATIVE_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
23
|
+
re.compile(r"ignore\s+(?:all\s+)?previous\s+(?:instructions?|context|rules?)", re.IGNORECASE),
|
|
24
|
+
re.compile(
|
|
25
|
+
r"(?:forget|disregard|override)\s+(?:all\s+)?(?:previous|above|prior|earlier)",
|
|
26
|
+
re.IGNORECASE,
|
|
27
|
+
),
|
|
28
|
+
re.compile(r"\bexecute\s+this\b", re.IGNORECASE),
|
|
29
|
+
re.compile(r"\byou\s+are\s+now\b", re.IGNORECASE),
|
|
30
|
+
re.compile(r"\bnew\s+(?:role|instruction|directive|persona)\s*:", re.IGNORECASE),
|
|
31
|
+
re.compile(r"\bfrom\s+now\s+on\b", re.IGNORECASE),
|
|
32
|
+
re.compile(r"\bdo\s+not\s+(?:follow|obey|listen)\b", re.IGNORECASE),
|
|
33
|
+
)
|
|
34
|
+
_URL_PATTERN = re.compile(r"https?://[^\s<>'\"]+", re.IGNORECASE)
|
|
35
|
+
_EXFILTRATION_URL_PATTERN = re.compile(
|
|
36
|
+
r"(?i)(?:\b(?:api[_-]?key|token|secret|payload|data|dump|upload|exfil|webhook)\b|webhook\.site|requestbin|pastebin|ngrok|transfer\.sh)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class MCPResponseThreat:
|
|
42
|
+
"""A threat detected in tool output."""
|
|
43
|
+
|
|
44
|
+
category: str
|
|
45
|
+
description: str
|
|
46
|
+
matched_pattern: str | None = None
|
|
47
|
+
details: dict[str, str] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class MCPResponseScanResult:
|
|
52
|
+
"""Result of scanning an MCP tool response."""
|
|
53
|
+
|
|
54
|
+
is_safe: bool
|
|
55
|
+
tool_name: str
|
|
56
|
+
threats: list[MCPResponseThreat] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def safe(cls, tool_name: str) -> "MCPResponseScanResult":
|
|
60
|
+
return cls(is_safe=True, tool_name=tool_name, threats=[])
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def unsafe(
|
|
64
|
+
cls,
|
|
65
|
+
tool_name: str,
|
|
66
|
+
*,
|
|
67
|
+
reason: str,
|
|
68
|
+
category: str = "error",
|
|
69
|
+
) -> "MCPResponseScanResult":
|
|
70
|
+
return cls(
|
|
71
|
+
is_safe=False,
|
|
72
|
+
tool_name=tool_name,
|
|
73
|
+
threats=[MCPResponseThreat(category=category, description=reason)],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MCPResponseScanner:
|
|
78
|
+
"""Scan tool responses before they are returned to an LLM.
|
|
79
|
+
|
|
80
|
+
The scanner looks for prompt-injection markers, credential leaks, and
|
|
81
|
+
likely exfiltration URLs, returning structured findings that callers can
|
|
82
|
+
use to block or sanitize unsafe tool output.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def scan_response(
|
|
86
|
+
self,
|
|
87
|
+
response_content: str | None,
|
|
88
|
+
tool_name: str = "unknown",
|
|
89
|
+
) -> MCPResponseScanResult:
|
|
90
|
+
"""Scan tool output and return a structured safety result.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
response_content: Raw tool output to inspect.
|
|
94
|
+
tool_name: Human-readable tool name for reporting.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
An ``MCPResponseScanResult`` marked safe when no threats are found.
|
|
98
|
+
Unexpected scanner failures return an unsafe result.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
if not response_content:
|
|
102
|
+
return MCPResponseScanResult.safe(tool_name)
|
|
103
|
+
|
|
104
|
+
threats: list[MCPResponseThreat] = []
|
|
105
|
+
threats.extend(
|
|
106
|
+
self._scan_patterns(
|
|
107
|
+
response_content,
|
|
108
|
+
patterns=_INSTRUCTION_TAG_PATTERNS,
|
|
109
|
+
category="instruction_injection",
|
|
110
|
+
description="Instruction tag detected in tool response.",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
threats.extend(
|
|
114
|
+
self._scan_patterns(
|
|
115
|
+
response_content,
|
|
116
|
+
patterns=_IMPERATIVE_PATTERNS,
|
|
117
|
+
category="prompt_injection",
|
|
118
|
+
description="Imperative instruction detected in tool response.",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
threats.extend(self._scan_credential_leaks(response_content))
|
|
122
|
+
threats.extend(self._scan_exfiltration_urls(response_content))
|
|
123
|
+
|
|
124
|
+
if not threats:
|
|
125
|
+
return MCPResponseScanResult.safe(tool_name)
|
|
126
|
+
|
|
127
|
+
logger.warning(
|
|
128
|
+
"MCP response scan found %s issue(s) in tool %s",
|
|
129
|
+
len(threats),
|
|
130
|
+
tool_name,
|
|
131
|
+
)
|
|
132
|
+
return MCPResponseScanResult(is_safe=False, tool_name=tool_name, threats=threats)
|
|
133
|
+
except Exception:
|
|
134
|
+
logger.error("MCP response scanning failed closed", exc_info=True)
|
|
135
|
+
return MCPResponseScanResult.unsafe(
|
|
136
|
+
tool_name,
|
|
137
|
+
reason="Response scanner error (fail-closed).",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def sanitize_response(
|
|
141
|
+
self,
|
|
142
|
+
response_content: str | None,
|
|
143
|
+
tool_name: str = "unknown",
|
|
144
|
+
) -> tuple[str, list[MCPResponseThreat]]:
|
|
145
|
+
"""Strip instruction tags from tool output and report stripped threats.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
response_content: Raw tool output to sanitize.
|
|
149
|
+
tool_name: Human-readable tool name for reporting.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
A tuple of ``(sanitized_content, stripped_threats)``. On failure the
|
|
153
|
+
method returns an empty string and a fail-closed error finding.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
if not response_content:
|
|
157
|
+
return "", []
|
|
158
|
+
|
|
159
|
+
sanitized = response_content
|
|
160
|
+
stripped: list[MCPResponseThreat] = []
|
|
161
|
+
for pattern in _INSTRUCTION_TAG_PATTERNS:
|
|
162
|
+
for match in pattern.finditer(sanitized):
|
|
163
|
+
stripped.append(
|
|
164
|
+
MCPResponseThreat(
|
|
165
|
+
category="instruction_injection",
|
|
166
|
+
description="Instruction tag stripped from tool response.",
|
|
167
|
+
matched_pattern=match.group(0),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
sanitized = pattern.sub("", sanitized)
|
|
171
|
+
|
|
172
|
+
return sanitized, stripped
|
|
173
|
+
except Exception:
|
|
174
|
+
logger.error("MCP response sanitization failed closed", exc_info=True)
|
|
175
|
+
return "", [
|
|
176
|
+
MCPResponseThreat(
|
|
177
|
+
category="error",
|
|
178
|
+
description=(
|
|
179
|
+
f"Response sanitization failed for tool '{tool_name}' (fail-closed)."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _scan_patterns(
|
|
186
|
+
content: str,
|
|
187
|
+
*,
|
|
188
|
+
patterns: tuple[re.Pattern[str], ...],
|
|
189
|
+
category: str,
|
|
190
|
+
description: str,
|
|
191
|
+
) -> list[MCPResponseThreat]:
|
|
192
|
+
threats: list[MCPResponseThreat] = []
|
|
193
|
+
for pattern in patterns:
|
|
194
|
+
for match in pattern.finditer(content):
|
|
195
|
+
threats.append(
|
|
196
|
+
MCPResponseThreat(
|
|
197
|
+
category=category,
|
|
198
|
+
description=description,
|
|
199
|
+
matched_pattern=match.group(0),
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return threats
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _scan_credential_leaks(content: str) -> list[MCPResponseThreat]:
|
|
206
|
+
threats: list[MCPResponseThreat] = []
|
|
207
|
+
for credential_match in CredentialRedactor.find_matches(content):
|
|
208
|
+
threats.append(
|
|
209
|
+
MCPResponseThreat(
|
|
210
|
+
category="credential_leak",
|
|
211
|
+
description=f"{credential_match.name} detected in tool response.",
|
|
212
|
+
matched_pattern=credential_match.name,
|
|
213
|
+
details={"credential_type": credential_match.name},
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
return threats
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _scan_exfiltration_urls(content: str) -> list[MCPResponseThreat]:
|
|
220
|
+
threats: list[MCPResponseThreat] = []
|
|
221
|
+
for match in _URL_PATTERN.finditer(content):
|
|
222
|
+
url = match.group(0)
|
|
223
|
+
if not _EXFILTRATION_URL_PATTERN.search(url):
|
|
224
|
+
continue
|
|
225
|
+
threats.append(
|
|
226
|
+
MCPResponseThreat(
|
|
227
|
+
category="data_exfiltration",
|
|
228
|
+
description="Potential data exfiltration URL detected in tool response.",
|
|
229
|
+
matched_pattern=url,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
return threats
|