Skip to content

Commit

Permalink
feat: add csp-violation-event-plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Anthony Rizzo committed Oct 10, 2024
1 parent f99498c commit 4c46345
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 0 deletions.
64 changes: 64 additions & 0 deletions src/event-schemas/csp-violation-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"$id": "com.amazon.rum.csp_violation_event",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CspViolationEvent",
"type": "object",
"properties": {
"version": {
"const": "1.0.0",
"type": "string",
"description": "Schema version."
},
"blockedURI": {
"type": "string",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/blockedURI"
},
"columnNumber": {
"type": "number",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/columnNumber"
},
"disposition": {
"type": "string",
"enum": ["enforce", "report"],
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/disposition"
},
"documentURI": {
"type": "string",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/documentURI"
},
"effectiveDirective": {
"type": "string",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/effectiveDirective"
},
"lineNumber": {
"type": "number",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/lineNumber"
},
"originalPolicy": {
"type": "string",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/originalPolicy"
},
"referrer": {
"type": ["string", "null"],
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/referrer"
},
"sample": {
"type": "string",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/sample"
},
"sourceFile": {
"type": ["string", "null"],
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/sourceFile"
},
"statusCode": {
"type": "number",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/statusCode"
},
"violatedDirective": {
"type": "string",
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/violatedDirective"
}
},
"additionalProperties": false,
"required": ["version"]
}
77 changes: 77 additions & 0 deletions src/plugins/event-plugins/CspViolationPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { InternalPlugin } from '../InternalPlugin';
import { CSP_VIOLATION_EVENT_TYPE } from '../utils/constant';

export const CSP_VIOLATION_EVENT_PLUGIN_ID = 'csp-violation';

export type CspViolationPluginConfig = {
ignore: (error: SecurityPolicyViolationEvent) => boolean;
};

export type PartialCspViolationPluginConfig = {
ignore?: (error: SecurityPolicyViolationEvent) => boolean;
};

const defaultConfig: CspViolationPluginConfig = {
ignore: () => false
};

export class CspViolationPlugin extends InternalPlugin {
private config: CspViolationPluginConfig;

constructor(config?: PartialCspViolationPluginConfig) {
super(CSP_VIOLATION_EVENT_PLUGIN_ID);
this.config = { ...defaultConfig, ...config };
}

enable(): void {
if (this.enabled) {
return;
}
this.addEventHandler();
this.enabled = true;
}

disable(): void {
if (!this.enabled) {
return;
}
this.removeEventHandler();
this.enabled = false;
}

record(cspViolationEvent: any): void {
this.recordCspViolationEvent(cspViolationEvent);
}

protected onload(): void {
this.addEventHandler();
}

private eventHandler = (
cspViolationEvent: SecurityPolicyViolationEvent
) => {
if (!this.config.ignore(cspViolationEvent)) {
this.recordCspViolationEvent(cspViolationEvent);
}
};

private recordCspViolationEvent(
cspViolationEvent: SecurityPolicyViolationEvent
) {
this.context?.record(CSP_VIOLATION_EVENT_TYPE, {
...cspViolationEvent,
version: '1.0.0'
});
}

private addEventHandler(): void {
window.addEventListener('securitypolicyviolation', this.eventHandler);
}

private removeEventHandler(): void {
window.removeEventListener(
'securitypolicyviolation',
this.eventHandler
);
}
}
164 changes: 164 additions & 0 deletions src/plugins/event-plugins/__tests__/CspViolationPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { context, getSession, record } from '../../../test-utils/test-utils';
import { CSP_VIOLATION_EVENT_TYPE } from '../../utils/constant';
import { CspViolationPlugin } from '../CspViolationPlugin';

declare global {
namespace jest {
interface Expect {
toBePositive(): any;
}
}
}

const eventDetails: Partial<SecurityPolicyViolationEvent> = {
violatedDirective: 'test:violatedDirective',
documentURI: 'http://documentURI',
blockedURI: 'https://blockedURI',
originalPolicy: 'test:originalPolicy',
referrer: 'test:referrer',
statusCode: 200,
effectiveDirective: 'test:effectiveDirective'
};

function dispatchCspViolationEvent() {
const event = new Event(
'securitypolicyviolation'
) as SecurityPolicyViolationEvent;
// its important to apply our expected event details to the event before dispatching it.
Object.assign(event, eventDetails);

dispatchEvent(event);
}

expect.extend({
toBePositive(recieved) {
const pass = recieved > 0;
if (pass) {
return {
message: () =>
`expected ${recieved} not to be a positive integer`,
pass: true
};
} else {
return {
message: () => `expected ${recieved} to be a positive integer`,
pass: false
};
}
}
});

describe('CspViolationPlugin tests', () => {
beforeEach(() => {
record.mockClear();
getSession.mockClear();
});

test('when an CspViolationEvent is triggered then the plugin records cspViolationEvent', async () => {
// Init
const plugin: CspViolationPlugin = new CspViolationPlugin();

// Run
plugin.load(context);
dispatchCspViolationEvent();
plugin.disable();

// Assert
expect(record).toHaveBeenCalledTimes(1);
expect(record.mock.calls[0][0]).toEqual(CSP_VIOLATION_EVENT_TYPE);
expect(record.mock.calls[0][1]).toMatchObject(
expect.objectContaining({
version: '1.0.0',
blockedURI: 'https://blockedURI',
documentURI: 'http://documentURI',
effectiveDirective: 'test:effectiveDirective',
originalPolicy: 'test:originalPolicy',
referrer: 'test:referrer',
statusCode: 200
})
);
});

test('when plugin disabled then plugin does not record events', async () => {
// Init
const plugin: CspViolationPlugin = new CspViolationPlugin();

// Run
plugin.load(context);
plugin.disable();

dispatchCspViolationEvent();
plugin.disable();

// Assert
expect(record).toHaveBeenCalledTimes(0);
});

test('when enabled then plugin records events', async () => {
// Init
const plugin: CspViolationPlugin = new CspViolationPlugin();

// Run
plugin.load(context);
plugin.disable();
plugin.enable();
dispatchCspViolationEvent();
plugin.disable();

// Assert
expect(record).toHaveBeenCalledTimes(1);
});

test('when record is used then errors are not passed to the ignore function', async () => {
// Init
const mockIgnore = jest.fn();
const plugin: CspViolationPlugin = new CspViolationPlugin({
ignore: mockIgnore
});

// Run
plugin.load(context);
const event = new Event(
'securitypolicyviolation'
) as SecurityPolicyViolationEvent;
plugin.record(event);
plugin.disable();

// Assert
expect(record).toHaveBeenCalled();
expect(mockIgnore).not.toHaveBeenCalled();
});

test('by default SecurityPolicyViolationEvents are not ignored', async () => {
// Init
const plugin: CspViolationPlugin = new CspViolationPlugin();

// Run
plugin.load(context);
dispatchCspViolationEvent();
plugin.disable();

// Assert
expect(record).toHaveBeenCalled();
});

test('when a specific documentUri is ignored then SecurityPolicyViolationEvents are not recorded', async () => {
// Init
const plugin: CspViolationPlugin = new CspViolationPlugin({
ignore: (e) => {
const ignoredDocuments = ['http://documentURI'];
return ignoredDocuments.includes(
(e as SecurityPolicyViolationEvent).documentURI
);
}
});

// Run
plugin.load(context);
dispatchCspViolationEvent();
plugin.disable();

// Assert
expect(record).not.toHaveBeenCalled();
});
});
3 changes: 3 additions & 0 deletions src/plugins/utils/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ export const SESSION_START_EVENT_TYPE = `${RUM_AMZ_PREFIX}.session_start_event`;

// Time to interactive event
export const TIME_TO_INTERACTIVE_EVENT_TYPE = `${RUM_AMZ_PREFIX}.time_to_interactive_event`;

// CSP violation event schemas
export const CSP_VIOLATION_EVENT_TYPE = `${RUM_AMZ_PREFIX}.csp_violation_event`;

0 comments on commit 4c46345

Please sign in to comment.