diff --git a/privacy.js b/privacy.js index 03478ba..1942036 100644 --- a/privacy.js +++ b/privacy.js @@ -1,5 +1,11 @@ // Public GitHub issues and customer-visible cards must never carry raw customer identifiers. // Keep this independent of route classification so every filing path has the same boundary. + +// A phone word and at most a few joining words right before the digits: "phone number is", +// "call me on", "tel.", "mobile:", "my number is". +const PHONE_CONTEXT = + /(?:phone|tel(?:ephone)?|t[eé]l[eé]phone|tel[eé]fono?|telefone|whatsapp|call|\b(?:mobile|cell|contact|ph)\b|\bmy\s+(?:number|no)\b)\.?(?:\s*(?:number|no\.?|is|me|us|at|on|[:=#-]))*\s*$/i; + function redactSensitive(text, { issue = false, preserveOfficialEmails = false } = {}) { let out = String(text || ''); const officialEmail = (value) => preserveOfficialEmails && @@ -17,7 +23,7 @@ function redactSensitive(text, { issue = false, preserveOfficialEmails = false } out = out.replace(/(? { const digits = match.replace(/\D/g, ''); const before = source.slice(Math.max(0, offset - 32), offset); - const hasPhoneContext = /(?:phone|tel(?:ephone)?|whatsapp|call)\s*(?:number|no\.?|me|at|on|:|=)?\s*$/i.test(before); + const hasPhoneContext = PHONE_CONTEXT.test(before); if (digits.length > 15 || (digits.length < 9 && !(hasPhoneContext && digits.length >= 7))) return match; const groups = match.match(/\d+/g) || []; const lengths = groups.map((group) => group.length).join('-'); diff --git a/test/privacy.test.js b/test/privacy.test.js index 81779b7..56ec5e4 100644 --- a/test/privacy.test.js +++ b/test/privacy.test.js @@ -1,5 +1,6 @@ const assert = require('node:assert/strict'); const test = require('node:test'); +const { issueBody } = require('../github'); const { redactSensitive } = require('../privacy'); test('provider-only official email exception does not weaken public issue redaction', () => { @@ -53,3 +54,37 @@ test('public repository issue numbers survive while customer ticket numbers do n assert.match(out, /GitHub issue #20172/); assert.doesNotMatch(out, /138637367|18063/); }); + +test('phone redaction catches a number introduced in ordinary words', () => { + const lines = [ + 'Phone number: 9876543210', + 'My phone number is 9876543210, please call.', + 'My phone is 4155550199', + 'call me on 9876543210', + 'Mobile 4155550199', + 'cell: 4155550199', + 'contact number 4155550199', + 'tel. 020 7946 0958', + 'Ph no: 9876543210', + 'my number is 9876543210', + 'Teléfono: 612345678', + ]; + for (const line of lines) { + const out = redactSensitive(line, { issue: true }); + assert.match(out, /\[phone\]/, line); + assert.doesNotMatch(out, /\d{4}/, line); + } +}); + +test('a phone word in the sentence does not turn builds and serials into phone numbers', () => { + const kept = 'On my phone the app is build 2026100312. Serial number is 0123456789012.'; + assert.equal(redactSensitive(kept, { issue: true }), kept); +}); + +test('a phone number written into a bug report stays out of the public issue', () => { + const body = issueBody({ + quote: 'The app crashes when I open Memories. Phone number: 9876543210 if you need to reach me.', + }); + assert.doesNotMatch(body, /9876543210/); + assert.ok(body.includes('The app crashes when I open Memories. Phone number: [phone] if you need to reach me.')); +});