agent-memory-guard 0.2.2__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_memory_guard/__init__.py +24 -0
- agent_memory_guard/detectors/__init__.py +18 -0
- agent_memory_guard/detectors/anomaly.py +89 -0
- agent_memory_guard/detectors/base.py +23 -0
- agent_memory_guard/detectors/injection.py +69 -0
- agent_memory_guard/detectors/leakage.py +70 -0
- agent_memory_guard/detectors/protected_keys.py +45 -0
- agent_memory_guard/events.py +50 -0
- agent_memory_guard/exceptions.py +24 -0
- agent_memory_guard/guard.py +393 -0
- agent_memory_guard/integrations/__init__.py +3 -0
- agent_memory_guard/integrations/langchain.py +96 -0
- agent_memory_guard/integrity.py +60 -0
- agent_memory_guard/policies/__init__.py +3 -0
- agent_memory_guard/policies/policy.py +162 -0
- agent_memory_guard/storage/__init__.py +4 -0
- agent_memory_guard/storage/memory_store.py +53 -0
- agent_memory_guard/storage/snapshots.py +73 -0
- agent_memory_guard-0.2.2.dist-info/METADATA +176 -0
- agent_memory_guard-0.2.2.dist-info/RECORD +23 -0
- agent_memory_guard-0.2.2.dist-info/WHEEL +5 -0
- agent_memory_guard-0.2.2.dist-info/licenses/LICENSE.md +1 -0
- agent_memory_guard-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""OWASP Agent Memory Guard — runtime defense against memory poisoning (ASI06)."""
|
|
2
|
+
|
|
3
|
+
from agent_memory_guard.events import Action, SecurityEvent, Severity
|
|
4
|
+
from agent_memory_guard.exceptions import (
|
|
5
|
+
IntegrityError,
|
|
6
|
+
MemoryGuardError,
|
|
7
|
+
PolicyViolation,
|
|
8
|
+
)
|
|
9
|
+
from agent_memory_guard.guard import MemoryGuard
|
|
10
|
+
from agent_memory_guard.policies.policy import Policy
|
|
11
|
+
|
|
12
|
+
__version__ = "0.2.2"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MemoryGuard",
|
|
16
|
+
"Policy",
|
|
17
|
+
"SecurityEvent",
|
|
18
|
+
"Severity",
|
|
19
|
+
"Action",
|
|
20
|
+
"MemoryGuardError",
|
|
21
|
+
"PolicyViolation",
|
|
22
|
+
"IntegrityError",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from agent_memory_guard.detectors.anomaly import (
|
|
2
|
+
RapidChangeDetector,
|
|
3
|
+
SizeAnomalyDetector,
|
|
4
|
+
)
|
|
5
|
+
from agent_memory_guard.detectors.base import DetectionResult, Detector
|
|
6
|
+
from agent_memory_guard.detectors.injection import PromptInjectionDetector
|
|
7
|
+
from agent_memory_guard.detectors.leakage import SensitiveDataDetector
|
|
8
|
+
from agent_memory_guard.detectors.protected_keys import ProtectedKeyDetector
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Detector",
|
|
12
|
+
"DetectionResult",
|
|
13
|
+
"PromptInjectionDetector",
|
|
14
|
+
"SensitiveDataDetector",
|
|
15
|
+
"SizeAnomalyDetector",
|
|
16
|
+
"RapidChangeDetector",
|
|
17
|
+
"ProtectedKeyDetector",
|
|
18
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections import deque
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_memory_guard.detectors.base import DetectionResult
|
|
8
|
+
from agent_memory_guard.detectors.injection import _stringify
|
|
9
|
+
from agent_memory_guard.events import Severity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SizeAnomalyDetector:
|
|
13
|
+
"""Flags memory writes that are unusually large or grow unusually fast."""
|
|
14
|
+
|
|
15
|
+
name = "size_anomaly"
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
max_bytes: int = 64 * 1024,
|
|
20
|
+
growth_factor: float = 10.0,
|
|
21
|
+
severity: Severity = Severity.MEDIUM,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._max_bytes = max_bytes
|
|
24
|
+
self._growth_factor = growth_factor
|
|
25
|
+
self._last_size: dict[str, int] = {}
|
|
26
|
+
self._severity = severity
|
|
27
|
+
|
|
28
|
+
def inspect(self, key: str, value: Any, *, operation: str) -> DetectionResult:
|
|
29
|
+
size = len(_stringify(value).encode("utf-8"))
|
|
30
|
+
previous = self._last_size.get(key)
|
|
31
|
+
self._last_size[key] = size
|
|
32
|
+
|
|
33
|
+
if size > self._max_bytes:
|
|
34
|
+
return DetectionResult(
|
|
35
|
+
detector=self.name,
|
|
36
|
+
matched=True,
|
|
37
|
+
severity=self._severity,
|
|
38
|
+
message=f"Memory value for '{key}' exceeds size limit ({size} > {self._max_bytes} bytes)",
|
|
39
|
+
metadata={"size": size, "limit": self._max_bytes},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if previous and previous > 0 and size > previous * self._growth_factor:
|
|
43
|
+
return DetectionResult(
|
|
44
|
+
detector=self.name,
|
|
45
|
+
matched=True,
|
|
46
|
+
severity=self._severity,
|
|
47
|
+
message=f"Memory value for '{key}' grew {size / previous:.1f}x in one write",
|
|
48
|
+
metadata={"size": size, "previous": previous},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return DetectionResult(self.name, matched=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RapidChangeDetector:
|
|
55
|
+
"""Flags suspiciously high write frequency on a single key (churn attack)."""
|
|
56
|
+
|
|
57
|
+
name = "rapid_change"
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
window_seconds: float = 5.0,
|
|
62
|
+
max_writes: int = 20,
|
|
63
|
+
severity: Severity = Severity.MEDIUM,
|
|
64
|
+
) -> None:
|
|
65
|
+
self._window = window_seconds
|
|
66
|
+
self._max = max_writes
|
|
67
|
+
self._writes: dict[str, deque[float]] = {}
|
|
68
|
+
self._severity = severity
|
|
69
|
+
|
|
70
|
+
def inspect(self, key: str, value: Any, *, operation: str) -> DetectionResult:
|
|
71
|
+
if operation != "write":
|
|
72
|
+
return DetectionResult(self.name, matched=False)
|
|
73
|
+
|
|
74
|
+
now = time.monotonic()
|
|
75
|
+
history = self._writes.setdefault(key, deque())
|
|
76
|
+
history.append(now)
|
|
77
|
+
cutoff = now - self._window
|
|
78
|
+
while history and history[0] < cutoff:
|
|
79
|
+
history.popleft()
|
|
80
|
+
|
|
81
|
+
if len(history) > self._max:
|
|
82
|
+
return DetectionResult(
|
|
83
|
+
detector=self.name,
|
|
84
|
+
matched=True,
|
|
85
|
+
severity=self._severity,
|
|
86
|
+
message=f"Rapid write churn on '{key}': {len(history)} writes in {self._window}s",
|
|
87
|
+
metadata={"writes": len(history), "window": self._window},
|
|
88
|
+
)
|
|
89
|
+
return DetectionResult(self.name, matched=False)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from agent_memory_guard.events import Severity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DetectionResult:
|
|
11
|
+
"""Verdict returned by a single detector."""
|
|
12
|
+
|
|
13
|
+
detector: str
|
|
14
|
+
matched: bool
|
|
15
|
+
severity: Severity = Severity.INFO
|
|
16
|
+
message: str = ""
|
|
17
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Detector(Protocol):
|
|
21
|
+
name: str
|
|
22
|
+
|
|
23
|
+
def inspect(self, key: str, value: Any, *, operation: str) -> DetectionResult: ...
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_memory_guard.detectors.base import DetectionResult
|
|
8
|
+
from agent_memory_guard.events import Severity
|
|
9
|
+
|
|
10
|
+
DEFAULT_INJECTION_PATTERNS: tuple[str, ...] = (
|
|
11
|
+
r"ignore (?:all |any |the )?(?:previous|prior|above) (?:instructions|messages|rules)",
|
|
12
|
+
r"disregard (?:all |any |the )?(?:previous|prior|above) (?:instructions|messages|rules)",
|
|
13
|
+
r"forget (?:all |any |the )?(?:previous|prior|above) (?:instructions|messages|rules)",
|
|
14
|
+
r"\byou are now\b.{0,40}(?:dan|jailbroken|admin|root|developer mode)",
|
|
15
|
+
r"\bsystem\s*[:\-]\s*you (?:are|must|will)",
|
|
16
|
+
r"</?\s*(?:system|assistant|tool)\s*>",
|
|
17
|
+
r"\bact as (?:an? )?(?:admin|root|system|developer|unrestricted)",
|
|
18
|
+
r"\b(?:reveal|print|leak|dump|exfiltrate)\s+(?:the\s+)?(?:system\s+)?(?:prompt|instructions|secrets|api[_\s-]?key)",
|
|
19
|
+
r"\bnew (?:instructions|directive|persona)\s*[:\-]",
|
|
20
|
+
r"\boverride\s+(?:safety|security|guardrails|policy)",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PromptInjectionDetector:
|
|
25
|
+
"""Regex-based screen for indirect prompt-injection markers in memory values."""
|
|
26
|
+
|
|
27
|
+
name = "prompt_injection"
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
patterns: Iterable[str] = DEFAULT_INJECTION_PATTERNS,
|
|
32
|
+
severity: Severity = Severity.HIGH,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._patterns = [re.compile(p, re.IGNORECASE | re.DOTALL) for p in patterns]
|
|
35
|
+
self._severity = severity
|
|
36
|
+
|
|
37
|
+
def inspect(self, key: str, value: Any, *, operation: str) -> DetectionResult:
|
|
38
|
+
text = _stringify(value)
|
|
39
|
+
if not text:
|
|
40
|
+
return DetectionResult(self.name, matched=False)
|
|
41
|
+
|
|
42
|
+
hits: list[str] = []
|
|
43
|
+
for pattern in self._patterns:
|
|
44
|
+
match = pattern.search(text)
|
|
45
|
+
if match:
|
|
46
|
+
hits.append(match.group(0))
|
|
47
|
+
|
|
48
|
+
if not hits:
|
|
49
|
+
return DetectionResult(self.name, matched=False)
|
|
50
|
+
|
|
51
|
+
return DetectionResult(
|
|
52
|
+
detector=self.name,
|
|
53
|
+
matched=True,
|
|
54
|
+
severity=self._severity,
|
|
55
|
+
message=f"Possible prompt-injection markers in '{key}'",
|
|
56
|
+
metadata={"hits": hits[:5], "operation": operation},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _stringify(value: Any) -> str:
|
|
61
|
+
if value is None:
|
|
62
|
+
return ""
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
return value
|
|
65
|
+
if isinstance(value, (list, tuple, set)):
|
|
66
|
+
return "\n".join(_stringify(v) for v in value)
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
return "\n".join(f"{k}: {_stringify(v)}" for k, v in value.items())
|
|
69
|
+
return str(value)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_memory_guard.detectors.base import DetectionResult
|
|
8
|
+
from agent_memory_guard.detectors.injection import _stringify
|
|
9
|
+
from agent_memory_guard.events import Severity
|
|
10
|
+
|
|
11
|
+
DEFAULT_LEAKAGE_PATTERNS: dict[str, str] = {
|
|
12
|
+
"aws_access_key": r"\bAKIA[0-9A-Z]{16}\b",
|
|
13
|
+
"aws_secret_key": r"(?i)aws(.{0,20})?(secret|private)?[\s_-]?access[\s_-]?key[\s_-]?[:=][\s\"']*([A-Za-z0-9/+=]{40})",
|
|
14
|
+
"github_token": r"\bghp_[A-Za-z0-9]{36}\b",
|
|
15
|
+
"github_oauth": r"\bgho_[A-Za-z0-9]{36}\b",
|
|
16
|
+
"openai_key": r"\bsk-[A-Za-z0-9_-]{20,}\b",
|
|
17
|
+
"anthropic_key": r"\bsk-ant-[A-Za-z0-9_-]{20,}\b",
|
|
18
|
+
"google_api_key": r"\bAIza[0-9A-Za-z_-]{35}\b",
|
|
19
|
+
"slack_token": r"\bxox[abpr]-[0-9A-Za-z-]{10,}\b",
|
|
20
|
+
"private_key_pem": r"-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----",
|
|
21
|
+
"jwt": r"\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b",
|
|
22
|
+
"credit_card": r"\b(?:\d[ -]*?){13,19}\b",
|
|
23
|
+
"ssn_us": r"\b\d{3}-\d{2}-\d{4}\b",
|
|
24
|
+
"email": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SensitiveDataDetector:
|
|
29
|
+
"""Flags secrets/PII present in memory values prior to write or after read."""
|
|
30
|
+
|
|
31
|
+
name = "sensitive_data"
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
patterns: dict[str, str] | None = None,
|
|
36
|
+
ignore: Iterable[str] = ("email",),
|
|
37
|
+
severity: Severity = Severity.HIGH,
|
|
38
|
+
) -> None:
|
|
39
|
+
merged = dict(DEFAULT_LEAKAGE_PATTERNS) if patterns is None else dict(patterns)
|
|
40
|
+
for name in ignore:
|
|
41
|
+
merged.pop(name, None)
|
|
42
|
+
self._patterns = {name: re.compile(p) for name, p in merged.items()}
|
|
43
|
+
self._severity = severity
|
|
44
|
+
|
|
45
|
+
def inspect(self, key: str, value: Any, *, operation: str) -> DetectionResult:
|
|
46
|
+
text = _stringify(value)
|
|
47
|
+
if not text:
|
|
48
|
+
return DetectionResult(self.name, matched=False)
|
|
49
|
+
|
|
50
|
+
findings: list[str] = []
|
|
51
|
+
for label, pattern in self._patterns.items():
|
|
52
|
+
if pattern.search(text):
|
|
53
|
+
findings.append(label)
|
|
54
|
+
|
|
55
|
+
if not findings:
|
|
56
|
+
return DetectionResult(self.name, matched=False)
|
|
57
|
+
|
|
58
|
+
return DetectionResult(
|
|
59
|
+
detector=self.name,
|
|
60
|
+
matched=True,
|
|
61
|
+
severity=self._severity,
|
|
62
|
+
message=f"Sensitive data ({', '.join(findings)}) detected in '{key}'",
|
|
63
|
+
metadata={"categories": findings, "operation": operation},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def redact(self, value: Any) -> Any:
|
|
67
|
+
text = _stringify(value)
|
|
68
|
+
for label, pattern in self._patterns.items():
|
|
69
|
+
text = pattern.sub(f"[REDACTED:{label}]", text)
|
|
70
|
+
return text
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_memory_guard.detectors.base import DetectionResult
|
|
8
|
+
from agent_memory_guard.events import Severity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProtectedKeyDetector:
|
|
12
|
+
"""Flags writes targeting keys declared immutable by policy."""
|
|
13
|
+
|
|
14
|
+
name = "protected_key"
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
protected: Iterable[str] = (),
|
|
19
|
+
severity: Severity = Severity.CRITICAL,
|
|
20
|
+
) -> None:
|
|
21
|
+
self._patterns = list(protected)
|
|
22
|
+
self._severity = severity
|
|
23
|
+
|
|
24
|
+
def add(self, pattern: str) -> None:
|
|
25
|
+
self._patterns.append(pattern)
|
|
26
|
+
|
|
27
|
+
def matches(self, key: str) -> str | None:
|
|
28
|
+
for pattern in self._patterns:
|
|
29
|
+
if fnmatch.fnmatchcase(key, pattern):
|
|
30
|
+
return pattern
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def inspect(self, key: str, value: Any, *, operation: str) -> DetectionResult:
|
|
34
|
+
if operation != "write":
|
|
35
|
+
return DetectionResult(self.name, matched=False)
|
|
36
|
+
match = self.matches(key)
|
|
37
|
+
if not match:
|
|
38
|
+
return DetectionResult(self.name, matched=False)
|
|
39
|
+
return DetectionResult(
|
|
40
|
+
detector=self.name,
|
|
41
|
+
matched=True,
|
|
42
|
+
severity=self._severity,
|
|
43
|
+
message=f"Write to protected key '{key}' (matched pattern '{match}')",
|
|
44
|
+
metadata={"pattern": match},
|
|
45
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Severity(str, Enum):
|
|
11
|
+
INFO = "info"
|
|
12
|
+
LOW = "low"
|
|
13
|
+
MEDIUM = "medium"
|
|
14
|
+
HIGH = "high"
|
|
15
|
+
CRITICAL = "critical"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Action(str, Enum):
|
|
19
|
+
ALLOW = "allow"
|
|
20
|
+
REDACT = "redact"
|
|
21
|
+
BLOCK = "block"
|
|
22
|
+
QUARANTINE = "quarantine"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class SecurityEvent:
|
|
27
|
+
"""Structured record of a guard decision, suitable for SIEM forwarding."""
|
|
28
|
+
|
|
29
|
+
detector: str
|
|
30
|
+
severity: Severity
|
|
31
|
+
action: Action
|
|
32
|
+
key: str
|
|
33
|
+
message: str
|
|
34
|
+
operation: str = "write"
|
|
35
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
timestamp: float = field(default_factory=time.time)
|
|
37
|
+
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict[str, Any]:
|
|
40
|
+
return {
|
|
41
|
+
"event_id": self.event_id,
|
|
42
|
+
"timestamp": self.timestamp,
|
|
43
|
+
"detector": self.detector,
|
|
44
|
+
"severity": self.severity.value,
|
|
45
|
+
"action": self.action.value,
|
|
46
|
+
"operation": self.operation,
|
|
47
|
+
"key": self.key,
|
|
48
|
+
"message": self.message,
|
|
49
|
+
"metadata": self.metadata,
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MemoryGuardError(Exception):
|
|
5
|
+
"""Base exception for Agent Memory Guard."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PolicyViolation(MemoryGuardError):
|
|
9
|
+
"""Raised when a memory operation violates an enforcement policy."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, rule: str | None = None, key: str | None = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.rule = rule
|
|
14
|
+
self.key = key
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IntegrityError(MemoryGuardError):
|
|
18
|
+
"""Raised when a memory entry fails its integrity baseline check."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message: str, key: str, expected: str, actual: str):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.key = key
|
|
23
|
+
self.expected = expected
|
|
24
|
+
self.actual = actual
|