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
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"')}`
}
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"
},
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
},
{},
)
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)
}
}
4.
The final step is to update the checksums.
yarn install mode='--update-lockfile'
Top comments (0)