DEV Community

Anton Golub
Anton Golub

Posted on

3 3

The missing `yarn audit fix` for Yarn 2+ Berry

As everybody knows, audit --fix feature is out of scope of Yarn 1 Classic yarn/7075 and it also has not been implemented yet (2021-12-12) for Yarn 2+ berry/3582.

Let's just fix it.

tldr

npm_config_yes=true npx yarn-audit-fix@latest
Enter fullscreen mode Exit fullscreen mode

1.

First of all, we need a lib to read/write yarnlock v2 files. @yarnpkg/lockfile seems the best choice, but it works with v1 only. Maybe nodejs-lockfile-parser?
Missed the mark again. It swaps checksums and does not provide dump/format API what is expected for the parser :). It turns out that we are missing yet another one lockfile processor. No problem. If we look closely, the new shiny yarn.lock v2 is a regular yaml with a little strange formatting like extra empty line delimiters, extra quotes, and so on.

import yaml from 'js-yaml'

export const parse = (raw: string): TLockfileObject => {
  const data = yaml.load(raw)
  delete data.__metadata

  return Object.entries(data).reduce<Record<string, any>>(
    (m, [key, value]: [string, any]) => {
      key.split(', ').forEach((k) => {
        m[k] = value
      })
      return m
    },
    {},
  )
}

export const format = (lockfile: TLockfileObject): string => {
  const keymap = Object.entries(lockfile).reduce<Record<string, any>>(
    (m, [k, { resolution }]) => {
      const entry = m[resolution] || (m[resolution] = [])
      entry.push(k)

      return m
    },
    {},
  )

  const data = Object.values(lockfile).reduce<Record<string, any>>(
    (m, value) => {
      const key = keymap[value.resolution].join(', ')
      m[key] = value

      return m
    },
    {
      __metadata: {
        version: 5,
        cacheKey: 8,
      },
    },
  )

  return `# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

${yaml.dump(data, {
  quotingType: '"',
  flowLevel: -1,
  lineWidth: -1,
})
  .replace(/\n([^\s"].+):\n/g, '\n"$1":\n')
  .replace(/\n(\S)/g, '\n\n$1')
  .replace(/resolution: ([^\n"]+)/g, 'resolution: "$1"')}`
}
Enter fullscreen mode Exit fullscreen mode

2.

We need to fetch audit data. Yarn Berry integrated audit API is much better than previous Classic, which returned the report in the form of chunks.
yarn npm audit --all --recursive --json gives exactly what we need:

{
  "actions": [],
  "advisories": {
    "1004946": {
      "findings": [
        {
          "version": "4.1.0",
          "paths": [
            "ts-patch>strip-ansi>ansi-regex",
            "lerna>npmlog>gauge>ansi-regex",
            "lerna>@lerna/bootstrap>npmlog>gauge>ansi-regex",
            ...
          ]
        }
      ],
      "metadata": null,
      "vulnerable_versions": ">2.1.1 <5.0.1",
      "module_name": "ansi-regex",
      "severity": "moderate",
      "github_advisory_id": "GHSA-93q8-gq69-wqmw",
      "cves": [
        "CVE-2021-3807"
      ],
      "access": "public",
      "patched_versions": ">=5.0.1",
      "updated": "2021-09-23T15:45:50.000Z",
      "recommendation": "Upgrade to version 5.0.1 or later",
      "cwe": "CWE-918",
      "found_by": null,
      "deleted": null,
      "id": 1004946,
      "references": "- https://nvd.nist.gov/vuln/detail/CVE-2021-3807\n- https://github.com/chalk/ansi-regex/commit/8d1d7cdb586269882c4bdc1b7325d0c58c8f76f9\n- https://huntr.dev/bounties/5b3cf33b-ede0-4398-9974-800876dfd994\n- https://github.com/chalk/ansi-regex/issues/38#issuecomment-924086311\n- https://app.snyk.io/vuln/SNYK-JS-ANSIREGEX-1583908\n- https://github.com/chalk/ansi-regex/issues/38#issuecomment-925924774\n- https://github.com/advisories/GHSA-93q8-gq69-wqmw",
      "created": "2021-11-18T16:00:48.472Z",
      "reported_by": null,
      "title": " Inefficient Regular Expression Complexity in chalk/ansi-regex",
      "npm_advisory_id": null,
      "overview": "ansi-regex is vulnerable to Inefficient Regular Expression Complexity",
      "url": "https://github.com/advisories/GHSA-93q8-gq69-wqmw"
    },
Enter fullscreen mode Exit fullscreen mode

We take only significant fields: vulnerable_versions, module_name, patched_versions

export const parseAuditReport = (data: string): TAuditReport =>
  Object.values(JSON.parse(data).advisories).reduce<TAuditReport>(
    (m, { vulnerable_versions, module_name, patched_versions }: any) => {
      m[module_name] = {
        patched_versions,
        vulnerable_versions,
        module_name,
      }
      return m
    },
    {},
  )
Enter fullscreen mode Exit fullscreen mode

3.

Almost done. Now we need to replace vulnerable packages versions in lockfile with the advisories, taking into account semver-compatibility and to remove the prev checksum fields. This brilliant idea was suggested by G. Kosev. Important note Seems like dependencies of the patched entries are not reloaded automatically by Yarn, so they need to be requested from the registry by hand: yarn npm info react --fields dependencies --json.

export const patchEntry = (
  entry: TLockfileEntry,
  name: string,
  newVersion: string,
  npmBin: string,
): TLockfileEntry => {
  entry.version = newVersion
  entry.resolution = `${name}@npm:${newVersion}`

  // NOTE seems like deps are not updated by `yarn mode='--update-lockfile'`, only checksums
  entry.dependencies =
    JSON.parse(
      invoke(
        npmBin,
        ['view', `${name}@${newVersion}`, 'dependencies', '--json'],
        process.cwd(),
        true,
        false,
      ) || 'null',
    ) || undefined

  delete entry.checksum

  return entry
}

export const _patch = (
  lockfile: TLockfileObject,
  report: TAuditReport,
  { flags, bins }: TContext,
  lockfileType: TLockfileType,
): TLockfileObject => {
  if (Object.keys(report).length === 0) {
    !flags.silent && console.log('Audit check found no issues')
    return lockfile
  }

  const upgraded: string[] = []

  for (const depSpec of Object.keys(lockfile)) {
    // @babel/code-frame@^7.0.0
    // @babel/code-frame@npm:^7.0.0

    const [, pkgName, desiredRange] =
      /^(@?[^@]+)@(?:\w+:)?(.+)$/.exec(depSpec) || []

    const pkgAudit = report[pkgName]
    if (!pkgAudit) continue
    const pkgSpec = lockfile[depSpec]
    if (sv.satisfies(pkgSpec.version, pkgAudit.vulnerable_versions)) {
      const fix = sv.minVersion(pkgAudit.patched_versions)?.format()
      if (fix === undefined) {
        console.error(
          "Can't find satisfactory version for",
          pkgAudit.module_name,
          pkgAudit.patched_versions,
        )
        continue
      }
      if (!sv.satisfies(fix, desiredRange) && !flags.force) {
        console.error(
          "Can't find patched version that satisfies",
          depSpec,
          'in',
          pkgAudit.patched_versions,
        )
        continue
      }
      upgraded.push(`${pkgName}@${fix}`)

      patchEntry(pkgSpec, pkgName, fix, bins.npm)
    }
  }
Enter fullscreen mode Exit fullscreen mode

4.

The final step is to update the checksums.

yarn install mode='--update-lockfile'
Enter fullscreen mode Exit fullscreen mode

Refs

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay