@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.
- package/Dockerfile +28 -0
- package/addon/context_classifier.py +375 -0
- package/addon/providers.py +483 -0
- package/addon/reporter.py +778 -0
- package/addon/requirements.txt +1 -0
- package/addon/rulemetric_addon.py +1288 -0
- package/addon/secret_redactor.py +130 -0
- package/addon/security_scanner.py +828 -0
- package/addon/session_linker.py +206 -0
- package/addon/sse_parser.py +364 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/anthropic.d.ts +8 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +135 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base.d.ts +11 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +43 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/bedrock.d.ts +8 -0
- package/dist/providers/bedrock.d.ts.map +1 -0
- package/dist/providers/bedrock.js +125 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/gemini.d.ts +9 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +140 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +116 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai.d.ts +9 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +129 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/session-linker.d.ts +6 -0
- package/dist/session-linker.d.ts.map +1 -0
- package/dist/session-linker.js +68 -0
- package/dist/session-linker.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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
|