audit.js 8.2 KB

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