feat: secret scanner — auto-detects and redacts API keys, tokens, credentials
Adds a secret scanning layer that runs after identity/explicit substitutions. Catches secrets the user didn't configure: - API keys: OpenAI (sk-), Anthropic (sk-ant-), Google (AIza), AWS (AKIA), GitHub (ghp_), GitLab (glpat-), Slack (xox), Stripe (sk_live/test), SendGrid (SG.) - Auth: Bearer tokens, key=value assignments (password=, secret=, api_key=, token=), private key blocks (-----BEGIN PRIVATE KEY-----) - Connection strings: mongodb://, postgres://, mysql:// with creds - PII: SSN (xxx-xx-xxxx), credit card numbers (Visa/MC/Amex/Discover) Redacted values shown in red in the Test tab. Secrets are truncated in the activity log (first 8 chars + "...") to avoid logging the full secret. Enabled by default, toggle in Options. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
+92
-3
@@ -253,19 +253,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Combined substitution: smart patterns first, then explicit
|
// Combined substitution: smart patterns + explicit + secret scan
|
||||||
// ============================================================
|
// ============================================================
|
||||||
function substituteAll(text) {
|
function substituteAll(text) {
|
||||||
const allReplacements = [];
|
const allReplacements = [];
|
||||||
|
|
||||||
// Smart patterns (broad catches)
|
// 1. Smart patterns (broad catches)
|
||||||
const smart = smartSubstitute(text, identity);
|
const smart = smartSubstitute(text, identity);
|
||||||
allReplacements.push(...smart.replacements);
|
allReplacements.push(...smart.replacements);
|
||||||
|
|
||||||
// Explicit mappings (specific overrides)
|
// 2. Explicit mappings (specific overrides)
|
||||||
const explicit = substitute(smart.text, mappings);
|
const explicit = substitute(smart.text, mappings);
|
||||||
allReplacements.push(...explicit.replacements);
|
allReplacements.push(...explicit.replacements);
|
||||||
|
|
||||||
|
// 3. Secret scanner (API keys, tokens, SSNs, credit cards, etc.)
|
||||||
|
if (settings.secretScanning !== false) {
|
||||||
|
const secrets = scanAndRedactSecrets(explicit.text);
|
||||||
|
allReplacements.push(...secrets.redactions);
|
||||||
|
return {
|
||||||
|
text: secrets.text,
|
||||||
|
replacements: allReplacements,
|
||||||
|
modified: allReplacements.length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: explicit.text,
|
text: explicit.text,
|
||||||
replacements: allReplacements,
|
replacements: allReplacements,
|
||||||
@@ -273,6 +284,84 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Secret Scanner (inline for page world)
|
||||||
|
// Detects API keys, tokens, passwords, SSNs, credit cards, etc.
|
||||||
|
// ============================================================
|
||||||
|
const SECRET_PATTERNS = [
|
||||||
|
// OpenAI
|
||||||
|
{ name: 'OpenAI Key', re: /\bsk-[A-Za-z0-9]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
|
||||||
|
{ name: 'OpenAI Project Key', re: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
|
||||||
|
// Anthropic
|
||||||
|
{ name: 'Anthropic Key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, to: '[REDACTED-ANTHROPIC-KEY]' },
|
||||||
|
// Google
|
||||||
|
{ name: 'Google API Key', re: /\bAIza[A-Za-z0-9_-]{35}\b/g, to: '[REDACTED-GOOGLE-KEY]' },
|
||||||
|
// AWS
|
||||||
|
{ name: 'AWS Access Key', re: /\bAKIA[A-Z0-9]{16}\b/g, to: '[REDACTED-AWS-KEY]' },
|
||||||
|
// GitHub
|
||||||
|
{ name: 'GitHub Token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, to: '[REDACTED-GITHUB-TOKEN]' },
|
||||||
|
// GitLab
|
||||||
|
{ name: 'GitLab Token', re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, to: '[REDACTED-GITLAB-TOKEN]' },
|
||||||
|
// Slack
|
||||||
|
{ name: 'Slack Token', re: /\bxox[bpras]-[A-Za-z0-9-]{10,}\b/g, to: '[REDACTED-SLACK-TOKEN]' },
|
||||||
|
// Stripe
|
||||||
|
{ name: 'Stripe Key', re: /\b[sr]k_(?:test|live)_[A-Za-z0-9]{20,}\b/g, to: '[REDACTED-STRIPE-KEY]' },
|
||||||
|
// SendGrid
|
||||||
|
{ name: 'SendGrid Key', re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g, to: '[REDACTED-SENDGRID-KEY]' },
|
||||||
|
// Bearer tokens
|
||||||
|
{ name: 'Bearer Token', re: /\bBearer\s+[A-Za-z0-9_\-./+=]{20,}\b/g, to: 'Bearer [REDACTED]' },
|
||||||
|
// Private keys
|
||||||
|
{ name: 'Private Key', re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, to: '[REDACTED-PRIVATE-KEY]' },
|
||||||
|
// Generic key=value assignments
|
||||||
|
{ name: 'API Key Assignment', re: /\b(?:api[_-]?key|apikey|api[_-]?secret|api[_-]?token)\s*[:=]\s*['"]?([A-Za-z0-9_\-./+=]{16,})['"]?/gi,
|
||||||
|
fn: (m) => m.replace(/[:=]\s*['"]?[A-Za-z0-9_\-./+=]{16,}['"]?/, '=[REDACTED]') },
|
||||||
|
{ name: 'Password/Secret Assignment', re: /\b(?:password|passwd|pwd|secret|token|auth[_-]?token|access[_-]?token)\s*[:=]\s*['"]?([^\s'"]{8,})['"]?/gi,
|
||||||
|
fn: (m) => m.replace(/[:=]\s*['"]?[^\s'"]{8,}['"]?/, '=[REDACTED]') },
|
||||||
|
// Connection strings with credentials
|
||||||
|
{ name: 'Connection String', re: /\b(?:mongodb|postgres|mysql|redis|amqp):\/\/[^\s"']+/gi,
|
||||||
|
fn: (m) => m.replace(/:\/\/([^:]+):([^@]+)@/, '://REDACTED:REDACTED@') },
|
||||||
|
// SSN
|
||||||
|
{ name: 'SSN', re: /\b\d{3}-\d{2}-\d{4}\b/g, to: '[REDACTED-SSN]' },
|
||||||
|
// Credit cards (Visa, MC, Amex, Discover)
|
||||||
|
{ name: 'Credit Card', re: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, to: '[REDACTED-CARD]' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function scanAndRedactSecrets(text) {
|
||||||
|
const redactions = [];
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
for (const pat of SECRET_PATTERNS) {
|
||||||
|
pat.re.lastIndex = 0;
|
||||||
|
const matches = [];
|
||||||
|
let m;
|
||||||
|
|
||||||
|
while ((m = pat.re.exec(result)) !== null) {
|
||||||
|
matches.push({ index: m.index, value: m[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) continue;
|
||||||
|
|
||||||
|
// Replace from end to preserve indices
|
||||||
|
for (let i = matches.length - 1; i >= 0; i--) {
|
||||||
|
const match = matches[i];
|
||||||
|
const replacement = pat.fn ? pat.fn(match.value) : pat.to;
|
||||||
|
redactions.push({
|
||||||
|
original: match.value.slice(0, 8) + '...', // Don't log the full secret
|
||||||
|
replaced: replacement,
|
||||||
|
category: 'secret',
|
||||||
|
pattern: pat.name,
|
||||||
|
});
|
||||||
|
result =
|
||||||
|
result.slice(0, match.index) +
|
||||||
|
replacement +
|
||||||
|
result.slice(match.index + match.value.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redactions.reverse();
|
||||||
|
return { text: result, redactions };
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Notify content script of substitutions (for badge + logging)
|
// Notify content script of substitutions (for badge + logging)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/**
|
||||||
|
* Silent Send - Secret Scanner
|
||||||
|
*
|
||||||
|
* Detects common secret/credential patterns in text and either
|
||||||
|
* warns or auto-redacts them. This catches things the identity-based
|
||||||
|
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
|
||||||
|
* cards, private keys, connection strings, etc.
|
||||||
|
*
|
||||||
|
* Each pattern has:
|
||||||
|
* - name: human-readable label
|
||||||
|
* - regex: detection pattern
|
||||||
|
* - redact: replacement string (or function)
|
||||||
|
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SECRET_PATTERNS = [
|
||||||
|
// --- API Keys ---
|
||||||
|
{
|
||||||
|
name: 'OpenAI API Key',
|
||||||
|
regex: /\bsk-[A-Za-z0-9]{20,}\b/g,
|
||||||
|
redact: '[REDACTED-OPENAI-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OpenAI Project Key',
|
||||||
|
regex: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g,
|
||||||
|
redact: '[REDACTED-OPENAI-PROJECT-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Anthropic API Key',
|
||||||
|
regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
|
||||||
|
redact: '[REDACTED-ANTHROPIC-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Google API Key',
|
||||||
|
regex: /\bAIza[A-Za-z0-9_-]{35}\b/g,
|
||||||
|
redact: '[REDACTED-GOOGLE-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AWS Access Key',
|
||||||
|
regex: /\bAKIA[A-Z0-9]{16}\b/g,
|
||||||
|
redact: '[REDACTED-AWS-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AWS Secret Key',
|
||||||
|
regex: /\b[A-Za-z0-9/+=]{40}\b(?=.*aws|.*secret)/gi,
|
||||||
|
redact: '[REDACTED-AWS-SECRET]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GitHub Token',
|
||||||
|
regex: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g,
|
||||||
|
redact: '[REDACTED-GITHUB-TOKEN]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GitLab Token',
|
||||||
|
regex: /\bglpat-[A-Za-z0-9_-]{20,}\b/g,
|
||||||
|
redact: '[REDACTED-GITLAB-TOKEN]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Slack Token',
|
||||||
|
regex: /\bxox[bpras]-[A-Za-z0-9-]{10,}\b/g,
|
||||||
|
redact: '[REDACTED-SLACK-TOKEN]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stripe Key',
|
||||||
|
regex: /\b[sr]k_(test|live)_[A-Za-z0-9]{20,}\b/g,
|
||||||
|
redact: '[REDACTED-STRIPE-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Twilio Key',
|
||||||
|
regex: /\bSK[a-f0-9]{32}\b/g,
|
||||||
|
redact: '[REDACTED-TWILIO-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SendGrid Key',
|
||||||
|
regex: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g,
|
||||||
|
redact: '[REDACTED-SENDGRID-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Heroku API Key',
|
||||||
|
regex: /\b[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g,
|
||||||
|
redact: null, // UUIDs are too common — flag but don't auto-redact
|
||||||
|
severity: 'warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Generic API Key',
|
||||||
|
regex: /\b(?:api[_-]?key|apikey|api[_-]?secret|api[_-]?token)\s*[:=]\s*['"]?([A-Za-z0-9_\-./+=]{16,})['"]?/gi,
|
||||||
|
redact: (match) => match.replace(/[:=]\s*['"]?[A-Za-z0-9_\-./+=]{16,}['"]?/, '=[REDACTED]'),
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Generic Secret/Password Assignment',
|
||||||
|
regex: /\b(?:password|passwd|pwd|secret|token|auth[_-]?token|access[_-]?token|bearer)\s*[:=]\s*['"]?([^\s'"]{8,})['"]?/gi,
|
||||||
|
redact: (match) => match.replace(/[:=]\s*['"]?[^\s'"]{8,}['"]?/, '=[REDACTED]'),
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Bearer Token',
|
||||||
|
regex: /\bBearer\s+[A-Za-z0-9_\-./+=]{20,}\b/g,
|
||||||
|
redact: 'Bearer [REDACTED]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Private Keys ---
|
||||||
|
{
|
||||||
|
name: 'Private Key Block',
|
||||||
|
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
||||||
|
redact: '[REDACTED-PRIVATE-KEY]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Connection Strings ---
|
||||||
|
{
|
||||||
|
name: 'Database Connection String',
|
||||||
|
regex: /\b(?:mongodb|postgres|mysql|redis|amqp):\/\/[^\s"']+/gi,
|
||||||
|
redact: (match) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(match);
|
||||||
|
if (url.password) url.password = 'REDACTED';
|
||||||
|
if (url.username) url.username = 'REDACTED';
|
||||||
|
return url.toString();
|
||||||
|
} catch {
|
||||||
|
return '[REDACTED-CONNECTION-STRING]';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- PII Patterns ---
|
||||||
|
{
|
||||||
|
name: 'US Social Security Number',
|
||||||
|
regex: /\b\d{3}-\d{2}-\d{4}\b/g,
|
||||||
|
redact: '[REDACTED-SSN]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Credit Card Number',
|
||||||
|
regex: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g,
|
||||||
|
redact: '[REDACTED-CARD]',
|
||||||
|
severity: 'critical',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Generic Long Hex/Base64 Strings ---
|
||||||
|
// Catches things that look like secrets but don't match known prefixes
|
||||||
|
{
|
||||||
|
name: 'Long Hex String (possible secret)',
|
||||||
|
regex: /\b[a-f0-9]{40,}\b/gi,
|
||||||
|
redact: null, // Too many false positives — warn only
|
||||||
|
severity: 'warning',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SecretScanner = {
|
||||||
|
/**
|
||||||
|
* Scan text for secrets. Returns list of findings.
|
||||||
|
*/
|
||||||
|
scan(text) {
|
||||||
|
const findings = [];
|
||||||
|
|
||||||
|
for (const pattern of SECRET_PATTERNS) {
|
||||||
|
// Reset regex lastIndex
|
||||||
|
pattern.regex.lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = pattern.regex.exec(text)) !== null) {
|
||||||
|
findings.push({
|
||||||
|
name: pattern.name,
|
||||||
|
value: match[0],
|
||||||
|
index: match.index,
|
||||||
|
length: match[0].length,
|
||||||
|
severity: pattern.severity,
|
||||||
|
redactTo: typeof pattern.redact === 'function'
|
||||||
|
? pattern.redact(match[0])
|
||||||
|
: pattern.redact,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate overlapping matches (keep the most specific / longest)
|
||||||
|
findings.sort((a, b) => a.index - b.index || b.length - a.length);
|
||||||
|
const deduped = [];
|
||||||
|
let lastEnd = -1;
|
||||||
|
for (const f of findings) {
|
||||||
|
if (f.index >= lastEnd) {
|
||||||
|
deduped.push(f);
|
||||||
|
lastEnd = f.index + f.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduped;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redact all critical secrets in text. Warnings are not auto-redacted.
|
||||||
|
* Returns { text, redactions[] }
|
||||||
|
*/
|
||||||
|
redact(text) {
|
||||||
|
const findings = this.scan(text);
|
||||||
|
const redactions = [];
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
// Process from end to preserve indices
|
||||||
|
const critical = findings
|
||||||
|
.filter(f => f.severity === 'critical' && f.redactTo)
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
for (const f of critical) {
|
||||||
|
result = result.slice(0, f.index) + f.redactTo + result.slice(f.index + f.length);
|
||||||
|
redactions.push({
|
||||||
|
original: f.value,
|
||||||
|
replaced: f.redactTo,
|
||||||
|
category: 'secret',
|
||||||
|
pattern: f.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse so they're in order
|
||||||
|
redactions.reverse();
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: result,
|
||||||
|
redactions,
|
||||||
|
warnings: findings.filter(f => f.severity === 'warning'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof globalThis !== 'undefined') {
|
||||||
|
globalThis.SecretScanner = SecretScanner;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SecretScanner;
|
||||||
@@ -18,6 +18,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
showHighlights: false,
|
showHighlights: false,
|
||||||
revealMode: false,
|
revealMode: false,
|
||||||
|
secretScanning: true,
|
||||||
maxLogEntries: 200,
|
maxLogEntries: 200,
|
||||||
customDomains: [],
|
customDomains: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'general'],
|
||||||
|
|||||||
@@ -24,6 +24,16 @@
|
|||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div>
|
||||||
|
<label>Secret scanning</label>
|
||||||
|
<p class="setting-desc">Auto-detect and redact API keys, tokens, passwords, SSNs, credit card numbers</p>
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="secretScanning" checked>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<label>Max log entries</label>
|
<label>Max log entries</label>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
// Apply settings to UI
|
// Apply settings to UI
|
||||||
$('#showHighlights').checked = settings.showHighlights || false;
|
$('#showHighlights').checked = settings.showHighlights || false;
|
||||||
|
$('#secretScanning').checked = settings.secretScanning !== false;
|
||||||
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
||||||
|
|
||||||
renderMappings();
|
renderMappings();
|
||||||
@@ -28,6 +29,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await Storage.saveSettings({ showHighlights: e.target.checked });
|
await Storage.saveSettings({ showHighlights: e.target.checked });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#secretScanning').addEventListener('change', async (e) => {
|
||||||
|
await Storage.saveSettings({ secretScanning: e.target.checked });
|
||||||
|
});
|
||||||
|
|
||||||
$('#maxLogEntries').addEventListener('change', async (e) => {
|
$('#maxLogEntries').addEventListener('change', async (e) => {
|
||||||
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
|
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
|
||||||
});
|
});
|
||||||
|
|||||||
+24
-4
@@ -1,5 +1,6 @@
|
|||||||
import SubstitutionEngine from '../lib/substitution-engine.js';
|
import SubstitutionEngine from '../lib/substitution-engine.js';
|
||||||
import SmartPatterns from '../lib/smart-patterns.js';
|
import SmartPatterns from '../lib/smart-patterns.js';
|
||||||
|
import SecretScanner from '../lib/secret-scanner.js';
|
||||||
import Storage from '../lib/storage.js';
|
import Storage from '../lib/storage.js';
|
||||||
import api from '../lib/browser-polyfill.js';
|
import api from '../lib/browser-polyfill.js';
|
||||||
|
|
||||||
@@ -331,31 +332,50 @@ function renderTestDiff() {
|
|||||||
|
|
||||||
const smartResult = SmartPatterns.substitute(input, identity);
|
const smartResult = SmartPatterns.substitute(input, identity);
|
||||||
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||||
|
const secretResult = SecretScanner.redact(explicitResult.text);
|
||||||
|
|
||||||
const allReplacements = [...smartResult.replacements, ...explicitResult.replacements];
|
const allReplacements = [
|
||||||
const finalText = explicitResult.text;
|
...smartResult.replacements,
|
||||||
|
...explicitResult.replacements,
|
||||||
|
...secretResult.redactions,
|
||||||
|
];
|
||||||
|
const finalText = secretResult.text;
|
||||||
|
|
||||||
if (finalText === input) {
|
if (finalText === input && secretResult.warnings.length === 0) {
|
||||||
output.textContent = input;
|
output.textContent = input;
|
||||||
stats.textContent = 'No substitutions detected';
|
stats.textContent = 'No substitutions detected';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = escapeHtml(finalText);
|
let html = escapeHtml(finalText);
|
||||||
for (const r of allReplacements) {
|
|
||||||
|
// Highlight identity + explicit substitutions in green
|
||||||
|
for (const r of [...smartResult.replacements, ...explicitResult.replacements]) {
|
||||||
const escapedReplaced = escapeHtml(r.replaced);
|
const escapedReplaced = escapeHtml(r.replaced);
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
escapedReplaced,
|
escapedReplaced,
|
||||||
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
|
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Highlight secret redactions in red
|
||||||
|
for (const r of secretResult.redactions) {
|
||||||
|
const escapedReplaced = escapeHtml(r.replaced);
|
||||||
|
html = html.replace(
|
||||||
|
escapedReplaced,
|
||||||
|
`<span class="sub-highlight" style="background:#fee2e2;color:#dc2626" title="${escapeHtml(r.pattern)}">${escapedReplaced}</span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
output.innerHTML = html;
|
output.innerHTML = html;
|
||||||
|
|
||||||
const smartCount = smartResult.replacements.length;
|
const smartCount = smartResult.replacements.length;
|
||||||
const explicitCount = explicitResult.replacements.length;
|
const explicitCount = explicitResult.replacements.length;
|
||||||
|
const secretCount = secretResult.redactions.length;
|
||||||
|
const warnCount = secretResult.warnings.length;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (smartCount > 0) parts.push(`${smartCount} smart`);
|
if (smartCount > 0) parts.push(`${smartCount} smart`);
|
||||||
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
||||||
|
if (secretCount > 0) parts.push(`${secretCount} secrets redacted`);
|
||||||
|
if (warnCount > 0) parts.push(`${warnCount} warnings`);
|
||||||
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user