1// this is the public class that is used by consumers. 2// the Advisory class handles all the calculation, and this 3// class handles all the IO with the registry and cache. 4const pacote = require('pacote') 5const cacache = require('cacache') 6const Advisory = require('./advisory.js') 7const { homedir } = require('os') 8const jsonParse = require('json-parse-even-better-errors') 9 10const _packument = Symbol('packument') 11const _cachePut = Symbol('cachePut') 12const _cacheGet = Symbol('cacheGet') 13const _cacheData = Symbol('cacheData') 14const _packuments = Symbol('packuments') 15const _cache = Symbol('cache') 16const _options = Symbol('options') 17const _advisories = Symbol('advisories') 18const _calculate = Symbol('calculate') 19 20class Calculator { 21 constructor (options = {}) { 22 this[_options] = { ...options } 23 this[_cache] = this[_options].cache || (homedir() + '/.npm/_cacache') 24 this[_options].cache = this[_cache] 25 this[_packuments] = new Map() 26 this[_cacheData] = new Map() 27 this[_advisories] = new Map() 28 } 29 30 get cache () { 31 return this[_cache] 32 } 33 34 get options () { 35 return { ...this[_options] } 36 } 37 38 async calculate (name, source) { 39 const k = `security-advisory:${name}:${source.id}` 40 if (this[_advisories].has(k)) { 41 return this[_advisories].get(k) 42 } 43 44 const p = this[_calculate](name, source) 45 this[_advisories].set(k, p) 46 return p 47 } 48 49 async [_calculate] (name, source) { 50 const k = `security-advisory:${name}:${source.id}` 51 const t = `metavuln:calculate:${k}` 52 process.emit('time', t) 53 const advisory = new Advisory(name, source, this[_options]) 54 // load packument and cached advisory 55 const [cached, packument] = await Promise.all([ 56 this[_cacheGet](advisory), 57 this[_packument](name), 58 ]) 59 process.emit('time', `metavuln:load:${k}`) 60 advisory.load(cached, packument) 61 process.emit('timeEnd', `metavuln:load:${k}`) 62 if (advisory.updated) { 63 await this[_cachePut](advisory) 64 } 65 this[_advisories].set(k, advisory) 66 process.emit('timeEnd', t) 67 return advisory 68 } 69 70 async [_cachePut] (advisory) { 71 const { name, id } = advisory 72 const key = `security-advisory:${name}:${id}` 73 process.emit('time', `metavuln:cache:put:${key}`) 74 const data = JSON.stringify(advisory) 75 const options = { ...this[_options] } 76 this[_cacheData].set(key, jsonParse(data)) 77 await cacache.put(this[_cache], key, data, options).catch(() => {}) 78 process.emit('timeEnd', `metavuln:cache:put:${key}`) 79 } 80 81 async [_cacheGet] (advisory) { 82 const { name, id } = advisory 83 const key = `security-advisory:${name}:${id}` 84 /* istanbul ignore if - should be impossible, since we memoize the 85 * advisory object itself using the same key, just being cautious */ 86 if (this[_cacheData].has(key)) { 87 return this[_cacheData].get(key) 88 } 89 90 process.emit('time', `metavuln:cache:get:${key}`) 91 const p = cacache.get(this[_cache], key, { ...this[_options] }) 92 .catch(() => ({ data: '{}' })) 93 .then(({ data }) => { 94 data = jsonParse(data) 95 process.emit('timeEnd', `metavuln:cache:get:${key}`) 96 this[_cacheData].set(key, data) 97 return data 98 }) 99 this[_cacheData].set(key, p) 100 return p 101 } 102 103 async [_packument] (name) { 104 if (this[_packuments].has(name)) { 105 return this[_packuments].get(name) 106 } 107 108 process.emit('time', `metavuln:packument:${name}`) 109 const p = pacote.packument(name, { ...this[_options] }) 110 .catch((er) => { 111 // presumably not something from the registry. 112 // an empty packument will have an effective range of * 113 return { 114 name, 115 versions: {}, 116 } 117 }) 118 .then(paku => { 119 process.emit('timeEnd', `metavuln:packument:${name}`) 120 this[_packuments].set(name, paku) 121 return paku 122 }) 123 this[_packuments].set(name, p) 124 return p 125 } 126} 127 128module.exports = Calculator 129