merge: integrate claude/read-repo-YMu21 branch
Merged changes from the other session: - Ko-fi support link in options footer - PPI → PII renaming across codebase - Legal/licensing updates (stronger disclaimer, non-commercial only) - Custom secret/auto-redact patterns - Internal refactoring of secret scanner to auto-redact naming - Custom domains in popup Options tab - Additional AI services (Perplexity, DeepSeek, HuggingChat, Poe) - Developer sites (GitHub, GitLab, Reddit, StackOverflow, Pastebin) - Dynamic content script registration for custom domains Resolved conflicts in options.html and popup.html (kept newer "Auto Redact" naming with custom patterns description). https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
@@ -12,27 +12,63 @@ derivative works, redistribute, and make non-production use of the
|
|||||||
Licensed Work.
|
Licensed Work.
|
||||||
|
|
||||||
The Licensor hereby grants you the right to make production use of
|
The Licensor hereby grants you the right to make production use of
|
||||||
the Licensed Work for personal, non-commercial purposes.
|
the Licensed Work for personal, non-commercial purposes only.
|
||||||
|
|
||||||
For commercial use, you must obtain a commercial license from the
|
Commercial use of the Licensed Work is not permitted under this
|
||||||
Licensor. Contact: [your-email-here]
|
license. This includes use by or on behalf of any organization,
|
||||||
|
business, or entity for revenue-generating purposes, internal
|
||||||
|
business operations, or as part of a commercial product or service.
|
||||||
|
|
||||||
Effective on the Change Date, the Licensor hereby grants you rights
|
Effective on the Change Date, the Licensor hereby grants you rights
|
||||||
under the terms of the Change License, and the rights granted above
|
under the terms of the Change License, and the rights granted above
|
||||||
terminate.
|
terminate.
|
||||||
|
|
||||||
If your use of the Licensed Work does not comply with the
|
|
||||||
requirements currently in effect as described in this License, you
|
|
||||||
must purchase a commercial license from the Licensor, or you must
|
|
||||||
refrain from using the Licensed Work.
|
|
||||||
|
|
||||||
All copies of the original and modified Licensed Work, and
|
All copies of the original and modified Licensed Work, and
|
||||||
derivative works of the Licensed Work, are subject to this License.
|
derivative works of the Licensed Work, are subject to this License.
|
||||||
|
|
||||||
THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS
|
DISCLAIMER OF WARRANTY
|
||||||
ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
||||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
THE LICENSED WORK IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY
|
WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
PURPOSE, NONINFRINGEMENT, ACCURACY, COMPLETENESS, OR RELIABILITY.
|
||||||
LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
|
|
||||||
|
THE LICENSOR DOES NOT WARRANT THAT THE LICENSED WORK WILL:
|
||||||
|
(A) DETECT, INTERCEPT, OR SUBSTITUTE ALL PERSONAL INFORMATION,
|
||||||
|
SECRETS, CREDENTIALS, OR SENSITIVE DATA IN ALL CIRCUMSTANCES;
|
||||||
|
(B) FUNCTION CORRECTLY ON ALL WEBSITES, SERVICES, OR PLATFORMS,
|
||||||
|
INCLUDING AFTER THIRD-PARTY CHANGES TO THEIR INTERFACES,
|
||||||
|
APIs, OR DATA FORMATS;
|
||||||
|
(C) BE FREE OF ERRORS, BUGS, INTERRUPTIONS, OR SECURITY
|
||||||
|
VULNERABILITIES;
|
||||||
|
(D) MEET ANY SPECIFIC PRIVACY, SECURITY, OR REGULATORY
|
||||||
|
REQUIREMENTS, INCLUDING BUT NOT LIMITED TO GDPR, HIPAA,
|
||||||
|
CCPA, PCI-DSS, OR ANY OTHER DATA PROTECTION FRAMEWORK.
|
||||||
|
|
||||||
|
LIMITATION OF LIABILITY
|
||||||
|
|
||||||
|
IN NO EVENT SHALL THE LICENSOR, CONTRIBUTORS, OR DISTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||||
|
CONSEQUENTIAL, OR PUNITIVE DAMAGES (INCLUDING BUT NOT LIMITED TO
|
||||||
|
LOSS OF DATA, LOSS OF PRIVACY, IDENTITY THEFT, UNAUTHORIZED
|
||||||
|
DISCLOSURE OF PERSONAL INFORMATION, REGULATORY FINES OR PENALTIES,
|
||||||
|
REPUTATIONAL HARM, LOSS OF BUSINESS, OR PROCUREMENT OF SUBSTITUTE
|
||||||
|
SERVICES) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||||
|
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF OR INABILITY TO USE
|
||||||
|
THE LICENSED WORK, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
THIS LIMITATION APPLIES REGARDLESS OF WHETHER THE FAILURE RESULTS
|
||||||
|
FROM: (A) THIRD-PARTY CHANGES TO WEBSITES, APIs, OR SERVICES THAT
|
||||||
|
CAUSE THE LICENSED WORK TO MALFUNCTION OR FAIL SILENTLY; (B) USER
|
||||||
|
MISCONFIGURATION OR FAILURE TO VERIFY SUBSTITUTIONS; (C) PATTERNS,
|
||||||
|
FORMATS, OR DATA TYPES NOT RECOGNIZED BY THE LICENSED WORK; OR
|
||||||
|
(D) ANY OTHER CAUSE BEYOND THE LICENSOR'S REASONABLE CONTROL.
|
||||||
|
|
||||||
|
THE LICENSED WORK IS A CONVENIENCE TOOL THAT REDUCES — BUT CANNOT
|
||||||
|
ELIMINATE — THE RISK OF INADVERTENT DISCLOSURE OF PERSONAL
|
||||||
|
INFORMATION. USERS ARE SOLELY RESPONSIBLE FOR VERIFYING THAT
|
||||||
|
SENSITIVE DATA HAS BEEN PROPERLY SUBSTITUTED BEFORE TRANSMISSION
|
||||||
|
AND FOR IMPLEMENTING ADDITIONAL SAFEGUARDS APPROPRIATE TO THEIR
|
||||||
|
USE CASE.
|
||||||
|
|||||||
+11
@@ -62,6 +62,17 @@ If this privacy policy changes, the updated version will be posted at this URL a
|
|||||||
|
|
||||||
For questions about this privacy policy, open an issue at: https://github.com/outis1one/silent-send/issues
|
For questions about this privacy policy, open an issue at: https://github.com/outis1one/silent-send/issues
|
||||||
|
|
||||||
|
## Limitations and disclaimer
|
||||||
|
|
||||||
|
Silent Send is a convenience tool that reduces the risk of sharing personal information with third-party services. It does not guarantee complete protection. Specifically:
|
||||||
|
|
||||||
|
- **Third-party changes:** Websites may change their APIs, interfaces, or data submission methods at any time. Such changes can cause Silent Send to stop intercepting or substituting data without any visible error. Silent Send contributors have no control over third-party websites and accept no responsibility for changes they make.
|
||||||
|
- **Coverage gaps:** Silent Send may not detect personal information in images, unusual text formats, non-standard encodings, dynamically generated content, or patterns not covered by built-in or custom rules.
|
||||||
|
- **User responsibility:** You are responsible for verifying that substitutions are applied correctly before transmitting sensitive data. The extension provides a Test tab for this purpose.
|
||||||
|
- **Not a compliance tool:** Silent Send does not guarantee compliance with any privacy regulation (GDPR, HIPAA, CCPA, PCI-DSS, or otherwise). Organizations requiring regulatory compliance should implement appropriate professional tooling and processes.
|
||||||
|
|
||||||
|
For the full limitation of liability, see the [LICENSE](LICENSE) file.
|
||||||
|
|
||||||
## Open source
|
## Open source
|
||||||
|
|
||||||
Silent Send's source code is publicly available at https://github.com/outis1one/silent-send — you can verify every claim in this policy by reading the code.
|
Silent Send's source code is publicly available at https://github.com/outis1one/silent-send — you can verify every claim in this policy by reading the code.
|
||||||
|
|||||||
@@ -98,20 +98,20 @@ Go to **Options** → **Transfer Data** → **Import CSV / Password Export**.
|
|||||||
|
|
||||||
### Document scanning
|
### Document scanning
|
||||||
|
|
||||||
When you upload files to an AI service, Silent Send extracts and scans the text for PPI before the file is sent:
|
When you upload files to an AI service, Silent Send extracts and scans the text for PII before the file is sent:
|
||||||
|
|
||||||
| Format | How it works |
|
| Format | How it works |
|
||||||
|---|---|
|
|---|---|
|
||||||
| PDF | Text extracted from content streams, PPI substituted, uploaded as clean plaintext |
|
| PDF | Text extracted from content streams, PII substituted, uploaded as clean plaintext |
|
||||||
| DOCX, XLSX, PPTX | XML text extracted from ZIP structure, PPI substituted, uploaded as plaintext |
|
| DOCX, XLSX, PPTX | XML text extracted from ZIP structure, PII substituted, uploaded as plaintext |
|
||||||
| ODT, ODS, ODP | OpenDocument XML text extracted, PPI substituted |
|
| ODT, ODS, ODP | OpenDocument XML text extracted, PII substituted |
|
||||||
| DOC, XLS | Legacy binary format — readable text runs extracted |
|
| DOC, XLS | Legacy binary format — readable text runs extracted |
|
||||||
| RTF | Formatting stripped, text extracted |
|
| RTF | Formatting stripped, text extracted |
|
||||||
| TXT, CSV, JSON, code files | Direct string substitution |
|
| TXT, CSV, JSON, code files | Direct string substitution |
|
||||||
| Images (PNG, JPG, etc.) | Not scanned — no text to extract |
|
| Images (PNG, JPG, etc.) | Not scanned — no text to extract |
|
||||||
| Scanned PDFs (image-only) | Not scanned — no text layer |
|
| Scanned PDFs (image-only) | Not scanned — no text layer |
|
||||||
|
|
||||||
For PDF/DOCX/XLSX uploads, a preview panel shows what PPI was found before uploading. You can choose to substitute and upload, or upload the original. Text files are substituted silently. The original file on your disk is never modified — substitution only happens to the in-flight upload.
|
For PDF/DOCX/XLSX uploads, a preview panel shows what PII was found before uploading. You can choose to substitute and upload, or upload the original. Text files are substituted silently. The original file on your disk is never modified — substitution only happens to the in-flight upload.
|
||||||
|
|
||||||
## First-time setup
|
## First-time setup
|
||||||
|
|
||||||
@@ -389,7 +389,7 @@ src/
|
|||||||
crypto.js — AES-256-GCM encryption, PBKDF2 key derivation, TOTP (RFC 6238), WebAuthn, key caching
|
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)
|
sync.js — Cross-browser sync with encryption (browser sync, Gist, folder, URL, sync codes)
|
||||||
storage.js — Browser storage wrapper with transparent at-rest encryption
|
storage.js — Browser storage wrapper with transparent at-rest encryption
|
||||||
auto-detect.js — PPI pattern detection (IPs, addresses, paths, proper nouns)
|
auto-detect.js — PII pattern detection (IPs, addresses, paths, proper nouns)
|
||||||
document-scanner.js — PDF/DOCX/XLSX/ODT/RTF text extraction and scanning
|
document-scanner.js — PDF/DOCX/XLSX/ODT/RTF text extraction and scanning
|
||||||
import-parser.js — Bulk import from CSV, password managers, autofill exports
|
import-parser.js — Bulk import from CSV, password managers, autofill exports
|
||||||
version-history.js — Sync version snapshots + rollback
|
version-history.js — Sync version snapshots + rollback
|
||||||
@@ -440,7 +440,7 @@ 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)
|
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
|
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
|
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)
|
4. **Compliance dashboard** shows which required fields are configured (without revealing actual PII)
|
||||||
5. **Policy updates** are polled automatically (hourly)
|
5. **Policy updates** are polled automatically (hourly)
|
||||||
|
|
||||||
### Org policy format
|
### Org policy format
|
||||||
@@ -492,7 +492,7 @@ When you enable **sync encryption** (Options → Sync Between Browsers → Sync
|
|||||||
| TOTP secret | Authenticator app shared secret |
|
| TOTP secret | Authenticator app shared secret |
|
||||||
| All sync data | Everything sent to Gist, sync folders, custom URLs, browser sync |
|
| All sync data | Everything sent to Gist, sync folders, custom URLs, browser sync |
|
||||||
|
|
||||||
**Only two things remain plaintext** — the encryption salt (needed to derive the key) and a verification blob (needed to check the password). Neither contains PPI.
|
**Only two things remain plaintext** — the encryption salt (needed to derive the key) and a verification blob (needed to check the password). Neither contains PII.
|
||||||
|
|
||||||
Without at-rest encryption, data is stored in plaintext in the browser's local storage (similar to cookies and localStorage). Anyone with file system access to your browser profile directory could read it.
|
Without at-rest encryption, data is stored in plaintext in the browser's local storage (similar to cookies and localStorage). Anyone with file system access to your browser profile directory could read it.
|
||||||
|
|
||||||
@@ -555,10 +555,35 @@ Silent Send works well for text you type and most document uploads, but some thi
|
|||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
Silent Send is provided "as is" without warranty of any kind. It is a convenience tool that reduces the chance of accidentally sharing personal information with AI services. It is not a security guarantee and should not be your only privacy protection. The source code is available for inspection — you don't have to take our word for it.
|
Silent Send is provided "as is" and "as available" without warranty of any kind, express or implied. It is a **convenience tool** that reduces — but cannot eliminate — the risk of accidentally sharing personal information with AI services. It is **not a security guarantee** and should not be your only privacy protection.
|
||||||
|
|
||||||
|
**What this means in practice:**
|
||||||
|
|
||||||
|
- Silent Send may fail to detect or substitute personal information in certain formats, edge cases, images, or data you haven't configured.
|
||||||
|
- Third-party websites (ChatGPT, Claude, Gemini, Reddit, GitHub, etc.) may change their interfaces, APIs, or data submission methods at any time. Such changes can cause Silent Send to stop intercepting data **without any visible error or warning**. The Silent Send contributors have no control over and accept no responsibility for third-party changes.
|
||||||
|
- **You are solely responsible** for verifying that your personal data has been properly substituted before sending. The Test tab in the popup is provided for this purpose.
|
||||||
|
- Silent Send is not a substitute for professional data protection, legal compliance, or security tooling. It does not guarantee compliance with any regulation (GDPR, HIPAA, CCPA, PCI-DSS, or otherwise).
|
||||||
|
- No contributor, maintainer, or distributor of Silent Send shall be liable for any damages arising from its use, including but not limited to privacy breaches, identity theft, data exposure, or regulatory penalties — whether caused by software bugs, third-party changes, misconfiguration, or any other reason.
|
||||||
|
|
||||||
|
Commercial licensees: a commercial license grants the right to use Silent Send in a commercial context. It does not create additional warranties, indemnification, or liability obligations beyond those stated in the license agreement.
|
||||||
|
|
||||||
|
The source code is available for inspection — you don't have to take our word for it.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[Business Source License 1.1](LICENSE) — free for personal, non-commercial use. Commercial use requires a paid license. The code converts to MIT on March 26, 2030.
|
[Business Source License 1.1](LICENSE) — free for personal, non-commercial use only. Commercial use is not permitted. The code converts to MIT on March 26, 2030.
|
||||||
|
|
||||||
Contributions welcome. If you find a bug, especially a privacy-related one, please [open an issue](https://github.com/outis1one/silent-send/issues).
|
Contributions welcome. If you find a bug, especially a privacy-related one, please [open an issue](https://github.com/outis1one/silent-send/issues).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Silent Send is a weekend project — built because the developer wanted it and could not find what the developer wanted (reveal especially). No donations needed. If you find it useful, great — enjoy. If it helps you keep your personally identifiable information private, that's thanks enough. More people having access to privacy is the goal.
|
||||||
|
|
||||||
|
That said, Claude access isn't free. If you have a desire to give a coffee back, it would be appreciated — but don't feel any obligation to tip.
|
||||||
|
|
||||||
|
**[Buy me a coffee on Ko-fi](https://ko-fi.com/YOUR_KOFI_USERNAME)**
|
||||||
|
|
||||||
|
A few things to know:
|
||||||
|
- Donations do not influence updates. Silent Send may or may not get updates based on the developer's usage and available time.
|
||||||
|
- Donating does not create any warranty, support obligation, or expectation of future development.
|
||||||
|
- This is and always will be a free, non-commercial project.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auto-detect PPI warning banner */
|
/* Auto-detect PII warning banner */
|
||||||
.ss-autodetect-warning {
|
.ss-autodetect-warning {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pre-send PPI warning (spellcheck-style, appears while typing) */
|
/* Pre-send PII warning (spellcheck-style, appears while typing) */
|
||||||
.ss-presend-warning {
|
.ss-presend-warning {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
|
|||||||
+47
-36
@@ -287,8 +287,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Combined substitution: smart patterns + explicit + secret scan
|
// Combined substitution: smart patterns + explicit + auto-redact
|
||||||
// + auto-detect warning for unconfigured PPI
|
// + auto-detect warning for unconfigured PII
|
||||||
// ============================================================
|
// ============================================================
|
||||||
function substituteAll(text) {
|
function substituteAll(text) {
|
||||||
const allReplacements = [];
|
const allReplacements = [];
|
||||||
@@ -301,20 +301,20 @@
|
|||||||
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.)
|
// 3. Auto Redact (API keys, tokens, SSNs, credit cards, custom patterns, etc.)
|
||||||
let finalText = explicit.text;
|
let finalText = explicit.text;
|
||||||
if (settings.secretScanning !== false) {
|
if (settings.autoRedact !== false) {
|
||||||
const secrets = scanAndRedactSecrets(finalText);
|
const redacted = runAutoRedact(finalText);
|
||||||
allReplacements.push(...secrets.redactions);
|
allReplacements.push(...redacted.redactions);
|
||||||
finalText = secrets.text;
|
finalText = redacted.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Auto-detect: scan the FINAL text for unconfigured PPI
|
// 4. Auto-detect: scan the FINAL text for unconfigured PII
|
||||||
// Auto-redact if enabled, otherwise just warn
|
// Auto-redact if enabled, otherwise just warn
|
||||||
if (settings.autoDetect !== false) {
|
if (settings.autoDetect !== false) {
|
||||||
const warnings = autoDetectPPI(finalText, identity);
|
const warnings = autoDetectPII(finalText, identity);
|
||||||
if (warnings.length > 0) {
|
if (warnings.length > 0) {
|
||||||
// Auto-redact detected PPI in the outbound text
|
// Auto-redact detected PII in the outbound text
|
||||||
if (settings.autoRedactDetected !== false) {
|
if (settings.autoRedactDetected !== false) {
|
||||||
for (let i = warnings.length - 1; i >= 0; i--) {
|
for (let i = warnings.length - 1; i >= 0; i--) {
|
||||||
const w = warnings[i];
|
const w = warnings[i];
|
||||||
@@ -343,9 +343,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Auto-Detect PPI Scanner (inline for page world)
|
// Auto-Detect PII Scanner (inline for page world)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const PPI_PATTERNS = [
|
const PII_PATTERNS = [
|
||||||
// Network
|
// Network
|
||||||
{ name: 'Private IP', re: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
|
{ name: 'Private IP', re: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
|
||||||
hint: 'Private IP address', cat: 'network' },
|
hint: 'Private IP address', cat: 'network' },
|
||||||
@@ -574,7 +574,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoDetectPPI(text, ident) {
|
function autoDetectPII(text, ident) {
|
||||||
if (!text || text.length < 5) return [];
|
if (!text || text.length < 5) return [];
|
||||||
const hasContext = CONTEXT_WORDS_RE.test(text);
|
const hasContext = CONTEXT_WORDS_RE.test(text);
|
||||||
|
|
||||||
@@ -595,7 +595,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findings = [];
|
const findings = [];
|
||||||
for (const pat of PPI_PATTERNS) {
|
for (const pat of PII_PATTERNS) {
|
||||||
if (pat.contextRequired && !hasContext) continue;
|
if (pat.contextRequired && !hasContext) continue;
|
||||||
pat.re.lastIndex = 0;
|
pat.re.lastIndex = 0;
|
||||||
let m;
|
let m;
|
||||||
@@ -649,7 +649,7 @@
|
|||||||
|
|
||||||
safeHTML(warningEl, `
|
safeHTML(warningEl, `
|
||||||
<div class="ss-ad-header">
|
<div class="ss-ad-header">
|
||||||
<strong>Silent Send detected potential PPI that may not be substituted:</strong>
|
<strong>Silent Send detected potential PII that may not be substituted:</strong>
|
||||||
<button class="ss-ad-close">×</button>
|
<button class="ss-ad-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
${items}
|
${items}
|
||||||
@@ -672,10 +672,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Secret Scanner (inline for page world)
|
// Auto Redact (inline for page world)
|
||||||
// Detects API keys, tokens, passwords, SSNs, credit cards, etc.
|
// Detects API keys, tokens, passwords, SSNs, credit cards,
|
||||||
|
// plus user-defined custom patterns from settings.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const SECRET_PATTERNS = [
|
const REDACT_PATTERNS = [
|
||||||
// OpenAI
|
// OpenAI
|
||||||
{ name: 'OpenAI Key', re: /\bsk-[A-Za-z0-9]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
|
{ 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]' },
|
{ name: 'OpenAI Project Key', re: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
|
||||||
@@ -713,11 +714,21 @@
|
|||||||
{ 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]' },
|
{ 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) {
|
function runAutoRedact(text) {
|
||||||
const redactions = [];
|
const redactions = [];
|
||||||
let result = text;
|
let result = text;
|
||||||
|
|
||||||
for (const pat of SECRET_PATTERNS) {
|
// Combine built-in + custom patterns
|
||||||
|
const allPatterns = [...REDACT_PATTERNS];
|
||||||
|
const custom = settings.customRedactPatterns || [];
|
||||||
|
for (const cp of custom) {
|
||||||
|
if (!cp.enabled || !cp.pattern) continue;
|
||||||
|
try {
|
||||||
|
allPatterns.push({ name: cp.name, re: new RegExp(cp.pattern, 'g'), to: cp.redact });
|
||||||
|
} catch { /* invalid regex — skip */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pat of allPatterns) {
|
||||||
pat.re.lastIndex = 0;
|
pat.re.lastIndex = 0;
|
||||||
const matches = [];
|
const matches = [];
|
||||||
let m;
|
let m;
|
||||||
@@ -735,7 +746,7 @@
|
|||||||
redactions.push({
|
redactions.push({
|
||||||
original: match.value.slice(0, 8) + '...', // Don't log the full secret
|
original: match.value.slice(0, 8) + '...', // Don't log the full secret
|
||||||
replaced: replacement,
|
replaced: replacement,
|
||||||
category: 'secret',
|
category: 'redact',
|
||||||
pattern: pat.name,
|
pattern: pat.name,
|
||||||
});
|
});
|
||||||
result =
|
result =
|
||||||
@@ -1036,7 +1047,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Document Upload Processing
|
// Document Upload Processing
|
||||||
//
|
//
|
||||||
// Scans files in FormData uploads for PPI. Supports PDF, DOCX,
|
// Scans files in FormData uploads for PII. Supports PDF, DOCX,
|
||||||
// XLSX, and text files. Shows preview for binary formats.
|
// XLSX, and text files. Shows preview for binary formats.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
@@ -1108,8 +1119,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan a document file for PPI. Strategy: extract text from any
|
* Scan a document file for PII. Strategy: extract text from any
|
||||||
* format, substitute PPI, upload as plaintext. The AI extracts text
|
* format, substitute PII, upload as plaintext. The AI extracts text
|
||||||
* from files anyway — no need to preserve formatting in a file
|
* from files anyway — no need to preserve formatting in a file
|
||||||
* the user never gets back. Original stays untouched on disk.
|
* the user never gets back. Original stays untouched on disk.
|
||||||
*
|
*
|
||||||
@@ -1366,7 +1377,7 @@
|
|||||||
|
|
||||||
safeHTML(docPreviewEl, `
|
safeHTML(docPreviewEl, `
|
||||||
<div class="ss-dp-header">
|
<div class="ss-dp-header">
|
||||||
<strong>PPI found in ${esc(filename)}</strong>
|
<strong>PII found in ${esc(filename)}</strong>
|
||||||
<span class="ss-dp-count">${preview.replacementCount} item(s)</span>
|
<span class="ss-dp-count">${preview.replacementCount} item(s)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ss-dp-note">${preview.note || ''}</div>
|
<div class="ss-dp-note">${preview.note || ''}</div>
|
||||||
@@ -1495,7 +1506,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also add auto-detect and secret scanner substitutions from this session
|
// Also add auto-detect and auto-redact substitutions from this session
|
||||||
for (const [key, entry] of sessionSubstitutions) {
|
for (const [key, entry] of sessionSubstitutions) {
|
||||||
if (!pairs.some(p => p.from.toLowerCase() === key)) {
|
if (!pairs.some(p => p.from.toLowerCase() === key)) {
|
||||||
pairs.push({ from: entry.replaced, to: entry.original });
|
pairs.push({ from: entry.replaced, to: entry.original });
|
||||||
@@ -1815,7 +1826,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Pre-Send PPI Detection — scans as you type/paste (spellcheck style)
|
// Pre-Send PII Detection — scans as you type/paste (spellcheck style)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Generate obviously-fake values using reserved/standard ranges
|
// Generate obviously-fake values using reserved/standard ranges
|
||||||
@@ -1879,7 +1890,7 @@
|
|||||||
|
|
||||||
safeHTML(preSendWarningEl, `
|
safeHTML(preSendWarningEl, `
|
||||||
<div class="ss-ad-header">
|
<div class="ss-ad-header">
|
||||||
<strong>Potential PPI detected — not yet configured:</strong>
|
<strong>Potential PII detected — not yet configured:</strong>
|
||||||
<button class="ss-ad-close">×</button>
|
<button class="ss-ad-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
${items}
|
${items}
|
||||||
@@ -1918,12 +1929,12 @@
|
|||||||
// Persist via storage bridge (handles encryption transparently)
|
// Persist via storage bridge (handles encryption transparently)
|
||||||
setStorageData('ss_mappings', mappings);
|
setStorageData('ss_mappings', mappings);
|
||||||
|
|
||||||
// Replace the PPI value in the current input right now
|
// Replace the PII value in the current input right now
|
||||||
if (inputEl) {
|
if (inputEl) {
|
||||||
replaceInInput(inputEl, real, fake);
|
replaceInInput(inputEl, real, fake);
|
||||||
// Re-scan — will dismiss warning if no more PPI remains
|
// Re-scan — will dismiss warning if no more PII remains
|
||||||
if (inputScanTimer) clearTimeout(inputScanTimer);
|
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||||
inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150);
|
inputScanTimer = setTimeout(() => scanInputForPII(inputEl), 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visual feedback
|
// Visual feedback
|
||||||
@@ -1946,7 +1957,7 @@
|
|||||||
// Re-scan to update warning
|
// Re-scan to update warning
|
||||||
if (inputEl) {
|
if (inputEl) {
|
||||||
if (inputScanTimer) clearTimeout(inputScanTimer);
|
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||||
inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150);
|
inputScanTimer = setTimeout(() => scanInputForPII(inputEl), 150);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1997,14 +2008,14 @@
|
|||||||
// Scan input on type and paste
|
// Scan input on type and paste
|
||||||
let inputScanTimer = null;
|
let inputScanTimer = null;
|
||||||
|
|
||||||
function scanInputForPPI(target) {
|
function scanInputForPII(target) {
|
||||||
const text = target.textContent || target.value || '';
|
const text = target.textContent || target.value || '';
|
||||||
if (!text || text.length < 5) {
|
if (!text || text.length < 5) {
|
||||||
if (preSendWarningEl) preSendWarningEl.classList.remove('visible');
|
if (preSendWarningEl) preSendWarningEl.classList.remove('visible');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const warnings = autoDetectPPI(text, identity);
|
const warnings = autoDetectPII(text, identity);
|
||||||
if (warnings.length > 0) {
|
if (warnings.length > 0) {
|
||||||
showPreSendWarning(warnings, target);
|
showPreSendWarning(warnings, target);
|
||||||
} else if (preSendWarningEl) {
|
} else if (preSendWarningEl) {
|
||||||
@@ -2018,7 +2029,7 @@
|
|||||||
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
||||||
// Debounce — don't scan on every keystroke
|
// Debounce — don't scan on every keystroke
|
||||||
if (inputScanTimer) clearTimeout(inputScanTimer);
|
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||||
inputScanTimer = setTimeout(() => scanInputForPPI(target), 800);
|
inputScanTimer = setTimeout(() => scanInputForPII(target), 800);
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@@ -2028,7 +2039,7 @@
|
|||||||
if (target.matches?.('[contenteditable], textarea, input[type="text"]') ||
|
if (target.matches?.('[contenteditable], textarea, input[type="text"]') ||
|
||||||
target.closest?.('[contenteditable]')) {
|
target.closest?.('[contenteditable]')) {
|
||||||
// Scan shortly after paste completes
|
// Scan shortly after paste completes
|
||||||
setTimeout(() => scanInputForPPI(target.closest?.('[contenteditable]') || target), 200);
|
setTimeout(() => scanInputForPII(target.closest?.('[contenteditable]') || target), 200);
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Silent Send - Auto-Detect
|
* Silent Send - Auto-Detect
|
||||||
*
|
*
|
||||||
* Scans text for potential PPI that the user hasn't configured.
|
* Scans text for potential PII that the user hasn't configured.
|
||||||
* This catches things the identity and secret scanner can't —
|
* This catches things the identity and auto-redact scanner can't —
|
||||||
* because the user forgot or didn't know to configure them.
|
* because the user forgot or didn't know to configure them.
|
||||||
*
|
*
|
||||||
* Returns warnings (not auto-redactions) so the user can decide.
|
* Returns warnings (not auto-redactions) so the user can decide.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PPI_PATTERNS = [
|
const PII_PATTERNS = [
|
||||||
// --- Network ---
|
// --- Network ---
|
||||||
{
|
{
|
||||||
name: 'Private IP Address',
|
name: 'Private IP Address',
|
||||||
@@ -21,7 +21,7 @@ const PPI_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,
|
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',
|
category: 'network',
|
||||||
hint: 'IP address — could identify your network',
|
hint: 'IP address — could identify your network',
|
||||||
// Exclude common non-PPI IPs
|
// Exclude common non-PII 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)$/,
|
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 PPI_PATTERNS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Context words that make ambiguous patterns more likely to be PPI
|
// Context words that make ambiguous patterns more likely to be PII
|
||||||
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 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 = {
|
const AutoDetect = {
|
||||||
/**
|
/**
|
||||||
* Scan text for potential unconfigured PPI.
|
* Scan text for potential unconfigured PII.
|
||||||
* Pass in identity so we can skip values the user already configured.
|
* Pass in identity so we can skip values the user already configured.
|
||||||
*
|
*
|
||||||
* Returns array of { name, value, hint, category, index }
|
* Returns array of { name, value, hint, category, index }
|
||||||
@@ -167,7 +167,7 @@ const AutoDetect = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pattern of PPI_PATTERNS) {
|
for (const pattern of PII_PATTERNS) {
|
||||||
// Skip context-dependent patterns if no context words present
|
// Skip context-dependent patterns if no context words present
|
||||||
if (pattern.contextRequired && !hasContext) continue;
|
if (pattern.contextRequired && !hasContext) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Silent Send - Secret Scanner
|
* Silent Send - Auto Redact
|
||||||
*
|
*
|
||||||
* Detects common secret/credential patterns in text and either
|
* Detects common secret/credential patterns in text and either
|
||||||
* warns or auto-redacts them. This catches things the identity-based
|
* warns or auto-redacts them. This catches things the identity-based
|
||||||
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
|
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
|
||||||
* cards, private keys, connection strings, etc.
|
* cards, private keys, connection strings, etc.
|
||||||
*
|
*
|
||||||
|
* Supports user-defined custom patterns for proprietary token formats.
|
||||||
|
*
|
||||||
* Each pattern has:
|
* Each pattern has:
|
||||||
* - name: human-readable label
|
* - name: human-readable label
|
||||||
* - regex: detection pattern
|
* - regex: detection pattern
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
|
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SECRET_PATTERNS = [
|
const REDACT_PATTERNS = [
|
||||||
// --- API Keys ---
|
// --- API Keys ---
|
||||||
{
|
{
|
||||||
name: 'OpenAI API Key',
|
name: 'OpenAI API Key',
|
||||||
@@ -161,14 +163,39 @@ const SECRET_PATTERNS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const SecretScanner = {
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan text for secrets. Returns list of findings.
|
* Scan text for secrets. Returns list of findings.
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Array} [customPatterns] — from settings.customRedactPatterns
|
||||||
*/
|
*/
|
||||||
scan(text) {
|
scan(text, customPatterns) {
|
||||||
const findings = [];
|
const findings = [];
|
||||||
|
const patterns = this._buildPatterns(customPatterns);
|
||||||
|
|
||||||
for (const pattern of SECRET_PATTERNS) {
|
for (const pattern of patterns) {
|
||||||
// Reset regex lastIndex
|
// Reset regex lastIndex
|
||||||
pattern.regex.lastIndex = 0;
|
pattern.regex.lastIndex = 0;
|
||||||
let match;
|
let match;
|
||||||
@@ -204,9 +231,11 @@ const SecretScanner = {
|
|||||||
/**
|
/**
|
||||||
* Redact all critical secrets in text. Warnings are not auto-redacted.
|
* Redact all critical secrets in text. Warnings are not auto-redacted.
|
||||||
* Returns { text, redactions[] }
|
* Returns { text, redactions[] }
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Array} [customPatterns] — from settings.customRedactPatterns
|
||||||
*/
|
*/
|
||||||
redact(text) {
|
redact(text, customPatterns) {
|
||||||
const findings = this.scan(text);
|
const findings = this.scan(text, customPatterns);
|
||||||
const redactions = [];
|
const redactions = [];
|
||||||
let result = text;
|
let result = text;
|
||||||
|
|
||||||
@@ -220,7 +249,7 @@ const SecretScanner = {
|
|||||||
redactions.push({
|
redactions.push({
|
||||||
original: f.value,
|
original: f.value,
|
||||||
replaced: f.redactTo,
|
replaced: f.redactTo,
|
||||||
category: 'secret',
|
category: 'redact',
|
||||||
pattern: f.name,
|
pattern: f.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -237,7 +266,7 @@ const SecretScanner = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (typeof globalThis !== 'undefined') {
|
if (typeof globalThis !== 'undefined') {
|
||||||
globalThis.SecretScanner = SecretScanner;
|
globalThis.AutoRedact = AutoRedact;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SecretScanner;
|
export default AutoRedact;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Silent Send - Document Scanner
|
* Silent Send - Document Scanner
|
||||||
*
|
*
|
||||||
* Scans uploaded documents for PPI and substitutes/redacts before
|
* Scans uploaded documents for PII and substitutes/redacts before
|
||||||
* the file reaches the AI service.
|
* the file reaches the AI service.
|
||||||
*
|
*
|
||||||
* Supported formats:
|
* Supported formats:
|
||||||
* - PDF: Extract text, scan for PPI, create sanitized plaintext version
|
* - PDF: Extract text, scan for PII, create sanitized plaintext version
|
||||||
* (PDFs can't be reliably edited in-place without breaking layout)
|
* (PDFs can't be reliably edited in-place without breaking layout)
|
||||||
* - DOCX: Parse XML, find-replace text, repackage ZIP (layout preserved)
|
* - DOCX: Parse XML, find-replace text, repackage ZIP (layout preserved)
|
||||||
* - XLSX: Parse cells, find-replace values, repackage (formatting preserved)
|
* - XLSX: Parse cells, find-replace values, repackage (formatting preserved)
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
*
|
*
|
||||||
* Modes:
|
* Modes:
|
||||||
* - Silent: substitute and upload (default for text files)
|
* - Silent: substitute and upload (default for text files)
|
||||||
* - Preview: show PPI findings, let user confirm before upload (default for PDF/DOCX/XLSX)
|
* - Preview: show PII findings, let user confirm before upload (default for PDF/DOCX/XLSX)
|
||||||
*
|
*
|
||||||
* Integration:
|
* Integration:
|
||||||
* - The fetch interceptor in content.js detects multipart/form-data uploads
|
* - The fetch interceptor in content.js detects multipart/form-data uploads
|
||||||
@@ -70,9 +70,9 @@ const DocumentScanner = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PDF: extract text, scan for PPI, create sanitized text file.
|
* PDF: extract text, scan for PII, create sanitized text file.
|
||||||
* PDFs can't be reliably edited in-place, so we extract text,
|
* PDFs can't be reliably edited in-place, so we extract text,
|
||||||
* substitute PPI, and send the clean text instead.
|
* substitute PII, and send the clean text instead.
|
||||||
*/
|
*/
|
||||||
async _processPDF(file, filename, substituteAll, options) {
|
async _processPDF(file, filename, substituteAll, options) {
|
||||||
const text = await this._extractPDFText(file);
|
const text = await this._extractPDFText(file);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* - Policy updates are applied automatically
|
* - Policy updates are applied automatically
|
||||||
*
|
*
|
||||||
* Privacy: the org admin can check compliance (are required fields
|
* Privacy: the org admin can check compliance (are required fields
|
||||||
* configured?) but CANNOT see individual PPI values.
|
* configured?) but CANNOT see individual PII values.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import api from './browser-polyfill.js';
|
import api from './browser-polyfill.js';
|
||||||
@@ -198,11 +198,11 @@ const OrgPolicy = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get org-required secret scanner patterns.
|
* Get org-required auto-redact patterns.
|
||||||
*
|
*
|
||||||
* @returns {Array} additional patterns to add to the secret scanner
|
* @returns {Array} additional patterns to add to auto-redact
|
||||||
*/
|
*/
|
||||||
async getOrgSecretPatterns() {
|
async getOrgRedactPatterns() {
|
||||||
const policy = await this.getPolicy();
|
const policy = await this.getPolicy();
|
||||||
if (!policy?.requiredSecretPatterns?.length) return [];
|
if (!policy?.requiredSecretPatterns?.length) return [];
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ const OrgPolicy = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the user's configuration meets org policy requirements.
|
* Check if the user's configuration meets org policy requirements.
|
||||||
* Returns compliance status WITHOUT revealing actual PPI values.
|
* Returns compliance status WITHOUT revealing actual PII values.
|
||||||
*
|
*
|
||||||
* @returns {{ compliant: boolean, missing: string[], configured: string[] }}
|
* @returns {{ compliant: boolean, missing: string[], configured: string[] }}
|
||||||
*/
|
*/
|
||||||
|
|||||||
+3
-2
@@ -23,7 +23,7 @@ const KEYS = {
|
|||||||
SETTINGS: 'ss_settings',
|
SETTINGS: 'ss_settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keys that contain sensitive PPI and should be encrypted at rest
|
// Keys that contain sensitive PII and should be encrypted at rest
|
||||||
// All user data keys are encrypted at rest — settings included since
|
// All user data keys are encrypted at rest — settings included since
|
||||||
// custom domains and configuration can reveal what services the user
|
// custom domains and configuration can reveal what services the user
|
||||||
// accesses. Only ss_sync_encryption (salt, verification blob) and
|
// accesses. Only ss_sync_encryption (salt, verification blob) and
|
||||||
@@ -34,12 +34,13 @@ const DEFAULT_SETTINGS = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
showHighlights: false,
|
showHighlights: false,
|
||||||
revealMode: false,
|
revealMode: false,
|
||||||
secretScanning: true,
|
autoRedact: true,
|
||||||
autoDetect: true,
|
autoDetect: true,
|
||||||
autoRedactDetected: true,
|
autoRedactDetected: true,
|
||||||
autoAddDetected: true,
|
autoAddDetected: true,
|
||||||
maxLogEntries: 100,
|
maxLogEntries: 100,
|
||||||
customDomains: [],
|
customDomains: [],
|
||||||
|
customRedactPatterns: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
|
||||||
browserSync: false,
|
browserSync: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,17 +49,38 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<label>Auto-redact secrets</label>
|
<label>Auto Redact</label>
|
||||||
<p class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit card numbers</p>
|
<p class="setting-desc">Automatically detect and redact API keys, tokens, passwords, SSNs, credit card numbers, and custom patterns</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="secretScanning" checked>
|
<input type="checkbox" id="autoRedactToggle" checked>
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Secret Patterns -->
|
||||||
|
<div style="margin:12px 0;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
|
||||||
|
<h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Custom Redact Patterns</h3>
|
||||||
|
<p class="setting-desc" style="margin-bottom:8px">
|
||||||
|
Define your own patterns to catch proprietary tokens, internal URLs with keys, or any format the built-in scanner doesn't cover.
|
||||||
|
</p>
|
||||||
|
<div id="customRedactList"></div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">
|
||||||
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||||
|
<input type="text" id="newRedactName" placeholder="Name (e.g. ControlD Token)" style="flex:1;min-width:140px;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||||
|
<input type="text" id="newRedactPattern" placeholder="Regex or prefix (e.g. ctrl_[A-Za-z0-9]{20,})" style="flex:2;min-width:200px;font-size:12px;font-family:monospace;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center">
|
||||||
|
<input type="text" id="newRedactReplacement" placeholder="Replacement (default: [REDACTED-NAME])" style="flex:1;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||||
|
<button class="btn btn-primary btn-sm" id="btnAddRedactPattern">Add Pattern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="setting-desc" style="margin-top:6px;margin-bottom:0">
|
||||||
|
<strong>Tip:</strong> For a URL like <code>https://dns.example.com/abc123</code>, use a pattern like <code>dns\.example\.com/[A-Za-z0-9;]+</code> to match the secret path segment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
|
||||||
<label>Auto-detect unconfigured PPI</label>
|
|
||||||
<p class="setting-desc">Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured</p>
|
<p class="setting-desc">Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
@@ -69,8 +90,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<label>Auto-redact detected PPI on send</label>
|
<label>Auto-redact detected PII on send</label>
|
||||||
<p class="setting-desc">Automatically replace detected PPI with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
|
<p class="setting-desc">Automatically replace detected PII with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="autoRedactDetected" checked>
|
<input type="checkbox" id="autoRedactDetected" checked>
|
||||||
@@ -79,8 +100,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<label>Offer to auto-add detected PPI</label>
|
<label>Offer to auto-add detected PII</label>
|
||||||
<p class="setting-desc">Show a + button on detected PPI to instantly create a mapping with a suggested fake value</p>
|
<p class="setting-desc">Show a + button on detected PII to instantly create a mapping with a suggested fake value</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="autoAddDetected" checked>
|
<input type="checkbox" id="autoAddDetected" checked>
|
||||||
@@ -611,6 +632,13 @@
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Silent Send v0.9.0</p>
|
<p>Silent Send v0.9.0</p>
|
||||||
|
<p style="font-size:11px;color:#9ca3af;margin-top:6px;max-width:600px">
|
||||||
|
Silent Send is a convenience tool, not a security guarantee. Third-party sites may change how they send data at any time, which can cause missed substitutions without warning. You are responsible for verifying your data before sending. See the <a href="https://github.com/outis1one/silent-send/blob/main/LICENSE" target="_blank" style="color:#6b7280">LICENSE</a> for full terms.
|
||||||
|
</p>
|
||||||
|
<p style="font-size:11px;color:#9ca3af;margin-top:8px">
|
||||||
|
Find Silent Send useful? No obligation, but if you'd like to help keep it going:
|
||||||
|
<a href="https://ko-fi.com/YOUR_KOFI_USERNAME" target="_blank" style="color:#6b7280">Buy me a coffee</a>
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+94
-3
@@ -26,7 +26,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;
|
$('#autoRedactToggle').checked = settings.autoRedact !== false;
|
||||||
$('#autoDetect').checked = settings.autoDetect !== false;
|
$('#autoDetect').checked = settings.autoDetect !== false;
|
||||||
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
|
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
|
||||||
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
|
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
|
||||||
@@ -299,8 +299,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await Storage.saveSettings({ showHighlights: e.target.checked });
|
await Storage.saveSettings({ showHighlights: e.target.checked });
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#secretScanning').addEventListener('change', async (e) => {
|
$('#autoRedactToggle').addEventListener('change', async (e) => {
|
||||||
await Storage.saveSettings({ secretScanning: e.target.checked });
|
await Storage.saveSettings({ autoRedact: e.target.checked });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom redact patterns
|
||||||
|
renderCustomRedactPatterns();
|
||||||
|
$('#btnAddRedactPattern').addEventListener('click', addCustomRedactPattern);
|
||||||
|
$('#newRedactPattern').addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') addCustomRedactPattern();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#autoDetect').addEventListener('change', async (e) => {
|
$('#autoDetect').addEventListener('change', async (e) => {
|
||||||
@@ -804,6 +811,90 @@ function renderDomains() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Custom Redact Patterns ---
|
||||||
|
|
||||||
|
function addCustomRedactPattern() {
|
||||||
|
const name = $('#newRedactName').value.trim();
|
||||||
|
const pattern = $('#newRedactPattern').value.trim();
|
||||||
|
const redact = $('#newRedactReplacement').value.trim();
|
||||||
|
|
||||||
|
if (!pattern) { alert('Pattern is required.'); return; }
|
||||||
|
|
||||||
|
// Validate regex
|
||||||
|
try {
|
||||||
|
new RegExp(pattern, 'g');
|
||||||
|
} catch (e) {
|
||||||
|
alert('Invalid regex: ' + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = name || 'Custom Pattern';
|
||||||
|
const replacement = redact || `[REDACTED-${label.toUpperCase().replace(/\s+/g, '-')}]`;
|
||||||
|
|
||||||
|
const patterns = settings.customRedactPatterns || [];
|
||||||
|
patterns.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: label,
|
||||||
|
pattern,
|
||||||
|
redact: replacement,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.customRedactPatterns = patterns;
|
||||||
|
Storage.saveSettings({ customRedactPatterns: patterns });
|
||||||
|
renderCustomRedactPatterns();
|
||||||
|
|
||||||
|
$('#newRedactName').value = '';
|
||||||
|
$('#newRedactPattern').value = '';
|
||||||
|
$('#newRedactReplacement').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCustomRedactPatterns() {
|
||||||
|
const list = $('#customRedactList');
|
||||||
|
if (!list) return;
|
||||||
|
const patterns = settings.customRedactPatterns || [];
|
||||||
|
|
||||||
|
if (patterns.length === 0) {
|
||||||
|
safeHTML(list, '<div style="font-size:12px;color:#9ca3af;padding:6px 0">No custom patterns defined. Built-in patterns cover common API keys, tokens, and credentials.</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
safeHTML(list, patterns.map((p, i) => `
|
||||||
|
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:#fff;border:1px solid #e5e7eb;border-radius:6px;margin-bottom:4px;flex-wrap:wrap">
|
||||||
|
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;min-width:0">
|
||||||
|
<input type="checkbox" class="redact-toggle" data-index="${i}" ${p.enabled ? 'checked' : ''}>
|
||||||
|
</label>
|
||||||
|
<span style="font-size:12px;font-weight:500;white-space:nowrap">${escapeHtml(p.name)}</span>
|
||||||
|
<code style="font-size:11px;color:#6b7280;background:#f3f4f6;padding:1px 5px;border-radius:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:250px" title="${escapeHtml(p.pattern)}">${escapeHtml(p.pattern)}</code>
|
||||||
|
<span style="font-size:11px;color:#9ca3af;margin-left:auto;white-space:nowrap">→ ${escapeHtml(p.redact)}</span>
|
||||||
|
<button class="btn btn-sm btn-danger btn-remove-redact" data-index="${i}" style="padding:2px 6px">×</button>
|
||||||
|
</div>
|
||||||
|
`).join(''));
|
||||||
|
|
||||||
|
// Toggle handlers
|
||||||
|
list.querySelectorAll('.redact-toggle').forEach(toggle => {
|
||||||
|
toggle.addEventListener('change', async () => {
|
||||||
|
const idx = parseInt(toggle.dataset.index, 10);
|
||||||
|
const patterns = settings.customRedactPatterns || [];
|
||||||
|
patterns[idx].enabled = toggle.checked;
|
||||||
|
settings.customRedactPatterns = patterns;
|
||||||
|
await Storage.saveSettings({ customRedactPatterns: patterns });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove handlers
|
||||||
|
list.querySelectorAll('.btn-remove-redact').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
const patterns = settings.customRedactPatterns || [];
|
||||||
|
patterns.splice(idx, 1);
|
||||||
|
settings.customRedactPatterns = patterns;
|
||||||
|
await Storage.saveSettings({ customRedactPatterns: patterns });
|
||||||
|
renderCustomRedactPatterns();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- Transfer Data (Export/Import All) ---
|
// --- Transfer Data (Export/Import All) ---
|
||||||
|
|
||||||
async function getAllData() {
|
async function getAllData() {
|
||||||
|
|||||||
+14
-10
@@ -198,15 +198,15 @@
|
|||||||
<section class="tab-content" id="tab-options">
|
<section class="tab-content" id="tab-options">
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-label">
|
<div class="setting-label">
|
||||||
<strong>Auto-redact secrets</strong>
|
<strong>Auto Redact</strong>
|
||||||
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards</span>
|
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards, and custom patterns</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="optSecretScanning" checked><span class="toggle-slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-label">
|
<div class="setting-label">
|
||||||
<strong>Auto-detect PPI</strong>
|
<strong>Auto-detect PII</strong>
|
||||||
<span class="setting-desc">Warn about unconfigured personal data (IPs, addresses, paths)</span>
|
<span class="setting-desc">Warn about unconfigured personal data (IPs, addresses, paths)</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="optAutoDetect" checked><span class="toggle-slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="optAutoDetect" checked><span class="toggle-slider"></span></label>
|
||||||
@@ -214,8 +214,8 @@
|
|||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-label">
|
<div class="setting-label">
|
||||||
<strong>Auto-redact detected PPI</strong>
|
<strong>Auto-redact detected PII</strong>
|
||||||
<span class="setting-desc">Replace detected PPI with placeholders on send</span>
|
<span class="setting-desc">Replace detected PII with placeholders on send</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-label">
|
<div class="setting-label">
|
||||||
<strong>Document scan preview</strong>
|
<strong>Document scan preview</strong>
|
||||||
<span class="setting-desc">Show PPI findings before uploading documents</span>
|
<span class="setting-desc">Show PII findings before uploading documents</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,9 +271,13 @@
|
|||||||
AES-256 encrypted at rest — unreadable without your password.</span>
|
AES-256 encrypted at rest — unreadable without your password.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="privacy-note" style="color:#b45309;background:#fef3c7;padding:6px 8px;border-radius:4px;margin-bottom:6px">
|
<div class="privacy-note" style="color:#b45309;background:#fef3c7;padding:6px 8px;border-radius:4px;margin-bottom:6px">
|
||||||
Silent Send is a convenience tool, not a security guarantee. It can miss
|
Silent Send is a convenience tool, not a security guarantee. It reduces
|
||||||
PPI in images, file uploads, unusual name forms, or data you forgot to
|
but cannot eliminate the risk of sharing personal data. It may miss PII
|
||||||
configure. Always verify sensitive messages before sending.
|
in images, unusual formats, or data you haven't configured. Third-party
|
||||||
|
sites may change how they send data at any time, which can cause missed
|
||||||
|
substitutions without warning. Always verify sensitive messages before
|
||||||
|
sending. By using this extension, you accept full responsibility for
|
||||||
|
verifying your data is protected.
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+22
-22
@@ -1,6 +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 AutoRedact from '../lib/auto-redact.js';
|
||||||
import AutoDetect from '../lib/auto-detect.js';
|
import AutoDetect from '../lib/auto-detect.js';
|
||||||
import Storage from '../lib/storage.js';
|
import Storage from '../lib/storage.js';
|
||||||
import SilentSendSync from '../lib/sync.js';
|
import SilentSendSync from '../lib/sync.js';
|
||||||
@@ -214,7 +214,7 @@ async function initUnlockedUI() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Load options tab settings
|
// Load options tab settings
|
||||||
$('#optSecretScanning').checked = settings.secretScanning !== false;
|
$('#optAutoRedact').checked = settings.autoRedact !== false;
|
||||||
$('#optAutoDetect').checked = settings.autoDetect !== false;
|
$('#optAutoDetect').checked = settings.autoDetect !== false;
|
||||||
$('#optAutoRedact').checked = settings.autoRedactDetected !== false;
|
$('#optAutoRedact').checked = settings.autoRedactDetected !== false;
|
||||||
$('#optHighlights').checked = settings.showHighlights || false;
|
$('#optHighlights').checked = settings.showHighlights || false;
|
||||||
@@ -223,7 +223,7 @@ async function initUnlockedUI() {
|
|||||||
|
|
||||||
// Options tab change handlers
|
// Options tab change handlers
|
||||||
const optHandlers = [
|
const optHandlers = [
|
||||||
['optSecretScanning', 'secretScanning'],
|
['optAutoRedact', 'autoRedact'],
|
||||||
['optAutoDetect', 'autoDetect'],
|
['optAutoDetect', 'autoDetect'],
|
||||||
['optAutoRedact', 'autoRedactDetected'],
|
['optAutoRedact', 'autoRedactDetected'],
|
||||||
['optHighlights', 'showHighlights'],
|
['optHighlights', 'showHighlights'],
|
||||||
@@ -715,16 +715,16 @@ 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 redactResult = AutoRedact.redact(explicitResult.text, settings.customRedactPatterns);
|
||||||
|
|
||||||
const allReplacements = [
|
const allReplacements = [
|
||||||
...smartResult.replacements,
|
...smartResult.replacements,
|
||||||
...explicitResult.replacements,
|
...explicitResult.replacements,
|
||||||
...secretResult.redactions,
|
...redactResult.redactions,
|
||||||
];
|
];
|
||||||
const finalText = secretResult.text;
|
const finalText = redactResult.text;
|
||||||
|
|
||||||
if (finalText === input && secretResult.warnings.length === 0) {
|
if (finalText === input && redactResult.warnings.length === 0) {
|
||||||
output.textContent = input;
|
output.textContent = input;
|
||||||
stats.textContent = 'No substitutions detected';
|
stats.textContent = 'No substitutions detected';
|
||||||
return;
|
return;
|
||||||
@@ -740,8 +740,8 @@ function renderTestDiff() {
|
|||||||
`<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
|
// Highlight auto-redactions in red
|
||||||
for (const r of secretResult.redactions) {
|
for (const r of redactResult.redactions) {
|
||||||
const escapedReplaced = escapeHtml(r.replaced);
|
const escapedReplaced = escapeHtml(r.replaced);
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
escapedReplaced,
|
escapedReplaced,
|
||||||
@@ -752,28 +752,28 @@ function renderTestDiff() {
|
|||||||
|
|
||||||
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 redactCount = redactResult.redactions.length;
|
||||||
const warnCount = secretResult.warnings.length;
|
const warnCount = redactResult.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 (redactCount > 0) parts.push(`${redactCount} auto-redacted`);
|
||||||
if (warnCount > 0) parts.push(`${warnCount} warnings`);
|
if (warnCount > 0) parts.push(`${warnCount} warnings`);
|
||||||
|
|
||||||
// Auto-detect unconfigured PPI in the final text
|
// Auto-detect unconfigured PII in the final text
|
||||||
const ppiWarnings = AutoDetect.scan(finalText, identity);
|
const piiWarnings = AutoDetect.scan(finalText, identity);
|
||||||
if (ppiWarnings.length > 0) parts.push(`${ppiWarnings.length} PPI detected`);
|
if (piiWarnings.length > 0) parts.push(`${piiWarnings.length} PII detected`);
|
||||||
|
|
||||||
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||||
|
|
||||||
// Show PPI warnings below stats
|
// Show PII warnings below stats
|
||||||
if (ppiWarnings.length > 0) {
|
if (piiWarnings.length > 0) {
|
||||||
const ppiDiv = document.createElement('div');
|
const piiDiv = document.createElement('div');
|
||||||
safeHTML(ppiDiv, `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
|
safeHTML(piiDiv, `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
|
||||||
<strong>Unconfigured PPI detected:</strong>
|
<strong>Unconfigured PII detected:</strong>
|
||||||
${ppiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
|
${piiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
|
||||||
</div>`);
|
</div>`);
|
||||||
stats.appendChild(ppiDiv);
|
stats.appendChild(piiDiv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user