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.
@@ -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