@rulemetric/proxy 0.2.2

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.
Files changed (47) hide show
  1. package/Dockerfile +28 -0
  2. package/addon/context_classifier.py +375 -0
  3. package/addon/providers.py +483 -0
  4. package/addon/reporter.py +778 -0
  5. package/addon/requirements.txt +1 -0
  6. package/addon/rulemetric_addon.py +1288 -0
  7. package/addon/secret_redactor.py +130 -0
  8. package/addon/security_scanner.py +828 -0
  9. package/addon/session_linker.py +206 -0
  10. package/addon/sse_parser.py +364 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +9 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/providers/anthropic.d.ts +8 -0
  16. package/dist/providers/anthropic.d.ts.map +1 -0
  17. package/dist/providers/anthropic.js +135 -0
  18. package/dist/providers/anthropic.js.map +1 -0
  19. package/dist/providers/base.d.ts +11 -0
  20. package/dist/providers/base.d.ts.map +1 -0
  21. package/dist/providers/base.js +43 -0
  22. package/dist/providers/base.js.map +1 -0
  23. package/dist/providers/bedrock.d.ts +8 -0
  24. package/dist/providers/bedrock.d.ts.map +1 -0
  25. package/dist/providers/bedrock.js +125 -0
  26. package/dist/providers/bedrock.js.map +1 -0
  27. package/dist/providers/gemini.d.ts +9 -0
  28. package/dist/providers/gemini.d.ts.map +1 -0
  29. package/dist/providers/gemini.js +140 -0
  30. package/dist/providers/gemini.js.map +1 -0
  31. package/dist/providers/index.d.ts +8 -0
  32. package/dist/providers/index.d.ts.map +1 -0
  33. package/dist/providers/index.js +116 -0
  34. package/dist/providers/index.js.map +1 -0
  35. package/dist/providers/openai.d.ts +9 -0
  36. package/dist/providers/openai.d.ts.map +1 -0
  37. package/dist/providers/openai.js +129 -0
  38. package/dist/providers/openai.js.map +1 -0
  39. package/dist/session-linker.d.ts +6 -0
  40. package/dist/session-linker.d.ts.map +1 -0
  41. package/dist/session-linker.js +68 -0
  42. package/dist/session-linker.js.map +1 -0
  43. package/dist/types.d.ts +41 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +2 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +33 -0
@@ -0,0 +1,130 @@
1
+ """Redact secrets from proxy snapshot content before storage.
2
+
3
+ Scans text fields for common secret patterns (API keys, tokens, passwords,
4
+ connection strings) and replaces them with redaction placeholders. This
5
+ prevents credentials accidentally included in LLM conversations from being
6
+ persisted in the RuleMetric database.
7
+
8
+ Runs in-process with zero external dependencies — pure regex, no ML models.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from typing import Any
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Secret patterns — ordered by specificity (most specific first)
18
+ # ---------------------------------------------------------------------------
19
+
20
+ # Each tuple: (name, compiled regex, group index to redact)
21
+ # The regex should match the full secret value including any prefix.
22
+
23
+ _SECRET_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
24
+ # API keys with known prefixes
25
+ ("anthropic_api_key", re.compile(r"sk-ant-[a-zA-Z0-9_-]{20,}")),
26
+ ("openai_api_key", re.compile(r"sk-[a-zA-Z0-9]{20,}")),
27
+ ("github_token", re.compile(r"gh[ps]_[a-zA-Z0-9]{36,}")),
28
+ ("github_fine_grained", re.compile(r"github_pat_[a-zA-Z0-9_]{20,}")),
29
+ ("aws_access_key", re.compile(r"AKIA[0-9A-Z]{16}")),
30
+ ("aws_secret_key", re.compile(r"(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['\"]?([a-zA-Z0-9/+=]{40})['\"]?")),
31
+ ("slack_token", re.compile(r"xox[baprs]-[a-zA-Z0-9-]+")),
32
+ ("stripe_key", re.compile(r"[sr]k_(live|test)_[a-zA-Z0-9]{20,}")),
33
+ ("npm_token", re.compile(r"npm_[a-zA-Z0-9]{36,}")),
34
+ ("supabase_key", re.compile(r"eyJ[a-zA-Z0-9_-]{100,}\.[a-zA-Z0-9_-]{100,}\.[a-zA-Z0-9_-]{40,}")),
35
+
36
+ # Connection strings with embedded credentials
37
+ ("postgres_url", re.compile(r"postgres(?:ql)?://[^:]+:[^@\s]+@[^\s\"']+")),
38
+ ("mysql_url", re.compile(r"mysql://[^:]+:[^@\s]+@[^\s\"']+")),
39
+ ("mongodb_url", re.compile(r"mongodb(?:\+srv)?://[^:]+:[^@\s]+@[^\s\"']+")),
40
+ ("redis_url", re.compile(r"redis://:[^@\s]+@[^\s\"']+")),
41
+
42
+ # Generic key=value patterns for known env var names
43
+ ("env_secret", re.compile(
44
+ r"(?:DATABASE_URL|JWT_SECRET|SESSION_SECRET|ENCRYPTION_KEY|"
45
+ r"SUPABASE_SERVICE_ROLE_KEY|PRIVATE_KEY|CLIENT_SECRET)"
46
+ r"\s*[=:]\s*['\"]?([^\s'\"]{8,})['\"]?"
47
+ )),
48
+
49
+ # Bearer tokens in inline examples
50
+ ("bearer_token", re.compile(r"Bearer\s+[a-zA-Z0-9_.-]{40,}")),
51
+
52
+ # Private keys (PEM format)
53
+ ("private_key_pem", re.compile(
54
+ r"-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----[\s\S]{20,}?-----END (?:RSA |EC |DSA )?PRIVATE KEY-----"
55
+ )),
56
+ ]
57
+
58
+ _REDACTED = "[REDACTED]"
59
+
60
+
61
+ def redact_text(text: str) -> tuple[str, list[dict[str, str]]]:
62
+ """Redact secrets from a text string.
63
+
64
+ Returns:
65
+ (redacted_text, findings) where findings is a list of
66
+ {"pattern": name, "sample": first_4_chars} for each match.
67
+ """
68
+ if not text:
69
+ return text, []
70
+
71
+ findings: list[dict[str, str]] = []
72
+
73
+ for name, pattern in _SECRET_PATTERNS:
74
+ matches = list(pattern.finditer(text))
75
+ for match in reversed(matches): # reverse to preserve indices
76
+ matched = match.group(0)
77
+ # Keep a tiny sample for debugging (first 4 chars + "...")
78
+ sample = matched[:4] + "..." if len(matched) > 4 else matched
79
+ findings.append({"pattern": name, "sample": sample})
80
+ text = text[:match.start()] + _REDACTED + text[match.end():]
81
+
82
+ return text, findings
83
+
84
+
85
+ def redact_snapshot(snapshot: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, str]]]:
86
+ """Redact secrets from all text fields in a proxy snapshot.
87
+
88
+ Mutates the snapshot in place and returns (snapshot, all_findings).
89
+ """
90
+ all_findings: list[dict[str, str]] = []
91
+
92
+ # Redact system_prompt
93
+ if snapshot.get("system_prompt"):
94
+ snapshot["system_prompt"], findings = redact_text(snapshot["system_prompt"])
95
+ all_findings.extend(findings)
96
+
97
+ # Redact messages (simplified content strings)
98
+ for msg in snapshot.get("messages", []):
99
+ if isinstance(msg, dict) and msg.get("content"):
100
+ msg["content"], findings = redact_text(msg["content"])
101
+ all_findings.extend(findings)
102
+
103
+ # Redact messages_raw (full API message objects with nested content blocks)
104
+ for msg in snapshot.get("messages_raw", []):
105
+ if isinstance(msg, dict):
106
+ content = msg.get("content")
107
+ if isinstance(content, str):
108
+ msg["content"], findings = redact_text(content)
109
+ all_findings.extend(findings)
110
+ elif isinstance(content, list):
111
+ for block in content:
112
+ if isinstance(block, dict) and isinstance(block.get("text"), str):
113
+ block["text"], findings = redact_text(block["text"])
114
+ all_findings.extend(findings)
115
+
116
+ # Redact system_blocks
117
+ for block in snapshot.get("system_blocks", []):
118
+ if isinstance(block, dict) and isinstance(block.get("text"), str):
119
+ block["text"], findings = redact_text(block["text"])
120
+ all_findings.extend(findings)
121
+
122
+ # Redact response content if present
123
+ resp = snapshot.get("response", {})
124
+ if isinstance(resp, dict):
125
+ for key in ("text", "content"):
126
+ if isinstance(resp.get(key), str):
127
+ resp[key], findings = redact_text(resp[key])
128
+ all_findings.extend(findings)
129
+
130
+ return snapshot, all_findings