1'use strict' 2exports.generate = generate 3exports.generateFromInstall = generateFromInstall 4exports.submitForInstallReport = submitForInstallReport 5exports.submitForFullReport = submitForFullReport 6exports.printInstallReport = printInstallReport 7exports.printParseableReport = printParseableReport 8exports.printFullReport = printFullReport 9 10const auditReport = require('npm-audit-report') 11const npmConfig = require('../config/figgy-config.js') 12const figgyPudding = require('figgy-pudding') 13const treeToShrinkwrap = require('../shrinkwrap.js').treeToShrinkwrap 14const packageId = require('../utils/package-id.js') 15const output = require('../utils/output.js') 16const npm = require('../npm.js') 17const qw = require('qw') 18const regFetch = require('npm-registry-fetch') 19const perf = require('../utils/perf.js') 20const npa = require('npm-package-arg') 21const uuid = require('uuid') 22const ssri = require('ssri') 23const cloneDeep = require('lodash.clonedeep') 24 25// used when scrubbing module names/specifiers 26const runId = uuid.v4() 27 28const InstallAuditConfig = figgyPudding({ 29 color: {}, 30 json: {}, 31 unicode: {} 32}, { 33 other (key) { 34 return /:registry$/.test(key) 35 } 36}) 37 38function submitForInstallReport (auditData) { 39 const opts = InstallAuditConfig(npmConfig()) 40 const scopedRegistries = [...opts.keys()].filter( 41 k => /:registry$/.test(k) 42 ).map(k => opts[k]) 43 scopedRegistries.forEach(registry => { 44 // we don't care about the response so destroy the stream if we can, or leave it flowing 45 // so it can eventually finish and clean up after itself 46 regFetch('/-/npm/v1/security/audits/quick', opts.concat({ 47 method: 'POST', 48 registry, 49 gzip: true, 50 body: auditData 51 })).then(_ => { 52 _.body.on('error', () => {}) 53 if (_.body.destroy) { 54 _.body.destroy() 55 } else { 56 _.body.resume() 57 } 58 }, _ => {}) 59 }) 60 perf.emit('time', 'audit submit') 61 return regFetch('/-/npm/v1/security/audits/quick', opts.concat({ 62 method: 'POST', 63 gzip: true, 64 body: auditData 65 })).then(response => { 66 perf.emit('timeEnd', 'audit submit') 67 perf.emit('time', 'audit body') 68 return response.json() 69 }).then(result => { 70 perf.emit('timeEnd', 'audit body') 71 return result 72 }) 73} 74 75function submitForFullReport (auditData) { 76 perf.emit('time', 'audit submit') 77 const opts = InstallAuditConfig(npmConfig()) 78 return regFetch('/-/npm/v1/security/audits', opts.concat({ 79 method: 'POST', 80 gzip: true, 81 body: auditData 82 })).then(response => { 83 perf.emit('timeEnd', 'audit submit') 84 perf.emit('time', 'audit body') 85 return response.json() 86 }).then(result => { 87 perf.emit('timeEnd', 'audit body') 88 result.runId = runId 89 return result 90 }) 91} 92 93function printInstallReport (auditResult) { 94 const opts = InstallAuditConfig(npmConfig()) 95 return auditReport(auditResult, { 96 reporter: 'install', 97 withColor: opts.color, 98 withUnicode: opts.unicode 99 }).then(result => output(result.report)) 100} 101 102function printFullReport (auditResult) { 103 const opts = InstallAuditConfig(npmConfig()) 104 return auditReport(auditResult, { 105 log: output, 106 reporter: opts.json ? 'json' : 'detail', 107 withColor: opts.color, 108 withUnicode: opts.unicode 109 }).then(result => output(result.report)) 110} 111 112function printParseableReport (auditResult) { 113 const opts = InstallAuditConfig(npmConfig()) 114 return auditReport(auditResult, { 115 log: output, 116 reporter: 'parseable', 117 withColor: opts.color, 118 withUnicode: opts.unicode 119 }).then(result => output(result.report)) 120} 121 122function generate (shrinkwrap, requires, diffs, install, remove) { 123 const sw = cloneDeep(shrinkwrap) 124 delete sw.lockfileVersion 125 sw.requires = scrubRequires(requires) 126 scrubDeps(sw.dependencies) 127 128 // sw.diffs = diffs || {} 129 sw.install = (install || []).map(scrubArg) 130 sw.remove = (remove || []).map(scrubArg) 131 return generateMetadata().then((md) => { 132 sw.metadata = md 133 return sw 134 }) 135} 136 137const scrubKeys = qw`version` 138const deleteKeys = qw`from resolved` 139 140function scrubDeps (deps) { 141 if (!deps) return 142 Object.keys(deps).forEach(name => { 143 if (!shouldScrubName(name) && !shouldScrubSpec(name, deps[name].version)) return 144 const value = deps[name] 145 delete deps[name] 146 deps[scrub(name)] = value 147 }) 148 Object.keys(deps).forEach(name => { 149 for (let toScrub of scrubKeys) { 150 if (!deps[name][toScrub]) continue 151 deps[name][toScrub] = scrubSpec(name, deps[name][toScrub]) 152 } 153 for (let toDelete of deleteKeys) delete deps[name][toDelete] 154 155 scrubRequires(deps[name].requires) 156 scrubDeps(deps[name].dependencies) 157 }) 158} 159 160function scrubRequires (reqs) { 161 if (!reqs) return reqs 162 Object.keys(reqs).forEach(name => { 163 const spec = reqs[name] 164 if (shouldScrubName(name) || shouldScrubSpec(name, spec)) { 165 delete reqs[name] 166 reqs[scrub(name)] = scrubSpec(name, spec) 167 } else { 168 reqs[name] = scrubSpec(name, spec) 169 } 170 }) 171 return reqs 172} 173 174function getScope (name) { 175 if (name[0] === '@') return name.slice(0, name.indexOf('/')) 176} 177 178function shouldScrubName (name) { 179 const scope = getScope(name) 180 const cfg = npm.config // avoid the no-dynamic-lookups test 181 return Boolean(scope && cfg.get(scope + ':registry')) 182} 183function shouldScrubSpec (name, spec) { 184 const req = npa.resolve(name, spec) 185 return !req.registry 186} 187 188function scrubArg (arg) { 189 const req = npa(arg) 190 let name = req.name 191 if (shouldScrubName(name) || shouldScrubSpec(name, req.rawSpec)) { 192 name = scrubName(name) 193 } 194 const spec = scrubSpec(req.name, req.rawSpec) 195 return name + '@' + spec 196} 197 198function scrubName (name) { 199 return shouldScrubName(name) ? scrub(name) : name 200} 201 202function scrubSpec (name, spec) { 203 const req = npa.resolve(name, spec) 204 if (req.registry) return spec 205 if (req.type === 'git') { 206 return 'git+ssh://' + scrub(spec) 207 } else if (req.type === 'remote') { 208 return 'https://' + scrub(spec) 209 } else if (req.type === 'directory') { 210 return 'file:' + scrub(spec) 211 } else if (req.type === 'file') { 212 return 'file:' + scrub(spec) + '.tar' 213 } else { 214 return scrub(spec) 215 } 216} 217 218module.exports.scrub = scrub 219function scrub (value, rid) { 220 return ssri.fromData((rid || runId) + ' ' + value, {algorithms: ['sha256']}).hexDigest() 221} 222 223function generateMetadata () { 224 const meta = {} 225 meta.npm_version = npm.version 226 meta.node_version = process.version 227 meta.platform = process.platform 228 meta.node_env = process.env.NODE_ENV 229 230 return Promise.resolve(meta) 231} 232/* 233 const head = path.resolve(npm.prefix, '.git/HEAD') 234 return readFile(head, 'utf8').then((head) => { 235 if (!head.match(/^ref: /)) { 236 meta.commit_hash = head.trim() 237 return 238 } 239 const headFile = head.replace(/^ref: /, '').trim() 240 meta.branch = headFile.replace(/^refs[/]heads[/]/, '') 241 return readFile(path.resolve(npm.prefix, '.git', headFile), 'utf8') 242 }).then((commitHash) => { 243 meta.commit_hash = commitHash.trim() 244 const proc = spawn('git', qw`diff --quiet --exit-code package.json package-lock.json`, {cwd: npm.prefix, stdio: 'ignore'}) 245 return new Promise((resolve, reject) => { 246 proc.once('error', reject) 247 proc.on('exit', (code, signal) => { 248 if (signal == null) meta.state = code === 0 ? 'clean' : 'dirty' 249 resolve() 250 }) 251 }) 252 }).then(() => meta, () => meta) 253*/ 254 255function generateFromInstall (tree, diffs, install, remove) { 256 const requires = {} 257 tree.requires.forEach((pkg) => { 258 requires[pkg.package.name] = tree.package.dependencies[pkg.package.name] || tree.package.devDependencies[pkg.package.name] || pkg.package.version 259 }) 260 261 const auditInstall = (install || []).filter((a) => a.name).map(packageId) 262 const auditRemove = (remove || []).filter((a) => a.name).map(packageId) 263 const auditDiffs = {} 264 diffs.forEach((action) => { 265 const mutation = action[0] 266 const child = action[1] 267 if (mutation !== 'add' && mutation !== 'update' && mutation !== 'remove') return 268 if (!auditDiffs[mutation]) auditDiffs[mutation] = [] 269 if (mutation === 'add') { 270 auditDiffs[mutation].push({location: child.location}) 271 } else if (mutation === 'update') { 272 auditDiffs[mutation].push({location: child.location, previous: packageId(child.oldPkg)}) 273 } else if (mutation === 'remove') { 274 auditDiffs[mutation].push({previous: packageId(child)}) 275 } 276 }) 277 278 return generate(treeToShrinkwrap(tree), requires, auditDiffs, auditInstall, auditRemove) 279} 280