• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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