Skip to content

Commit bb780bf

Browse files
committed
feat: local PII/Secret detection guard
1 parent e8233cd commit bb780bf

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

qwed_sdk/client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,26 @@ def verify_fact(
154154
) -> VerificationResult:
155155
"""Verify a factual claim against context."""
156156
start = time.time()
157+
158+
# Try local PII scanning first (Privacy Safety - Mode B)
159+
try:
160+
from qwed_new.guards.pii_guard import PIIGuard
161+
guard = PIIGuard()
162+
# Treat the 'claim' (response) as the content to scan for leaks
163+
pii_result = guard.scan(claim)
164+
165+
if not pii_result["verified"]:
166+
# PII Detected locally - Return immediately
167+
return VerificationResult(
168+
status="UNVERIFIED",
169+
is_verified=False,
170+
result=pii_result,
171+
latency_ms=(time.time() - start) * 1000
172+
)
173+
except ImportError:
174+
pass # Fallback to remote if local guard missing
175+
176+
# Remote verification (if no local PII or Guard missing)
157177
data = self._request(
158178
"POST",
159179
"/verify/fact",

src/qwed_new/guards/pii_guard.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import re
2+
from typing import Dict, Any, List
3+
4+
class PIIGuard:
5+
"""
6+
Local-first PII and Secret detection using regex heuristics.
7+
Lightweight, offline-capable scanner for sensitive data.
8+
"""
9+
10+
def __init__(self):
11+
# Pre-compiled regex patterns for performance
12+
self.patterns = {
13+
"openai_api_key": re.compile(r"sk-(proj-)?[\w-]{20,}"),
14+
"anthropic_api_key": re.compile(r"sk-ant-[\w-]{20,}"),
15+
"aws_access_key": re.compile(r"(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
16+
"ssh_private_key": re.compile(r"-----BEGIN (?:RSA|DSA|EC|OPENSSH) PRIVATE KEY-----"),
17+
"email": re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b"),
18+
# Identifying common password declarations (heuristic)
19+
"password_assignment": re.compile(r"(?i)(password|passwd|pwd|secret)\s*[:=]\s*['\"]?(\S{6,})['\"]?"),
20+
# Generic high-entropy strings usually associated with secrets (checking later if needed)
21+
22+
# Additional PII
23+
"phone_number": re.compile(r"(\+\d{1,3}[-.]?)?\(?\d{3}\)?[-.]?\d{3}[-.]?\d{4}"),
24+
}
25+
26+
def scan(self, content: str) -> Dict[str, Any]:
27+
"""
28+
Scans the provided content for secrets and PII.
29+
Returns a dictionary with detection results.
30+
"""
31+
detected_types = []
32+
violations = []
33+
34+
for name, pattern in self.patterns.items():
35+
matches = pattern.findall(content)
36+
if matches:
37+
# Special handling for password heuristic to avoid false positives on simple words
38+
if name == "password_assignment":
39+
# matches will be list of tuples (key, value)
40+
# We accept it if the value part looks like a secret
41+
valid_matches = [m for m in matches if len(m[1]) > 5] # simple length check
42+
if not valid_matches:
43+
continue
44+
45+
detected_types.append(name)
46+
# Redact content for safe logging (showing first few chars only)
47+
for m in matches:
48+
snippet = str(m)[:10] + "..."
49+
violations.append(f"Potential {name} detected: {snippet}")
50+
51+
if detected_types:
52+
return {
53+
"verified": False,
54+
"risk": "PII_LEAK",
55+
"types": detected_types,
56+
"violations": violations,
57+
"message": f"Secrets detected: {', '.join(detected_types)}"
58+
}
59+
60+
return {
61+
"verified": True,
62+
"scan_method": "REGEX_HEURISTICS",
63+
"message": "No obvious secrets found."
64+
}

0 commit comments

Comments
 (0)