├── .gitattributes ├── .bowerrc ├── public ├── img │ ├── happy.png │ ├── happy.xcf │ ├── sad.png │ ├── sad.xcf │ ├── shocked.png │ └── shocked.xcf ├── views │ ├── partials │ │ ├── analyze.html │ │ ├── graph.html │ │ ├── graphTemplate.html │ │ ├── saveme.html │ │ ├── projects.html │ │ ├── newProject.html │ │ ├── group.html │ │ ├── inline.html │ │ ├── builder.html │ │ ├── contact.html │ │ ├── project.html │ │ ├── table.html │ │ ├── filter.html │ │ ├── filters.html │ │ ├── overview.html │ │ ├── query.html │ │ └── home.html │ ├── error.html │ └── index.html ├── js │ ├── csp-parse.js │ ├── directives.js │ ├── filters.js │ ├── services.js │ └── app.js └── css │ └── style.css ├── .gitignore ├── models ├── index.js ├── filter.js ├── project.js └── report.js ├── routes ├── index.js ├── api.js ├── reports.js ├── endpoint.js ├── util.js ├── filters.js ├── groups.js └── projects.js ├── app.json ├── docs ├── TODO.md └── api.md ├── test ├── filters.js ├── test.js └── twitter.json ├── logger.js ├── bin └── www ├── options.js ├── bower.json ├── package.json ├── README.md └── app.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /public/img/happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0nrad/caspr/HEAD/public/img/happy.png -------------------------------------------------------------------------------- /public/img/happy.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0nrad/caspr/HEAD/public/img/happy.xcf -------------------------------------------------------------------------------- /public/img/sad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0nrad/caspr/HEAD/public/img/sad.png -------------------------------------------------------------------------------- /public/img/sad.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0nrad/caspr/HEAD/public/img/sad.xcf -------------------------------------------------------------------------------- /public/img/shocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0nrad/caspr/HEAD/public/img/shocked.png -------------------------------------------------------------------------------- /public/img/shocked.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0nrad/caspr/HEAD/public/img/shocked.xcf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | .sass-cache 4 | public/bower_components 5 | dump 6 | bin/certs 7 | exceptions.log 8 | log.log 9 | -------------------------------------------------------------------------------- /public/views/partials/analyze.html: -------------------------------------------------------------------------------- 1 |

hai

2 |

Anything

3 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /public/views/partials/graph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose') 4 | var Project = require('./project'); 5 | var Entry = require('./report'); 6 | var Filter = require('./filter'); 7 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var router = express.Router(); 5 | 6 | router.get('/', function(req, res) { 7 | res.render('index'); 8 | }); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Caspr", 3 | "description": "Content security policy aggregator", 4 | "repository": "https://github.com/c0nrad/caspr", 5 | "keywords": ["node", "express", "static", "CSP"], 6 | "addons": [ 7 | "mongolab:sandbox" 8 | ] 9 | } -------------------------------------------------------------------------------- /public/views/partials/graphTemplate.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var router = express.Router(); 5 | 6 | var projectRoutes = require('./projects'); 7 | var reportRoutes = require('./reports'); 8 | var groupRoutes = require('./groups'); 9 | var filterRoutes = require('./filters'); 10 | 11 | router.use('/', projectRoutes); 12 | router.use('/', reportRoutes); 13 | router.use('/', groupRoutes); 14 | router.use('/', filterRoutes); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | ## Endpoint 4 | 5 | ### Better classification 6 | ### raw strings should be ordered by key? 7 | ### List all different policies seen 8 | 9 | ## API 10 | 11 | ### Better error handling 12 | ### Generalize routes 13 | - dates 14 | ### Combine -> stats route into projects 15 | 16 | ## Webapp 17 | 18 | ### Proxy Service 19 | ### Flash warnings 20 | ### Calculate NONCES? 21 | ### Update main interface 22 | 23 | 24 | --- 25 | Express auto documentation generator? 26 | npm module on csp handling -------------------------------------------------------------------------------- /public/views/partials/saveme.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Project Title

4 |
5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /models/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'); 4 | var Schema = mongoose.Schema; 5 | 6 | var FilterSchema = new Schema({ 7 | active: {type: Boolean, default: true }, 8 | field: { type: String, default: 'blocked-uri' }, 9 | expression: {type: String, default: '/^https/' }, 10 | name: { type: String, default: 'Block HTTPS' }, 11 | project: {type: Schema.Types.ObjectId, ref: 'Project' } 12 | }); 13 | 14 | FilterSchema.index({ project: 1 }); 15 | 16 | module.exports = mongoose.model('Filter', FilterSchema); 17 | -------------------------------------------------------------------------------- /test/filters.js: -------------------------------------------------------------------------------- 1 | var reports = require('./twitter.json').reports; 2 | 3 | var filters = [ { 4 | name: 'blocked-uri', 5 | expression: /^https/, 6 | type: '' 7 | }] 8 | 9 | for (var i = 0; i < filters.length; ++i) { 10 | var filter = filters[i]; 11 | var name = filter.name; 12 | var expression = filter.expression; 13 | 14 | for (var r = 0; r < reports.length; ++r) { 15 | var report = reports[r]['csp-report']; 16 | if (report[name].match(expression)) { 17 | console.log('match', report[name]); 18 | } 19 | } 20 | } 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/views/partials/projects.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

Projects

5 | New Project 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 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 | 11 | 12 |
13 | 14 | 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 |
Project Number of Reports Date Last Activity
{{project.name}} {{project.stats.totalReports || 0}} {{project.stats.dateLastActivity | date:'short' }}
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 23 | 24 | 25 | 26 |
Location Count Line Number Column Number Source File Last Seen Details
{{group['csp-report']['document-uri']}} 18 | {{group.count}} {{group['csp-report']['line-number']}} 20 | {{group['csp-report']['column-number']}} 21 | {{group['csp-report']['source-file']}} 22 | {{group.latest | fromNow}} Details
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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
Allow Count Last Seen Origin Details
{{group.count}} {{group.latest | fromNow}} {{group['csp-report']['blocked-uri'] }} Details
31 |
32 | 33 | -------------------------------------------------------------------------------- /public/views/partials/contact.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Code / Deployment

4 | github.com/caspr 5 | Deploy 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 |

{{project.name}}

20 | 21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Count Last Timestamp Directive Blocked URI Document URI Policy
{{group.count}} {{group.latest | date: 'short'}} {{group.directive}} {{urlDisplay(group['csp-report']['blocked-uri'])}} {{urlDisplay(group['csp-report']['document-uri'])}} Details
 {{ group['csp-report'] | stringify }} 
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 |
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |
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 |
50 |
51 | 52 |
53 |
54 |
55 |
56 | 57 |
58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/views/partials/filters.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Add Filter

4 |
5 | 6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 64 | 65 |
Name Field Expression Action
{{filter.name}} {{filter.field}} {{filter.expression}} More
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 |
60 |

Import Data

61 | 62 | 63 | 64 |

Backup Data

65 | 66 | 67 | 68 |

Previous Backups

69 | 1408644782927.json (coming soon)
70 | 1408644782927.json (coming soon)
71 | 1408644782927.json (coming soon) 72 |
73 | 74 |
75 |

Settings

76 | 79 |
80 | 81 | 82 |
83 | 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 | ![Caspr](https://raw.githubusercontent.com/c0nrad/caspr/master/public/img/happy.png) 6 | 7 | 8 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](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 |
5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 | 24 | 27 | 30 | 33 |
34 | 35 | 38 | 41 | 44 | 47 |
48 | 49 | 50 | 51 |
52 |
53 | 54 |
55 | 56 |
57 | - 58 | 59 | + 60 |
61 |
62 | 63 |
64 | 65 |
66 | - 67 | 68 | + 69 |
70 |
71 | 72 | 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 | 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 | } --------------------------------------------------------------------------------