Skip to content

Commit 4c46345

Browse files
author
Anthony Rizzo
committed
feat: add csp-violation-event-plugin
1 parent f99498c commit 4c46345

File tree

4 files changed

+308
-0
lines changed

4 files changed

+308
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"$id": "com.amazon.rum.csp_violation_event",
3+
"$schema": "https://json-schema.org/draft/2020-12/schema",
4+
"title": "CspViolationEvent",
5+
"type": "object",
6+
"properties": {
7+
"version": {
8+
"const": "1.0.0",
9+
"type": "string",
10+
"description": "Schema version."
11+
},
12+
"blockedURI": {
13+
"type": "string",
14+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/blockedURI"
15+
},
16+
"columnNumber": {
17+
"type": "number",
18+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/columnNumber"
19+
},
20+
"disposition": {
21+
"type": "string",
22+
"enum": ["enforce", "report"],
23+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/disposition"
24+
},
25+
"documentURI": {
26+
"type": "string",
27+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/documentURI"
28+
},
29+
"effectiveDirective": {
30+
"type": "string",
31+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/effectiveDirective"
32+
},
33+
"lineNumber": {
34+
"type": "number",
35+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/lineNumber"
36+
},
37+
"originalPolicy": {
38+
"type": "string",
39+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/originalPolicy"
40+
},
41+
"referrer": {
42+
"type": ["string", "null"],
43+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/referrer"
44+
},
45+
"sample": {
46+
"type": "string",
47+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/sample"
48+
},
49+
"sourceFile": {
50+
"type": ["string", "null"],
51+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/sourceFile"
52+
},
53+
"statusCode": {
54+
"type": "number",
55+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/statusCode"
56+
},
57+
"violatedDirective": {
58+
"type": "string",
59+
"description": "https://developer.mozilla.org/docs/Web/API/SecurityPolicyViolationEvent/violatedDirective"
60+
}
61+
},
62+
"additionalProperties": false,
63+
"required": ["version"]
64+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { InternalPlugin } from '../InternalPlugin';
2+
import { CSP_VIOLATION_EVENT_TYPE } from '../utils/constant';
3+
4+
export const CSP_VIOLATION_EVENT_PLUGIN_ID = 'csp-violation';
5+
6+
export type CspViolationPluginConfig = {
7+
ignore: (error: SecurityPolicyViolationEvent) => boolean;
8+
};
9+
10+
export type PartialCspViolationPluginConfig = {
11+
ignore?: (error: SecurityPolicyViolationEvent) => boolean;
12+
};
13+
14+
const defaultConfig: CspViolationPluginConfig = {
15+
ignore: () => false
16+
};
17+
18+
export class CspViolationPlugin extends InternalPlugin {
19+
private config: CspViolationPluginConfig;
20+
21+
constructor(config?: PartialCspViolationPluginConfig) {
22+
super(CSP_VIOLATION_EVENT_PLUGIN_ID);
23+
this.config = { ...defaultConfig, ...config };
24+
}
25+
26+
enable(): void {
27+
if (this.enabled) {
28+
return;
29+
}
30+
this.addEventHandler();
31+
this.enabled = true;
32+
}
33+
34+
disable(): void {
35+
if (!this.enabled) {
36+
return;
37+
}
38+
this.removeEventHandler();
39+
this.enabled = false;
40+
}
41+
42+
record(cspViolationEvent: any): void {
43+
this.recordCspViolationEvent(cspViolationEvent);
44+
}
45+
46+
protected onload(): void {
47+
this.addEventHandler();
48+
}
49+
50+
private eventHandler = (
51+
cspViolationEvent: SecurityPolicyViolationEvent
52+
) => {
53+
if (!this.config.ignore(cspViolationEvent)) {
54+
this.recordCspViolationEvent(cspViolationEvent);
55+
}
56+
};
57+
58+
private recordCspViolationEvent(
59+
cspViolationEvent: SecurityPolicyViolationEvent
60+
) {
61+
this.context?.record(CSP_VIOLATION_EVENT_TYPE, {
62+
...cspViolationEvent,
63+
version: '1.0.0'
64+
});
65+
}
66+
67+
private addEventHandler(): void {
68+
window.addEventListener('securitypolicyviolation', this.eventHandler);
69+
}
70+
71+
private removeEventHandler(): void {
72+
window.removeEventListener(
73+
'securitypolicyviolation',
74+
this.eventHandler
75+
);
76+
}
77+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { context, getSession, record } from '../../../test-utils/test-utils';
2+
import { CSP_VIOLATION_EVENT_TYPE } from '../../utils/constant';
3+
import { CspViolationPlugin } from '../CspViolationPlugin';
4+
5+
declare global {
6+
namespace jest {
7+
interface Expect {
8+
toBePositive(): any;
9+
}
10+
}
11+
}
12+
13+
const eventDetails: Partial<SecurityPolicyViolationEvent> = {
14+
violatedDirective: 'test:violatedDirective',
15+
documentURI: 'http://documentURI',
16+
blockedURI: 'https://blockedURI',
17+
originalPolicy: 'test:originalPolicy',
18+
referrer: 'test:referrer',
19+
statusCode: 200,
20+
effectiveDirective: 'test:effectiveDirective'
21+
};
22+
23+
function dispatchCspViolationEvent() {
24+
const event = new Event(
25+
'securitypolicyviolation'
26+
) as SecurityPolicyViolationEvent;
27+
// its important to apply our expected event details to the event before dispatching it.
28+
Object.assign(event, eventDetails);
29+
30+
dispatchEvent(event);
31+
}
32+
33+
expect.extend({
34+
toBePositive(recieved) {
35+
const pass = recieved > 0;
36+
if (pass) {
37+
return {
38+
message: () =>
39+
`expected ${recieved} not to be a positive integer`,
40+
pass: true
41+
};
42+
} else {
43+
return {
44+
message: () => `expected ${recieved} to be a positive integer`,
45+
pass: false
46+
};
47+
}
48+
}
49+
});
50+
51+
describe('CspViolationPlugin tests', () => {
52+
beforeEach(() => {
53+
record.mockClear();
54+
getSession.mockClear();
55+
});
56+
57+
test('when an CspViolationEvent is triggered then the plugin records cspViolationEvent', async () => {
58+
// Init
59+
const plugin: CspViolationPlugin = new CspViolationPlugin();
60+
61+
// Run
62+
plugin.load(context);
63+
dispatchCspViolationEvent();
64+
plugin.disable();
65+
66+
// Assert
67+
expect(record).toHaveBeenCalledTimes(1);
68+
expect(record.mock.calls[0][0]).toEqual(CSP_VIOLATION_EVENT_TYPE);
69+
expect(record.mock.calls[0][1]).toMatchObject(
70+
expect.objectContaining({
71+
version: '1.0.0',
72+
blockedURI: 'https://blockedURI',
73+
documentURI: 'http://documentURI',
74+
effectiveDirective: 'test:effectiveDirective',
75+
originalPolicy: 'test:originalPolicy',
76+
referrer: 'test:referrer',
77+
statusCode: 200
78+
})
79+
);
80+
});
81+
82+
test('when plugin disabled then plugin does not record events', async () => {
83+
// Init
84+
const plugin: CspViolationPlugin = new CspViolationPlugin();
85+
86+
// Run
87+
plugin.load(context);
88+
plugin.disable();
89+
90+
dispatchCspViolationEvent();
91+
plugin.disable();
92+
93+
// Assert
94+
expect(record).toHaveBeenCalledTimes(0);
95+
});
96+
97+
test('when enabled then plugin records events', async () => {
98+
// Init
99+
const plugin: CspViolationPlugin = new CspViolationPlugin();
100+
101+
// Run
102+
plugin.load(context);
103+
plugin.disable();
104+
plugin.enable();
105+
dispatchCspViolationEvent();
106+
plugin.disable();
107+
108+
// Assert
109+
expect(record).toHaveBeenCalledTimes(1);
110+
});
111+
112+
test('when record is used then errors are not passed to the ignore function', async () => {
113+
// Init
114+
const mockIgnore = jest.fn();
115+
const plugin: CspViolationPlugin = new CspViolationPlugin({
116+
ignore: mockIgnore
117+
});
118+
119+
// Run
120+
plugin.load(context);
121+
const event = new Event(
122+
'securitypolicyviolation'
123+
) as SecurityPolicyViolationEvent;
124+
plugin.record(event);
125+
plugin.disable();
126+
127+
// Assert
128+
expect(record).toHaveBeenCalled();
129+
expect(mockIgnore).not.toHaveBeenCalled();
130+
});
131+
132+
test('by default SecurityPolicyViolationEvents are not ignored', async () => {
133+
// Init
134+
const plugin: CspViolationPlugin = new CspViolationPlugin();
135+
136+
// Run
137+
plugin.load(context);
138+
dispatchCspViolationEvent();
139+
plugin.disable();
140+
141+
// Assert
142+
expect(record).toHaveBeenCalled();
143+
});
144+
145+
test('when a specific documentUri is ignored then SecurityPolicyViolationEvents are not recorded', async () => {
146+
// Init
147+
const plugin: CspViolationPlugin = new CspViolationPlugin({
148+
ignore: (e) => {
149+
const ignoredDocuments = ['http://documentURI'];
150+
return ignoredDocuments.includes(
151+
(e as SecurityPolicyViolationEvent).documentURI
152+
);
153+
}
154+
});
155+
156+
// Run
157+
plugin.load(context);
158+
dispatchCspViolationEvent();
159+
plugin.disable();
160+
161+
// Assert
162+
expect(record).not.toHaveBeenCalled();
163+
});
164+
});

src/plugins/utils/constant.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ export const SESSION_START_EVENT_TYPE = `${RUM_AMZ_PREFIX}.session_start_event`;
2929

3030
// Time to interactive event
3131
export const TIME_TO_INTERACTIVE_EVENT_TYPE = `${RUM_AMZ_PREFIX}.time_to_interactive_event`;
32+
33+
// CSP violation event schemas
34+
export const CSP_VIOLATION_EVENT_TYPE = `${RUM_AMZ_PREFIX}.csp_violation_event`;

0 commit comments

Comments
 (0)