Skip to content
Open
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
48 changes: 48 additions & 0 deletions app/src/features/share/shareContent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,54 @@ describe('redactSensitive', () => {
expect(redactSensitive('mail jane.doe@example.com now')).toContain('[email]');
});

// The last-resort rule requires an upper-case character so it does not fire on
// prose. Everything below is lower-case + digits, so it reached the card intact
// until these patterns were added. Each value is a shape-accurate dummy, not a
// real credential.
//
// Every vendor-prefixed value is assembled from parts on purpose: written as a
// single literal it carries the vendor's exact token shape, which is what a
// secret scanner keys on. GitHub push protection already rejects the Slack form
// outright, and that would block this file for anyone pushing it.
const assembled = (separator: string, ...parts: string[]) => parts.join(separator);

test.each([
['slack bot token', assembled('-', 'xoxb', '123456789012', '987654321098', 'abcdefghijklmnop')],
[
'slack user token',
assembled('-', 'xoxp', '987654321098', '123456789012', 'zyxwvutsrqponmlk'),
],
['gitlab personal access token', assembled('-', 'glpat', 'abcdefghij1234567890')],
['huggingface token', assembled('_', 'hf', 'abcdefghijklmnopqrstuvwxyz1234')],
])('scrubs an all-lower-case %s', (_label, secret) => {
const out = redactSensitive(`saved ${secret} for you`);
expect(out).toContain('[redacted]');
expect(out).not.toContain(secret);
});

test.each([
['slack', 'https://hooks.slack.com/services/T00000000/B00000000/abcdefghijklmnopqrst'],
[
'discord',
'https://discord.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz012345',
],
])('scrubs a %s webhook URL, which carries its secret in the path', (_label, url) => {
expect(redactSensitive(`posting to ${url} now`)).not.toContain(url);
});

test('survives stripMarkdown running first, as buildFallbackHeadline runs it', () => {
// stripMarkdown removes `_`, so a pattern that insisted on it would never match.
const hfToken = assembled('_', 'hf', 'abcdefghijklmnopqrstuvwxyz1234');
const head = buildFallbackHeadline(`Done. Saved ${hfToken} for you.`);
expect(head).not.toContain('abcdefghijklmnopqrstuvwxyz');
expect(head).toContain('[redacted]');
});

test('does not fire on ordinary lower-case prose', () => {
const clean = 'renamed the folder to archive and moved twelve files into it';
expect(redactSensitive(clean)).toBe(clean);
});

test('is idempotent and leaves clean prose untouched', () => {
const clean = 'Summarised three months of emails in twelve seconds';
expect(redactSensitive(clean)).toBe(clean);
Expand Down
13 changes: 13 additions & 0 deletions app/src/features/share/shareContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ const REDACTIONS: ReadonlyArray<{ re: RegExp; with: string }> = [
{ re: /\bBearer\s+[A-Za-z0-9._-]{12,}\b/gi, with: '[redacted]' },
// AWS access key ids.
{ re: /\bAKIA[0-9A-Z]{16}\b/g, with: '[redacted]' },
// Vendor-prefixed credentials whose body is lower-case and digits only. The
// last-resort rule below cannot reach these: it requires an upper-case letter so
// it does not fire on prose, and an all-lower-case token slips straight past it.
// The `_` is optional because `stripMarkdown` strips emphasis characters before
// this runs, so `hf_abc…` arrives here as `hfabc…`.
{ re: /\b(?:xox[abeprs]|xapp)-[A-Za-z0-9-]{10,}/g, with: '[redacted]' },
{ re: /\bglpat-[A-Za-z0-9_-]{16,}/g, with: '[redacted]' },
{ re: /\bhf_?[a-z0-9]{20,}\b/g, with: '[redacted]' },
// Webhook URLs carry the secret in the path, so the whole URL has to go.
{
re: /\bhttps?:\/\/(?:hooks\.slack\.com|discord(?:app)?\.com\/api\/webhooks)\/[^\s"'`)]+/gi,
with: '[redacted]',
},
// Long opaque hex runs (>= 32 chars) that look like secrets, not prose.
{ re: /\b[A-Fa-f0-9]{32,}\b/g, with: '[redacted]' },
// Long opaque base64/base64url runs (>= 24 chars) that mix upper/lower/digit,
Expand Down
Loading