antongolub/npm-registry-firewall

View on GitHub
src/main/js/firewall/plugins/audit.js

Summary

Maintainability
A
0 mins
Test Coverage
A
97%
import {semver} from '../../semver.js'
import {request} from '../../http/index.js'
import {withCache} from '../../cache.js'
import {asArray, makeDeferred, setFnName, tryQueue} from '../../util.js'
import {logger} from '../../logger.js'

const severityOrder = ['critical', 'high', 'moderate', 'low', 'any' ]

export const auditPlugin = async ({entry: {name, version}, options = {}, boundContext: {registry}}) => {
  options.any = options.any || options['*']
  const advisories = await getAdvisories(name, options.registry || registry)
  const vulns = advisories.filter(({vulnerable_versions}) => semver.satisfies(version, vulnerable_versions))
  const worst = Math.min(...vulns.map(({severity}) => severityOrder.indexOf(severity)))
  const directive = worst !== -1 && options[severityOrder.slice(worst).find(l => options[l])]

  return directive || false
}

auditPlugin.warmup = ({name, registry}) => {
  logger.debug('audit: warming up cache for', name)
  return getAdvisories(name, registry)
}

setFnName(auditPlugin, 'audit-plugin')

const getAdvisories = async (name, registry) => {
  const registries = asArray(registry)
  const args = registries.map(r => [name, r])

  return tryQueue(getAdvisoriesDebounced, ...args)
}

const queues = {}

const getAdvisoriesDebounced = async (name, registry) =>
  withCache(`audit-${name}`, () => {
    const {promise, resolve, reject} = makeDeferred()
    const queue = (queues[registry] = queues[registry] || [])

    queue.push({name, resolve, reject})

    processQueue(queue, registry)
    return promise
  }, 3600_000)

let auditConcurrency = 10
const processQueue = async (queue, registry) => {
  if (auditConcurrency === 0) {
    return
  }

  if (queue.length === 0) {
    return
  }

  auditConcurrency -= 1
  await new Promise(r => setTimeout(r, 5))

  if (queue.length === 0) {
    auditConcurrency += 1
    return
  }

  const batch = queue.slice()
  queue.length = 0

  // batch = await asyncFilter(batch, async ({name}) => !(await cache.has(name)))

  logger.info('audit: fetching advisories for', batch.map(({name}) => name))

  try {
    const advisories = batch.length
      ? await getAdvisoriesBatch(batch.map(({name}) => name), registry)
      : {}
    batch.forEach(({name, resolve}) => resolve(advisories[name] || []))
  } catch (e) {
    batch.forEach(({reject}) => reject(e))
  } finally {
    auditConcurrency += 1
    processQueue(queue, registry)
  }
}

export const getAdvisoriesBatch = async (batch = [], registry) => {
  const data = JSON.stringify(batch.reduce((m, name) => {
    m[name] = ['0.0.0']
    return m
  }, {}))
  const headers = {
    'user-agent': 'npm/8.5.0 node/v16.14.2 darwin x64 workspaces/false',
    'npm-command': 'audit',
    'content-type': 'application/json',
    accept: '*/*'
  }
  const {body} = await request({
    method: 'POST',
    url: `${registry}/-/npm/v1/security/advisories/bulk`,
    data,
    headers,
    gzip: true
  })

  return JSON.parse(body)
}

export default auditPlugin