revert: restore entire src/ from pre-doc-scanner commit (e4b44a7)
Going back to a known-good baseline. This version had: - Working reveal mode with CSS Highlight API - Working substitution (fetch + XHR hooks) - Smart patterns (names, emails, phones, usernames) - Encryption/sync (password, TOTP, WebAuthn) - Multiple identity profiles - Activity log - Secret scanner - Auto-detect PII warnings - Pre-send PII detection Kept current manifests (UUID, data_collection_permissions, version). No renames applied — uses original naming (secretScanning, PPI, etc). Will re-apply renames and new features from this working base. https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
+15
-93
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* Silent Send - Auto-Detect
|
||||
*
|
||||
* Scans text for potential PII that the user hasn't configured.
|
||||
* This catches things the identity and auto-redact scanner can't —
|
||||
* Scans text for potential PPI that the user hasn't configured.
|
||||
* This catches things the identity and secret scanner can't —
|
||||
* because the user forgot or didn't know to configure them.
|
||||
*
|
||||
* Returns warnings (not auto-redactions) so the user can decide.
|
||||
*/
|
||||
|
||||
const PII_PATTERNS = [
|
||||
const PPI_PATTERNS = [
|
||||
// --- Network ---
|
||||
{
|
||||
name: 'Private IP Address',
|
||||
@@ -21,7 +21,7 @@ const PII_PATTERNS = [
|
||||
regex: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g,
|
||||
category: 'network',
|
||||
hint: 'IP address — could identify your network',
|
||||
// Exclude common non-PII IPs
|
||||
// Exclude common non-PPI IPs
|
||||
exclude: /^(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+|8\.8\.[84]\.[84]|1\.1\.1\.1|1\.0\.0\.1)$/,
|
||||
},
|
||||
{
|
||||
@@ -126,12 +126,12 @@ const PII_PATTERNS = [
|
||||
},
|
||||
];
|
||||
|
||||
// Context words that make ambiguous patterns more likely to be PII
|
||||
// Context words that make ambiguous patterns more likely to be PPI
|
||||
const CONTEXT_WORDS = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|home|live|lives|reside|zip|postal)\b/i;
|
||||
|
||||
const AutoDetect = {
|
||||
/**
|
||||
* Scan text for potential unconfigured PII.
|
||||
* Scan text for potential unconfigured PPI.
|
||||
* Pass in identity so we can skip values the user already configured.
|
||||
*
|
||||
* Returns array of { name, value, hint, category, index }
|
||||
@@ -167,7 +167,7 @@ const AutoDetect = {
|
||||
}
|
||||
}
|
||||
|
||||
for (const pattern of PII_PATTERNS) {
|
||||
for (const pattern of PPI_PATTERNS) {
|
||||
// Skip context-dependent patterns if no context words present
|
||||
if (pattern.contextRequired && !hasContext) continue;
|
||||
|
||||
@@ -221,15 +221,16 @@ const AutoDetect = {
|
||||
*/
|
||||
_detectProperNouns(text, configured) {
|
||||
const findings = [];
|
||||
// Only match TWO OR MORE consecutive capitalized words
|
||||
// Single capitalized words cause too many false positives (sentence starts)
|
||||
const re = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+)\b/g;
|
||||
const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g;
|
||||
let m;
|
||||
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
const fullMatch = m[1];
|
||||
if (!fullMatch) continue;
|
||||
|
||||
const before = text.slice(Math.max(0, m.index - 2), m.index);
|
||||
const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before);
|
||||
|
||||
const words = fullMatch.split(/\s+/);
|
||||
const properWords = words.filter(w =>
|
||||
w.length >= 3 &&
|
||||
@@ -237,14 +238,15 @@ const AutoDetect = {
|
||||
!configured.has(w.toLowerCase())
|
||||
);
|
||||
|
||||
if (properWords.length < 2) continue; // need at least 2 proper words
|
||||
if (properWords.length === 0) continue;
|
||||
if (isSentenceStart && properWords.length === 1 && words.length === 1) continue;
|
||||
|
||||
const value = properWords.join(' ');
|
||||
if (value.length >= 5 && !configured.has(value.toLowerCase())) {
|
||||
if (value.length >= 3 && !configured.has(value.toLowerCase())) {
|
||||
findings.push({
|
||||
name: 'Possible Name/Org',
|
||||
value,
|
||||
hint: 'Capitalized phrase — could be a name, company, or project',
|
||||
hint: 'Capitalized word — could be a name, company, or project',
|
||||
category: 'name',
|
||||
});
|
||||
}
|
||||
@@ -260,8 +262,6 @@ const AutoDetect = {
|
||||
};
|
||||
|
||||
// Common English words to exclude from proper noun detection
|
||||
// Comprehensive list including verbs, nouns, adjectives that appear
|
||||
// in titles, headings, UI buttons, and instructions
|
||||
const COMMON_WORDS = new Set([
|
||||
'the', 'and', 'but', 'for', 'not', 'you', 'all', 'can', 'had', 'her',
|
||||
'was', 'one', 'our', 'out', 'are', 'has', 'his', 'how', 'its', 'may',
|
||||
@@ -281,84 +281,6 @@ const COMMON_WORDS = new Set([
|
||||
'number', 'other', 'point', 'right', 'small', 'state', 'thing',
|
||||
'think', 'those', 'three', 'through', 'under', 'until', 'water',
|
||||
'world', 'write', 'might', 'should', 'because', 'although',
|
||||
// Common verbs (titles, headings, buttons, instructions)
|
||||
'generate', 'design', 'manage', 'process', 'handle', 'check', 'verify',
|
||||
'submit', 'apply', 'accept', 'reject', 'approve', 'deny', 'confirm',
|
||||
'cancel', 'delete', 'remove', 'edit', 'modify', 'view', 'display',
|
||||
'search', 'filter', 'sort', 'select', 'choose', 'pick', 'enter',
|
||||
'upload', 'download', 'install', 'enable', 'disable', 'activate',
|
||||
'connect', 'disconnect', 'sync', 'refresh', 'reload', 'reset',
|
||||
'save', 'load', 'store', 'restore', 'backup', 'copy', 'paste',
|
||||
'lock', 'unlock', 'encrypt', 'decrypt', 'sign', 'register', 'login',
|
||||
'logout', 'subscribe', 'share', 'publish', 'deploy', 'launch',
|
||||
'merge', 'split', 'join', 'link', 'attach', 'insert', 'append',
|
||||
'format', 'parse', 'convert', 'transform', 'translate', 'compile',
|
||||
'execute', 'render', 'animate', 'validate', 'sanitize', 'escape',
|
||||
'create', 'build', 'start', 'stop', 'open', 'close', 'run', 'send',
|
||||
// Common nouns (titles, headings, labels)
|
||||
'account', 'action', 'address', 'alert', 'analysis', 'application',
|
||||
'area', 'article', 'asset', 'background', 'badge', 'banner', 'board',
|
||||
'body', 'border', 'bottom', 'box', 'browser', 'buffer', 'button',
|
||||
'cache', 'calendar', 'card', 'category', 'center', 'channel', 'chart',
|
||||
'chat', 'child', 'choice', 'client', 'cloud', 'code', 'collection',
|
||||
'color', 'column', 'command', 'comment', 'community', 'company',
|
||||
'component', 'config', 'configuration', 'connection', 'console',
|
||||
'contact', 'container', 'content', 'context', 'control', 'count',
|
||||
'country', 'custom', 'dashboard', 'data', 'database', 'date', 'day',
|
||||
'default', 'description', 'design', 'desktop', 'detail', 'device',
|
||||
'dialog', 'directory', 'document', 'domain', 'draft', 'driver',
|
||||
'edge', 'editor', 'element', 'email', 'engine', 'entry', 'environment',
|
||||
'error', 'event', 'example', 'extension', 'feature', 'feedback',
|
||||
'field', 'file', 'filter', 'folder', 'font', 'footer', 'form',
|
||||
'frame', 'function', 'gallery', 'general', 'global', 'grid',
|
||||
'guide', 'handler', 'header', 'health', 'help', 'history', 'home',
|
||||
'host', 'icon', 'image', 'index', 'info', 'input', 'instance',
|
||||
'interface', 'issue', 'item', 'job', 'key', 'label', 'language',
|
||||
'layout', 'level', 'library', 'light', 'limit', 'line', 'link',
|
||||
'list', 'local', 'location', 'log', 'logo', 'main', 'manager',
|
||||
'manual', 'map', 'media', 'member', 'memory', 'menu', 'message',
|
||||
'method', 'mobile', 'modal', 'mode', 'model', 'module', 'monitor',
|
||||
'navigation', 'network', 'node', 'note', 'notification', 'object',
|
||||
'option', 'order', 'origin', 'output', 'overlay', 'overview', 'owner',
|
||||
'package', 'page', 'panel', 'parent', 'parser', 'password', 'path',
|
||||
'pattern', 'permission', 'photo', 'pipeline', 'placeholder', 'plan',
|
||||
'platform', 'player', 'plugin', 'point', 'policy', 'pool', 'popup',
|
||||
'port', 'position', 'post', 'power', 'preview', 'primary', 'print',
|
||||
'priority', 'process', 'product', 'profile', 'program', 'progress',
|
||||
'project', 'prompt', 'property', 'protocol', 'provider', 'proxy',
|
||||
'public', 'query', 'queue', 'quick', 'range', 'rate', 'reader',
|
||||
'record', 'region', 'release', 'remote', 'report', 'request',
|
||||
'resource', 'response', 'result', 'review', 'role', 'root', 'route',
|
||||
'row', 'rule', 'runtime', 'sample', 'scanner', 'schema', 'scope',
|
||||
'screen', 'script', 'search', 'section', 'security', 'select',
|
||||
'sender', 'server', 'service', 'session', 'setting', 'settings',
|
||||
'setup', 'share', 'shell', 'shortcut', 'sidebar', 'signal', 'simple',
|
||||
'single', 'site', 'size', 'slider', 'snapshot', 'socket', 'solution',
|
||||
'source', 'space', 'stage', 'standard', 'status', 'step', 'storage',
|
||||
'stream', 'string', 'style', 'subject', 'success', 'summary',
|
||||
'support', 'switch', 'symbol', 'syntax', 'system', 'table', 'target',
|
||||
'task', 'team', 'template', 'terminal', 'test', 'text', 'theme',
|
||||
'thread', 'title', 'token', 'tool', 'toolbar', 'tooltip', 'total',
|
||||
'track', 'traffic', 'tree', 'trigger', 'type', 'unit', 'update',
|
||||
'upload', 'user', 'utility', 'value', 'variable', 'version', 'video',
|
||||
'view', 'virtual', 'warning', 'watch', 'web', 'widget', 'width',
|
||||
'window', 'wizard', 'word', 'worker', 'workspace', 'wrapper', 'zone',
|
||||
// Common adjectives
|
||||
'active', 'advanced', 'available', 'basic', 'clean', 'clear', 'complete',
|
||||
'connected', 'correct', 'critical', 'current', 'dark', 'deep',
|
||||
'different', 'direct', 'double', 'dynamic', 'easy', 'empty', 'entire',
|
||||
'exact', 'extra', 'fast', 'final', 'fixed', 'flat', 'free', 'fresh',
|
||||
'full', 'generic', 'given', 'hidden', 'initial', 'inner', 'internal',
|
||||
'invalid', 'latest', 'live', 'major', 'maximum', 'minimum', 'minor',
|
||||
'mixed', 'modern', 'multiple', 'native', 'natural', 'nested', 'normal',
|
||||
'online', 'optional', 'outer', 'overall', 'partial', 'pending', 'plain',
|
||||
'popular', 'possible', 'previous', 'private', 'proper', 'protected',
|
||||
'random', 'raw', 'ready', 'real', 'recent', 'related', 'relative',
|
||||
'required', 'responsive', 'safe', 'secure', 'selected', 'sensitive',
|
||||
'separate', 'shared', 'silent', 'similar', 'smart', 'smooth', 'solid',
|
||||
'special', 'specific', 'stable', 'static', 'strict', 'strong',
|
||||
'supported', 'unique', 'universal', 'unknown', 'upper', 'valid',
|
||||
'various', 'visible', 'visual', 'whole', 'wide',
|
||||
// Programming / tech terms
|
||||
'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
|
||||
'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* - Policy updates are applied automatically
|
||||
*
|
||||
* Privacy: the org admin can check compliance (are required fields
|
||||
* configured?) but CANNOT see individual PII values.
|
||||
* configured?) but CANNOT see individual PPI values.
|
||||
*/
|
||||
|
||||
import api from './browser-polyfill.js';
|
||||
@@ -198,11 +198,11 @@ const OrgPolicy = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Get org-required auto-redact patterns.
|
||||
* Get org-required secret scanner patterns.
|
||||
*
|
||||
* @returns {Array} additional patterns to add to auto-redact
|
||||
* @returns {Array} additional patterns to add to the secret scanner
|
||||
*/
|
||||
async getOrgRedactPatterns() {
|
||||
async getOrgSecretPatterns() {
|
||||
const policy = await this.getPolicy();
|
||||
if (!policy?.requiredSecretPatterns?.length) return [];
|
||||
|
||||
@@ -220,7 +220,7 @@ const OrgPolicy = {
|
||||
|
||||
/**
|
||||
* Check if the user's configuration meets org policy requirements.
|
||||
* Returns compliance status WITHOUT revealing actual PII values.
|
||||
* Returns compliance status WITHOUT revealing actual PPI values.
|
||||
*
|
||||
* @returns {{ compliant: boolean, missing: string[], configured: string[] }}
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* Silent Send - Auto Redact
|
||||
* 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.
|
||||
*
|
||||
* Supports user-defined custom patterns for proprietary token formats.
|
||||
*
|
||||
* Each pattern has:
|
||||
* - name: human-readable label
|
||||
* - regex: detection pattern
|
||||
@@ -15,7 +13,7 @@
|
||||
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
|
||||
*/
|
||||
|
||||
const REDACT_PATTERNS = [
|
||||
const SECRET_PATTERNS = [
|
||||
// --- API Keys ---
|
||||
{
|
||||
name: 'OpenAI API Key',
|
||||
@@ -163,39 +161,14 @@ const REDACT_PATTERNS = [
|
||||
},
|
||||
];
|
||||
|
||||
const AutoRedact = {
|
||||
/**
|
||||
* Build the full pattern list (built-in + custom).
|
||||
* Custom patterns come from settings.customRedactPatterns.
|
||||
*/
|
||||
_buildPatterns(customPatterns) {
|
||||
const all = [...REDACT_PATTERNS];
|
||||
if (Array.isArray(customPatterns)) {
|
||||
for (const cp of customPatterns) {
|
||||
if (!cp.enabled || !cp.pattern) continue;
|
||||
try {
|
||||
all.push({
|
||||
name: cp.name || 'Custom Pattern',
|
||||
regex: new RegExp(cp.pattern, 'g'),
|
||||
redact: cp.redact || '[REDACTED-CUSTOM]',
|
||||
severity: 'critical',
|
||||
});
|
||||
} catch { /* invalid regex — skip */ }
|
||||
}
|
||||
}
|
||||
return all;
|
||||
},
|
||||
|
||||
const SecretScanner = {
|
||||
/**
|
||||
* Scan text for secrets. Returns list of findings.
|
||||
* @param {string} text
|
||||
* @param {Array} [customPatterns] — from settings.customRedactPatterns
|
||||
*/
|
||||
scan(text, customPatterns) {
|
||||
scan(text) {
|
||||
const findings = [];
|
||||
const patterns = this._buildPatterns(customPatterns);
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for (const pattern of SECRET_PATTERNS) {
|
||||
// Reset regex lastIndex
|
||||
pattern.regex.lastIndex = 0;
|
||||
let match;
|
||||
@@ -231,11 +204,9 @@ const AutoRedact = {
|
||||
/**
|
||||
* Redact all critical secrets in text. Warnings are not auto-redacted.
|
||||
* Returns { text, redactions[] }
|
||||
* @param {string} text
|
||||
* @param {Array} [customPatterns] — from settings.customRedactPatterns
|
||||
*/
|
||||
redact(text, customPatterns) {
|
||||
const findings = this.scan(text, customPatterns);
|
||||
redact(text) {
|
||||
const findings = this.scan(text);
|
||||
const redactions = [];
|
||||
let result = text;
|
||||
|
||||
@@ -249,7 +220,7 @@ const AutoRedact = {
|
||||
redactions.push({
|
||||
original: f.value,
|
||||
replaced: f.redactTo,
|
||||
category: 'redact',
|
||||
category: 'secret',
|
||||
pattern: f.name,
|
||||
});
|
||||
}
|
||||
@@ -266,7 +237,7 @@ const AutoRedact = {
|
||||
};
|
||||
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.AutoRedact = AutoRedact;
|
||||
globalThis.SecretScanner = SecretScanner;
|
||||
}
|
||||
|
||||
export default AutoRedact;
|
||||
export default SecretScanner;
|
||||
+3
-4
@@ -23,7 +23,7 @@ const KEYS = {
|
||||
SETTINGS: 'ss_settings',
|
||||
};
|
||||
|
||||
// Keys that contain sensitive PII and should be encrypted at rest
|
||||
// Keys that contain sensitive PPI and should be encrypted at rest
|
||||
// All user data keys are encrypted at rest — settings included since
|
||||
// custom domains and configuration can reveal what services the user
|
||||
// accesses. Only ss_sync_encryption (salt, verification blob) and
|
||||
@@ -34,13 +34,12 @@ const DEFAULT_SETTINGS = {
|
||||
enabled: true,
|
||||
showHighlights: false,
|
||||
revealMode: false,
|
||||
autoRedact: true,
|
||||
secretScanning: true,
|
||||
autoDetect: true,
|
||||
autoRedactDetected: true,
|
||||
autoAddDetected: true,
|
||||
maxLogEntries: 100,
|
||||
maxLogEntries: 200,
|
||||
customDomains: [],
|
||||
customRedactPatterns: [],
|
||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
|
||||
browserSync: false,
|
||||
};
|
||||
|
||||
+11
-24
@@ -12,9 +12,8 @@
|
||||
* 5. Custom HTTP endpoint — any URL supporting GET + PUT (WebDAV,
|
||||
* self-hosted server, cloud function, etc.).
|
||||
*
|
||||
* Encryption: all sync channels REQUIRE encryption with a password
|
||||
* (AES-256-GCM) and/or TOTP verification. Syncing without encryption
|
||||
* is not permitted — users must set up encryption before enabling sync.
|
||||
* Encryption: all sync channels can optionally encrypt data with a
|
||||
* password (AES-256-GCM) and/or require TOTP verification.
|
||||
* Authentication is cached with a configurable TTL so the user only
|
||||
* needs to authenticate when the cache expires and new data exists.
|
||||
*
|
||||
@@ -371,9 +370,6 @@ const SilentSendSync = {
|
||||
const StorageModule = (await import('./storage.js')).default;
|
||||
await StorageModule.decryptAllData();
|
||||
|
||||
// Disable all sync channels since encryption is mandatory for sync
|
||||
await StorageModule.saveSettings({ browserSync: false });
|
||||
|
||||
await api.storage.local.remove('ss_sync_encryption');
|
||||
await SilentSendCrypto.clearCachedKey();
|
||||
await SilentSendCrypto.clearWebAuthnCredential();
|
||||
@@ -419,7 +415,7 @@ const SilentSendSync = {
|
||||
*/
|
||||
async _encryptForSync(data) {
|
||||
const config = await this._getSyncEncryption();
|
||||
if (!config?.enabled) return { data: null, encrypted: false, needsEncryption: true };
|
||||
if (!config?.enabled) return { data, encrypted: false };
|
||||
|
||||
const keyInfo = await this._getEncryptionKey();
|
||||
if (!keyInfo) {
|
||||
@@ -583,15 +579,12 @@ const SilentSendSync = {
|
||||
async exportSyncCode() {
|
||||
const data = await this._getAllData();
|
||||
|
||||
// Encrypt (mandatory)
|
||||
// Encrypt if enabled
|
||||
const result = await this._encryptForSync(data);
|
||||
if (result.needsEncryption) {
|
||||
return { needsEncryption: true };
|
||||
}
|
||||
if (result.needsAuth) {
|
||||
return { needsAuth: true };
|
||||
}
|
||||
const payload = result.data;
|
||||
const payload = result.data || data;
|
||||
|
||||
const json = JSON.stringify(payload);
|
||||
return btoa(unescape(encodeURIComponent(json)));
|
||||
@@ -647,10 +640,10 @@ const SilentSendSync = {
|
||||
try {
|
||||
const data = await this._getAllData();
|
||||
|
||||
// Encrypt (mandatory)
|
||||
// Encrypt if enabled
|
||||
const result = await this._encryptForSync(data);
|
||||
if (result.needsEncryption || result.needsAuth) return; // skip — encryption required
|
||||
const payload = result.data;
|
||||
if (result.needsAuth) return; // silently skip — will sync on next auth
|
||||
const payload = result.data || data;
|
||||
|
||||
const json = JSON.stringify(payload);
|
||||
|
||||
@@ -716,15 +709,12 @@ const SilentSendSync = {
|
||||
try {
|
||||
const data = await this._getAllData();
|
||||
|
||||
// Encrypt (mandatory)
|
||||
// Encrypt if enabled
|
||||
const encResult = await this._encryptForSync(data);
|
||||
if (encResult.needsEncryption) {
|
||||
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
|
||||
}
|
||||
if (encResult.needsAuth) {
|
||||
return { success: false, needsAuth: true, reason: 'Authentication required.' };
|
||||
}
|
||||
const payload = encResult.data;
|
||||
const payload = encResult.data || data;
|
||||
|
||||
const content = JSON.stringify(payload, null, 2);
|
||||
const stored = await api.storage.local.get('ss_gist_id');
|
||||
@@ -820,13 +810,10 @@ const SilentSendSync = {
|
||||
const data = await this._getAllData();
|
||||
|
||||
const encResult = await this._encryptForSync(data);
|
||||
if (encResult.needsEncryption) {
|
||||
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
|
||||
}
|
||||
if (encResult.needsAuth) {
|
||||
return { success: false, needsAuth: true, reason: 'Authentication required.' };
|
||||
}
|
||||
const payload = encResult.data;
|
||||
const payload = encResult.data || data;
|
||||
|
||||
const resp = await fetch(url, {
|
||||
method,
|
||||
|
||||
Reference in New Issue
Block a user