fix: redigování citlivých dat před odesláním do Sentry
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 1m28s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 1m28s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
Console eventy nesou celé argumenty console.error — u axios chyb tedy i config s hlavičkami a request body (Bolt token, Gotify/NTFY klíče). beforeSend nově rekurzivně rediguje citlivé klíče a tokeny v řetězcích napříč extra/breadcrumbs/contexts/request/exception.
This commit is contained in:
@@ -6,6 +6,30 @@ import path from 'path';
|
|||||||
// proto si tento soubor načítá .env sám — index.ts na to dojde až po importech.
|
// proto si tento soubor načítá .env sám — index.ts na to dojde až po importech.
|
||||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${process.env.NODE_ENV ?? 'production'}`) });
|
dotenv.config({ path: path.resolve(__dirname, `../.env.${process.env.NODE_ENV ?? 'production'}`) });
|
||||||
|
|
||||||
|
/** Klíče, jejichž hodnoty se před odesláním do Sentry redigují (hlavičky, tokeny, API klíče…). */
|
||||||
|
const SENSITIVE_KEY = /authorization|cookie|token|secret|passw|api_?key|(^|[-_])key$/i;
|
||||||
|
/** Tokeny/klíče schované uvnitř řetězců (query parametry, serializovaná JSON body). */
|
||||||
|
const SENSITIVE_STRING = /((?:token|key|secret|password)"?\s*[=:]\s*"?)[^&\s"',}]+/gi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rekurzivně rediguje citlivé hodnoty v datech eventu. Console eventy nesou celé
|
||||||
|
* argumenty console.error — u axios chyb tedy i config s hlavičkami a request body
|
||||||
|
* (Bolt token, Gotify/NTFY klíče), které nesmí odejít do Sentry.
|
||||||
|
*/
|
||||||
|
export function scrubEventData(value: unknown, depth = 0): unknown {
|
||||||
|
if (depth > 8) return '[max depth]';
|
||||||
|
if (typeof value === 'string') return value.replace(SENSITIVE_STRING, '$1[redacted]');
|
||||||
|
if (Array.isArray(value)) return value.map(v => scrubEventData(v, depth + 1));
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(value)) {
|
||||||
|
out[k] = SENSITIVE_KEY.test(k) ? '[redacted]' : scrubEventData(v, depth + 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
// Bez SENTRY_DSN (dev, Jest, E2E) zůstává Sentry úplně vypnuté.
|
// Bez SENTRY_DSN (dev, Jest, E2E) zůstává Sentry úplně vypnuté.
|
||||||
if (process.env.SENTRY_DSN) {
|
if (process.env.SENTRY_DSN) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
@@ -15,6 +39,14 @@ if (process.env.SENTRY_DSN) {
|
|||||||
// console.error/warn se hlásí jako eventy, console.log (např. přechody stavů
|
// console.error/warn se hlásí jako eventy, console.log (např. přechody stavů
|
||||||
// Bolt trackingu) se k chybám přikládá jako breadcrumbs (výchozí integrace).
|
// Bolt trackingu) se k chybám přikládá jako breadcrumbs (výchozí integrace).
|
||||||
integrations: [Sentry.captureConsoleIntegration({ levels: ['error', 'warn'] })],
|
integrations: [Sentry.captureConsoleIntegration({ levels: ['error', 'warn'] })],
|
||||||
|
beforeSend(event) {
|
||||||
|
if (event.extra) event.extra = scrubEventData(event.extra) as typeof event.extra;
|
||||||
|
if (event.breadcrumbs) event.breadcrumbs = scrubEventData(event.breadcrumbs) as typeof event.breadcrumbs;
|
||||||
|
if (event.contexts) event.contexts = scrubEventData(event.contexts) as typeof event.contexts;
|
||||||
|
if (event.request) event.request = scrubEventData(event.request) as typeof event.request;
|
||||||
|
if (event.exception) event.exception = scrubEventData(event.exception) as typeof event.exception;
|
||||||
|
return event;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
console.log('Sentry: inicializováno');
|
console.log('Sentry: inicializováno');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { scrubEventData } from '../instrument';
|
||||||
|
|
||||||
|
describe('scrubEventData', () => {
|
||||||
|
test('rediguje citlivé klíče (hlavičky, tokeny, API klíče)', () => {
|
||||||
|
const scrubbed = scrubEventData({
|
||||||
|
headers: { Authorization: 'Bearer abc123', 'X-Gotify-Key': 'gk1', accept: 'application/json' },
|
||||||
|
data: { token: 'deadbeef'.repeat(8) },
|
||||||
|
apiKey: 'k',
|
||||||
|
}) as any;
|
||||||
|
expect(scrubbed.headers.Authorization).toBe('[redacted]');
|
||||||
|
expect(scrubbed.headers['X-Gotify-Key']).toBe('[redacted]');
|
||||||
|
expect(scrubbed.headers.accept).toBe('application/json');
|
||||||
|
expect(scrubbed.data.token).toBe('[redacted]');
|
||||||
|
expect(scrubbed.apiKey).toBe('[redacted]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rediguje tokeny uvnitř řetězců (query parametry, JSON body)', () => {
|
||||||
|
expect(scrubEventData('https://ntfy.example.com/topic?token=tajny&x=1')).toBe(
|
||||||
|
'https://ntfy.example.com/topic?token=[redacted]&x=1'
|
||||||
|
);
|
||||||
|
expect(scrubEventData('{"token":"deadbeef","other":1}')).toBe('{"token":"[redacted]","other":1}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nechává běžná data beze změny včetně vnořených polí', () => {
|
||||||
|
const input = { breadcrumbs: [{ message: 'Bolt tracking: stav accepted → preparing', level: 'log' }], count: 2 };
|
||||||
|
expect(scrubEventData(input)).toEqual(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user