12 |
13 | Project
14 | Number of Reports
15 | Date Last Activity
16 |
17 |
18 | {{project.name}}
19 | {{project.stats.totalReports || 0}}
20 | {{project.stats.dateLastActivity | date:'short' }}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/logger.js:
--------------------------------------------------------------------------------
1 | var logger = require('winston');
2 |
3 | logger.setLevels({debug:0,info: 1,silly:2,warn: 3,error:4,});
4 | logger.addColors({debug: 'green',info: 'cyan',silly: 'magenta',warn: 'yellow',error: 'red'});
5 |
6 | logger.remove(logger.transports.Console);
7 | logger.add(logger.transports.Console, { level: 'debug', colorize:true, timestamp : true });
8 | logger.add(logger.transports.File, { level: "debug", timestamp: true, filename: "log.log", json: true})
9 |
10 | logger.handleExceptions(new logger.transports.File({ filename: './exceptions.log' }))
11 | logger.handleExceptions(new logger.transports.Console({ level: 'debug', colorize:true, timestamp : true }))
12 |
13 |
14 | module.exports = logger
--------------------------------------------------------------------------------
/public/views/partials/newProject.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Project Name
11 |
12 |
13 |
14 |
Done!
15 |
16 |
17 |
18 |
What do you want to call this project?
19 |
20 |
After this we start analyzing your policy!
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var debug = require('debug')('caspr');
3 | var app = require('../app');
4 | var https = require('https');
5 | var http = require('http');
6 | var fs = require('fs');
7 | var logger = require('../logger');
8 | var opts = require('../options');
9 |
10 | if (opts.ssl) {
11 | var options = {
12 | key: fs.readFileSync('./bin/certs/key.pem'),
13 | cert: fs.readFileSync('./bin/certs/cert.pem'),
14 | };
15 |
16 | var server = https.createServer(options, app).listen(443, function() {
17 | logger.info("SSL server listening on port " + 443);
18 | });
19 | }
20 |
21 | var server = app.listen(opts.port, function() {
22 | logger.info('HTTP server listening on port ' + opts.port);
23 | });
24 |
--------------------------------------------------------------------------------
/options.js:
--------------------------------------------------------------------------------
1 | var logger = require('./logger');
2 | var opts = require("nomnom")
3 | .option('port', {
4 | abbr: 'p',
5 | default: process.env.PORT || 3000,
6 | help: 'Port to run http caspr'
7 | })
8 | .option('ssl', {
9 | flag: true,
10 | default: false,
11 | help: 'Run ssl on port 443'
12 | })
13 | .option('sslKeyFile', {
14 | default: "./bin/certs/key.pem",
15 | help: "SSL key file for ssl"
16 | })
17 | .option('sslCertFile', {
18 | default: "./bin/certs/cert.pem",
19 | help: "SSL certificate file for ssl"
20 | })
21 | .option('cappedCollectionSize', {
22 | default: 0, //gigabyte
23 | help: 'Size of report collection in bytes'
24 | })
25 | .parse();
26 |
27 | logger.info(opts);
28 | module.exports = opts;
29 |
30 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "caspr",
3 | "version": "0.0.0",
4 | "homepage": "https://github.com/c0nrad/caspr",
5 | "description": "Content Security Policy Aggregator",
6 | "main": "app.js",
7 | "authors": [
8 | "Stuart C. Larsen"
9 | ],
10 | "license": "MIT",
11 | "private": true,
12 | "dependencies": {
13 | "angular-resource": "~1.2.22",
14 | "angular": "~1.2.20",
15 | "angular-route": "~1.2.22",
16 | "angularjs": "*",
17 | "bootstrap": "~3.2.0",
18 | "moment": "~2.8.1",
19 | "underscore": "~1.6.0",
20 | "animate.css": "~3.2.0",
21 | "d3": "~3.4",
22 | "angular-ui-router": "~0.2.10",
23 | "angular-nvd3": "0.0.9"
24 | },
25 | "resolutions": {
26 | "d3": "~3.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "caspr",
3 | "version": "0.0.1",
4 | "private": true,
5 | "engines": {
6 | "node": "0.11.x"
7 | },
8 | "scripts": {
9 | "start": "node ./bin/www",
10 | "postinstall": "./node_modules/bower/bin/bower install"
11 | },
12 | "dependencies": {
13 | "async": "*",
14 | "body-parser": "~1.0.0",
15 | "bower": "*",
16 | "connect": "*",
17 | "cookie-parser": "~1.0.1",
18 | "csp-parse": "0.0.0",
19 | "debug": "~0.7.4",
20 | "ejs": "*",
21 | "express": "~4.2.0",
22 | "jade": "~1.3.0",
23 | "moment": "*",
24 | "mongoose": "*",
25 | "morgan": "~1.0.0",
26 | "nomnom": "*",
27 | "static-favicon": "~1.0.0",
28 | "underscore": "*",
29 | "winston": "*"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/public/views/partials/group.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Classification
6 | {{group.report.classification}}
7 |
8 | Directive
9 | {{group.report.directive}}
10 |
11 |
12 |
13 |
14 |
15 |
Reports in Group:
16 |
{{group.count}}
17 |
18 |
19 |
20 |
21 |
First Seen (in range):
22 |
{{group.firstSeen | fromNow }}
23 |
24 |
25 |
26 |
27 |
Last Seen:
28 |
{{group.lastSeen | fromNow }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {{group.report['csp-report'] | stringify }}
41 |
--------------------------------------------------------------------------------
/public/views/partials/inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Directive: {{directive}}
5 |
6 |
7 | Location
8 | Count
9 | Line Number
10 | Column Number
11 | Source File
12 | Last Seen
13 | Details
14 |
15 |
16 |
17 | {{group['csp-report']['document-uri']}}
18 | {{group.count}}
19 | {{group['csp-report']['line-number']}}
20 | {{group['csp-report']['column-number']}}
21 | {{group['csp-report']['source-file']}}
22 | {{group.latest | fromNow}}
23 | Details
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/public/views/partials/builder.html:
--------------------------------------------------------------------------------
1 |
2 | Last Seen Policy:
3 | {{project.policy}}
4 |
5 | Proposed Policy
6 | {{policy}}
7 |
8 |
9 |
10 |
Directive: {{directive}}
11 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/public/views/partials/contact.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Code / Deployment
4 |
github.com/caspr
5 |
6 |
7 |
Need help setting up Content Security Policy?
8 |
9 | I'm happy to help anyone setup CSP on their website. If it takes more than an hour and is a closed source project, rates may apply.
10 |
11 |
12 |
Github
13 |
14 |
Email
15 |
16 |
Hire Me?
17 |
18 |
I'm graduting college December 2014, and looking for a fun job. Somewhere between app dev and security.
19 |
20 |
Email
21 |
Resume
22 |
23 |
Special Thanks
24 |
25 |
Special thanks to medina and jfalken for help and support!
26 |
27 |
28 |
--------------------------------------------------------------------------------
/models/project.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var mongoose = require('mongoose');
4 | var Schema = mongoose.Schema;
5 |
6 | var async = require('async');
7 |
8 | var ProjectSchema = new Schema({
9 | name: {type: String, default: 'My Project'},
10 | ts: {type: Date, default: Date.now},
11 | policy: {type: String, default: ''},
12 | hash: {type: String},
13 | endpoint: {type: String},
14 | hidden: {type: Boolean, default: true}
15 | });
16 |
17 | ProjectSchema.index({ hash: 1 });
18 | ProjectSchema.index({ endpoint: 1 });
19 | ProjectSchema.index({ hidden: 1 });
20 |
21 | ProjectSchema.pre('save', function(next) {
22 | if (!this.isNew) {
23 | return next();
24 | }
25 |
26 | var _this = this;
27 |
28 | async.auto({
29 | setHash: function(next) {
30 | require('crypto').randomBytes(32, function(ex, buf) {
31 | var hash = buf.toString('hex');
32 | _this.hash = hash;
33 | next();
34 | });
35 | },
36 |
37 | setEndpoint: function(next) {
38 | require('crypto').randomBytes(32, function(ex, buf) {
39 | var hash = buf.toString('hex');
40 | _this.endpoint = hash;
41 | next();
42 | });
43 | }
44 | }, function(err){
45 | next(err);
46 | });
47 | });
48 |
49 | var Project = mongoose.model('Project', ProjectSchema);
50 |
51 | module.exports = Project;
52 |
--------------------------------------------------------------------------------
/public/views/partials/project.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 | Reports: {{reportCount}} / {{stats.totalReports}}
16 | Groups: {{groupCount}} / {{stats.uniqueReportsTotal}}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/models/report.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 | var Schema = mongoose.Schema;
3 | var opts = require('nomnom');
4 | var logger = require('../logger');
5 |
6 | var opts = require('../options');
7 |
8 | var reportOptions = {};
9 | if (opts.cappedCollectionSize !== 0) {
10 | logger.info('Capping report collection at', opts.cappedCollectionSize);
11 | reportOptions.capped = opts.cappedCollectionSize;
12 | }
13 |
14 | var ReportSchema = new Schema({
15 | project: {type: Schema.Types.ObjectId, ref: 'Project'},
16 |
17 | ts: {type: Date, default: Date.now},
18 |
19 | // Original Content
20 | ip: String,
21 | headers: String,
22 | original: String,
23 |
24 | //Sanitized reports
25 | raw: String,
26 | 'csp-report': {
27 | 'document-uri': String,
28 | 'referrer': String,
29 | 'blocked-uri': String,
30 | 'violated-directive': String,
31 | 'original-policy': String,
32 | 'source-file': String,
33 | 'line-number': Number,
34 | 'column-number': Number,
35 | 'status-code': Number,
36 | 'effective-directive': String
37 | },
38 |
39 | // Guess work
40 | classification: String,
41 | directive: String,
42 | name: String,
43 |
44 | }, reportOptions);
45 |
46 | // XXX: lrn2index
47 | ReportSchema.index({ project: 1 });
48 | ReportSchema.index({ 'raw': 1 });
49 | ReportSchema.index({ ts: 1 });
50 | ReportSchema.index({ directive: 1 });
51 |
52 | var Report = mongoose.model('Report', ReportSchema);
53 |
54 | module.exports = Report;
55 |
--------------------------------------------------------------------------------
/public/js/csp-parse.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // 'script-src' -> 'srcipt'
4 | function demote(directive) {
5 | return directive.split('-')[0];
6 | }
7 |
8 | // 'script' -> 'script-src'
9 | function promote(directive) {
10 | if (directive.split('-').length === 1) {
11 | return directive + '-src';
12 | }
13 | return directive;
14 | }
15 |
16 | function Policy(policy) {
17 | this.raw = policy;
18 | this.directives = [];
19 |
20 | var directives = this.raw.split(';');
21 | for (var i = 0; i < directives.length; ++i) {
22 | var directive = directives[i].trim();
23 | var tokens = directive.split(/\s+/);
24 |
25 | var name = tokens[0];
26 | if (!name) {
27 | continue;
28 | }
29 | var values = tokens.slice(1, tokens.length);
30 | this.directives[name] = values;
31 | }
32 | return this;
33 | }
34 |
35 | Policy.prototype.get = function(directive) {
36 | directive = promote(directive);
37 | return this.directives[directive];
38 | };
39 |
40 | Policy.prototype.add = function(directive, value) {
41 | directive = promote(directive);
42 | if (!this.directives[directive]) {
43 | this.directives[directive] = [value];
44 | } else {
45 | this.directives[directive].push(value);
46 | }
47 | return this.directives[directive];
48 | };
49 |
50 | Policy.prototype.toString = function() {
51 | var out = '';
52 | for (var directive in this.directives) {
53 | out += directive + ' ' + this.directives[directive].join(' ') + '; ';
54 | }
55 | return out.trim();
56 | };
57 |
--------------------------------------------------------------------------------
/public/views/partials/table.html:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/routes/reports.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var express = require('express');
4 | var router = express.Router();
5 | var async = require('async');
6 | var _ = require('underscore');
7 |
8 | var mongoose = require('mongoose');
9 | var Project = mongoose.model('Project');
10 | var Report = mongoose.model('Report');
11 |
12 | router.get('/projects/:hash/reports', function(req, res, next) {
13 |
14 | req.query = _.defaults(req.query, {
15 | endDate: new Date(),
16 | startDate: new Date(0),
17 | limit: 1000,
18 | });
19 |
20 | var startDate = new Date( Number(req.query.startDate));
21 | var endDate = new Date( Number(req.query.endDate));
22 | var limit = Number(req.query.limit);
23 |
24 | async.auto({
25 | project: function(next) {
26 | Project.findOne({hash: req.params.hash}, next);
27 | },
28 |
29 | reports: ['project', function(next, results) {
30 | var project = results.project;
31 | if (!project) {
32 | return next('not a valid project');
33 | }
34 |
35 | Report.find({project: project._id, ts: {$gt: startDate, $lt: endDate}}).limit(limit).exec(next);
36 | }]
37 | }, function(err, results) {
38 | if (err) {
39 | return next(err);
40 | }
41 |
42 | res.send(results);
43 | });
44 | });
45 |
46 | router.delete('/projects/:hash/reports', function(req, res, next) {
47 | async.auto({
48 | project: function(next) {
49 | Project.findOne({hash: req.params.hash}, next);
50 | },
51 |
52 | clear: ['project', function(next, results) {
53 | var project = results.project;
54 |
55 | if (!project) {
56 | return next('not a valid project');
57 | }
58 |
59 | Report.find({project: project._id}).remove(next);
60 | }]
61 | }, function(err) {
62 | if (err) {
63 | return next(err);
64 | }
65 |
66 | res.send('okay');
67 | });
68 | });
69 |
70 | module.exports = router;
71 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | var request = require('request');
2 | var reports = require('./twitter.json');
3 | var async = require('async');
4 |
5 |
6 | var HOST = "http://localhost:3000"
7 | var PROJECT = { __v: 0, _id: '53fc9c3b848738830d0f5e71', policy: '', endpoint: '6b2b17f15128b6f3a293dfabfd7a92bd6904f0f8d07564db739c1eb1787df2a2', hash: 'e56782a2a2c80c89653884d76cb367df7a2823726b27b5d44144e9e0999fed84', name: 'test1408567936874' }
8 | //var PROJECT = undefined
9 |
10 | // Create a project
11 | async.auto({
12 |
13 | project: function(next) {
14 | if (PROJECT != undefined)
15 | return next(null, PROJECT);
16 |
17 | request.post(HOST+"/api/projects", {form: {name: "test" + new Date().getTime() }}, function (error, response, body) {
18 | next(error, JSON.parse(body));
19 | })
20 | },
21 |
22 | reports: function(next) {
23 | var reports = require('./twitter.json');
24 | if (reports.reports.length == 0)
25 | return next('No reports found!');
26 | next(null, reports.reports);
27 | },
28 |
29 | postReports: ["project", "reports", function(next, results) {
30 | var project = results.project;
31 | var reports = results.reports;
32 |
33 | console.log("ABC", project._id);
34 |
35 | var URL = HOST + "/endpoint/" + project.endpoint;
36 | for (var i = 0; i < reports.length; ++i) {
37 | request.post(URL, {headers: {'Content-Type':'application/csp-report'}, body: JSON.stringify(reports[i])}, function(err, response, body) {
38 | if (err) return next(err);
39 | })
40 | }
41 | next();
42 | }]
43 |
44 |
45 | }, function(err, results) {
46 | if (err)
47 | console.log(err, results);
48 | var project = results.project;
49 | console.log(project);
50 | console.log(HOST + "/#/p/" + project._id);
51 | });
52 |
--------------------------------------------------------------------------------
/public/js/directives.js:
--------------------------------------------------------------------------------
1 | var app = angular.module('app');
2 |
3 | app.directive('graph', function() {
4 | return {
5 | restrict: 'EA',
6 | scope: {
7 | series: "=",
8 | count: "@"
9 | },
10 | templateUrl: 'views/partials/graphTemplate.html',
11 |
12 | link: function(scope, element, attrs) {
13 |
14 | scope.$watch('series', function(newVal, oldVal) {
15 | graph(newVal);
16 | })
17 |
18 | function graph() {
19 | document.querySelector('#tschart').innerHTML = '';
20 | document.querySelector('#legend').innerHTML = '';
21 | document.querySelector('#y_axis').innerHTML = '';
22 |
23 | var series = scope.series;
24 | if (series == undefined)
25 | return
26 |
27 | var graph = new Rickshaw.Graph( {
28 | height: 540,
29 | element: document.querySelector('#tschart'),
30 | renderer: 'bar',
31 | series: series
32 | });
33 |
34 | var x_axis = new Rickshaw.Graph.Axis.Time({
35 | graph: graph,
36 | timeFixture: new Rickshaw.Fixtures.Time.Local()
37 | });
38 |
39 | var y_axis = new Rickshaw.Graph.Axis.Y( {
40 | graph: graph,
41 | orientation: 'left',
42 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
43 | element: document.getElementById('y_axis'),
44 | });
45 |
46 | var legend = new Rickshaw.Graph.Legend( {
47 | element: document.querySelector('#legend'),
48 | graph: graph
49 | });
50 |
51 | var highlighter = new Rickshaw.Graph.Behavior.Series.Highlight({
52 | graph: graph,
53 | legend: legend
54 | });
55 |
56 | var shelving = new Rickshaw.Graph.Behavior.Series.Toggle({
57 | graph: graph,
58 | legend: legend
59 | });
60 |
61 | graph.render();
62 | }
63 | }
64 | }
65 | });
--------------------------------------------------------------------------------
/public/views/partials/filter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
25 |
26 |
27 |
28 |
29 |
Reports Blocked:
30 |
{{filter.count}}
31 |
32 |
33 |
34 |
35 |
Unique Reports Blocked:
36 |
{{filteredGroups.length}}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Count: {{group.count}}View {{group['csp-report'] | stringify}}
47 |
48 |
49 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/public/views/partials/filters.html:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
27 |
28 | Name
29 | Field
30 | Expression
31 | Action
32 |
33 |
34 |
35 |
36 | {{filter.name}}
37 | {{filter.field}}
38 | {{filter.expression}}
39 | More
40 |
41 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/public/views/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Starter Template for Bootstrap
13 |
14 |
15 |
16 |
17 |
18 |
27 |
28 |
29 |
30 |
31 |
51 |
52 |
53 |
54 |
ERROR OF SOME SORT?
55 |
56 |
57 |
58 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/public/views/partials/overview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Please add the following header to your website:
9 |
Content-Security-Header: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self'; report-uri {{protocol}}//{{host}}/endpoint/{{project.endpoint}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Last Seen Policy
21 |
{{project.policy}}
22 |
23 |
Project Report-URI
24 |
{{protocol}}//{{host}}/endpoint/{{project.endpoint}}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Total Report:
35 |
{{stats.totalReports}}
36 |
37 |
38 |
39 |
40 |
Total Unique Reports:
41 |
{{stats.uniqueReportsTotal}}
42 |
43 |
44 |
45 |
46 |
Date Created:
47 |
{{project.ts | fromNow }}
48 |
49 |
50 |
51 |
52 |
Date Last Report:
53 |
{{ stats.dateLastActivity | fromNow }}
54 |
55 |
56 |
57 |
58 |
59 |
73 |
74 |
75 |
Settings
76 |
77 | Hide Project
78 |
79 |
80 |
81 | Delete all reports
82 |
83 | Delete Project
84 |
85 |
86 |
--------------------------------------------------------------------------------
/public/js/filters.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var app = angular.module('app');
4 |
5 | app.filter('directiveType', function() {
6 | return function(input, allowedDirectives) {
7 | if (!input) {
8 | return;
9 | }
10 |
11 | var out = []
12 | for (var i = 0; i < input.length; ++i) {
13 | var report = input[i];
14 | var directive = report.directive.split('-')[0];
15 |
16 | if (allowedDirectives[directive])
17 | out.push(report);
18 | }
19 |
20 | return out;
21 | };
22 | });
23 |
24 | app.filter('stringify', function() {
25 | return function(obj) {
26 | return JSON.stringify(obj, null, 2);
27 | };
28 | });
29 |
30 | app.filter('fromNow', function() {
31 | return function(date) {
32 | return moment(date).fromNow();
33 | };
34 | });
35 |
36 |
37 | app.filter('notInline', function() {
38 | return function(groups) {
39 | if (!groups) {
40 | return [];
41 | }
42 |
43 | groups = _.clone(groups);
44 |
45 |
46 | var out = [];
47 | for (var i = 0; i < groups.length; ++i) {
48 | var group = groups[i];
49 | if (!!group['csp-report']['blocked-uri']) {
50 | out.push(group);
51 | }
52 | }
53 | return out;
54 | };
55 | });
56 |
57 | app.filter('groupByInline', function() {
58 | return function(groups) {
59 | if (!groups) {
60 | return [];
61 | }
62 |
63 | groups = _.clone(groups);
64 |
65 | var out = [];
66 | for (var i = 0; i < groups.length; ++i) {
67 | var group = groups[i];
68 | if (!group['csp-report']['blocked-uri']) {
69 | out.push(group);
70 | }
71 | }
72 | return out;
73 | };
74 | });
75 |
76 | app.filter('groupByDirective', function() {
77 | return function(groups) {
78 | if (!groups) {
79 | return groups;
80 | }
81 |
82 | var out = _.clone(groups);
83 | return _.groupBy(out, function(g) { return g['directive']; });
84 | };
85 | });
86 |
87 | app.filter('groupByBlocked', function() {
88 | return function(groups) {
89 | if (!groups) {
90 | return groups;
91 | }
92 |
93 | var out = _.clone(groups);
94 |
95 |
96 | out = _.groupBy(out, function(g) { return g['csp-report']['blocked-uri'] });
97 |
98 | out = _.map(out, function(groupedGroups) {
99 | var group = groupedGroups[0];
100 | var sum = 0;
101 | for (var i = 0; i < groupedGroups.length; ++i) {
102 | sum += groupedGroups[i].count;
103 | }
104 |
105 | group.count = sum;
106 | return group;
107 | });
108 |
109 | return out;
110 | };
111 | });
112 |
113 | app.filter('reportAndCount', function() {
114 | return function(groups) {
115 | if (!groups || groups.length === 0) {
116 | return [];
117 | }
118 |
119 | return _.map(groups, function(group) {
120 | var out = group['csp-report'];
121 | out.count = group.count;
122 | return out;
123 | });
124 | };
125 | });
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Caspr (not under development)
2 | ## If you are looking for a CSP reporting tool, please check out https://csper.io .
3 |
4 |
5 | 
6 |
7 |
8 | [](https://heroku.com/deploy?template=https://github.com/c0nrad/caspr)
9 |
10 | Caspr is a Content-Security-Policy report endpoint, aggregator, and analyzer.
11 |
12 | It contains three parts:
13 | - A Content-Security-Report report endpoint for collecting reports
14 | - [A RESTful API for interacting / downloading reports](https://raw.githubusercontent.com/c0nrad/caspr/master/docs/api.md)
15 | - [A web app for analyzing reports](http://caspr.io/#/p/e73f40cd722426dd6df4c81fb56285335747fa29728bc72bd07cbcf5c2829d21)
16 |
17 |
18 | ## What is Content-Security-Policy?
19 |
20 | https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Introducing_Content_Security_Policy
21 |
22 | ## Deployment
23 |
24 | Either use Heroku, or to install manually, install NodeJS/npm/MongoDB(>2.6).
25 |
26 | ```bash
27 | git clone https://github.com/c0nrad/caspr.git
28 | cd caspr
29 | npm install
30 | forever bin/www
31 | ```
32 |
33 | ## Options
34 |
35 | ```
36 | $> node bin/www --help
37 |
38 | Usage: node www [options]
39 |
40 | Options:
41 | -p, --port Port to run http caspr [3000]
42 | --ssl Run ssl on port 443 [false]
43 | --sslKeyFile SSL key file for ssl [./bin/certs/key.pem]
44 | --sslCertFile SSL certificate file for ssl [./bin/certs/cert.pem]
45 | --cappedCollectionSize Size of report collection in bytes [0]
46 | ```
47 |
48 | ### SSL
49 |
50 | To use caspr with SSL, set sslKeyFIle and sslCertFile to the location of your cert and private key file on disk with `--sslKeyFile` and `--sslCertFIle`.
51 |
52 | ```bash
53 | forever bin/www --ssl --sslKeyFile /var/certs/key.pem --sslCertFile /var/certs/cert.pem
54 | ```
55 |
56 | ### Capped Collections
57 |
58 | MongoDB supports capped collections, meaning you can specifiy a maximum size for the reports collection in your DB.
59 |
60 | For my own deployments I usually set it around 1GB, but on Heroku the maximum size of the free version is .5GB.
61 |
62 | To use capped collections, either set it manually or pass the size in bytes you'd like the reports collection to be.
63 |
64 | ```bash
65 | forever bin/www --capped 500000000 // .5GB
66 | ```
67 |
68 | ```
69 | use caspr
70 | db.runCommand({convertToCapped: 'reports', size: 500000000 })
71 | ```
72 | http://docs.mongodb.org/manual/reference/command/convertToCapped/
73 |
74 | ## How do I dump all reports?
75 |
76 | All reports are stored within MongoDB. So a script such as the following can be used to dump all reports into a json file
77 |
78 | dump.js
79 | ```
80 | cursor = db.getSiblingDB('caspr').reports.find();
81 | while ( cursor.hasNext() ) {
82 | printjson( cursor.next() );
83 | }
84 | ```
85 |
86 | ```bash
87 | mongo dump.js > dump.json
88 | ```
89 |
90 | ## Contact
91 |
92 | c0nrad@c0nrad.io
93 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var express = require('express');
4 | var path = require('path');
5 | var favicon = require('static-favicon');
6 | var logger = require('morgan');
7 | var cookieParser = require('cookie-parser');
8 | var bodyParser = require('body-parser');
9 | var winston = require('./logger');
10 | var mongoose = require('mongoose');
11 |
12 | var mongoUri = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost/caspr';
13 | mongoose.connect(mongoUri);
14 |
15 | // load models
16 | require('./models/index');
17 |
18 | var app = express();
19 |
20 | // view engine setup
21 | app.engine('html', require('ejs').renderFile);
22 | app.set('views', path.join(__dirname, 'public/views'));
23 | app.set('view engine', 'html');
24 |
25 | var CSPParser = function(req, res, next) {
26 | if (req.get('Content-Type') === 'application/csp-report') {
27 | var data='';
28 | req.setEncoding('utf8');
29 | req.on('data', function(chunk) {
30 | data += chunk;
31 | });
32 |
33 | req.on('end', function() {
34 | req.body.data = data;
35 | next();
36 | });
37 | } else {
38 | next();
39 | }
40 | };
41 |
42 | app.use(favicon());
43 | app.use(logger('dev'));
44 | app.use(bodyParser());
45 | app.use(CSPParser);
46 | app.use(cookieParser());
47 | app.use(express.static(path.join(__dirname, 'public')));
48 |
49 | var allowCrossDomain = function(req, res, next) {
50 | res.header('Content-Security-Policy-Report-Only', "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self'; report-uri http://localhost/endpoint/example");
51 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
52 | res.header('Pragma', 'no-cache');
53 | res.header('Expires', 0);
54 | next();
55 | };
56 |
57 | app.use(allowCrossDomain);
58 |
59 | var index = require('./routes/index');
60 | var api = require('./routes/api');
61 | var endpoint = require('./routes/endpoint');
62 |
63 | app.use('/', index);
64 | app.use('/api', api);
65 | app.use('/endpoint', endpoint);
66 |
67 | /// catch 404 and forward to error handler
68 | app.use(function(req, res, next) {
69 | var err = new Error('Not Found');
70 | err.status = 404;
71 | next(err);
72 | });
73 |
74 | /// error handlers
75 |
76 | // development error handler
77 | // will print stacktrace
78 | if (app.get('env') === 'development') {
79 | app.use(function(err, req, res, next) {
80 | winston.warn(err.message);
81 | res.status(err.status || 500);
82 | res.json({
83 | message: err.message,
84 | error: err
85 | });
86 | });
87 | }
88 |
89 | // production error handler
90 | // no stacktraces leaked to user
91 | app.use(function(err, req, res, next) {
92 | winston.warn(err.message);
93 | res.status(err.status || 500);
94 | res.render('error', {
95 | message: err.message,
96 | error: {}
97 | });
98 | });
99 |
100 |
101 | module.exports = app;
102 |
--------------------------------------------------------------------------------
/public/views/partials/query.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
17 |
18 |
53 |
54 |
62 |
63 |
71 |
72 |
73 | Filters On
74 |
75 |
76 |
--------------------------------------------------------------------------------
/routes/endpoint.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var express = require('express');
4 | var router = express.Router();
5 | var async = require('async');
6 | var mongoose = require('mongoose');
7 |
8 | var logger = require('../logger');
9 |
10 | var Project = mongoose.model('Project');
11 | var Report = mongoose.model('Report');
12 |
13 | var url = require('url');
14 |
15 | router.post('/:endpoint', function(req, res) {
16 | var endpoint = req.params.endpoint;
17 |
18 | async.auto({
19 | project: function(next) {
20 | Project.findOne({endpoint: endpoint}).exec(next);
21 | },
22 |
23 | report: ['project', function(next, results) {
24 |
25 | var project = results.project;
26 | if (!project) {
27 | return next('Project doesn\'t exist.');
28 | }
29 |
30 | if (!req.body.data) {
31 | return next('Not a valid report');
32 | }
33 |
34 | var report = JSON.parse(req.body.data)['csp-report'];
35 | report = sanitizeReport(report);
36 |
37 | var r = new Report({
38 | project: project._id,
39 |
40 | original: req.body.data,
41 | ip: req.ip,
42 | headers: JSON.stringify(req.headers),
43 |
44 | raw: JSON.stringify(report),
45 | 'csp-report': report,
46 |
47 | directive: getDirective(report),
48 | classification: getType(report),
49 | name: getName(report),
50 | });
51 |
52 | r.save(next);
53 | }],
54 |
55 | lastSeenPolicy: ['project', 'report', function(next, results) {
56 | var project = results.project;
57 | var report = results.report[0];
58 |
59 | if (!!report['csp-report']['original-policy']) {
60 | project.policy = report['csp-report']['original-policy'];
61 | }
62 |
63 | project.save(next);
64 | }]
65 |
66 | }, function(err) {
67 | if (err) {
68 | logger.error(err);
69 | return res.send(err, 400);
70 | }
71 |
72 | res.send('Okay');
73 | });
74 | });
75 |
76 | function getDirective(report) {
77 | var directive = report['violated-directive'];
78 | if (directive !== undefined && directive !== '') {
79 | directive = directive.split(' ')[0];
80 | }
81 | return directive;
82 | }
83 |
84 | function getType(report) {
85 | var directive = getDirective(report);
86 |
87 | if (report['blocked-uri'] === '' || report['blocked-uri'] === 'self') {
88 | return 'inline';
89 | } else {
90 | return 'unauthorized-host';
91 | }
92 | }
93 |
94 | function getName(report) {
95 | return getDirective(report) + ' - ' + getType(report) + ' - ' + report['document-uri'] + ' - ' + report['blocked-uri'];
96 | }
97 |
98 | function stripQuery(uri) {
99 | if (!uri) {
100 | return uri;
101 | }
102 |
103 | var urlObj = url.parse(uri);
104 | urlObj.query = '';
105 | urlObj.search = '';
106 | urlObj.hash = '';
107 | return url.format(urlObj);
108 | }
109 |
110 | function stripPath(uri) {
111 | if (!uri) {
112 | return uri;
113 | }
114 |
115 | var urlObj = url.parse(uri);
116 | urlObj.query = '';
117 | urlObj.search = '';
118 | urlObj.hash = '';
119 | urlObj.path = '';
120 | urlObj.pathname = '';
121 | return url.format(urlObj);
122 | }
123 |
124 | function sanitizeReport(report) {
125 | report['document-uri'] = stripQuery(report['document-uri']);
126 | report['blocked-uri'] = stripPath(report['blocked-uri']);
127 | return report;
128 | }
129 |
130 | module.exports = router;
131 |
--------------------------------------------------------------------------------
/public/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Caspr
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/public/views/partials/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Caspr the friendly CSP Report Aggregator.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
What is XSS?
19 |
Cross Site Scripting (XSS), is a class of attacks that allow malicious users to execute scripts on another user's behalf. Using XSS, attackers can perform actions on behalf of another user, hijack user information/sessions, or deface websites.
20 |
21 |
XSS attacks are currently one of the most popular methods for attacking web applications, and are number 3 on OWAPS Top 10 Most Critical Web Application Security Risks (https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project).
22 |
23 |
Until recently, the most common defense against XSS attacks was proper escaping of all untrusted data. Most modern web frameworks escape all data by default, making this a very simple solution. But it is still common for injected data to be displayed without proper escaping.
24 |
25 |
26 |
27 |
What is Content Security Policy?
28 |
29 |
Content Security Policy (CSP) is a new HTTP header for specifying allowed orgins of resources. Normally when a browser loads a web page, it trust everything that it recieves. Before pesky attackers this was just fine. But when attackers perform an XSS attack, the browser see's the injected javascript and assumes it was what the server intended and executes it.
30 |
31 |
Content Security Policy is a way to white-list sources of different types of resources. For example you could specify that the browser should only execute javascript if it comes from the webserver in the form on .js files, NOT if it is in any HTML. Pretty much most of the time javascript injected via XSS is embedded into the HTML, so this would effectively stop a very large portion of XSS attacks.
32 |
33 |
34 |
35 |
36 |
What is Caspr?
37 |
38 |
Caspr is a Content Security Policy report aggregator. It allows website owners to analyze where their Content Security Policy is being violated, and how their policy is holding up over time.
39 |
40 |
41 | Captures and Stores CSP reports
42 | Publishes an API for viewing CSP reports.
43 | Displays reports in a nice condensed form.
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
Analyzing CSP policies is hard.
57 |
But Caspr is here to help.
58 |
59 |
60 |
Get Started!
61 |
Example
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/routes/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var mongoose = require('mongoose');
4 | var Report = mongoose.model('Report');
5 |
6 | var _ = require('underscore');
7 |
8 | exports.allDirectives = ['default-src', 'script-src', 'style-src', 'img-src', 'font-src', 'connect-src', 'media-src', 'object-src']
9 |
10 | exports.buckets = function(bucketSize, startDate, endDate, data) {
11 | var hist = {};
12 | startDate = Math.round(startDate / 1000);
13 | endDate = Math.round(endDate / 1000);
14 | bucketSize = Math.round(bucketSize);
15 |
16 | // So, round startDate up, and endDate down. So if day/hour, then only 24 groups with priority on new reports
17 | startDate -= (startDate % bucketSize) + bucketSize;
18 | endDate -= endDate % bucketSize;
19 |
20 | for (var d = startDate; d <= endDate; d += bucketSize) {
21 | hist[d] = 0;
22 | }
23 |
24 | for (var i = 0 ; i < data.length; ++i) {
25 | var reportDate = Math.round(data[i] / 1000);
26 | reportDate -= (reportDate % bucketSize);
27 |
28 | // Since we offset startDate and endDate, it's possible we'll ignore
29 | if (reportDate < startDate || reportDate > endDate) {
30 | continue;
31 | }
32 |
33 | if (hist[reportDate] === undefined) {
34 | console.log(reportDate, hist, startDate, endDate, data);
35 | console.error('THIS IS BAD');
36 | }
37 | hist[reportDate] += 1;
38 | }
39 |
40 | var keys = _.keys(hist);
41 | var out = [];
42 | for (var j = 0; j < keys.length; ++j) {
43 | var key = keys[j];
44 | out.push({x: Number(key)*1000, y: hist[key] });
45 | }
46 |
47 |
48 | out = _.sortBy(out, function(a) {return a.x; });
49 |
50 | return out;
51 | };
52 |
53 | exports.aggregateGroups = function(startDate, endDate, directives, limit, projectId, filters, filterExclusion, next) {
54 | var queryMatch = [
55 | {
56 | $match: {
57 | project: projectId,
58 | ts: {$gt: startDate, $lt: endDate},
59 | directive: {$in: directives}
60 | }
61 | },
62 | ];
63 |
64 | var filterMatch = buildMatchFilters(filters, filterExclusion);
65 |
66 | var group = [
67 | {
68 | $group: {
69 | _id: '$raw',
70 | count: {$sum: 1},
71 | 'csp-report': {$last: '$csp-report'},
72 | reportId: {$last: '$_id'},
73 | data: { $push: '$ts' },
74 | latest: { $max: '$ts' },
75 | directive: {$last: '$directive' },
76 | classification: {$last: '$classification' },
77 | name: {$last: '$name' },
78 | }
79 | },
80 | { $sort : { count: -1 } },
81 | { $limit: limit }
82 | ];
83 |
84 | var aggregation = _.reduce([queryMatch, filterMatch, group], function(a, b) { return a.concat(b); }, []);
85 | Report.aggregate(aggregation).exec(next);
86 | };
87 |
88 | exports.filterGroups = function(filters, groups) {
89 | buildMatchFilters(filters);
90 | return groups;
91 | };
92 |
93 | var buildMatchFilters = exports.buildMatchFilters = function(filters, filterExclusion) {
94 |
95 | var out = [];
96 | for (var i = 0; i < filters.length; ++i) {
97 | var filter = filters[i];
98 | var expression = filter.expression;
99 | var field = 'csp-report.' + filter.field;
100 | if (expression[0] === '/' && expression[expression.length-1] === '/') {
101 | expression = expression.substring(1, filter.expression.length - 1);
102 | }
103 |
104 | var exp = {};
105 | if (filterExclusion) {
106 | exp[field] = { '$not': new RegExp(expression) };
107 | } else {
108 | exp[field] = new RegExp(expression);
109 | }
110 |
111 | var match = {
112 | '$match': exp
113 | };
114 |
115 | out.push(match);
116 | }
117 | return out;
118 | };
119 |
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 50px;
3 | }
4 |
5 | .title {
6 | font-family: 'Unkempt', cursive;
7 | color: #428bca;
8 | }
9 |
10 | .intro {
11 | font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;
12 | font-size: 20px;
13 | line-height: 1.5;
14 | -webkit-font-smoothing: antialiased;
15 | }
16 |
17 | .block {
18 | padding-top: 50px;
19 | padding-bottom: 50px;
20 | }
21 |
22 | .introImage {
23 | width: 200px;
24 | }
25 |
26 | .textBlock {
27 | padding: 10px;
28 | }
29 |
30 | .worriesBlock {
31 | padding: 15px;
32 | margin-top: 50px;
33 | margin-bottom: 50px;
34 |
35 | font-size: 32px;
36 | }
37 |
38 | .sellBlock {
39 | padding: 20px;
40 | }
41 | .sellBlockText {
42 | padding: 20px;
43 | margin-top: 50px;
44 | margin-bottom: 50px;
45 | font-size: 36px;
46 | }
47 |
48 | .exampleLink {
49 | text-align: right;
50 | font-size: 14px;
51 | }
52 |
53 | .chart {
54 | height:200px;
55 | width:200px;
56 | }
57 |
58 | #chart_container {
59 | position: relative;
60 | font-family: Arial, Helvetica, sans-serif;
61 | }
62 | #tschart {
63 | margin-left: 40px;
64 | }
65 | #y_axis {
66 | position: absolute;
67 | top: 0;
68 | bottom: 0;
69 | width: 40px;
70 | }
71 | #legend {
72 | margin: 20px 20px 0px 20px;
73 | }
74 |
75 | #graphControl {
76 | magin: 20px 20px 20px 0px;
77 | }
78 |
79 | .statNumber {
80 | font-size: 36px;
81 | text-align: center;
82 | }
83 |
84 | .policy {
85 | padding-bottom: 20px;
86 | }
87 |
88 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
89 | .ng-cloak, .x-ng-cloak,
90 | .ng-hide {
91 | display: none !important;
92 | }
93 |
94 | ng\:form {
95 | display: block;
96 | }
97 |
98 | /*
99 | * Base structure
100 | */
101 |
102 | /* Move down content because we have a fixed navbar that is 50px tall */
103 | body {
104 | padding-top: 50px;
105 | }
106 |
107 |
108 | /*
109 | * Global add-ons
110 | */
111 |
112 | .sub-header {
113 | padding-bottom: 10px;
114 | border-bottom: 1px solid #eee;
115 | }
116 |
117 | /*
118 | * Top navigation
119 | * Hide default border to remove 1px line.
120 | */
121 | .navbar-fixed-top {
122 | border: 0;
123 | }
124 |
125 | /*
126 | * Sidebar
127 | */
128 |
129 | /* Hide for mobile, show later */
130 | .sidebar {
131 | display: none;
132 | }
133 | @media (min-width: 768px) {
134 | .sidebar {
135 | position: fixed;
136 | top: 51px;
137 | bottom: 0;
138 | left: 0;
139 | z-index: 1000;
140 | display: block;
141 | padding: 20px;
142 | overflow-x: hidden;
143 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
144 | background-color: #f5f5f5;
145 | border-right: 1px solid #eee;
146 | }
147 | }
148 |
149 | /* Sidebar navigation */
150 | .nav-sidebar {
151 | margin-right: -21px; /* 20px padding + 1px border */
152 | margin-bottom: 20px;
153 | margin-left: -20px;
154 | }
155 | .nav-sidebar > li > a {
156 | padding-right: 20px;
157 | padding-left: 20px;
158 | }
159 | .nav-sidebar > .active > a,
160 | .nav-sidebar > .active > a:hover,
161 | .nav-sidebar > .active > a:focus {
162 | color: #fff;
163 | background-color: #428bca;
164 | }
165 |
166 |
167 | /*
168 | * Main content
169 | */
170 |
171 | .main {
172 | padding: 20px;
173 | }
174 | @media (min-width: 768px) {
175 | .main {
176 | padding-right: 40px;
177 | padding-left: 40px;
178 | }
179 | }
180 | .main .page-header {
181 | margin-top: 0;
182 | }
183 |
184 |
185 | /*
186 | * Placeholder dashboard ideas
187 | */
188 |
189 | .placeholders {
190 | margin-bottom: 30px;
191 | text-align: center;
192 | }
193 | .placeholders h4 {
194 | margin-bottom: 0;
195 | }
196 | .placeholder {
197 | margin-bottom: 20px;
198 | }
199 | .placeholder img {
200 | display: inline-block;
201 | border-radius: 50%;
202 | }
--------------------------------------------------------------------------------
/routes/filters.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var express = require('express');
4 | var router = express.Router();
5 |
6 | var mongoose = require('mongoose');
7 | var Filter = mongoose.model('Filter');
8 | var Project = mongoose.model('Project');
9 |
10 | var async = require('async');
11 | var _ = require('underscore');
12 | var util = require('./util');
13 |
14 | router.post('/projects/:hash/filters', function(req, res, next) {
15 | var filter = _.pick(req.body, 'active', 'field', 'expression', 'name');
16 |
17 | console.log('before', req.query, filter);
18 | filter = _.defaults(filter, {
19 | active: true,
20 | field: 'blocked-uri',
21 | expression: '/^httpz:/',
22 | name: 'Block the HTTPZ protocol'
23 | });
24 |
25 | console.log('after', filter);
26 |
27 | async.auto({
28 | project: function(next) {
29 | Project.findOne({hash: req.params.hash}, next);
30 | },
31 |
32 | filter: ['project', function(next, results) {
33 | var project = results.project;
34 |
35 | if (!project) {
36 | return next('not a valid project');
37 | }
38 |
39 | filter.project = project._id;
40 | var f = new Filter(filter);
41 | f.save(next);
42 | }]
43 | }, function(err, results) {
44 | if (err) {
45 | return next(err);
46 | }
47 |
48 | res.send(results.filter[0]);
49 | });
50 | });
51 |
52 | router.put('/projects/:hash/filters/:filter', function(req, res, next) {
53 | var params = _.pick(req.body, 'active', 'field', 'expression', 'name');
54 |
55 | async.auto({
56 | project: function(next) {
57 | Project.findOne({hash: req.params.hash}, next);
58 | },
59 |
60 | filter: ['project', function(next, results) {
61 | var project = results.project;
62 | if (!project) {
63 | return next('not a valid project');
64 | }
65 |
66 | Filter.findByIdAndUpdate(req.params.filter, {$set: params}, next);
67 | }]
68 | }, function(err, results) {
69 | if (err) {
70 | return next(err);
71 | }
72 |
73 | res.send(results.filter);
74 | });
75 | });
76 |
77 | router.delete('/projects/:hash/filters/:filter', function(req, res, next) {
78 |
79 | async.auto({
80 | project: function(next) {
81 | Project.findOne({hash: req.params.hash}, next);
82 | },
83 |
84 | filter: ['project', function(next, results) {
85 | var project = results.project;
86 | if (!project) {
87 | return next('not a valid project');
88 | }
89 |
90 | Filter.findById(req.params.filter).remove(next);
91 | }]
92 | }, function(err, results) {
93 | if (err) {
94 | return next(err);
95 | }
96 |
97 | res.send('okay');
98 | });
99 | });
100 |
101 |
102 | router.get('/projects/:hash/filters', function(req, res, next) {
103 |
104 | async.auto({
105 | project: function(next) {
106 | Project.findOne({hash: req.params.hash}, next);
107 | },
108 |
109 | filters: ['project', function(next, results) {
110 | var project = results.project;
111 | if (!project) {
112 | return next('not a valid project');
113 | }
114 |
115 | Filter.find({project: project._id}).exec(next);
116 | }]
117 | }, function(err, results) {
118 | if (err) {
119 | return next(err);
120 | }
121 |
122 | res.send(results.filters);
123 | });
124 | });
125 |
126 | router.get('/projects/:hash/filters/:filter', function(req, res, next) {
127 | async.auto({
128 | project: function(next) {
129 | Project.findOne({hash: req.params.hash}, next);
130 | },
131 |
132 | filter: function(next) {
133 | Filter.findById(req.params.filter, next);
134 | },
135 |
136 | filteredGroups: ['filter', 'project', function(next, results) {
137 | var filter = results.filter;
138 | var project = results.project;
139 |
140 | if (!filter) {
141 | return next('not a valid filter');
142 | }
143 |
144 | if (!project) {
145 | return next('not a valid project');
146 | }
147 |
148 | return util.aggregateGroups(new Date(0), new Date(), util.allDirectives, 1000, project._id, [filter], false, next);
149 | }]
150 |
151 | }, function(err, results) {
152 | if (err) {
153 | return next(err);
154 | }
155 | res.send(results);
156 | });
157 | });
158 |
159 | module.exports = router;
160 |
--------------------------------------------------------------------------------
/routes/groups.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var express = require('express');
4 | var router = express.Router();
5 |
6 | var mongoose = require('mongoose');
7 | var Project = mongoose.model('Project');
8 | var Report = mongoose.model('Report');
9 | var Filter = mongoose.model('Filter');
10 |
11 | var async = require('async');
12 | var _ = require('underscore');
13 | var moment = require('moment');
14 |
15 | var util = require('./util');
16 |
17 | router.get('/projects/:hash/groups', function(req, res, next) {
18 |
19 | req.query = _.defaults(req.query, {
20 | endDate: new Date(),
21 | startDate: moment().subtract('day', 1).toDate(),
22 | directives: ['default-src', 'script-src', 'style-src', 'img-src', 'font-src', 'connect-src', 'media-src', 'object-src'],
23 | limit: 50,
24 | bucket: 60 * 60,
25 | filters: false,
26 | filterExclusion: true,
27 | seriesCount: 0
28 | });
29 |
30 | var startDate = new Date( Number(req.query.startDate));
31 | var endDate = new Date( Number(req.query.endDate));
32 | var directives = req.query.directives;
33 | var limit = Number(req.query.limit);
34 | var bucket = Number(req.query.bucket);
35 | var doFilter = JSON.parse(req.query.filters);
36 | var filterExclusion = JSON.parse(req.query.filterExclusion);
37 | var seriesCount = Number(req.query.seriesCount);
38 |
39 | if (!_.isArray(directives)) {
40 | directives = [directives];
41 | }
42 |
43 | async.auto({
44 | project: function(next) {
45 | Project.findOne({hash: req.params.hash}, next);
46 | },
47 |
48 | filters: ['project', function(next, results) {
49 | var project = results.project;
50 | if (!project) {
51 | return next('project does not exist');
52 | }
53 | Filter.find({project: project._id}, next);
54 | }],
55 |
56 | groupBuckets: ['project', 'filters', function(next, results) {
57 | var project = results.project;
58 | var filters = doFilter ? results.filters : [];
59 |
60 | return util.aggregateGroups(startDate, endDate, directives, limit, project._id, filters, filterExclusion, next);
61 | }],
62 |
63 | filteredBuckets: ['groupBuckets', 'filters', function(next, results) {
64 | var groups = results.groupBuckets;
65 | var filters = results.filters;
66 |
67 | var filteredGroups = util.filterGroups(filters, groups);
68 | return next(null, filteredGroups);
69 | }],
70 |
71 | groups: ['groupBuckets', 'filteredBuckets', function(next, results) {
72 | var groups = results.filteredBuckets;
73 | var count = groups.length;
74 |
75 | if (seriesCount !== -1) {
76 | count = Math.min(seriesCount, count);
77 | }
78 |
79 | for (var i = 0; i < count; ++i) {
80 | groups[i].data = util.buckets(bucket, startDate, endDate, groups[i].data);
81 | }
82 |
83 | return next(null, groups);
84 | }]
85 |
86 | }, function(err, results) {
87 | if (err) {
88 | return next(err);
89 | }
90 |
91 | res.json(results.groups);
92 | });
93 | });
94 |
95 | router.get('/projects/:hash/groups/:report', function(req, res, next) {
96 | async.auto({
97 | project: function(next) {
98 | Project.findOne({hash: req.params.hash}, next);
99 | },
100 |
101 | report: ['project', function(next, results) {
102 | var project = results.project;
103 | if (!project) {
104 | return next('not a valid project');
105 | }
106 | Report.findOne({_id: req.params.report, project: project._id}, next);
107 | }],
108 |
109 | reports: ['report', function(next, results) {
110 | var project = results.project;
111 | var report = results.report;
112 | Report.find({project: project._id, 'raw': report.raw}, next);
113 | }],
114 |
115 | group: ['reports', function(next, results) {
116 | var reports = results.reports;
117 | var report = results.report;
118 |
119 | var dates = _.pluck(reports, 'ts');
120 |
121 | var group = {
122 | report: reports[0],
123 | data: util.buckets(req.query.bucket, req.query.startDate, req.query.endDate, dates),
124 | count: reports.length,
125 | name: report.name,
126 | firstSeen: dates[0],
127 | lastSeen: dates[dates.length-1]
128 | };
129 |
130 | next(null, group);
131 | }]
132 | }, function(err, results) {
133 | if (err) {
134 | return next(err);
135 | }
136 |
137 | res.send(results.group);
138 | });
139 | });
140 |
141 | module.exports = router;
142 |
--------------------------------------------------------------------------------
/routes/projects.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var express = require('express');
4 | var router = express.Router();
5 |
6 | var mongoose = require('mongoose');
7 | var Project = mongoose.model('Project');
8 | var Report = mongoose.model('Report');
9 | var Filter = mongoose.model('Filter');
10 |
11 | var async = require('async');
12 | var _ = require('underscore');
13 |
14 | router.get('/projects', function(req, res, next) {
15 | Project.find({hidden: false}).exec(function(err, projects) {
16 | if (err) {
17 | return next(err);
18 | }
19 |
20 | console.log(projects);
21 |
22 | res.json(projects);
23 | });
24 | });
25 |
26 | router.post('/projects', function(req, res, next) {
27 | req.body = _.pick(req.body, 'name', 'hidden');
28 | var p = new Project(req.body);
29 |
30 | p.save(function(err, project) {
31 | if (err) {
32 | return next(err);
33 | }
34 |
35 | res.json(project);
36 | });
37 | });
38 |
39 | router.put('/projects/:hash', function(req, res, next) {
40 | var params = _.pick(req.body, 'name', 'hidden');
41 | Project.findOne({hash: req.params.hash}).exec(function(err, project) {
42 | if (!project) {
43 | return next('project does not exist');
44 | }
45 |
46 | if (err) {
47 | return next(err);
48 | }
49 | _.extend(project, params);
50 | project.save(function(err, results) {
51 | if (err) {
52 | res.send(results);
53 | }
54 | });
55 | });
56 | });
57 |
58 | router.get('/projects/:hash', function(req, res, next) {
59 | Project.findOne({hash: req.params.hash}).exec(function(err, project) {
60 | if (project === null) {
61 | return next('project doesn\'t exist');
62 | }
63 |
64 | if (err) {
65 | return next(err);
66 | }
67 |
68 | res.json(project);
69 | });
70 | });
71 |
72 | router.delete('/projects/:hash', function(req, res, next) {
73 | async.auto({
74 | project: function(next) {
75 | Project.findOne({hash: req.params.hash}, function(err, project) {
76 | if (err) {
77 | return next(err);
78 | }
79 |
80 | if (!project) {
81 | return next('not a valid project');
82 | }
83 |
84 | return next(err, project);
85 | });
86 | },
87 |
88 | deleteReports: ['project', function(next, results){
89 | var project = results.project;
90 |
91 | Report.find({project: project._id}).remove(next);
92 | }],
93 |
94 | deleteFilters: ['project', function(next, results) {
95 | var project = results.project;
96 |
97 | Filter.find({project: project._id}).remove(next);
98 | }],
99 |
100 | deleteProject: ['project', function(next, results) {
101 | var project = results.project;
102 |
103 | Project.findById(project._id).remove(next);
104 | }]
105 | }, function(err) {
106 | if (err) {
107 | return next(err);
108 | }
109 |
110 | res.send('okay');
111 | });
112 | });
113 |
114 | router.get('/projects/:hash/stats', function(req, res, next) {
115 | async.auto({
116 | project: function(next) {
117 | Project.findOne({hash: req.params.hash}, function(err, project) {
118 | if (err) {
119 | return next(err);
120 | }
121 |
122 | if (!project) {
123 | return next('not a valid project');
124 | }
125 |
126 | return next(err, project);
127 | });
128 | },
129 |
130 | totalReports: ['project', function(next, results) {
131 | var project = results.project;
132 | Report.find({project: project._id}).count(next);
133 | }],
134 |
135 | uniqueReportsTotal: ['project', function(next, results) {
136 | var project = results.project;
137 |
138 | Report.aggregate([
139 | {
140 | $match: {
141 | project: project._id
142 | }
143 | },
144 | {
145 | $group: {
146 | _id: '$raw'
147 | }
148 | }
149 | ]).exec(function(err, groups) {
150 | if (!groups) {
151 | return next(err, 0);
152 | }
153 | next(err, groups.length);
154 | });
155 | }],
156 |
157 | dateLastActivity: ['project', function(next, results) {
158 | var project = results.project;
159 | Report.find({project: project._id}, 'ts').sort({ts: -1}).limit(1).exec(function(err, results) {
160 | if (results.length === 0) {
161 | return next(err);
162 | }
163 | next(err, results[0].ts);
164 | });
165 | }]
166 | }, function(err, results) {
167 | if (err) {
168 | return next(err);
169 | }
170 |
171 | res.send(results);
172 | });
173 | });
174 |
175 | module.exports = router;
176 |
--------------------------------------------------------------------------------
/public/js/services.js:
--------------------------------------------------------------------------------
1 | var app = angular.module('app');
2 |
3 | //Resources
4 | app.factory('Project', function($resource) {
5 | return $resource('/api/projects/:hash', {hash: '@hash'}, {update: {method: 'PUT'}, groups: {method: 'GET', url:'/api/projects/:hash/groups', isArray: true}, clear: {method: 'delete', url: '/api/projects/:hash/reports'}});
6 | });
7 |
8 | app.factory('Report', function($resource) {
9 | return $resource('/api/reports/:id', {id: '@id'}, {update: {method: 'PUT'}})
10 | });
11 |
12 | app.factory('Group', function($resource) {
13 | return $resource('/api/projects/:hash/groups/:group', {hash: 'hash'}, {});
14 | })
15 |
16 | app.factory('Stats', function($resource) {
17 | return $resource('/api/projects/:hash/stats', {}, {});
18 | })
19 |
20 | app.factory('Filter', function($resource) {
21 | return $resource('/api/projects/:hash/filters/:id', {id: '@id', hash: '@hash'}, {update: {method: 'PUT'}});
22 | })
23 |
24 | app.service('GraphService', function() {
25 | var out = {}
26 |
27 | out.displayName = function(line) {
28 | if (line == undefined)
29 | return "undefined"
30 |
31 | if (line.length < 50) {
32 | return line;
33 | }
34 |
35 | return line.substring(0, 47) + '...';
36 | }
37 |
38 | out.buildSeries = function(groups, count, startDate, endDate, bucket) {
39 | if (groups == undefined || groups.length == 0)
40 | return;
41 |
42 | if (count <= 0)
43 | count = 1
44 |
45 | var series = _.chain(groups).first(count).map(function(group) {
46 | return {
47 | key: out.displayName(group.name),
48 | values: out.zeroFill(group.data, startDate, endDate, bucket) //out.zeroFill(group.data, startDate, endDate, bucket),
49 | };
50 | }).value();
51 |
52 | console.log(series)
53 |
54 | return series;
55 | }
56 |
57 | // Takes [{x: date, y: count}]
58 | out.zeroFill = function(data, startDate, endDate, bucket) {
59 | return data;
60 | }
61 |
62 | out.cleanDate = function(d) {
63 | if (_.isDate(d)) {
64 | return d.getTime()/1000;
65 | } else {
66 | return d/1000;
67 | }
68 | }
69 |
70 | out.buildOptions = function(range){
71 | return {
72 | "chart": {
73 | "type": "multiBarChart",
74 | "height": 450,
75 | "margin": {
76 | "top": 20,
77 | "right": 20,
78 | "bottom": 60,
79 | "left": 45
80 | },
81 | "clipEdge": true,
82 | "staggerLabels": true,
83 | "transitionDuration": 500,
84 | "stacked": true,
85 | "xAxis": {
86 | "axisLabel": "Time (ms)",
87 | "showMaxMin": false,
88 | tickFormat: function(d) {
89 | if (range === "hour" || range == "")
90 | return moment(d).format('LT')
91 | if (range == "day")
92 | return moment(d).format('LT')
93 | if (range == "week")
94 | return moment(d).format('ll');
95 | if (range == "month")
96 | return moment(d).format('ll');
97 | }
98 | },
99 | "yAxis": {
100 | "axisLabel": "Y Axis",
101 | "axisLabelDistance": 40
102 | }
103 | }
104 | }
105 | }
106 |
107 | return out;
108 | })
109 |
110 | app.service('QueryParams', function() {
111 | var allDirectiveOn = {default: true, script: true, style: true, img: true, font: true, connect: true, media: true, object: true };
112 | var allDirectiveOff = {default: false, script: false, style: false, img: false, font: false, connect: false, media: false, object: false };
113 |
114 | var out = {}
115 | out.seriesCount = 6;
116 | out.range = "";
117 | out.stateDate = new Date();
118 | out.endDate = new Date();
119 | out.bucketSize = "hour";
120 | out.bucket = 60 * 60; // hour in seconds
121 | out.limit = 50;
122 | out.directives = allDirectiveOn;
123 | out.filters = true;
124 |
125 | out.allOn = function() {
126 | out.directives = _.clone(allDirectiveOn);
127 | };
128 |
129 | out.allOff = function() {
130 | out.directives = _.clone(allDirectiveOff);
131 | };
132 |
133 | out.setRange = function(period) {
134 | out.range = period;
135 | out.endDate = new Date().getTime();
136 | out.startDate = moment().subtract(period, 1).toDate().getTime();
137 | }
138 | out.setRange('day');
139 |
140 | out.setBucket = function(bucketString) {
141 | if (bucketString === "second") {
142 | out.bucket = 1;
143 | } else if (bucketString === "minute") {
144 | out.bucket = 60;
145 | } else if (bucketString === "hour") {
146 | out.bucket = 60 * 60;
147 | } else if (bucketString === "day") {
148 | out.bucket = 60 * 60 * 24;
149 | }
150 |
151 | out.bucketSize = bucketString;
152 | }
153 |
154 | return out;
155 | })
156 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | The API is restful and produces json.
4 |
5 | ## Project
6 | Projects are the top level unit. You should have one project per website/csp policy.
7 |
8 | ```
9 | {
10 | // The report endpoint
11 | "endpoint": "ff4546ba0f580e6eaa75b2b69bdd1a7fe7504d3ddaa4a8e377bfc8965681654e",
12 |
13 | // How to access the report
14 | "hash": "6297b74e2ace5b4b6fb9c9458f53914d442fd89008ef0ae3d6c24c2c1829b2af",
15 |
16 | // MongoDB key
17 | "_id": ObjectId("540b64b6828700884dc0e151"),
18 |
19 | // Can only be accessable if the hash is known
20 | "hidden": true,
21 |
22 | // last seen policy
23 | "policy": "",
24 |
25 | // date project was created
26 | "ts": ISODate("2014-09-06T19:47:02.732Z"),
27 |
28 | // name of the project
29 | "name": "asdasd",
30 | }
31 | ```
32 |
33 | ### POST /api/projects
34 | Creates and returns a new project. Note, if the project is hidden, this is the only time you'll be able to see the hash.
35 |
36 | - name: String, "New Project", name of your project
37 | - hidden: Boolean, true, only accessable if you know the project hash
38 |
39 | ### GET /api/projects
40 | Returns a list of all projects
41 |
42 | ### GET /api/projects/:hash
43 | Returns an individual projects
44 |
45 | ### DELETE /api/projects/:hash
46 | Deletes a project, along with all associates reports and filters
47 |
48 |
49 | ## Report
50 | Wrapper object around CSP report.
51 | ```
52 | {
53 | // MongoDB ID of parent project
54 | "project": ObjectId("53fc9ccf7d49ae120f598f78"),
55 |
56 | // ip address of the HTTP post report
57 | "ip": "::ffff:127.0.0.1",
58 |
59 | // headers from the HTTP post report
60 | "headers": "{\"content-type\":\"application/csp-report\",\"host\":\"localhost:3000\",\"content-length\":\"139\",\"connection\":\"close\"}",
61 |
62 | // the raw/original report recieved
63 | "original": "{\"csp-report\":{\"document-uri\":\"https://twitter.com\",\"violated-directive\":\"style-src 'self' https://www.host.com:443\",\"blocked-uri\":\"self\"}}",
64 |
65 | // Sanitized report in string form
66 | "raw": "{\"document-uri\":\"https://twitter.com/\",\"violated-directive\":\"style-src 'self' https://www.host.com:443\",\"blocked-uri\":\"\"}",
67 |
68 | // the violated directive from the report
69 | "directive": "style-src",
70 |
71 | // type of report
72 | "classification": "inline-style",
73 |
74 | // a unique was to view the report (will be changed soon)
75 | "name": "style-src - inline-style - https://twitter.com/ - ",
76 |
77 | // Mongodb ID
78 | "_id": ObjectId("540b6f839fb885db53c9dce9"),
79 |
80 | // The actual report (saves all the fields it recieves)
81 | "csp-report": {
82 | "document-uri": "https://twitter.com/",
83 | "violated-directive": "style-src 'self' https://www.host.com:443",
84 | "blocked-uri": ""
85 | },
86 |
87 | // time stamp the report was recieved
88 | "ts": ISODate("2014-09-06T20:33:07.651Z"),
89 | "__v": 0
90 | }
91 | ```
92 |
93 |
94 | ### GET /api/projects/:hash/reports
95 | Returns a json list of reports
96 |
97 | - startDate: Date/Number, specify start range for reports
98 | - endDate: Date/Number, specify end range for reports
99 | - limit: limit the number of reports
100 |
101 | ### DELETE /api/projects/:hash/reports
102 | Delete all reports belonging to the project
103 |
104 |
105 | ## Groups
106 | Groups are collections of reports
107 |
108 | ```
109 | {
110 | // The report structure that was aggregated on
111 | _id: {
112 | document-uri: "http://caspr.io/#/p/e73f40cd722426dd6df4c81fb56285335747fa29728bc72bd07cbcf5c2829d21/analyze",
113 | referrer: "",
114 | violated-directive: "style-src 'self'",
115 | original-policy: "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self'; report-uri https://caspr.io/endpoint/example",
116 | blocked-uri: "",
117 | source-file: "http://caspr.io/bower_components/jquery/dist/jquery.min.js",
118 | line-number: 3,
119 | column-number: 16171,
120 | status-code: 200
121 | },
122 |
123 | // Number of reports in group
124 | count: 16,
125 |
126 | // An example report
127 | csp-report: {
128 | document-uri: "http://caspr.io/#/p/e73f40cd722426dd6df4c81fb56285335747fa29728bc72bd07cbcf5c2829d21/analyze",
129 | referrer: "",
130 | violated-directive: "style-src 'self'",
131 | original-policy: "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self'; report-uri https://caspr.io/endpoint/example",
132 | blocked-uri: "",
133 | source-file: "http://caspr.io/bower_components/jquery/dist/jquery.min.js",
134 | line-number: 3,
135 | column-number: 16171,
136 | status-code: 200
137 | },
138 |
139 | // An example report ID
140 | reportId: "540a9647b4a18afe317c2dca",
141 |
142 | // All the reports are put in time buckets to build histograms
143 | data: [ { x: 1409950800000, y: 0 }, ... , { x: 1410037200000, y: 0 }, { x: 1410040800000, y: 0 } ],
144 |
145 | // Last seen report in the group
146 | latest: "2014-09-06T05:06:15.799Z",
147 |
148 | // directive of the group
149 | directive: "style-src",
150 |
151 | // classification of the reports in the group
152 | classification: "inline-style",
153 |
154 | // An example name of a report in the group
155 | name: "style-src - inline-style - http://caspr.io/#/p/e73f40cd722426dd6df4c81fb56285335747fa29728bc72bd07cbcf5c2829d21/analyze - "
156 | }
157 | ```
158 |
159 | ### GET /api/projects/:hash/groups
160 | Returns reports aggregated into buckets of similar reports. Report dates will also be bucketed into bins
161 |
162 | - startDate: Date, yesterday, the date to start aggregating on
163 | - endDate: Date, now, the date to stop aggregating on
164 | - directives: ['default-src',...], only aggregate certain directives
165 | - limit: Number, 50, number of groups to return
166 | - bucket: Number, 60 * 60: Number of seconds to make each bucket
167 | - filters: Boolean, false, apply each filter
168 | - filterExclusion: Boolean, true, should filters block (true) or pass (false)
169 | - seriesCount: Number, 0, how many groups should have bucket data
170 |
171 | ### GET /api/projects/:hash/groups/:report
172 | Groups all reports related to a report
173 |
174 | - bucket: Number, 60 * 60: Number of seconds to make each bucket
175 | - startDate: Date, yesterday, the date to start aggregating on
176 | - endDate: Date, now, the date to stop aggregating on
177 |
178 |
179 | ## Filter
180 | A way to hide reports/groups from a project
181 | ```
182 | {
183 | // Project the filter belongs to
184 | "project": ObjectId("53fc9ccf7d49ae120f598f78"),
185 |
186 | // MongoDB OD
187 | "_id": ObjectId("540b77cec53fa2935d306f42"),
188 |
189 | // Name of the filter, not really important
190 | "name": "Block the HTTPS protocol",
191 |
192 | // The regexp that will be used for blocking
193 | "expression": "/^https:/",
194 |
195 | // The field to check the regexp against
196 | "field": "blocked-uri",
197 |
198 | // Is the filter currently active
199 | "active": true,
200 | }
201 | ```
202 |
203 | ### POST /api/projects/:hash/filters
204 | Create a new filter for a specific project
205 |
206 | - active: Boolean, true, is the filter active
207 | - field: string, blocked-uri, the csp report field to match the regexp against
208 | - expression: string, '/^httpz:/', the regexp to block against
209 | - name: string, 'Block the HTTPZ protocol', give the filter a name
210 |
211 | ### PUT /api/projects/:hash/filters/:filter
212 | Update an existing filter
213 |
214 | - active: Boolean, true, is the filter active
215 | - field: string, blocked-uri, the csp report field to match the regexp against
216 | - expression: string, '/^httpz:/', the regexp to block against
217 | - name: string, 'Block the HTTPZ protocol', give the filter a name
218 |
219 | ### DELETE /api/projects/:hash/filters/:filter
220 | Delete an existing filter
221 |
222 | ### GET /api/projects/:hash/filter
223 | Get all filters belonging to a project
224 |
225 | ### GET /api/projects/:hash/filter/:filter
226 | Get a specific filter
227 |
--------------------------------------------------------------------------------
/public/js/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var app = angular.module('app', ['ngResource', 'ui.router', 'nvd3']);
4 |
5 |
6 | app.config(function($stateProvider, $urlRouterProvider) {
7 |
8 | $urlRouterProvider.otherwise('/');
9 |
10 | $stateProvider
11 | .state('home', {
12 | url: '/',
13 | templateUrl: 'views/partials/home.html'
14 | })
15 |
16 | .state('projects', {
17 | url: '/projects',
18 | templateUrl: 'views/partials/projects.html',
19 | controller: 'ProjectsController'
20 | })
21 |
22 | .state('new', {
23 | url: '/p/new',
24 | templateUrl: 'views/partials/newProject.html',
25 | controller: 'NewProjectController'
26 | })
27 |
28 | .state('contact', {
29 | url: '/contact',
30 | templateUrl: 'views/partials/contact.html',
31 | })
32 |
33 | .state('project', {
34 | abstract: true,
35 | url: '/p/:hash',
36 | templateUrl: 'views/partials/project.html',
37 | controller: 'ProjectController',
38 | resolve: {
39 | project: function(Project, $stateParams) {
40 | return Project.get({hash: $stateParams.hash});
41 | },
42 |
43 | stats: function(Stats, $stateParams) {
44 | return Stats.get({hash: $stateParams.hash});
45 | }
46 | }
47 | })
48 |
49 | .state('project.overview', {
50 | url: '',
51 | templateUrl: 'views/partials/overview.html',
52 | controller: 'OverviewController',
53 | })
54 |
55 | .state('project.analyze', {
56 | url: '/analyze',
57 | views: {
58 | 'table': {
59 | templateUrl: 'views/partials/table.html',
60 | controller: 'TableController'
61 | },
62 | 'graph': {
63 | templateUrl: 'views/partials/graph.html',
64 | controller: 'GraphController'
65 | },
66 | 'query': {
67 | templateUrl: 'views/partials/query.html',
68 | controller: 'QueryController'
69 | }
70 | }
71 | })
72 |
73 | .state('project.builder', {
74 | url: '/builder',
75 | templateUrl: 'views/partials/builder.html',
76 | controller: 'BuilderController'
77 | })
78 |
79 | .state('project.inline', {
80 | url: '/inline',
81 | templateUrl: 'views/partials/inline.html',
82 | controller: 'InlineController'
83 | })
84 |
85 | .state('project.group', {
86 | url: '/group/:group',
87 | templateUrl: 'views/partials/group.html',
88 | controller: 'GroupController',
89 | })
90 |
91 | .state('project.query', {
92 | url: '/query',
93 | templateUrl: 'views/partials/query.html',
94 | controller: 'QueryController'
95 | })
96 |
97 | .state('project.filters', {
98 | url: '/filters',
99 | templateUrl: 'views/partials/filters.html',
100 | controller: 'FiltersController'
101 | })
102 |
103 | .state('project.filter', {
104 | url: '/filter/:filter',
105 | templateUrl: 'views/partials/filter.html',
106 | controller: 'FilterController'
107 | })
108 |
109 | .state('project.graph', {
110 | url: '/graph',
111 | templateUrl: 'views/partials/graph.html',
112 | controller: 'GraphController'
113 | })
114 |
115 | .state('project.table', {
116 | url: '/table',
117 | templateUrl: 'views/partials/table.html',
118 | controller: 'TableController'
119 | });
120 | }).run(function($rootScope, $state) {
121 | $rootScope.$state = $state;
122 | });
123 |
124 | app.controller('HomeController', function() {});
125 |
126 | app.controller('NavController', function() {
127 |
128 | });
129 |
130 | app.controller('ProjectsController', function(Project, $scope, Stats) {
131 | Project.query(function(projects) {
132 | var out = [];
133 | for (var i = 0; i < projects.length; ++i) {
134 | var project = projects[i];
135 | project.stats = Stats.get({hash: project.hash});
136 | out.push(project);
137 | }
138 | $scope.projects = out;
139 | });
140 | });
141 |
142 | app.controller('NewProjectController', function($scope, Project, $location) {
143 | $scope.project = new Project({name: ''});
144 | $scope.save = function() {
145 | $scope.project.$save(function(project) {
146 | $location.url('/p/' + project.hash);
147 | });
148 | };
149 | });
150 |
151 | app.controller('OverviewController', function($scope, $state, $rootScope, stats, project, Project) {
152 | $scope.host = window.location.host;
153 | $scope.protocol = window.location.protocol;
154 |
155 | $scope.deleteReports = function() {
156 | Project.clear({hash: project.hash}, function(results) {
157 | $rootScope.$emit('loadProject');
158 | });
159 | };
160 |
161 | $scope.deleteProject = function() {
162 | Project.delete({hash: project.hash}, function(results) {
163 | $state.go('projects');
164 | });
165 | };
166 | });
167 |
168 | app.controller('ProjectController', function($scope, $rootScope, $stateParams, project, stats, Project, Filter, Group, Stats, QueryParams) {
169 | $scope.project = project;
170 | $scope.stats = stats;
171 | $scope.groups = [];
172 | $scope.filteredGroups = [];
173 | $scope.reportCount = 0;
174 | $scope.groupCount = 0;
175 | $scope.filteredCount = 0;
176 |
177 | $rootScope.$on('loadProject', function() {
178 | $scope.project = Project.get({hash: $stateParams.hash});
179 | $scope.stats = Stats.get({hash: $stateParams.hash});
180 | });
181 |
182 | $rootScope.$on('loadGroups', function() {
183 | var params = _.pick(QueryParams, 'startDate', 'endDate', 'bucket', 'limit', 'directives', 'filters', 'seriesCount');
184 | params.directives = _.chain(params.directives).pairs().filter(function(a) {return a[1]; }).map(function(a) { return a[0]+'-src'}).value()
185 | params.hash = $stateParams.hash;
186 |
187 | Group.query(params, function(groups) {
188 | $scope.groups = groups;
189 | });
190 | });
191 | });
192 |
193 | app.controller('BuilderController', function($scope, QueryParams, $rootScope, project) {
194 | QueryParams.seriesCount = 0;
195 | QueryParams.limit = 100;
196 | $rootScope.$emit('loadGroups');
197 | $rootScope.$emit('loadProject');
198 |
199 | $scope.$watch('groups', function(newGroups, oldGroups) {
200 | if (newGroups === oldGroups) {
201 | return;
202 | }
203 |
204 | var groups = _.clone(newGroups);
205 | var notInlineGroups = _.filter(groups, function(g) { return g.classification !== 'inline'; });
206 | var directiveGroups = _.groupBy(notInlineGroups, function(g) { return g.directive; });
207 | for ( var directive in directiveGroups) {
208 | var directiveGroup = directiveGroups[directive];
209 |
210 | var groupedDirectiveGroup = _.groupBy(directiveGroup, function(g) { return g['csp-report']['blocked-uri']; });
211 | directiveGroups[directive] = _.map(groupedDirectiveGroup, function(groupDirectiveGroup) {
212 | var count = _.reduce(groupDirectiveGroup, function(prev, curr) { return prev + curr.count; }, 0);
213 | var group = _.max(groupDirectiveGroup, function(g) { return new Date(g.latest); });
214 | group.count = count;
215 | return group;
216 | });
217 | }
218 |
219 | $scope.directiveGroups = directiveGroups;
220 | });
221 |
222 | $scope.policy = project.policy;
223 | $scope.newOrigins = [];
224 |
225 | $scope.toggleOrigin = function(directive, value) {
226 | var pair = {directive: directive, value: value};
227 | for (var i = 0; i < $scope.newOrigins.length; ++i) {
228 | var originPair = $scope.newOrigins[i];
229 | if (originPair.directive === pair.directive && originPair.value === pair.value) {
230 | $scope.newOrigins.splice(i, 1);
231 | buildPolicy();
232 | return;
233 | }
234 | }
235 | $scope.newOrigins.push(pair);
236 | buildPolicy();
237 | };
238 |
239 | function buildPolicy() {
240 | var start = project.policy;
241 | var policy = new Policy(start);
242 | for (var i = 0; i < $scope.newOrigins.length; ++i) {
243 | var pair = $scope.newOrigins[i];
244 | policy.add(pair.directive, pair.value);
245 | }
246 | $scope.policy = policy.toString();
247 | }
248 | });
249 |
250 | app.controller('InlineController', function($scope, QueryParams, $rootScope) {
251 | QueryParams.seriesCount = 0;
252 | QueryParams.limit = 100;
253 | $rootScope.$emit('loadGroups');
254 | $rootScope.$emit('loadProject');
255 |
256 | $scope.$watch('groups', function(newGroups) {
257 | var groups = _.clone(newGroups);
258 | var inlineGroups = _.filter(groups, function(g) { return g.classification === 'inline'; });
259 | var directiveGroups = _.groupBy(inlineGroups, function(g) { return g.directive; });
260 | $scope.directiveGroups = directiveGroups;
261 | });
262 |
263 | });
264 |
265 | app.controller('GroupController', function($scope, $stateParams, Group, QueryParams, GraphService) {
266 | $scope.group = Group.get({hash: $scope.project.hash, group: $stateParams.group, startDate: QueryParams.startDate, endDate: QueryParams.endDate, bucket: QueryParams.bucket}, function(group) {
267 | $scope.data = GraphService.buildSeries([group], QueryParams.seriesCount, QueryParams.startDate, QueryParams.endDate, QueryParams.bucket);
268 | $scope.options = GraphService.buildOptions(QueryParams.range);
269 | });
270 | });
271 |
272 | app.controller('QueryController', function($scope, QueryParams, $rootScope) {
273 | $scope.params = QueryParams;
274 |
275 | $scope.$watch('params', function() {
276 | $rootScope.$emit('loadGroups');
277 | }, true);
278 | });
279 |
280 | app.controller('TableController', function($scope) {
281 | $scope.predicate = 'count';
282 | $scope.tableReversed = true;
283 | $scope.tableSort = function(predicate) {
284 | if ($scope.predicate === predicate) {
285 | $scope.tableReversed = ! $scope.tableReversed;
286 | return;
287 | }
288 |
289 | $scope.predicate = predicate;
290 | };
291 |
292 | $scope.urlDisplay = function(line) {
293 | if (line.length < 50) {
294 | return line;
295 | }
296 |
297 | return line.substring(0, 47) + '...';
298 | };
299 | });
300 |
301 | app.controller('FilterController', function(GraphService, $scope, $state, $stateParams, Filter, QueryParams, project) {
302 |
303 | function loadFilter() {
304 | Filter.get({hash: $scope.project.hash, id: $stateParams.filter}, function(results) {
305 | $scope.filter = results.filter;
306 | $scope.filteredGroups = results.filteredGroups;
307 | $scope.filter.count = _.reduce(results.filteredGroups, function(c, group) { return c + group.count }, 0)
308 |
309 | //$scope.data = GraphService.buildSeries($scope.filteredGroups, $scope.filteredGroups.length);
310 | //$scope.options = GraphService.buildSeries(QueryParams.range)
311 | });
312 | }
313 | loadFilter();
314 |
315 | $scope.saveFilter = function() {
316 | Filter.update({hash: project.hash, id: $scope.filter._id}, $scope.filter, function() {
317 | loadFilter();
318 | });
319 | };
320 |
321 | $scope.deleteFilter = function() {
322 | Filter.delete({hash: project.hash, id: $scope.filter._id}, function() {
323 | $state.go('project.filters');
324 | });
325 | };
326 | });
327 |
328 | app.controller('FiltersController', function($scope, $rootScope, Filter, $stateParams, project) {
329 | $scope.filters = Filter.query({hash: project.hash});
330 |
331 | function reloadFilters() {
332 | $scope.filters = Filter.query({hash: $stateParams.hash});
333 | $rootScope.$emit('loadGroups');
334 | }
335 |
336 | $scope.addFilter = function() {
337 | $scope.filter.$save();
338 | $scope.filter = new Filter({hash: project.hash, name: 'Name', expression: '/expression/', field: 'blocked-uri' });
339 | reloadFilters();
340 | };
341 |
342 | $scope.saveFilter = function(index) {
343 | $scope.filters[index].$update({hash: project.hash, id: $scope.filters[index]._id}, function() {
344 | reloadFilters();
345 | });
346 | };
347 |
348 | $scope.deleteFilter = function(index) {
349 | $scope.filters[index].$delete({hash: project.hash, id: $scope.filters[index]._id}, function() {
350 | reloadFilters();
351 | });
352 | };
353 | $scope.filter = new Filter({ hash: project.hash, name: 'Name', expression: '/expression/', field: 'blocked-uri' });
354 | });
355 |
356 | app.controller('GraphController', function(GraphService, QueryParams, $scope) {
357 | QueryParams.seriesCount = 6;
358 |
359 | $scope.$watch('groups', function(newVal) {
360 | if (newVal === undefined || newVal.length === 0) {
361 | return;
362 | }
363 | $scope.data = GraphService.buildSeries(newVal, QueryParams.seriesCount, QueryParams.startDate, QueryParams.endDate, QueryParams.bucket);
364 | $scope.options = GraphService.buildOptions(QueryParams.range);
365 | });
366 |
367 | });
368 |
--------------------------------------------------------------------------------
/test/twitter.json:
--------------------------------------------------------------------------------
1 | {
2 | "reports": [
3 | {"csp-report":{"document-uri":"https://twitter.com:3000/","referrer":"","violated-directive":"script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:","original-policy":"default-src 'self'; connect-src 'self'; font-src 'self'; frame-src http://owaspappseccalifornia2014.sched.org https://www.ssllabs.com https://cloudfront.net http://*.twitter.com https://*.twitter.com https://twitter.com; img-src 'self' https: https://si0.twimg.com http://www.google-analytics.com https://www.google-analytics.com http://cdn.schd.ws data:; media-src 'self'; object-src 'self'; script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:; style-src 'self' 'unsafe-inline' https://platform.twitter.com http://cdn.schd.ws; report-uri http://localhost:8888/scribes/csp_report;","blocked-uri":"http://www.google-analytics.com","source-file":"http://example.org:3000/assets/application.js?body=1","line-number":23,"column-number":63,"status-code":200}},
4 | {"csp-report":{"document-uri":"https://twitter.com:3000/location","referrer":"http://example.org:3000/speakers","violated-directive":"script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:","original-policy":"default-src 'self'; connect-src 'self'; font-src 'self'; frame-src http://owaspappseccalifornia2014.sched.org https://www.ssllabs.com https://cloudfront.net http://*.twitter.com https://*.twitter.com https://twitter.com; img-src 'self' https: https://si0.twimg.com http://www.google-analytics.com https://www.google-analytics.com http://cdn.schd.ws data:; media-src 'self'; object-src 'self'; script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:; style-src 'self' 'unsafe-inline' https://platform.twitter.com http://cdn.schd.ws; report-uri http://localhost:8888/scribes/csp_report;","blocked-uri":"","status-code":200}},
5 | {"csp-report":{"document-uri":"https://twitter.com:3000/location","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; connect-src https://twadmedia.s3.amazonaws.com https://upload.twitter.com https://ton-u.twitter.com 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; script-src 'self'; style-src 'self'; report-uri 0:8888/scribes/csp_report;","blocked-uri":"","status-code":200}},
6 | {"csp-report":{"document-uri":"https://twitter.com:3000/location","referrer":"http://example.org:3000/speakers","violated-directive":"script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:","original-policy":"default-src 'self'; connect-src 'self'; font-src 'self'; frame-src http://owaspappseccalifornia2014.sched.org https://www.ssllabs.com https://cloudfront.net http://*.twitter.com https://*.twitter.com https://twitter.com; img-src 'self' https: https://si0.twimg.com http://www.google-analytics.com https://www.google-analytics.com http://cdn.schd.ws data:; media-src 'self'; object-src 'self'; script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:; style-src 'self' 'unsafe-inline' https://platform.twitter.com http://cdn.schd.ws; report-uri http://localhost:8888/scribes/csp_report;","blocked-uri":"https://google.com","status-code":200}},
7 | {"csp-report":{"document-uri":"https://twitter.com:3000/location/1234/asdf","referrer":"http://example.org:3000/speakers","violated-directive":"script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:","original-policy":"default-src 'self'; connect-src 'self'; font-src 'self'; frame-src http://owaspappseccalifornia2014.sched.org https://www.ssllabs.com https://cloudfront.net http://*.twitter.com https://*.twitter.com https://twitter.com; img-src 'self' https: https://si0.twimg.com http://www.google-analytics.com https://www.google-analytics.com http://cdn.schd.ws data:; media-src 'self'; object-src 'self'; script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:; style-src 'self' 'unsafe-inline' https://platform.twitter.com http://cdn.schd.ws; report-uri http://localhost:8888/scribes/csp_report;","blocked-uri":"https://google.com","status-code":200}},
8 | {"csp-report":{"document-uri":"http://localhost:3000/sup","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; connect-src https://twadmedia.s3.amazonaws.com https://upload.twitter.com https://ton-u.twitter.com 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; script-src 'self'; style-src 'self'; report-uri 0:8888/scribes/csp_report;","blocked-uri":"","source-file":"helloworld.htm","line-number":36,"column-number":12,"status-code":200}},
9 | {"csp-report":{"document-uri":"https://twitter.com","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"","source-file":"resource://foobar","status-code":200}},
10 | {"csp-report":{"document-uri":"https://twitter.com","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"","source-file":"chromenull://","status-code":200}},
11 | {"csp-report":{"document-uri":"https://twitter.com","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"chromeinvokeimmediate://3f52095921a1ae8440569d12dafe22cd","source-file":"example.js","status-code":200}},
12 | {"csp-report":{"document-uri":"https://twitter.com","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"webviewprogressproxy://","source-file":"example.js","status-code":200}},
13 | {"csp-report":{"document-uri":"https://twitter.com","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"mbinit://","source-file":"example.js","status-code":200}},
14 | {"csp-report":{"document-uri":"https://twitter.com","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"","source-file":"https://ajax.googleapis.com/foo/bar","status-code":200}},
15 | {"csp-report":{"document-uri":"https://twitter.com","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"","source-file":"https://superfish.com/foo/bar","status-code":200}},
16 | {"csp-report":{"document-uri":"https://twitter.com:3000/sup","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; connect-src https://twadmedia.s3.amazonaws.com https://upload.twitter.com https://ton-u.twitter.com 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; script-src 'self'; style-src 'self'; report-uri 0:8888/scribes/csp_report;","blocked-uri":"","source-file":"chrome-extension://cfhdojbkjhnklbpkdaibdccddilifddb","line-number":36,"column-number":12,"status-code":200}},
17 | {"csp-report":{"document-uri":"https://twitter.com:3000/sup","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; connect-src https://twadmedia.s3.amazonaws.com https://upload.twitter.com https://ton-u.twitter.com 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; script-src 'self'; style-src 'self'; report-uri 0:8888/scribes/csp_report;","blocked-uri":"","source-file":"safari://cfhdojbkjhnklbpkdaibdccddilifddb","line-number":36,"column-number":12,"status-code":200}},
18 | {"csp-report":{"document-uri":"https://twitter.com:3000/sup","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; connect-src https://twadmedia.s3.amazonaws.com https://upload.twitter.com https://ton-u.twitter.com 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; script-src 'self'; style-src 'self'; report-uri 0:8888/scribes/csp_report;","blocked-uri":"","source-file":"helloworld.htm","line-number":36,"column-number":12,"status-code":200,"script-sample":"haha,LastPass,lol"}},
19 | {"csp-report":{"document-uri":"https://twitter.com:3000/sup","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; connect-src https://twadmedia.s3.amazonaws.com https://upload.twitter.com https://ton-u.twitter.com 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; script-src 'self'; style-src 'self'; report-uri 0:8888/scribes/csp_report;","blocked-uri":"http://blockedhost.com/somepath/somefile","source-file":"helloworld.htm","line-number":36,"column-number":12,"status-code":200}},
20 | {"csp-report":{"document-uri":"https://twitter.com/foo/bar", "referrer": "https://www.google.com/", "violated-directive": "default-src self", "original-policy": "default-src self; report-uri /csp-hotline.php", "blocked-uri": "http://evilhackerscripts.com"}},
21 | {"csp-report":{"document-uri":"https://twitter.com:3000/location","violated-directive":"script-src 'self'","original-policy":"default-src 'self'; connect-src 'self'; font-src 'self'; frame-src http://owaspappseccalifornia2014.sched.org https://www.ssllabs.com https://cloudfront.net http://*.twitter.com https://*.twitter.com https://twitter.com; img-src 'self' https: https://si0.twimg.com http://www.google-analytics.com https://www.google-analytics.com http://cdn.schd.ws data:; media-src 'self'; object-src 'self'; script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:; style-src 'self' 'unsafe-inline' https://platform.twitter.com http://cdn.schd.ws; report-uri http://localhost:8888/scribes/csp_report;","blocked-uri":"","status-code":200}},
22 | {"csp-report":{"document-uri":"https://twitter.com:3000/sup","referrer":"","violated-directive":"style-src 'self'","original-policy":"allow ‘self’; options inline-script eval-script; report-uri 0:8888/scribes/csp_report;","blocked-uri":"","source-file":"example.js","line-number":36,"column-number":12,"status-code":200}},
23 | {"csp-report":{"document-uri":"https://twitter.com:3000/location","violated-directive":"script-src 'self'","original-policy":"default-src; connect-src 'self'; font-src 'self'; frame-src http://owaspappseccalifornia2014.sched.org https://www.ssllabs.com https://cloudfront.net http://*.twitter.com https://*.twitter.com https://twitter.com; img-src 'self' https: https://si0.twimg.com http://www.google-analytics.com https://www.google-analytics.com http://cdn.schd.ws data:; media-src 'self'; object-src 'self'; script-src 'self' https://syndication.twitter.com https://ssl.google-analytics.com http://owaspappseccalifornia2014.sched.org https://www.google-analytics.com https://platform.twitter.com https://*.twimg.com about:; style-src 'self' 'unsafe-inline' https://platform.twitter.com http://cdn.schd.ws; report-uri http://localhost:8888/scribes/csp_report;","blocked-uri":"","status-code":200}},
24 | {"csp-report":{"document-uri":"https://translate.twitter.com/user/foo/bar","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"","source-file":"example.html","status-code":200}},
25 | {"csp-report":{"document-uri":"https://twitter.com/012345","referrer":"","violated-directive":"style-src 'self'","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"","source-file":"example.html","status-code":200}},
26 | {"csp-report":{"document-uri":"https://www.twitter.com","violated-directive":"style-src 'self' https://www.host.com:443","blocked-uri":"https://www.google.com"}},
27 | {"csp-report":{"document-uri":"https://jsl.infostatsvc.com/?GcUrlVisit_2=879218357|,|d6df659f-0417-4c39-84be-fe39b5998720|,|https%3A%2F%2Ftwitter.com%2Faccount%2Freset_password|,|https%3A%2F%2Fus-mg5.mail.yahoo.com%2Fneo%2Flaunch%3F.rand%3D21sg399bb8444|,|45|,|1405529661337|,|maucampo|,|d026701b-a289-c30c-fd01-0045af4cc829","referrer":"","violated-directive":"style-src 'self' https://jsl.infostatsvc.com/?GcUrlVisit_2=879218357|,|d6df659f-0417-4c39-84be-fe39b5998720|,|https%3A%2F%2Ftwitter.com%2Faccount%2Freset_password|,|https%3A%2F%2Fus-mg5.mail.yahoo.com%2Fneo%2Flaunch%3F.rand%3D21sg399bb8444|,|45|,|1405529661337|,|maucampo|,|d026701b-a289-c30c-fd01-0045af4cc829","original-policy":"default-src 'self'; style-src 'self';","blocked-uri":"https://jsl.infostatsvc.com/?GcUrlVisit_2=879218357|,|d6df659f-0417-4c39-84be-fe39b5998720|,|https%3A%2F%2Ftwitter.com%2Faccount%2Freset_password|,|https%3A%2F%2Fus-mg5.mail.yahoo.com%2Fneo%2Flaunch%3F.rand%3D21sg399bb8444|,|45|,|1405529661337|,|maucampo|,|d026701b-a289-c30c-fd01-0045af4cc829","source-file":"example.html","status-code":200}},
28 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"frame-src https://*:* http://*.twimg.com:80 http://itunes.apple.com:80 about://*:* javascript://*:*","blocked-uri":"https://www.google.com"}},
29 | {"csp-report":{"document-uri":"about:blank","violated-directive":"style-src 'self' https://www.host.com:443","blocked-uri":"https://www.google.com"}},
30 | {"csp-report":{"document-uri":"https://api.twitter.com/oauth/authenticate?oauth_token=sometoken","referrer":"https://api.twitter.com/oauth/authorize?oauth_token=sometoken","violated-directive":"style-src 'self' https://www.host.com:443","blocked-uri":"swsdk-load.complete://https://api.twitter.com/oauth/authorize?oauth_token=sometoken"}},
31 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"style-src 'self' https://www.host.com:443","blocked-uri":"self"}},
32 | {"csp-report":{"violated-directive":"script-src 'unsafe-inline' 'unsafe-eval' about: https:","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/6.1.5 Safari/537.77.4","blocked-uri":"http://js.blinkadr.com","document-uri":"about:blank","line-number":"2","classification":"mixed_content","referrer":"","source-file":"http://js.blinkadr.com"}},
33 | {"csp-report":{"violated-directive":"img-src https: data:","status-code":"0","user_agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.46 Safari/537.36","blocked-uri":"http://nzj.divdriver.net","document-uri":"https://twitter.com/foobar","column-number":"5837","line-number":"1","classification":"mixed_content","referrer":"https://twitter.com/foobar","source-file":"https://nzj.divdriver.net"}},
34 | {"csp-report":{"violated-directive":"script-src 'unsafe-inline' 'unsafe-eval' about://*:* https://*:*","user_agent":"Mozilla/5.0 (Windows NT 6.3; rv:31.0) Gecko/20100101 Firefox/31.0","blocked-uri":"http://v.zilionfast.in/0/?t=vrt","document-uri":"https://twitter.com/search?q=foobar","classification":"unauthorized_host","referrer":"https://twitter.com/"}},
35 | {"csp-report":{"violated-directive":"script-src 'unsafe-inline' 'unsafe-eval' about://*:* https://*:*","user_agent":"Mozilla/5.0 (Windows NT 6.1; rv:30.0) Gecko/20100101 Firefox/30.0","blocked-uri":"http://istatic.datafastguru.info/fo/min/wpgb.js?bname=foobar","document-uri":"https://twitter.com/foobar","classification":"unauthorized_host","referrer":"https://twitter.com/"}},
36 | {"csp-report":{"violated-directive":"img-src https://*:* data://*:*","user_agent":"Mozilla/5.0 (Windows NT 6.1; rv:30.0) Gecko/20100101 Firefox/30.0","blocked-uri":"tmtbff://tmtoolbar-privacyscanner/content/PSPromotion/img/TM_logo.png","document-uri":"https://twitter.com/","classification":"unauthorized_host","referrer":""}},
37 | {"csp-report":{"violated-directive":"img-src https: data:","status-code":"0","user_agent":"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36 OPR/23.0.1522.60","blocked-uri":"http://widgets.amung.us","document-uri":"https://twitter.com/","classification":"mixed_content","referrer":"https://twitter.com/login"}},
38 | {"csp-report":{"violated-directive":"connect-src https://*:*","user_agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0","blocked-uri":"http://xls.searchfun.in/whocares","document-uri":"https://twitter.com/","classification":"unauthorized_host","referrer":"twitter.com"}},
39 | {"csp-report":{"violated-directive":"script-src 'unsafe-inline' 'unsafe-eval' about: https:","user_agent":"Mozilla/5.0 (Linux; Android 4.4.2; ko-kr; SAMSUNG SM-N750K/KTU1BND2 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36","blocked-uri":"http://static.image2play.com","document-uri":"https://twitter.com/foobar","classification":"mixed_content","source-file":"https://twitter.com/foobar"}},
40 | {"csp-report":{"document-uri":"https://www.notfromus.com","violated-directive":"style-src 'self' https://www.host.com:443","blocked-uri":"https://someblockeduri.com"}},
41 | {"csp-report":{"document-uri":"thiscannotbeparsed://","violated-directive":"style-src 'self' https://www.host.com:443","blocked-uri":"https://someblockeduri.com"}},
42 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"style-src 'self' https://www.host.com:443","blocked-uri":"https://twitter.com"}},
43 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"script-src https://:443 about://*:* https://*:* http://*.twimg.com:80 https: about://*:443 javascript://*:* http://itunes.apple.com:80","blocked-uri":"https://www.google.com"}},
44 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"script-src 'self' https://www.host.com:443","blocked-uri":"symres:/images/Widgets/norton_tb_mm_logo_watermark.png"}},
45 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"script-src 'self' https://www.host.com:443","blocked-uri":"data:text/javascript,(function()%7Bvar%20e%3D%22addons.mozilla.org%22%3B%0Avar%20c%3D%22FastestFox%22%3B"}},
46 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"script-src 'self' https://www.host.com:443","blocked-uri":"mxaddon-pkg"}},
47 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"script-src 'self' https://www.host.com:443","blocked-uri":"jar:file:///data/data/org.mozilla.firefox/files/mozilla/f6d28num.default/extensions/privacydefense@privacydefense.net.xpi!/1.0.4/content/icon64.png"}},
48 | {"csp-report":{"document-uri":"https://twitter.com","violated-directive":"script-src 'self' https://www.host.com:443","blocked-uri":"https://i_spigjs_info.tlscdn.com/spig/javascript.js?hid=52&channel=FF"}}
49 | ]
50 | }
--------------------------------------------------------------------------------