From 68615742f8f9e4b38f1a5cd05cbc924f71289ebe Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 21 Oct 2024 17:31:08 +0200 Subject: [PATCH 1/6] feat(server): Add Reporting API server helpers --- .../suites/reporting-api/index.html | 9 ++ .../suites/reporting-api/localhost-key.pem | 27 ++++++ .../suites/reporting-api/localhost.pem | 16 ++++ .../suites/reporting-api/server.mjs | 58 +++++++++++++ packages/core/src/envelope.ts | 49 +++++++++++ packages/core/src/index.ts | 2 + packages/core/src/reporting.ts | 83 +++++++++++++++++++ packages/types/src/datacategory.ts | 2 + packages/types/src/envelope.ts | 10 ++- packages/types/src/index.ts | 3 + packages/types/src/reporting.ts | 61 ++++++++++++++ packages/utils/src/envelope.ts | 1 + 12 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/reporting-api/index.html create mode 100644 dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem create mode 100644 dev-packages/node-integration-tests/suites/reporting-api/localhost.pem create mode 100644 dev-packages/node-integration-tests/suites/reporting-api/server.mjs create mode 100644 packages/core/src/reporting.ts create mode 100644 packages/types/src/reporting.ts diff --git a/dev-packages/node-integration-tests/suites/reporting-api/index.html b/dev-packages/node-integration-tests/suites/reporting-api/index.html new file mode 100644 index 000000000000..5bceebc87f85 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/reporting-api/index.html @@ -0,0 +1,9 @@ + + + Reporting API + + + +

Reporting API

+ + diff --git a/dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem b/dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem new file mode 100644 index 000000000000..0dc1f76b48b0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuyGGuR1WxHZI6NzLKBton8MQKwrOgruuH3OX3xgFc6U1ZCec +B59n7zgnNkSHBAv+x+9I1pDnwSFGKtuqSOU1sHC4usRkKNTXN+mU172AdWW7N8mz +np8yFe08OELmRPXgJmo1q/5LXfO/I6MNuKAVHm8MKWn1e9uxlkcPMW1nOXvuPwk8 +BcPvXh7w5CSjG7z2wY+D9YkH3KWyAt1oJaETS7iSfdQy+pfZy/Nq6//n/+Nu7yuQ +NIWR73+ejWtQAzi58RHYjlBEOwhtBdxgApJX5OpQMiy+ncFNpAU2YQa/RLRtarNy +mrWqAStr3gBeeL8R61nX57hFoUM6S65cKnKIRQIDAQABAoIBAQCm9H+FkxaBy+K6 +15rt2p5aw6ceL9MVsqrkZrZeFclvZzuecvRznJYXSSs68KLhSm5zJRsATGJo3e4D +eN6RkOZ41+kIwQV3pIWr3dutK+Z7V1tUp8F4ySHfjDyJGa7mYdQtkd7257eISFsF +SYmJalHNSFg6bs3VRqpHoHh+qdRJ5K8ZuwXtZg0vBPovJbKcVRDvO59CVXyNpB87 +Z4FtP0Z3aTyhCbMjNhs3Zg55qBv/sD7LKJdcGzxHnQIGWqjZMbLC2ncWpcKd9DOv +oPI0J0FeG1wB0EQ+w394SBfK9GSSmFpQA8IR22H61V9n9YQWmv1QRBzQberrx0/B +Fsm19FehAoGBAO7r34JAoj905Lprsa2B1TlkGr+K+Rihda9L+9snzlgErmoAtroa +OFpZpWBeK3TIYEwZfQzavvQjtjWlXo53GaOLbEpJZAoZgx7OLptq9JQvGxP/e56E +6uQP6AyWqYNBE1yQ6nf20oZUkROYugtrNAFhfjjeY3Iv2N/L/bhynkCpAoGBAMiB +6uvcEJ0lRjQxenOcdGyrHogDpyGCpW1AjBM715wzvv8riu+mU0WctPclhAp0ihmI +loW/NVGHHOHrXHTkzxxC8H1TMA7XFSvyBKr/I0E02YTl3aFCYj96d9YUDtJ+ii+A +TEX4LxOEgEaV38FV+NqKYsOUSOFtryV9XN8eaCA9AoGATRq2EPUG52+z+S2UM6h4 +xYK21yXkabyBnEbrSri38NPh7be5QKoBkbbolgcOAyw/V4/KOYHGqkd0IC/0Kgnp +rkvqcLbqoA/HrwNfKy0DLWdeV7/VhzziCSPRAW8F2aQAuZjS8lAndM6J20cok1LP +f7qU34l1kn46gU713Lawt7ECgYEAr7Bu+UY15IxxrHAiXMUdms6CDSouOwWwED/7 +vSq1bTBGTm6H6h9yXc/HHbvorASbsW3mfsEhZhOe8jJ6LA8Fjzz0XswSkx5RddU1 +0+OFr7AwXOvRvGhfkEGuWY3vwu+QA7lGnBSwo0h54d+XVWAQkuWpTrhS9/xU+OOV +Cggpsv0CgYBxLAfSj5ZS50dn+J8FaQypPbCMDKb2gYk7RZ1SpTLBeYmWHYbD/oht +8js8hzj2hfj5Eu2LJWxamjS0dbDlizzVWkbVfk/e/hCPnIurXvUaFwVbkcu20psn +V+wc23onQvNUZUM8cZQ+ApS38NiaX66HvgOVtaOCcCiv2PF7EuOxrA== +-----END RSA PRIVATE KEY----- diff --git a/dev-packages/node-integration-tests/suites/reporting-api/localhost.pem b/dev-packages/node-integration-tests/suites/reporting-api/localhost.pem new file mode 100644 index 000000000000..72a4a7aad27f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/reporting-api/localhost.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICljCCAX4CCQChkPJBaaS+ojANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1 +czAeFw0yNDEwMjExMjI4MTBaFw0yNTEwMjExMjI4MTBaMA0xCzAJBgNVBAYTAnVz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuyGGuR1WxHZI6NzLKBto +n8MQKwrOgruuH3OX3xgFc6U1ZCecB59n7zgnNkSHBAv+x+9I1pDnwSFGKtuqSOU1 +sHC4usRkKNTXN+mU172AdWW7N8mznp8yFe08OELmRPXgJmo1q/5LXfO/I6MNuKAV +Hm8MKWn1e9uxlkcPMW1nOXvuPwk8BcPvXh7w5CSjG7z2wY+D9YkH3KWyAt1oJaET +S7iSfdQy+pfZy/Nq6//n/+Nu7yuQNIWR73+ejWtQAzi58RHYjlBEOwhtBdxgApJX +5OpQMiy+ncFNpAU2YQa/RLRtarNymrWqAStr3gBeeL8R61nX57hFoUM6S65cKnKI +RQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBnCF6XsEDFWIS4Uknvhj6Ris8iWI4F +JmXVQ3EMBkk7nk9od1OMXcYdSmrJs5eQLiCl+xZOIeA1Gvtrl+NEjWLbKwcgtkTW +0aIkOMxutAVUK+KKqjzkxJ4gvcdKpCtRH+6YllLpYwisdSOk6bby3g0TAfViY2wN +J4iqtjLRq5UWsIPuFPB8y3l4nhavtAHV6uOQNvl6mXEUj9QDE1BsNC8Fs24Eu/jv +DB7da1V8whpm1KDyp1dc7+ioXYTISZ0r2IcK/XRjzfiH1s7Xvf4juYLVYhDSpwdJ +wQgwssHT71WhtvAtn16bike+ZHZbqkiENZRcStDMJiyeHEiSpT/7Kibl +-----END CERTIFICATE----- diff --git a/dev-packages/node-integration-tests/suites/reporting-api/server.mjs b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs new file mode 100644 index 000000000000..feb7bab71928 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs @@ -0,0 +1,58 @@ +import * as https from 'https'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { defaultStackParser as browserStackParser } from '@sentry/browser'; +import { handleReportingApi } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +const __dirname = new URL('.', import.meta.url).pathname; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +import { readFileSync } from 'fs'; +import { join } from 'path'; +import express from 'express'; + +const app = express(); + +app.use(express.json({ type: 'application/reports+json' })); + +app.post('/reporting-api', async (req, res) => { + await handleReportingApi(req.body, browserStackParser); + res.sendStatus(200); +}); + +const port = 9000; + +app.get('/', (req, res) => { + const file = readFileSync(join(__dirname, 'index.html'), { encoding: 'utf-8' }); + + res.setHeader('Content-Type', 'text/html'); + res.setHeader( + 'Reporting-Endpoints', + `csp-endpoint="https://localhost:${port}/reporting-api", default="https://localhost:${port}/reporting-api"`, + ); + res.setHeader('Content-Security-Policy', "default-src 'self'; report-to csp-endpoint"); + res.setHeader( + 'Origin-Trial', + 'ApD+E2izWNtaaRBeZ5BXu46aV0l1MSUzJTPERkU3yf+53pAOHj3rARpjb08itVJklPYx7iNEv5//s2dtXUFIvgMAAABzeyJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo5MDAwIiwiZmVhdHVyZSI6IkRvY3VtZW50UG9saWN5SW5jbHVkZUpTQ2FsbFN0YWNrc0luQ3Jhc2hSZXBvcnRzIiwiZXhwaXJ5IjoxNzQyMzQyMzk5fQ==', + ); + res.setHeader('Document-Policy', 'include-js-call-stacks-in-crash-reports'); + res.send(file); +}); + +const options = { + key: readFileSync(join(__dirname, 'localhost-key.pem')), + cert: readFileSync(join(__dirname, 'localhost.pem')), +}; + +const server = https.createServer(options, app); + +server.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`{"port":${port}}`); +}); diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index d8b8c03e888b..d9ba11e36588 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -1,10 +1,15 @@ import type { + CSPReport, + CSPReportPayload, Client, + DeprecatedCSPReport, DsnComponents, DynamicSamplingContext, Event, EventEnvelope, EventItem, + RawSecurityEnvelope, + RawSecurityItem, SdkInfo, SdkMetadata, Session, @@ -18,8 +23,10 @@ import type { import { createEnvelope, createEventEnvelopeHeaders, + dropUndefinedKeys, dsnToString, getSdkMetadataForEnvelopeHeader, + uuid4, } from '@sentry/utils'; import { createSpanEnvelopeItem } from '@sentry/utils'; import { getDynamicSamplingContextFromSpan } from './tracing/dynamicSamplingContext'; @@ -135,3 +142,45 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? return createEnvelope(headers, items); } + +function convertToDeprecatedPayload(report: CSPReportPayload): DeprecatedCSPReport { + return { + 'csp-report': { + 'document-uri': report.documentURI, + referrer: report.referrer, + 'blocked-uri': report.blockedURI, + 'effective-directive': report.effectiveDirective, + 'violated-directive': report.effectiveDirective, + 'original-policy': report.originalPolicy, + disposition: report.disposition, + 'status-code': report.statusCode, + status: report.statusCode.toString(), + 'script-sample': report.sourceFile, + sample: report.sample, + }, + }; +} + +/** + * Create an Envelope from a CSP report. + */ +export function createRawSecurityEnvelope( + report: CSPReport, + dsn: DsnComponents, + tunnel?: string, + release?: string, + environment?: string, +): RawSecurityEnvelope { + const envelopeHeaders = { + event_id: uuid4(), + ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), + ...(report.user_agent && { user_agent: report.user_agent }), + }; + + const eventItem: RawSecurityItem = [ + { type: 'raw_security', sentry_release: release, sentry_environment: environment }, + dropUndefinedKeys(convertToDeprecatedPayload(report.body)), + ]; + + return createEnvelope(envelopeHeaders, [eventItem]); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 24cea1bea7ca..dc7cdbe7576f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -113,3 +113,5 @@ export { captureFeedback } from './feedback'; export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim'; export { SDK_VERSION } from '@sentry/utils'; + +export { handleReportingApi, handlerReportingApiRequest } from './reporting'; diff --git a/packages/core/src/reporting.ts b/packages/core/src/reporting.ts new file mode 100644 index 000000000000..40a6833a65bf --- /dev/null +++ b/packages/core/src/reporting.ts @@ -0,0 +1,83 @@ +import type { Event, Report, StackParser, Stacktrace } from '@sentry/types'; +import { uuid4 } from '@sentry/utils'; +import { getClient } from './currentScopes'; +import { createRawSecurityEnvelope } from './envelope'; + +/** Handles Requests from the Reporting API */ +export async function handlerReportingApiRequest( + request: Request, + browserStackParser?: StackParser, + client = getClient(), +): Promise { + if (request.method !== 'POST') { + return new Response('Expected POST', { status: 405 }); + } + + if (request.headers.get('Content-Type') !== 'application/reports+json') { + return new Response('Expected "application/reports+json" Content-Type', { status: 415 }); + } + const reports = await request.json(); + await handleReportingApi(reports, browserStackParser, client); + return new Response(undefined, { status: 200 }); +} + +/** Handles Reports from the Reporting API */ +export async function handleReportingApi( + reports: Report[], + browserStackParser?: StackParser, + client = getClient(), +): Promise { + if (!client) { + // eslint-disable-next-line no-console + console.warn('[Reporting API] No client available'); + return; + } + + const dsn = client.getDsn(); + if (!dsn) { + // eslint-disable-next-line no-console + console.warn('[Reporting API] No DSN set'); + return; + } + + for (const report of reports) { + if (report.type === 'crash') { + const event: Event = { + level: 'fatal', + message: 'Crashed', + request: { + url: report.url, + ...(report.user_agent && { headers: { 'User-Agent': report.user_agent } }), + }, + }; + + if (report.body.reason === 'oom') { + event.message = 'Crashed: Out of memory'; + } else if (report.body.reason === 'unresponsive') { + event.message = 'Crashed: Unresponsive'; + } + + if (report.body.stack) { + event.exception = { + values: [ + { + type: 'Crashed', + value: event.message, + stacktrace: { + ...(browserStackParser && report.body.stack && { frames: browserStackParser(report.body.stack) }), + }, + }, + ], + }; + delete event.message; + } + + client.captureEvent(event); + } else if (report.type === 'csp-violation') { + const options = client.getOptions(); + const envelope = createRawSecurityEnvelope(report, dsn, options.tunnel, options.release, options.environment); + + await client.sendEnvelope(envelope); + } + } +} diff --git a/packages/types/src/datacategory.ts b/packages/types/src/datacategory.ts index bd1c0b693e4d..860c9b501ad3 100644 --- a/packages/types/src/datacategory.ts +++ b/packages/types/src/datacategory.ts @@ -30,5 +30,7 @@ export type DataCategory = | 'metric_bucket' // Span | 'span' + // Security report + | 'security' // Unknown data category | 'unknown'; diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 29e8fc123b16..ec376c5d53d2 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -6,6 +6,7 @@ import type { Event } from './event'; import type { FeedbackEvent, UserFeedback } from './feedback'; import type { Profile, ProfileChunk } from './profiling'; import type { ReplayEvent, ReplayRecordingData } from './replay'; +import type { DeprecatedCSPReport } from './reporting'; import type { SdkInfo } from './sdkinfo'; import type { SerializedSession, SessionAggregates } from './session'; import type { SpanJSON } from './span'; @@ -41,7 +42,8 @@ export type EnvelopeItemType = | 'replay_recording' | 'check_in' | 'statsd' - | 'span'; + | 'span' + | 'raw_security'; export type BaseEnvelopeHeaders = { [key: string]: unknown; @@ -84,6 +86,7 @@ type ProfileItemHeaders = { type: 'profile' }; type ProfileChunkItemHeaders = { type: 'profile_chunk' }; type StatsdItemHeaders = { type: 'statsd'; length: number }; type SpanItemHeaders = { type: 'span' }; +type RawSecurityHeaders = { type: 'raw_security'; sentry_release?: string; sentry_environment?: string }; export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; @@ -100,6 +103,7 @@ export type FeedbackItem = BaseEnvelopeItem; export type ProfileItem = BaseEnvelopeItem; export type ProfileChunkItem = BaseEnvelopeItem; export type SpanItem = BaseEnvelopeItem>; +export type RawSecurityItem = BaseEnvelopeItem; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; type SessionEnvelopeHeaders = { sent_at: string }; @@ -120,6 +124,7 @@ export type CheckInEnvelope = BaseEnvelope; export type StatsdEnvelope = BaseEnvelope; export type SpanEnvelope = BaseEnvelope; export type ProfileChunkEnvelope = BaseEnvelope; +export type RawSecurityEnvelope = BaseEnvelope; export type Envelope = | EventEnvelope @@ -129,6 +134,7 @@ export type Envelope = | ReplayEnvelope | CheckInEnvelope | StatsdEnvelope - | SpanEnvelope; + | SpanEnvelope + | RawSecurityEnvelope; export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b100c1e9c26a..b4e352813207 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -48,6 +48,8 @@ export type { ProfileItem, ProfileChunkEnvelope, ProfileChunkItem, + RawSecurityEnvelope, + RawSecurityItem, SpanEnvelope, SpanItem, } from './envelope'; @@ -173,3 +175,4 @@ export type { export type { ParameterizedString } from './parameterize'; export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './profiling'; export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy'; +export type { Report, CSPReport, CSPReportPayload, DeprecatedCSPReport } from './reporting'; diff --git a/packages/types/src/reporting.ts b/packages/types/src/reporting.ts new file mode 100644 index 000000000000..0a391fbefc8e --- /dev/null +++ b/packages/types/src/reporting.ts @@ -0,0 +1,61 @@ +// Types from here +// https://www.w3.org/TR/CSP3/ + +interface Base { + readonly age?: number; + readonly body?: unknown; + readonly url: string; + readonly user_agent?: string; +} + +type CrashReportPayload = { + readonly crashId?: string; + readonly reason?: 'unresponsive' | 'oom'; + readonly stack?: string; +}; + +interface CrashReport extends Base { + readonly type: 'crash'; + readonly body: CrashReportPayload; +} + +type Disposition = 'enforce' | 'report' | 'reporting'; + +export interface CSPReportPayload { + readonly blockedURI?: string; + readonly blockedURL?: string; + readonly columnNumber?: number; + readonly disposition: Disposition; + readonly documentURI: string; + readonly documentURL: string; + readonly effectiveDirective: string; + readonly lineNumber?: number; + readonly originalPolicy: string; + readonly referrer?: string; + readonly sample?: string; + readonly sourceFile?: string; + readonly statusCode: number; +} + +export interface CSPReport extends Base { + readonly type: 'csp-violation'; + readonly body: CSPReportPayload; +} + +export type Report = CrashReport | CSPReport; + +export interface DeprecatedCSPReport { + readonly 'csp-report': { + readonly 'document-uri'?: string; + readonly referrer?: string; + readonly 'blocked-uri'?: string; + readonly 'effective-directive'?: string; + readonly 'violated-directive'?: string; + readonly 'original-policy'?: string; + readonly disposition: Disposition; + readonly 'status-code'?: number; + readonly status?: string; + readonly 'script-sample'?: string; + readonly sample?: string; + }; +} diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index 8bf29788edf0..cad0fcf54926 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -224,6 +224,7 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { feedback: 'feedback', span: 'span', statsd: 'metric_bucket', + raw_security: 'security', }; /** From ca351994c12f3d090cc805b5363240d4b4b37848 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 21 Oct 2024 17:55:30 +0200 Subject: [PATCH 2/6] Lint --- packages/core/src/reporting.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/reporting.ts b/packages/core/src/reporting.ts index 40a6833a65bf..0369f89c7bfd 100644 --- a/packages/core/src/reporting.ts +++ b/packages/core/src/reporting.ts @@ -1,5 +1,4 @@ -import type { Event, Report, StackParser, Stacktrace } from '@sentry/types'; -import { uuid4 } from '@sentry/utils'; +import type { Event, Report, StackParser } from '@sentry/types'; import { getClient } from './currentScopes'; import { createRawSecurityEnvelope } from './envelope'; From 003e611588d3d3474833ec346f0cda05e9a5ebb3 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 29 Oct 2024 13:21:59 +0100 Subject: [PATCH 3/6] Add test --- .../suites/reporting-api/index.html | 9 --- .../suites/reporting-api/localhost-key.pem | 27 ------- .../suites/reporting-api/localhost.pem | 16 ---- .../suites/reporting-api/server.mjs | 73 +++++++++---------- .../suites/reporting-api/test.ts | 52 +++++++++++++ .../node-integration-tests/utils/runner.ts | 16 ++++ packages/core/src/envelope.ts | 5 +- packages/core/src/index.ts | 2 +- packages/core/src/reporting.ts | 25 +------ 9 files changed, 110 insertions(+), 115 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/reporting-api/index.html delete mode 100644 dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem delete mode 100644 dev-packages/node-integration-tests/suites/reporting-api/localhost.pem create mode 100644 dev-packages/node-integration-tests/suites/reporting-api/test.ts diff --git a/dev-packages/node-integration-tests/suites/reporting-api/index.html b/dev-packages/node-integration-tests/suites/reporting-api/index.html deleted file mode 100644 index 5bceebc87f85..000000000000 --- a/dev-packages/node-integration-tests/suites/reporting-api/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - Reporting API - - - -

Reporting API

- - diff --git a/dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem b/dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem deleted file mode 100644 index 0dc1f76b48b0..000000000000 --- a/dev-packages/node-integration-tests/suites/reporting-api/localhost-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAuyGGuR1WxHZI6NzLKBton8MQKwrOgruuH3OX3xgFc6U1ZCec -B59n7zgnNkSHBAv+x+9I1pDnwSFGKtuqSOU1sHC4usRkKNTXN+mU172AdWW7N8mz -np8yFe08OELmRPXgJmo1q/5LXfO/I6MNuKAVHm8MKWn1e9uxlkcPMW1nOXvuPwk8 -BcPvXh7w5CSjG7z2wY+D9YkH3KWyAt1oJaETS7iSfdQy+pfZy/Nq6//n/+Nu7yuQ -NIWR73+ejWtQAzi58RHYjlBEOwhtBdxgApJX5OpQMiy+ncFNpAU2YQa/RLRtarNy -mrWqAStr3gBeeL8R61nX57hFoUM6S65cKnKIRQIDAQABAoIBAQCm9H+FkxaBy+K6 -15rt2p5aw6ceL9MVsqrkZrZeFclvZzuecvRznJYXSSs68KLhSm5zJRsATGJo3e4D -eN6RkOZ41+kIwQV3pIWr3dutK+Z7V1tUp8F4ySHfjDyJGa7mYdQtkd7257eISFsF -SYmJalHNSFg6bs3VRqpHoHh+qdRJ5K8ZuwXtZg0vBPovJbKcVRDvO59CVXyNpB87 -Z4FtP0Z3aTyhCbMjNhs3Zg55qBv/sD7LKJdcGzxHnQIGWqjZMbLC2ncWpcKd9DOv -oPI0J0FeG1wB0EQ+w394SBfK9GSSmFpQA8IR22H61V9n9YQWmv1QRBzQberrx0/B -Fsm19FehAoGBAO7r34JAoj905Lprsa2B1TlkGr+K+Rihda9L+9snzlgErmoAtroa -OFpZpWBeK3TIYEwZfQzavvQjtjWlXo53GaOLbEpJZAoZgx7OLptq9JQvGxP/e56E -6uQP6AyWqYNBE1yQ6nf20oZUkROYugtrNAFhfjjeY3Iv2N/L/bhynkCpAoGBAMiB -6uvcEJ0lRjQxenOcdGyrHogDpyGCpW1AjBM715wzvv8riu+mU0WctPclhAp0ihmI -loW/NVGHHOHrXHTkzxxC8H1TMA7XFSvyBKr/I0E02YTl3aFCYj96d9YUDtJ+ii+A -TEX4LxOEgEaV38FV+NqKYsOUSOFtryV9XN8eaCA9AoGATRq2EPUG52+z+S2UM6h4 -xYK21yXkabyBnEbrSri38NPh7be5QKoBkbbolgcOAyw/V4/KOYHGqkd0IC/0Kgnp -rkvqcLbqoA/HrwNfKy0DLWdeV7/VhzziCSPRAW8F2aQAuZjS8lAndM6J20cok1LP -f7qU34l1kn46gU713Lawt7ECgYEAr7Bu+UY15IxxrHAiXMUdms6CDSouOwWwED/7 -vSq1bTBGTm6H6h9yXc/HHbvorASbsW3mfsEhZhOe8jJ6LA8Fjzz0XswSkx5RddU1 -0+OFr7AwXOvRvGhfkEGuWY3vwu+QA7lGnBSwo0h54d+XVWAQkuWpTrhS9/xU+OOV -Cggpsv0CgYBxLAfSj5ZS50dn+J8FaQypPbCMDKb2gYk7RZ1SpTLBeYmWHYbD/oht -8js8hzj2hfj5Eu2LJWxamjS0dbDlizzVWkbVfk/e/hCPnIurXvUaFwVbkcu20psn -V+wc23onQvNUZUM8cZQ+ApS38NiaX66HvgOVtaOCcCiv2PF7EuOxrA== ------END RSA PRIVATE KEY----- diff --git a/dev-packages/node-integration-tests/suites/reporting-api/localhost.pem b/dev-packages/node-integration-tests/suites/reporting-api/localhost.pem deleted file mode 100644 index 72a4a7aad27f..000000000000 --- a/dev-packages/node-integration-tests/suites/reporting-api/localhost.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICljCCAX4CCQChkPJBaaS+ojANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1 -czAeFw0yNDEwMjExMjI4MTBaFw0yNTEwMjExMjI4MTBaMA0xCzAJBgNVBAYTAnVz -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuyGGuR1WxHZI6NzLKBto -n8MQKwrOgruuH3OX3xgFc6U1ZCecB59n7zgnNkSHBAv+x+9I1pDnwSFGKtuqSOU1 -sHC4usRkKNTXN+mU172AdWW7N8mznp8yFe08OELmRPXgJmo1q/5LXfO/I6MNuKAV -Hm8MKWn1e9uxlkcPMW1nOXvuPwk8BcPvXh7w5CSjG7z2wY+D9YkH3KWyAt1oJaET -S7iSfdQy+pfZy/Nq6//n/+Nu7yuQNIWR73+ejWtQAzi58RHYjlBEOwhtBdxgApJX -5OpQMiy+ncFNpAU2YQa/RLRtarNymrWqAStr3gBeeL8R61nX57hFoUM6S65cKnKI -RQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBnCF6XsEDFWIS4Uknvhj6Ris8iWI4F -JmXVQ3EMBkk7nk9od1OMXcYdSmrJs5eQLiCl+xZOIeA1Gvtrl+NEjWLbKwcgtkTW -0aIkOMxutAVUK+KKqjzkxJ4gvcdKpCtRH+6YllLpYwisdSOk6bby3g0TAfViY2wN -J4iqtjLRq5UWsIPuFPB8y3l4nhavtAHV6uOQNvl6mXEUj9QDE1BsNC8Fs24Eu/jv -DB7da1V8whpm1KDyp1dc7+ioXYTISZ0r2IcK/XRjzfiH1s7Xvf4juYLVYhDSpwdJ -wQgwssHT71WhtvAtn16bike+ZHZbqkiENZRcStDMJiyeHEiSpT/7Kibl ------END CERTIFICATE----- diff --git a/dev-packages/node-integration-tests/suites/reporting-api/server.mjs b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs index feb7bab71928..942505af5e89 100644 --- a/dev-packages/node-integration-tests/suites/reporting-api/server.mjs +++ b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs @@ -1,20 +1,14 @@ -import * as https from 'https'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import { defaultStackParser as browserStackParser } from '@sentry/browser'; import { handleReportingApi } from '@sentry/core'; import * as Sentry from '@sentry/node'; -const __dirname = new URL('.', import.meta.url).pathname; - Sentry.init({ - debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, }); -import { readFileSync } from 'fs'; -import { join } from 'path'; import express from 'express'; const app = express(); @@ -26,33 +20,38 @@ app.post('/reporting-api', async (req, res) => { res.sendStatus(200); }); -const port = 9000; - -app.get('/', (req, res) => { - const file = readFileSync(join(__dirname, 'index.html'), { encoding: 'utf-8' }); - - res.setHeader('Content-Type', 'text/html'); - res.setHeader( - 'Reporting-Endpoints', - `csp-endpoint="https://localhost:${port}/reporting-api", default="https://localhost:${port}/reporting-api"`, - ); - res.setHeader('Content-Security-Policy', "default-src 'self'; report-to csp-endpoint"); - res.setHeader( - 'Origin-Trial', - 'ApD+E2izWNtaaRBeZ5BXu46aV0l1MSUzJTPERkU3yf+53pAOHj3rARpjb08itVJklPYx7iNEv5//s2dtXUFIvgMAAABzeyJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo5MDAwIiwiZmVhdHVyZSI6IkRvY3VtZW50UG9saWN5SW5jbHVkZUpTQ2FsbFN0YWNrc0luQ3Jhc2hSZXBvcnRzIiwiZXhwaXJ5IjoxNzQyMzQyMzk5fQ==', - ); - res.setHeader('Document-Policy', 'include-js-call-stacks-in-crash-reports'); - res.send(file); -}); - -const options = { - key: readFileSync(join(__dirname, 'localhost-key.pem')), - cert: readFileSync(join(__dirname, 'localhost.pem')), -}; - -const server = https.createServer(options, app); - -server.listen(port, () => { - // eslint-disable-next-line no-console - console.log(`{"port":${port}}`); -}); +startExpressServerAndSendPortToRunner(app); + +// Below is to support testing with a browser. We don't test this yet because we don't want to add the overhead of +// installing playwright to the node-integration-tests. We should consider this in the future. + +// const port = 9000; + +// app.get('/', (req, res) => { +// const file = readFileSync(join(__dirname, 'index.html'), { encoding: 'utf-8' }); + +// res.setHeader('Content-Type', 'text/html'); +// res.setHeader( +// 'Reporting-Endpoints', +// `csp-endpoint="https://localhost:${port}/reporting-api", default="https://localhost:${port}/reporting-api"`, +// ); +// res.setHeader('Content-Security-Policy', "default-src 'self'; report-to csp-endpoint"); +// res.setHeader('Document-Policy', 'include-js-call-stacks-in-crash-reports'); +// res.setHeader( +// 'Origin-Trial', +// 'ApD+E2izWNtaaRBeZ5BXu46aV0l1MSUzJTPERkU3yf+53pAOHj3rARpjb08itVJklPYx7iNEv5//s2dtXUFIvgMAAABzeyJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo5MDAwIiwiZmVhdHVyZSI6IkRvY3VtZW50UG9saWN5SW5jbHVkZUpTQ2FsbFN0YWNrc0luQ3Jhc2hSZXBvcnRzIiwiZXhwaXJ5IjoxNzQyMzQyMzk5fQ==', +// ); +// res.send(file); +// }); + +// const options = { +// key: readFileSync(join(__dirname, 'localhost-key.pem')), +// cert: readFileSync(join(__dirname, 'localhost.pem')), +// }; + +// const server = https.createServer(options, app); + +// server.listen(port, () => { +// // eslint-disable-next-line no-console +// console.log(`{"port":${port}}`); +// }); diff --git a/dev-packages/node-integration-tests/suites/reporting-api/test.ts b/dev-packages/node-integration-tests/suites/reporting-api/test.ts new file mode 100644 index 000000000000..595d3a220471 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/reporting-api/test.ts @@ -0,0 +1,52 @@ +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +describe('Reporting API', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should forward Reporting API requests as raw CSP envelopes', done => { + const runner = createRunner(__dirname, 'server.mjs') + .expect({ + raw_security: { + 'csp-report': { + 'document-uri': 'https://localhost:9000/', + referrer: '', + 'blocked-uri': 'https://example.com/script.js', + 'effective-directive': 'script-src-elem', + 'original-policy': "default-src 'self'; report-to csp-endpoint", + disposition: 'enforce', + 'status-code': 200, + status: '200', + sample: '', + }, + }, + }) + .start(done); + + runner.makeRequest( + 'post', + '/reporting-api', + { 'Content-Type': 'application/reports+json' }, + JSON.stringify([ + { + age: 0, + body: { + blockedURL: 'https://example.com/script.js', + disposition: 'enforce', + documentURL: 'https://localhost:9000/', + effectiveDirective: 'script-src-elem', + originalPolicy: "default-src 'self'; report-to csp-endpoint", + referrer: '', + sample: '', + statusCode: 200, + }, + type: 'csp-violation', + url: 'https://localhost:9000/', + user_agent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + }, + ]), + ); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index bde5bd06cd21..728a46ca945f 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -4,6 +4,7 @@ import { join } from 'path'; import { SDK_VERSION } from '@sentry/node'; import type { ClientReport, + DeprecatedCSPReport, Envelope, EnvelopeItemType, Event, @@ -165,6 +166,9 @@ type Expected = } | { client_report: Partial | ((event: ClientReport) => void); + } + | { + raw_security: Partial | ((event: DeprecatedCSPReport) => void); }; type ExpectedEnvelopeHeader = @@ -265,6 +269,7 @@ export function createRunner(...paths: string[]) { } } + // eslint-disable-next-line complexity function newEnvelope(envelope: Envelope): void { for (const item of envelope[1]) { const envelopeItemType = item[0].type; @@ -360,6 +365,17 @@ export function createRunner(...paths: string[]) { expectCallbackCalled(); } + + if ('raw_security' in expected) { + const rawSecurity = item[1] as DeprecatedCSPReport; + if (typeof expected.raw_security === 'function') { + expected.raw_security(rawSecurity); + } else { + expect(rawSecurity).toMatchObject(expected.raw_security); + } + + expectCallbackCalled(); + } } catch (e) { complete(e as Error); } diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index d9ba11e36588..64d25c780e64 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -146,11 +146,10 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? function convertToDeprecatedPayload(report: CSPReportPayload): DeprecatedCSPReport { return { 'csp-report': { - 'document-uri': report.documentURI, + 'document-uri': report.documentURI || report.documentURL, referrer: report.referrer, - 'blocked-uri': report.blockedURI, + 'blocked-uri': report.blockedURI || report.blockedURL, 'effective-directive': report.effectiveDirective, - 'violated-directive': report.effectiveDirective, 'original-policy': report.originalPolicy, disposition: report.disposition, 'status-code': report.statusCode, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dc7cdbe7576f..e18c341051cb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -114,4 +114,4 @@ export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim'; export { SDK_VERSION } from '@sentry/utils'; -export { handleReportingApi, handlerReportingApiRequest } from './reporting'; +export { handleReportingApi } from './reporting'; diff --git a/packages/core/src/reporting.ts b/packages/core/src/reporting.ts index 0369f89c7bfd..6fe78d118d0e 100644 --- a/packages/core/src/reporting.ts +++ b/packages/core/src/reporting.ts @@ -2,24 +2,6 @@ import type { Event, Report, StackParser } from '@sentry/types'; import { getClient } from './currentScopes'; import { createRawSecurityEnvelope } from './envelope'; -/** Handles Requests from the Reporting API */ -export async function handlerReportingApiRequest( - request: Request, - browserStackParser?: StackParser, - client = getClient(), -): Promise { - if (request.method !== 'POST') { - return new Response('Expected POST', { status: 405 }); - } - - if (request.headers.get('Content-Type') !== 'application/reports+json') { - return new Response('Expected "application/reports+json" Content-Type', { status: 415 }); - } - const reports = await request.json(); - await handleReportingApi(reports, browserStackParser, client); - return new Response(undefined, { status: 200 }); -} - /** Handles Reports from the Reporting API */ export async function handleReportingApi( reports: Report[], @@ -56,18 +38,17 @@ export async function handleReportingApi( event.message = 'Crashed: Unresponsive'; } - if (report.body.stack) { + if (report.body.stack && browserStackParser) { event.exception = { values: [ { type: 'Crashed', value: event.message, - stacktrace: { - ...(browserStackParser && report.body.stack && { frames: browserStackParser(report.body.stack) }), - }, + stacktrace: { frames: browserStackParser(report.body.stack) }, }, ], }; + delete event.message; } From 379063febc76bafa13e541156b4c06ca67e16ddb Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 29 Oct 2024 20:03:37 +0100 Subject: [PATCH 4/6] Rename exported function and remove stack parsing --- .../suites/reporting-api/server.mjs | 5 ++-- packages/core/src/index.ts | 2 +- packages/core/src/reporting.ts | 26 ++++--------------- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/reporting-api/server.mjs b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs index 942505af5e89..c59d18022135 100644 --- a/dev-packages/node-integration-tests/suites/reporting-api/server.mjs +++ b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs @@ -1,6 +1,5 @@ import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import { defaultStackParser as browserStackParser } from '@sentry/browser'; -import { handleReportingApi } from '@sentry/core'; +import { captureReportingApi } from '@sentry/core'; import * as Sentry from '@sentry/node'; Sentry.init({ @@ -16,7 +15,7 @@ const app = express(); app.use(express.json({ type: 'application/reports+json' })); app.post('/reporting-api', async (req, res) => { - await handleReportingApi(req.body, browserStackParser); + await captureReportingApi(req.body); res.sendStatus(200); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e18c341051cb..f3247d5488a8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -114,4 +114,4 @@ export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim'; export { SDK_VERSION } from '@sentry/utils'; -export { handleReportingApi } from './reporting'; +export { captureReportingApi } from './reporting'; diff --git a/packages/core/src/reporting.ts b/packages/core/src/reporting.ts index 6fe78d118d0e..ad6e5cc462e8 100644 --- a/packages/core/src/reporting.ts +++ b/packages/core/src/reporting.ts @@ -1,13 +1,11 @@ -import type { Event, Report, StackParser } from '@sentry/types'; +import type { Client, Event, Report } from '@sentry/types'; import { getClient } from './currentScopes'; import { createRawSecurityEnvelope } from './envelope'; -/** Handles Reports from the Reporting API */ -export async function handleReportingApi( - reports: Report[], - browserStackParser?: StackParser, - client = getClient(), -): Promise { +/** Captures reports from the Reporting API */ +export async function captureReportingApi(reports: Report[], options?: { client?: Client }): Promise { + const client = options?.client || getClient(); + if (!client) { // eslint-disable-next-line no-console console.warn('[Reporting API] No client available'); @@ -38,20 +36,6 @@ export async function handleReportingApi( event.message = 'Crashed: Unresponsive'; } - if (report.body.stack && browserStackParser) { - event.exception = { - values: [ - { - type: 'Crashed', - value: event.message, - stacktrace: { frames: browserStackParser(report.body.stack) }, - }, - ], - }; - - delete event.message; - } - client.captureEvent(event); } else if (report.type === 'csp-violation') { const options = client.getOptions(); From 70e9ad82051c84b3a23cbd4158caa0ec0b27cb09 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 29 Oct 2024 20:33:23 +0100 Subject: [PATCH 5/6] simplify usage --- .../node-integration-tests/suites/reporting-api/server.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/reporting-api/server.mjs b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs index c59d18022135..8359ac00fafd 100644 --- a/dev-packages/node-integration-tests/suites/reporting-api/server.mjs +++ b/dev-packages/node-integration-tests/suites/reporting-api/server.mjs @@ -12,9 +12,7 @@ import express from 'express'; const app = express(); -app.use(express.json({ type: 'application/reports+json' })); - -app.post('/reporting-api', async (req, res) => { +app.post('/reporting-api', express.json({ type: 'application/reports+json' }), async (req, res) => { await captureReportingApi(req.body); res.sendStatus(200); }); From 14fdfa01f4ad0ad3afebbf2689958513dc5bc8d7 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 29 Oct 2024 21:35:41 +0100 Subject: [PATCH 6/6] Fix linting --- packages/core/src/reporting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/reporting.ts b/packages/core/src/reporting.ts index ad6e5cc462e8..60ed2f7fc991 100644 --- a/packages/core/src/reporting.ts +++ b/packages/core/src/reporting.ts @@ -4,7 +4,7 @@ import { createRawSecurityEnvelope } from './envelope'; /** Captures reports from the Reporting API */ export async function captureReportingApi(reports: Report[], options?: { client?: Client }): Promise { - const client = options?.client || getClient(); + const client = options ? options.client : getClient(); if (!client) { // eslint-disable-next-line no-console