-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add csp-violation-event-plugin
- Loading branch information
Anthony Rizzo
committed
Oct 10, 2024
1 parent
f99498c
commit 4c46345
Showing
4 changed files
with
308 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
164
src/plugins/event-plugins/__tests__/CspViolationPlugin.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters