feat: password import, proper noun detection, full README update
Import parser: - Passwords now imported from Chrome/Firefox/Bitwarden/1Password CSVs as exact-match auto-redacted mappings (→ [REDACTED-PASSWORD-N]) - Catches passwords in any context, not just key=value patterns Proper noun heuristic: - Auto-detect scanner now catches capitalized words mid-sentence as potential names, company names, or project names - Filters against 200+ common English words, programming terms, days, months to reduce false positives - Added to both content.js (page world) and auto-detect.js (popup) README: - Documented proper noun detection with examples - Documented bulk import with all supported formats - Added Sync features section (auto sync, conflict resolution, version history, connected devices) - Added Organization/Team section with policy format - Added tamper protection documentation - Added Legal section with liability analysis - Updated architecture with all new modules https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
@@ -60,6 +60,36 @@ A browser extension (Chrome + Firefox) that intercepts personal information and
|
||||
| `123-45-6789` | `[REDACTED-SSN]` |
|
||||
| `4111 1111 1111 1111` | `[REDACTED-CARD]` |
|
||||
|
||||
### Proper noun detection (automatic)
|
||||
|
||||
The auto-detect scanner also catches capitalized words mid-sentence that might be names, company names, or project names you forgot to configure. For example:
|
||||
|
||||
| You type | What happens |
|
||||
|----------|-------------|
|
||||
| `...talked to Sarah about the deploy` | Flags "Sarah" as a possible name |
|
||||
| `...the Acme Corp internal API` | Flags "Acme Corp" as a possible organization |
|
||||
| `...pushed to Project Atlas staging` | Flags "Project Atlas" as a possible project name |
|
||||
|
||||
These are flagged as warnings (not auto-redacted) so you can decide whether to add them as mappings. Common English words, programming terms, days, and months are excluded to reduce false positives.
|
||||
|
||||
### Bulk import (speed up setup)
|
||||
|
||||
Import your existing data from password managers and browser autofill to pre-populate identity and mappings:
|
||||
|
||||
| Source | What's imported |
|
||||
|---|---|
|
||||
| Chrome password CSV | Usernames, emails, domains, passwords (auto-redacted) |
|
||||
| Firefox logins CSV | Usernames, emails, domains, passwords (auto-redacted) |
|
||||
| Bitwarden CSV | Usernames, emails, domains, passwords (auto-redacted) |
|
||||
| 1Password CSV | Usernames, emails, domains, passwords (auto-redacted) |
|
||||
| Browser autofill CSV | Names, emails, phones, addresses |
|
||||
| Plain CSV (2 columns) | Real → substitute pairs |
|
||||
| Plain text (1 per line) | Auto-categorized values needing substitutes |
|
||||
|
||||
Passwords are imported as exact-match mappings (e.g. `MyS3cret!` → `[REDACTED-PASSWORD-1]`) so they get caught in any context — not just `password=value` patterns.
|
||||
|
||||
Go to **Options** → **Transfer Data** → **Import CSV / Password Export**.
|
||||
|
||||
## First-time setup
|
||||
|
||||
After installing the extension, **it does nothing until you configure it**. The icon will be gray to remind you.
|
||||
@@ -266,9 +296,75 @@ src/
|
||||
crypto.js — AES-256-GCM encryption, PBKDF2 key derivation, TOTP (RFC 6238), WebAuthn, key caching
|
||||
sync.js — Cross-browser sync with encryption (browser sync, Gist, folder, URL, sync codes)
|
||||
storage.js — Browser storage wrapper with transparent at-rest encryption
|
||||
auto-detect.js — PPI pattern detection (IPs, addresses, paths, proper nouns)
|
||||
import-parser.js — Bulk import from CSV, password managers, autofill exports
|
||||
version-history.js — Sync version snapshots + rollback
|
||||
merge.js — Three-way field-level merge for sync conflicts
|
||||
org-policy.js — Organization policy management (shared rules, compliance)
|
||||
tamper-guard.js — Admin password protection for destructive actions
|
||||
browser-polyfill.js — Chrome/Firefox API compatibility
|
||||
```
|
||||
|
||||
## Sync features
|
||||
|
||||
### Auto sync
|
||||
|
||||
When configured, the extension automatically pushes and pulls settings on a configurable interval (5/15/30/60 minutes) using GitHub Gist or a custom URL endpoint. Local changes trigger an immediate push.
|
||||
|
||||
### Conflict resolution
|
||||
|
||||
When both this device and another device change the same data between syncs, the extension performs a three-way merge:
|
||||
- Non-conflicting changes are merged automatically
|
||||
- True conflicts (same field changed on both sides) are presented in a side-by-side UI where you choose "Keep Local" or "Keep Remote" for each conflict
|
||||
|
||||
### Version history + rollback
|
||||
|
||||
Every sync operation saves a snapshot of your data. You can browse previous versions and restore any snapshot. Configurable max snapshots (default 10).
|
||||
|
||||
### Connected devices
|
||||
|
||||
Each device registers itself with a name and browser type. The device list is shared via sync data so you can see all connected devices, when they last synced, and remove old ones.
|
||||
|
||||
## Organization / Team
|
||||
|
||||
For teams that want to enforce privacy rules across all members:
|
||||
|
||||
1. **Admin** creates a JSON policy file hosted at any URL (static file, S3, cloud function)
|
||||
2. **Team members** join by entering the policy URL or an invite code in Options → Organization
|
||||
3. **Org rules merge** with personal rules — required mappings are always active and cannot be disabled
|
||||
4. **Compliance dashboard** shows which required fields are configured (without revealing actual PPI)
|
||||
5. **Policy updates** are polled automatically (hourly)
|
||||
|
||||
### Org policy format
|
||||
|
||||
```json
|
||||
{
|
||||
"orgId": "acme-corp",
|
||||
"orgName": "Acme Corp",
|
||||
"version": 2,
|
||||
"requiredMappings": [
|
||||
{ "real": "acme-internal.com", "substitute": "example-corp.com", "category": "domain" }
|
||||
],
|
||||
"requiredSecretPatterns": [
|
||||
{ "name": "Acme Token", "regex": "acme_[a-z0-9]{32}", "redact": "[REDACTED-ACME-TOKEN]" }
|
||||
],
|
||||
"sharedIdentityRules": {
|
||||
"requireCatchAllEmail": true,
|
||||
"requiredCategories": ["name", "email", "domain"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tamper protection
|
||||
|
||||
Optional admin password (separate from vault password) that gates destructive actions:
|
||||
- Disabling the extension
|
||||
- Clearing data or mappings
|
||||
- Leaving an organization
|
||||
- Exporting data in plaintext
|
||||
|
||||
This is a deterrent for casual tampering. It cannot prevent browser-level uninstall or developer tools access.
|
||||
|
||||
## Privacy & Security
|
||||
|
||||
- All data stays local in browser storage — no external servers, no telemetry, no analytics
|
||||
@@ -351,6 +447,25 @@ Reveal mode only replaces values that were **actually substituted** in outbound
|
||||
|
||||
**You should still review sensitive messages before sending.** Silent Send is a safety net, not a guarantee. Think of it like a spell checker for privacy — it catches most things, but you wouldn't send a legal document without proofreading.
|
||||
|
||||
## Legal
|
||||
|
||||
### Liability
|
||||
|
||||
Silent Send is provided **"as is" without warranty of any kind**. The disclaimer section above explicitly documents known limitations. Users should not rely on Silent Send as their sole privacy protection.
|
||||
|
||||
To mitigate litigation risk:
|
||||
- The extension clearly states it is a **convenience tool, not a security guarantee** in both the README and the popup footer
|
||||
- Known failure modes are documented (images, file uploads, encoded data, unconfigured data)
|
||||
- The "~85-90% correctness" estimate sets realistic expectations
|
||||
- No marketing claims of "complete protection" or "guaranteed privacy" are made
|
||||
- The BSL license includes standard liability limitation language
|
||||
|
||||
This is comparable to how antivirus software, ad blockers, and password managers handle liability — they document limitations, disclaim warranties, and don't claim perfection. The key is to never overstate what the tool does.
|
||||
|
||||
### Open source
|
||||
|
||||
The source code is available under BSL 1.1. Contributions are welcome. If you find a bug, especially a privacy-related one, please report it.
|
||||
|
||||
## License
|
||||
|
||||
[Business Source License 1.1](LICENSE) — free for personal, non-commercial use. Commercial use requires a paid license. The code automatically converts to MIT on March 26, 2030.
|
||||
|
||||
@@ -346,6 +346,114 @@
|
||||
|
||||
const CONTEXT_WORDS_RE = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|zip|postal|date\s+of\s+birth)\b/i;
|
||||
|
||||
// Common English words that are capitalized but aren't proper nouns.
|
||||
// Used by the proper noun heuristic to reduce false positives.
|
||||
const COMMON_CAPITALIZED = new Set([
|
||||
'the', 'a', 'an', 'and', 'or', 'but', 'if', 'then', 'else', 'when',
|
||||
'at', 'by', 'for', 'with', 'about', 'against', 'between', 'through',
|
||||
'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up',
|
||||
'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further',
|
||||
'then', 'once', 'here', 'there', 'all', 'each', 'every', 'both',
|
||||
'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not',
|
||||
'only', 'own', 'same', 'so', 'than', 'too', 'very', 'can', 'will',
|
||||
'just', 'should', 'now', 'also', 'into', 'could', 'would', 'may',
|
||||
'might', 'shall', 'must', 'need', 'have', 'has', 'had', 'do', 'does',
|
||||
'did', 'be', 'is', 'am', 'are', 'was', 'were', 'been', 'being',
|
||||
'get', 'got', 'make', 'made', 'go', 'went', 'gone', 'take', 'took',
|
||||
'come', 'came', 'see', 'saw', 'know', 'knew', 'think', 'thought',
|
||||
'say', 'said', 'tell', 'told', 'give', 'gave', 'find', 'found',
|
||||
'want', 'let', 'put', 'set', 'run', 'keep', 'try', 'start', 'turn',
|
||||
'show', 'hear', 'play', 'move', 'live', 'believe', 'bring', 'happen',
|
||||
'write', 'provide', 'sit', 'stand', 'lose', 'pay', 'meet', 'include',
|
||||
'continue', 'learn', 'change', 'lead', 'understand', 'watch', 'follow',
|
||||
'stop', 'create', 'speak', 'read', 'allow', 'add', 'spend', 'grow',
|
||||
'open', 'walk', 'win', 'offer', 'remember', 'love', 'consider', 'appear',
|
||||
'buy', 'wait', 'serve', 'die', 'send', 'expect', 'build', 'stay',
|
||||
'fall', 'cut', 'reach', 'kill', 'remain', 'suggest', 'raise', 'pass',
|
||||
'sell', 'require', 'report', 'decide', 'pull', 'develop', 'note',
|
||||
'however', 'because', 'although', 'since', 'while', 'where', 'which',
|
||||
'what', 'who', 'how', 'why', 'this', 'that', 'these', 'those',
|
||||
'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her',
|
||||
'us', 'them', 'my', 'your', 'his', 'its', 'our', 'their',
|
||||
'new', 'old', 'big', 'small', 'long', 'short', 'good', 'bad',
|
||||
'great', 'little', 'right', 'left', 'first', 'last', 'next',
|
||||
'sure', 'like', 'well', 'back', 'still', 'even', 'much', 'many',
|
||||
// Programming/tech words that appear capitalized
|
||||
'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
|
||||
'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
|
||||
'import', 'export', 'default', 'const', 'let', 'var', 'async', 'await',
|
||||
'try', 'catch', 'throw', 'finally', 'switch', 'case', 'break',
|
||||
'note', 'example', 'warning', 'important', 'todo', 'fixme', 'hack',
|
||||
'step', 'option', 'result', 'value', 'key', 'data', 'info',
|
||||
'file', 'code', 'test', 'debug', 'config', 'setup', 'update',
|
||||
// Common sentence starters that aren't names
|
||||
'please', 'thanks', 'hello', 'hi', 'hey', 'dear', 'sincerely',
|
||||
'regards', 'best', 'cheers', 'sorry', 'yes', 'no', 'ok', 'okay',
|
||||
// Days and months (not PPI)
|
||||
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
||||
'january', 'february', 'march', 'april', 'may', 'june', 'july',
|
||||
'august', 'september', 'october', 'november', 'december',
|
||||
'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun',
|
||||
'jan', 'feb', 'mar', 'apr', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Detect proper nouns (potential names, company names, project names)
|
||||
* that aren't configured in identity. Uses capitalization heuristics:
|
||||
* - Capitalized words not at the start of a sentence
|
||||
* - Multi-word capitalized sequences (e.g. "Acme Corp", "Project Atlas")
|
||||
* - Filters out common English words and programming terms
|
||||
*/
|
||||
function detectProperNouns(text, configured) {
|
||||
const findings = [];
|
||||
// Match capitalized words that aren't at the very start of the text
|
||||
// and aren't after a period/newline (sentence start)
|
||||
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;
|
||||
|
||||
// Check if this is at the start of a sentence
|
||||
const before = text.slice(Math.max(0, m.index - 2), m.index);
|
||||
const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before);
|
||||
|
||||
// Split into individual words and check each
|
||||
const words = fullMatch.split(/\s+/);
|
||||
const properWords = words.filter(w =>
|
||||
w.length >= 3 &&
|
||||
!COMMON_CAPITALIZED.has(w.toLowerCase()) &&
|
||||
!configured.has(w.toLowerCase())
|
||||
);
|
||||
|
||||
if (properWords.length === 0) continue;
|
||||
|
||||
// Single capitalized word at sentence start = likely not a proper noun
|
||||
if (isSentenceStart && properWords.length === 1 && words.length === 1) continue;
|
||||
|
||||
// Multi-word capitalized sequence is likely a proper noun
|
||||
// Single capitalized word mid-sentence is likely a proper noun
|
||||
const value = properWords.join(' ');
|
||||
if (value.length >= 3 && !configured.has(value.toLowerCase())) {
|
||||
findings.push({
|
||||
name: 'Possible Name/Org',
|
||||
value,
|
||||
hint: 'Capitalized word — could be a name, company, or project',
|
||||
category: 'name',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
const seen = new Set();
|
||||
return findings.filter(f => {
|
||||
if (seen.has(f.value)) return false;
|
||||
seen.add(f.value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function autoDetectPPI(text, ident) {
|
||||
if (!text || text.length < 5) return [];
|
||||
const hasContext = CONTEXT_WORDS_RE.test(text);
|
||||
@@ -374,6 +482,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Proper noun heuristic — catch names, company names, project names
|
||||
// that aren't configured in identity
|
||||
const properNouns = detectProperNouns(text, configured);
|
||||
findings.push(...properNouns);
|
||||
|
||||
// Deduplicate by value
|
||||
const seen = new Set();
|
||||
return findings.filter(f => {
|
||||
|
||||
+86
-3
@@ -193,21 +193,104 @@ const AutoDetect = {
|
||||
}
|
||||
}
|
||||
|
||||
// Proper noun heuristic — catch names, company names, project names
|
||||
const properNouns = this._detectProperNouns(text, configured);
|
||||
findings.push(...properNouns);
|
||||
|
||||
// Deduplicate overlapping matches
|
||||
findings.sort((a, b) => a.index - b.index);
|
||||
findings.sort((a, b) => (a.index || 0) - (b.index || 0));
|
||||
const deduped = [];
|
||||
let lastEnd = -1;
|
||||
for (const f of findings) {
|
||||
if (f.index >= lastEnd) {
|
||||
const idx = f.index || 0;
|
||||
if (idx >= lastEnd) {
|
||||
deduped.push(f);
|
||||
lastEnd = f.index + f.value.length;
|
||||
lastEnd = idx + f.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
return deduped;
|
||||
},
|
||||
|
||||
/**
|
||||
* Detect capitalized words mid-sentence that might be proper nouns
|
||||
* (names, company names, project names) not in the configured set.
|
||||
*/
|
||||
_detectProperNouns(text, configured) {
|
||||
const findings = [];
|
||||
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 &&
|
||||
!COMMON_WORDS.has(w.toLowerCase()) &&
|
||||
!configured.has(w.toLowerCase())
|
||||
);
|
||||
|
||||
if (properWords.length === 0) continue;
|
||||
if (isSentenceStart && properWords.length === 1 && words.length === 1) continue;
|
||||
|
||||
const value = properWords.join(' ');
|
||||
if (value.length >= 3 && !configured.has(value.toLowerCase())) {
|
||||
findings.push({
|
||||
name: 'Possible Name/Org',
|
||||
value,
|
||||
hint: 'Capitalized word — could be a name, company, or project',
|
||||
category: 'name',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
return findings.filter(f => {
|
||||
if (seen.has(f.value)) return false;
|
||||
seen.add(f.value);
|
||||
return true;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Common English words to exclude from proper noun detection
|
||||
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',
|
||||
'new', 'now', 'old', 'see', 'way', 'who', 'did', 'get', 'let', 'say',
|
||||
'she', 'too', 'use', 'also', 'back', 'been', 'call', 'came', 'come',
|
||||
'could', 'each', 'even', 'find', 'from', 'give', 'good', 'great',
|
||||
'have', 'here', 'high', 'into', 'just', 'keep', 'know', 'last', 'like',
|
||||
'live', 'long', 'look', 'made', 'make', 'many', 'more', 'most', 'much',
|
||||
'must', 'name', 'next', 'only', 'over', 'part', 'people', 'place',
|
||||
'same', 'show', 'side', 'since', 'some', 'still', 'such', 'take',
|
||||
'tell', 'than', 'that', 'them', 'then', 'there', 'these', 'they',
|
||||
'this', 'time', 'turn', 'used', 'very', 'want', 'well', 'were',
|
||||
'what', 'when', 'where', 'which', 'while', 'will', 'with', 'work',
|
||||
'would', 'year', 'your', 'about', 'after', 'again', 'being', 'between',
|
||||
'both', 'before', 'down', 'during', 'first', 'found', 'group',
|
||||
'however', 'important', 'large', 'later', 'little', 'never',
|
||||
'number', 'other', 'point', 'right', 'small', 'state', 'thing',
|
||||
'think', 'those', 'three', 'through', 'under', 'until', 'water',
|
||||
'world', 'write', 'might', 'should', 'because', 'although',
|
||||
// Programming / tech terms
|
||||
'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
|
||||
'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
|
||||
'import', 'export', 'default', 'const', 'async', 'await',
|
||||
'note', 'example', 'warning', 'step', 'option', 'result', 'value',
|
||||
'key', 'data', 'info', 'file', 'code', 'test', 'debug', 'config',
|
||||
'setup', 'update', 'please', 'thanks', 'hello', 'sorry',
|
||||
// Days and months
|
||||
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
||||
'january', 'february', 'march', 'april', 'may', 'june', 'july',
|
||||
'august', 'september', 'october', 'november', 'december',
|
||||
]);
|
||||
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.AutoDetect = AutoDetect;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
* Imports as identity fields with blank substitutes so the user
|
||||
* can see what needs mapping and fill in fakes.
|
||||
*
|
||||
* 3. Chrome password CSV export — extracts usernames, names, URLs
|
||||
* 3. Chrome password CSV export — extracts usernames, names, URLs, passwords
|
||||
* Columns: name, url, username, password, note
|
||||
* Passwords are NEVER imported. Only usernames/emails/URLs.
|
||||
* Passwords are imported as auto-redacted mappings (e.g. → [REDACTED-PASSWORD-1]).
|
||||
*
|
||||
* 4. Firefox password CSV export — similar to Chrome
|
||||
* Columns: url, username, password, ...
|
||||
@@ -139,7 +139,10 @@ const ImportParser = {
|
||||
|
||||
/**
|
||||
* Parse Chrome/Firefox password CSV export.
|
||||
* NEVER imports passwords — only usernames, emails, and domains.
|
||||
* Imports usernames, emails, domains, AND passwords.
|
||||
* Passwords are imported as mappings with auto-generated redaction
|
||||
* substitutes (e.g. "[REDACTED-PASSWORD-1]") so they get caught
|
||||
* if pasted into any context — not just key=value patterns.
|
||||
*/
|
||||
parsePasswordCSV(text) {
|
||||
const result = this._empty();
|
||||
@@ -148,12 +151,15 @@ const ImportParser = {
|
||||
|
||||
const headers = this._splitCSVLine(lines[0], ',').map(h => h.trim().toLowerCase());
|
||||
const usernameIdx = headers.findIndex(h => h === 'username' || h === 'login_username' || h === 'user');
|
||||
const passwordIdx = headers.findIndex(h => h === 'password' || h === 'login_password');
|
||||
const urlIdx = headers.findIndex(h => h === 'url' || h === 'login_uri' || h === 'origin' || h === 'web site');
|
||||
const nameIdx = headers.findIndex(h => h === 'name' || h === 'title');
|
||||
|
||||
const seenEmails = new Set();
|
||||
const seenUsernames = new Set();
|
||||
const seenDomains = new Set();
|
||||
const seenPasswords = new Set();
|
||||
let passwordCount = 0;
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cols = this._splitCSVLine(lines[i], ',');
|
||||
@@ -172,6 +178,22 @@ const ImportParser = {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract password — import as a redacted mapping
|
||||
if (passwordIdx >= 0 && cols[passwordIdx]) {
|
||||
const password = cols[passwordIdx].trim();
|
||||
// Skip very short or empty passwords, and deduplicate
|
||||
if (password && password.length >= 4 && !seenPasswords.has(password)) {
|
||||
seenPasswords.add(password);
|
||||
passwordCount++;
|
||||
result.mappings.push({
|
||||
real: password,
|
||||
substitute: `[REDACTED-PASSWORD-${passwordCount}]`,
|
||||
category: 'password',
|
||||
caseSensitive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract domain from URL
|
||||
if (urlIdx >= 0 && cols[urlIdx]) {
|
||||
try {
|
||||
@@ -202,9 +224,12 @@ const ImportParser = {
|
||||
|
||||
const headers = this._splitCSVLine(lines[0], ',').map(h => h.trim().toLowerCase());
|
||||
const usernameIdx = headers.findIndex(h => h.includes('username'));
|
||||
const passwordIdx = headers.findIndex(h => h.includes('password'));
|
||||
const uriIdx = headers.findIndex(h => h.includes('uri') || h.includes('url'));
|
||||
|
||||
const seen = new Set();
|
||||
const seenPasswords = new Set();
|
||||
let passwordCount = 0;
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cols = this._splitCSVLine(lines[i], ',');
|
||||
@@ -221,6 +246,20 @@ const ImportParser = {
|
||||
}
|
||||
}
|
||||
|
||||
if (passwordIdx >= 0 && cols[passwordIdx]) {
|
||||
const pw = cols[passwordIdx].trim();
|
||||
if (pw && pw.length >= 4 && !seenPasswords.has(pw)) {
|
||||
seenPasswords.add(pw);
|
||||
passwordCount++;
|
||||
result.mappings.push({
|
||||
real: pw,
|
||||
substitute: `[REDACTED-PASSWORD-${passwordCount}]`,
|
||||
category: 'password',
|
||||
caseSensitive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (uriIdx >= 0 && cols[uriIdx]) {
|
||||
try {
|
||||
const domain = new URL(cols[uriIdx].trim()).hostname;
|
||||
|
||||
Reference in New Issue
Block a user