zsyp 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -1,7 +1,6 @@
1
1
  [![NPM version][npm-image]][npm-url]
2
- [![Build Status][travis-image]][travis-url]
2
+ [![Build Status][build-image]][build-url]
3
3
  [![Dependency Status][deps-image]][deps-url]
4
- [![Dev Dependency Status][deps-dev-image]][deps-dev-url]
5
4
 
6
5
  # zsyp
7
6
 
@@ -72,14 +71,11 @@ MIT © [Damian Krzeminski](https://pirxpilot.me)
72
71
  [capped collection]: https://docs.mongodb.com/manual/core/capped-collections/
73
72
  [dotenv]: https://www.npmjs.com/package/dotenv
74
73
 
75
- [npm-image]: https://img.shields.io/npm/v/zsyp.svg
74
+ [npm-image]: https://img.shields.io/npm/v/zsyp
76
75
  [npm-url]: https://npmjs.org/package/zsyp
77
76
 
78
- [travis-url]: https://travis-ci.com/pirxpilot/zsyp
79
- [travis-image]: https://img.shields.io/travis/com/pirxpilot/zsyp.svg
77
+ [build-url]: https://github.com/pirxpilot/zsyp/actions/workflows/check.yaml
78
+ [build-image]: https://img.shields.io/github/workflow/status/pirxpilot/zsyp/check
80
79
 
81
- [deps-image]: https://img.shields.io/david/pirxpilot/zsyp.svg
82
- [deps-url]: https://david-dm.org/pirxpilot/zsyp
83
-
84
- [deps-dev-image]: https://img.shields.io/david/dev/pirxpilot/zsyp.svg
85
- [deps-dev-url]: https://david-dm.org/pirxpilot/zsyp?type=dev
80
+ [deps-image]: https://img.shields.io/librariesio/release/npm/zsyp
81
+ [deps-url]: https://libraries.io/npm/zsyp
package/index.js CHANGED
@@ -1,24 +1,12 @@
1
1
  require('dotenv').config({ path: '/etc/default/zsyp' });
2
2
 
3
- const connect = require('@pirxpilot/connect');
4
- const router = require('./lib/csp');
5
- const makeLogger = require('./lib/logger');
3
+ const makeApp = require('./lib/app');
6
4
 
7
5
  const {
8
6
  ZSYP_PORT: PORT = 3090,
9
- ZSYP_DOMAINS: domains,
10
- ZSYP_DB: database = 'mongodb://localhost/zsyp'
11
7
  } = process.env;
12
8
 
13
- const app = connect();
14
- const log = makeLogger({ database });
15
-
16
- app.use(function (req, res, next) {
17
- req.log = log;
18
- next();
19
- });
20
- app.use(router({ domains }));
21
-
9
+ const app = makeApp();
22
10
 
23
11
  module.exports = app;
24
12
 
package/lib/app.js ADDED
@@ -0,0 +1,26 @@
1
+ const connect = require('@pirxpilot/connect');
2
+ const mniam = require('mniam');
3
+ const { json } = require('body-parser');
4
+ const router = require('./router');
5
+ const event = require('./event');
6
+
7
+ module.exports = makeApp;
8
+
9
+ const {
10
+ ZSYP_DOMAINS: domains,
11
+ ZSYP_DB: database = 'mongodb://localhost/zsyp'
12
+ } = process.env;
13
+
14
+ function makeApp(opts = {}) {
15
+ const app = connect();
16
+
17
+ opts.db = mniam.db(database);
18
+ app.db = opts.db;
19
+
20
+ app.use(json({ limit: 5000, type: ['*/json', 'application/csp-report'] }));
21
+ app.use('/csp', router({ ...opts, name: 'csp', domains }));
22
+ app.use('/event', router({ ...opts, converter: event.converter }));
23
+
24
+ return app;
25
+ }
26
+
@@ -0,0 +1,23 @@
1
+ module.exports = makeConverter;
2
+
3
+ function makeConverter({ converter }) {
4
+
5
+ async function convert(req, res, next) {
6
+ try {
7
+ const { item, meta } = await converter(req.body);
8
+ req.item = item;
9
+ req.meta = meta;
10
+ return next();
11
+ } catch(e) {
12
+ return next(e);
13
+ }
14
+ }
15
+
16
+ return converter ? convert : keep;
17
+ }
18
+
19
+ function keep(req, res, next) {
20
+ req.item = req.body;
21
+ return next();
22
+ }
23
+
package/lib/event.js ADDED
@@ -0,0 +1,56 @@
1
+ const stackParser = require('error-stack-parser');
2
+ const { resolve } = require('./source-map');
3
+
4
+ module.exports = {
5
+ converter
6
+ };
7
+
8
+ async function converter(event) {
9
+ const { type = 'event' } = event;
10
+ let item;
11
+ let name;
12
+ switch (type) {
13
+ case 'error':
14
+ case 'exception':
15
+ name = 'error';
16
+ item = await convertError(event);
17
+ break;
18
+ default:
19
+ name = 'event';
20
+ item = event;
21
+ break;
22
+ }
23
+ return {
24
+ item,
25
+ meta: { name }
26
+ };
27
+ }
28
+
29
+ async function convertError(error) {
30
+ const { an, av } = error;
31
+ const frames = safeParseStack(error).map(mapFrame);
32
+ error.stack = await Promise.all(frames);
33
+ delete error.type;
34
+ return error;
35
+
36
+ function safeParseStack(error) {
37
+ try {
38
+ return stackParser.parse(error);
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ async function mapFrame(frame) {
45
+ const { fileName, lineNumber, columnNumber = 0, functionName } = frame;
46
+ const resolved = await resolve({ an, av }, [normalizeFilename(fileName), lineNumber, columnNumber]);
47
+ resolved.splice(3, 0, functionName);
48
+ return resolved;
49
+ }
50
+ }
51
+
52
+ function normalizeFilename(fileName) {
53
+ const file = new URL(fileName, 'http://localhost');
54
+ // strip hostname and leading slashes
55
+ return file.pathname.replace(/^\/+/, '');
56
+ }
package/lib/filter.js CHANGED
@@ -4,18 +4,17 @@ module.exports = makeFilter;
4
4
 
5
5
  function makeFilter({ domains }) {
6
6
 
7
- const domainRe = domains && new RegExp(domains);
7
+ if (!domains) {
8
+ return;
9
+ }
8
10
 
11
+ const domainRe = domains && new RegExp(domains);
9
12
  return filter;
10
13
 
11
14
  function filter({ body: csp }, res, next) {
12
- if (!domainRe) {
13
- return next();
14
- }
15
15
  const uri = csp['csp-report']['document-uri'];
16
16
  const { hostname } = new URL(uri);
17
17
  if (domainRe.test(hostname)) {
18
- console.log('OK!!!!!!!!');
19
18
  return next();
20
19
  }
21
20
  }
package/lib/from.js CHANGED
@@ -3,7 +3,8 @@ const { parse } = require('useragent');
3
3
  module.exports = from;
4
4
 
5
5
  function from(req, res, next) {
6
- const ua = req.headers['user-agent'];
6
+ const { headers, body } = req;
7
+ const ua = body?.from?.ua ?? headers['user-agent'];
7
8
  const {
8
9
  family,
9
10
  major,
@@ -20,7 +21,7 @@ function from(req, res, next) {
20
21
  name: os.family,
21
22
  version: os.major
22
23
  },
23
- ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress
24
+ ip: body?.from?.ip ?? headers['x-forwarded-for'] ?? req.connection.remoteAddress
24
25
  };
25
26
  if (device.family !== 'Other') {
26
27
  data.device = device.family;
package/lib/logger.js CHANGED
@@ -1,19 +1,28 @@
1
- const { db } = require('mniam');
2
1
  const debug = require('debug')('zsyp:logger');
3
2
 
4
3
  module.exports = makeLogger;
5
4
 
6
- function makeLogger({ database }) {
7
- const csp = db(database).collection({ name: 'csp'});
5
+ function makeLogger({ db, name }) {
6
+ const cache = Object.create(null);
7
+
8
+ const getCollection = name ?
9
+ () => collectionFromCache(name) :
10
+ ({ name }) => collectionFromCache(name);
8
11
 
9
12
  return log;
10
13
 
11
- function log(report) {
12
- debug('saving %j', report);
13
- csp.insertOne(report, function(err) {
14
- if (err) {
15
- console.error(err);
16
- }
17
- });
14
+ function log({ from, item, meta }, res, next) {
15
+ debug('saving %j', item);
16
+ getCollection(meta)
17
+ .insertOne({ from, ...item })
18
+ .then(() => next())
19
+ .catch(err => { console.error(err); next(); });
20
+ }
21
+
22
+ function collectionFromCache(name) {
23
+ if (!cache[name]) {
24
+ cache[name] = db.collection({ name });
25
+ }
26
+ return cache[name];
18
27
  }
19
28
  }
package/lib/router.js ADDED
@@ -0,0 +1,32 @@
1
+ const Router = require('router');
2
+
3
+ const from = require('./from');
4
+ const makeFilter = require('./filter');
5
+ const makeConverter = require('./converter');
6
+ const makeLogger = require('./logger');
7
+
8
+ module.exports = function (opts) {
9
+ const router = new Router({
10
+ strict: true,
11
+ caseSensitive: true
12
+ });
13
+
14
+ const stack = [
15
+ respond,
16
+ from,
17
+ makeFilter(opts),
18
+ makeConverter(opts),
19
+ makeLogger(opts),
20
+ opts.finalMiddleware
21
+ ].filter(Boolean);
22
+
23
+ router.post('/', stack);
24
+
25
+ return router;
26
+ };
27
+
28
+ function respond(req, res, next) {
29
+ res.statusCode = 204; // empty
30
+ res.end();
31
+ next();
32
+ }
@@ -0,0 +1,57 @@
1
+ const { readFile } = require('fs').promises;
2
+ const path = require('path');
3
+ const { SourceMapConsumer } = require('source-map');
4
+ const LRU = require('lru-cache');
5
+
6
+ module.exports = {
7
+ resolve,
8
+ clear
9
+ };
10
+
11
+ const {
12
+ ZSYP_SOURCE_MAP_DIR = '/var/lib/zsyp',
13
+ ZSYP_SOURCE_MAP_CACHE_SIZE = 30
14
+ } = process.env;
15
+
16
+
17
+ const cache = new LRU({
18
+ max: ZSYP_SOURCE_MAP_CACHE_SIZE,
19
+ fetchMethod: fetchSourceMap,
20
+ dispose: smc => smc && smc.destroy()
21
+ });
22
+
23
+ async function resolve({ an, av }, frame) {
24
+ const [source, line, column ] = frame;
25
+ const smc = await loadSourceMap(an, av, source);
26
+ if (!smc) {
27
+ return frame;
28
+ }
29
+ const pos = smc.originalPositionFor({ line, column });
30
+ if (!pos) {
31
+ return frame;
32
+ }
33
+ const resolved = [pos.source, pos.line, pos.column];
34
+ if (pos.name) {
35
+ resolved.push(pos.name);
36
+ }
37
+ return resolved;
38
+ }
39
+
40
+ function clear() {
41
+ cache.clear();
42
+ }
43
+
44
+ function loadSourceMap(app, version, source) {
45
+ const filename = path.resolve(ZSYP_SOURCE_MAP_DIR, app, version, source);
46
+ return cache.fetch(filename);
47
+ }
48
+
49
+ async function fetchSourceMap(filename) {
50
+ try {
51
+ const txt = await readFile(filename + '.map');
52
+ const map = JSON.parse(txt);
53
+ return new SourceMapConsumer(map);
54
+ } catch {
55
+ // ignore errors during map parsing
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zsyp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "CSP violation reports logger.",
5
5
  "author": {
6
6
  "name": "Damian Krzeminski",
@@ -19,14 +19,18 @@
19
19
  "@pirxpilot/connect": "^4.0.0",
20
20
  "body-parser": "~1",
21
21
  "debug": "~2 || ~3 || ~4",
22
- "dotenv": "~8",
23
- "mniam": "^2.1.0",
22
+ "dotenv": "~16",
23
+ "error-stack-parser": "^2.1.4",
24
+ "lru-cache": "^7.10.1",
25
+ "mniam": "^3.0.0",
24
26
  "router": "~1",
27
+ "source-map": "^0.7.4",
28
+ "supertest": "~6",
25
29
  "useragent": "^2.3.0"
26
30
  },
27
31
  "devDependencies": {
28
32
  "jshint": "~2",
29
- "tape": "~4"
33
+ "tape": "~5"
30
34
  },
31
35
  "scripts": {
32
36
  "test": "make check"
package/History.md DELETED
@@ -1,17 +0,0 @@
1
-
2
- 1.0.0 / 2020-04-18
3
- ==================
4
-
5
- * update docs
6
- * update links to Travis build results
7
- * update dependencies
8
-
9
- 0.0.2 / 2019-07-17
10
- ==================
11
-
12
- * fix reports domain filtering
13
-
14
- 0.0.1 / 2019-07-15
15
- ==================
16
-
17
- * basic implementation: UA parsing, domain filtering, mongo backend
package/lib/csp.js DELETED
@@ -1,36 +0,0 @@
1
- const Router = require('router');
2
- const { json } = require('body-parser');
3
-
4
- const from = require('./from');
5
- const makeFilter = require('./filter');
6
-
7
- function respond(req, res, next) {
8
- res.statusCode = 204; // empty
9
- res.end();
10
- next();
11
- }
12
-
13
-
14
- function log({ from, body: csp, log }) {
15
- const report = {
16
- from,
17
- ...csp
18
- };
19
-
20
- log(report);
21
- }
22
-
23
- module.exports = function (opts) {
24
- const router = new Router();
25
- const filter = makeFilter(opts);
26
-
27
- router.post('/csp',
28
- from,
29
- json({ limit: 5000, type: [ '*/json', 'application/csp-report' ] }),
30
- respond,
31
- filter,
32
- log
33
- );
34
-
35
- return router;
36
- };