Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion privacy.js
Original file line number Diff line number Diff line change
@@ -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 &&
Expand All @@ -17,7 +23,7 @@ function redactSensitive(text, { issue = false, preserveOfficialEmails = false }
out = out.replace(/(?<![\w#])\+?(?:\(\d{1,4}\)|\d{1,4})(?:[\s().-]*(?:\(\d{1,4}\)|\d{1,4})){1,5}(?!\w)/g, (match, offset, source) => {
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('-');
Expand Down
35 changes: 35 additions & 0 deletions test/privacy.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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.'));
});