diff --git a/packages/plugin-js-packages/src/lib/package-managers/package-managers.ts b/packages/plugin-js-packages/src/lib/package-managers/package-managers.ts index 8772da89a..8a093b7f1 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/package-managers.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/package-managers.ts @@ -2,12 +2,12 @@ import type { PackageManagerId } from '../config.js'; import { npmPackageManager } from './npm/npm.js'; import { pnpmPackageManager } from './pnpm/pnpm.js'; import type { PackageManager } from './types.js'; -import { yarnv1PackageManager } from './yarn-classic/yarn-classic.js'; -import { yarnv2PackageManager } from './yarn-modern/yarn-modern.js'; +import { yarnClassicPackageManager } from './yarn-classic/yarn-classic.js'; +import { yarnModernPackageManager } from './yarn-modern/yarn-modern.js'; export const packageManagers: Record = { npm: npmPackageManager, - 'yarn-classic': yarnv1PackageManager, - 'yarn-modern': yarnv2PackageManager, + 'yarn-classic': yarnClassicPackageManager, + 'yarn-modern': yarnModernPackageManager, pnpm: pnpmPackageManager, }; diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.ts index 3c8f57af4..f31d07384 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.ts @@ -2,16 +2,16 @@ import { fromJsonLines } from '@code-pushup/utils'; import type { AuditResult, Vulnerability } from '../../runner/audit/types.js'; import { filterAuditResult } from '../../runner/utils.js'; import type { - Yarnv1AuditAdvisory, - Yarnv1AuditResultJson, - Yarnv1AuditSummary, + YarnClassicAuditAdvisory, + YarnClassicAuditResultJson, + YarnClassicAuditSummary, } from './types.js'; -export function yarnv1ToAuditResult(output: string): AuditResult { - const yarnv1Result = fromJsonLines(output); - const [yarnv1Advisory, yarnv1Summary] = validateYarnv1Result(yarnv1Result); +export function yarnClassicToAuditResult(output: string): AuditResult { + const yarnResult = fromJsonLines(output); + const [yarnAdvisories, yarnSummary] = validateYarnAuditResult(yarnResult); - const vulnerabilities = yarnv1Advisory.map( + const vulnerabilities = yarnAdvisories.map( ({ data: { resolution, advisory } }): Vulnerability => { const { id, path } = resolution; const directDependency = path.slice(0, path.indexOf('>')); @@ -39,8 +39,8 @@ export function yarnv1ToAuditResult(output: string): AuditResult { ); const summary = { - ...yarnv1Summary.data.vulnerabilities, - total: Object.values(yarnv1Summary.data.vulnerabilities).reduce( + ...yarnSummary.data.vulnerabilities, + total: Object.values(yarnSummary.data.vulnerabilities).reduce( (acc, amount) => acc + amount, 0, ), @@ -50,17 +50,15 @@ export function yarnv1ToAuditResult(output: string): AuditResult { return filterAuditResult({ vulnerabilities, summary }, 'id'); } -function validateYarnv1Result( - result: Yarnv1AuditResultJson, -): [Yarnv1AuditAdvisory[], Yarnv1AuditSummary] { +function validateYarnAuditResult( + result: YarnClassicAuditResultJson, +): [YarnClassicAuditAdvisory[], YarnClassicAuditSummary] { const summary = result.at(-1); if (summary?.type !== 'auditSummary') { throw new Error('Invalid Yarn v1 audit result - no summary found.'); } - const vulnerabilities = result.filter( - (item): item is Yarnv1AuditAdvisory => item.type === 'auditAdvisory', - ); + const vulnerabilities = result.filter(item => item.type === 'auditAdvisory'); return [vulnerabilities, summary]; } diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.unit.test.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.unit.test.ts index 5c0a393d2..bb25b0c5d 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/audit-result.unit.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from 'vitest'; import { toJsonLines } from '@code-pushup/utils'; import type { AuditResult } from '../../runner/audit/types.js'; -import { yarnv1ToAuditResult } from './audit-result.js'; -import type { Yarnv1AuditAdvisory, Yarnv1AuditSummary } from './types.js'; +import { yarnClassicToAuditResult } from './audit-result.js'; +import type { + YarnClassicAuditAdvisory, + YarnClassicAuditSummary, +} from './types.js'; -describe('yarnv1ToAuditResult', () => { +describe('yarnClassicToAuditResult', () => { it('should transform Yarn v1 audit to unified audit result', () => { const advisory = { type: 'auditAdvisory', @@ -19,7 +22,7 @@ describe('yarnv1ToAuditResult', () => { url: 'https://github.com/advisories', }, }, - } satisfies Yarnv1AuditAdvisory; + } satisfies YarnClassicAuditAdvisory; const summary = { type: 'auditSummary', data: { @@ -31,10 +34,10 @@ describe('yarnv1ToAuditResult', () => { info: 0, }, }, - } satisfies Yarnv1AuditSummary; + } satisfies YarnClassicAuditSummary; expect( - yarnv1ToAuditResult(toJsonLines([advisory, summary])), + yarnClassicToAuditResult(toJsonLines([advisory, summary])), ).toEqual({ vulnerabilities: [ { @@ -57,7 +60,7 @@ describe('yarnv1ToAuditResult', () => { data: {}, type: 'auditAdvisory', }; - expect(() => yarnv1ToAuditResult(toJsonLines([advisory]))).toThrow( + expect(() => yarnClassicToAuditResult(toJsonLines([advisory]))).toThrow( 'no summary found', ); }); diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts index 414cdbca9..2f3b0351b 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts @@ -1,9 +1,9 @@ import type { OutdatedDependency } from '../../runner/outdated/types.js'; -import type { Yarnv1FieldName } from './types.js'; +import type { YarnClassicFieldName } from './types.js'; export const outdatedtoFieldMapper: Record< keyof OutdatedDependency, - Yarnv1FieldName + YarnClassicFieldName > = { name: 'Package', current: 'Current', @@ -12,7 +12,7 @@ export const outdatedtoFieldMapper: Record< url: 'URL', }; -export const REQUIRED_OUTDATED_FIELDS: Yarnv1FieldName[] = [ +export const REQUIRED_OUTDATED_FIELDS: YarnClassicFieldName[] = [ 'Package', 'Current', 'Latest', diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts index ca29ac1d0..ace6cf9ea 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts @@ -13,15 +13,15 @@ import { outdatedtoFieldMapper, } from './constants.js'; import { - type Yarnv1FieldName, - type Yarnv1OutdatedResultJson, - yarnv1FieldNames, + type YarnClassicFieldName, + type YarnClassicOutdatedResultJson, + yarnClassicFieldNames, } from './types.js'; -export function yarnv1ToOutdatedResult(output: string): OutdatedResult { - const yarnv1Outdated = fromJsonLines(output); - const fields = yarnv1Outdated[1]?.data.head ?? []; - const dependencies = yarnv1Outdated[1]?.data.body ?? []; +export function yarnClassicToOutdatedResult(output: string): OutdatedResult { + const yarnOutdated = fromJsonLines(output); + const fields = yarnOutdated[1]?.data.head ?? []; + const dependencies = yarnOutdated[1]?.data.body ?? []; // no outdated dependencies if (dependencies.length === 0) { @@ -46,7 +46,7 @@ export function yarnv1ToOutdatedResult(output: string): OutdatedResult { } export function validateOutdatedFields(head: string[]) { - const relevantFields = head.filter(isYarnv1FieldName); + const relevantFields = head.filter(isYarnClassicFieldName); if (hasAllRequiredFields(relevantFields)) { return true; } @@ -54,16 +54,16 @@ export function validateOutdatedFields(head: string[]) { throw new Error( `Yarn v1 outdated: Template [${head.join( ', ', - )}] does not contain all required fields [${yarnv1FieldNames.join(', ')}]`, + )}] does not contain all required fields [${yarnClassicFieldNames.join(', ')}]`, ); } -function isYarnv1FieldName(value: string): value is Yarnv1FieldName { - const names: readonly string[] = yarnv1FieldNames; +function isYarnClassicFieldName(value: string): value is YarnClassicFieldName { + const names: readonly string[] = yarnClassicFieldNames; return names.includes(value); } -function hasAllRequiredFields(head: Yarnv1FieldName[]) { +function hasAllRequiredFields(head: YarnClassicFieldName[]) { return REQUIRED_OUTDATED_FIELDS.every(field => head.includes(field)); } diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts index c47113e64..25355e4ec 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts @@ -8,11 +8,11 @@ import { REQUIRED_OUTDATED_FIELDS } from './constants.js'; import { getOutdatedFieldIndexes, validateOutdatedFields, - yarnv1ToOutdatedResult, + yarnClassicToOutdatedResult, } from './outdated-result.js'; -import type { Yarnv1FieldName } from './types.js'; +import type { YarnClassicFieldName } from './types.js'; -describe('yarnv1ToOutdatedResult', () => { +describe('yarnClassicToOutdatedResult', () => { const yarnInfo = { type: 'info', data: 'Colours' }; it('should transform Yarn v1 outdated to unified outdated result', () => { @@ -25,13 +25,13 @@ describe('yarnv1ToOutdatedResult', () => { 'Latest', 'Package Type', 'URL', - ] satisfies Yarnv1FieldName[], + ] satisfies YarnClassicFieldName[], body: [['nx', '16.8.1', '17.0.0', 'dependencies', 'https://nx.dev/']], }, }; expect( - yarnv1ToOutdatedResult(toJsonLines([yarnInfo, table])), + yarnClassicToOutdatedResult(toJsonLines([yarnInfo, table])), ).toEqual([ { name: 'nx', @@ -62,7 +62,7 @@ describe('yarnv1ToOutdatedResult', () => { }; expect( - yarnv1ToOutdatedResult(toJsonLines([yarnInfo, table])), + yarnClassicToOutdatedResult(toJsonLines([yarnInfo, table])), ).toEqual([ { name: 'cypress', @@ -76,7 +76,9 @@ describe('yarnv1ToOutdatedResult', () => { it('should transform no dependencies to empty array', () => { const table = { type: 'table', data: { head: [], body: [] } }; - expect(yarnv1ToOutdatedResult(toJsonLines([yarnInfo, table]))).toEqual([]); + expect(yarnClassicToOutdatedResult(toJsonLines([yarnInfo, table]))).toEqual( + [], + ); }); }); diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts index 5d1461a9a..0419a6952 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts @@ -1,7 +1,7 @@ import type { PackageAuditLevel } from '../../config.js'; // Subset of Yarn v1 audit JSON type -export type Yarnv1AuditAdvisory = { +export type YarnClassicAuditAdvisory = { type: 'auditAdvisory'; data: { resolution: { @@ -19,20 +19,19 @@ export type Yarnv1AuditAdvisory = { }; }; -export type Yarnv1AuditSummary = { +export type YarnClassicAuditSummary = { type: 'auditSummary'; data: { vulnerabilities: Record; }; }; -// NOTE: When rest operator can be at the beginning, the process will be much simpler -export type Yarnv1AuditResultJson = [ - ...Yarnv1AuditAdvisory[], - Yarnv1AuditSummary, +export type YarnClassicAuditResultJson = [ + ...YarnClassicAuditAdvisory[], + YarnClassicAuditSummary, ]; -export const yarnv1FieldNames = [ +export const yarnClassicFieldNames = [ 'Package', 'Current', 'Latest', @@ -40,10 +39,10 @@ export const yarnv1FieldNames = [ 'URL', ] as const; -export type Yarnv1FieldName = (typeof yarnv1FieldNames)[number]; +export type YarnClassicFieldName = (typeof yarnClassicFieldNames)[number]; -type Yarnv1Info = { type: 'info' }; -type Yarnv1Table = { +type YarnClassicInfo = { type: 'info' }; +type YarnClassicTable = { type: 'table'; data: { head: string[]; @@ -51,4 +50,6 @@ type Yarnv1Table = { }; }; -export type Yarnv1OutdatedResultJson = [Yarnv1Info, Yarnv1Table] | []; +export type YarnClassicOutdatedResultJson = + | [YarnClassicInfo, YarnClassicTable] + | []; diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/yarn-classic.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/yarn-classic.ts index af6a7d08f..ce716b688 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/yarn-classic.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/yarn-classic.ts @@ -1,10 +1,10 @@ import { dependencyGroupToLong } from '../../constants.js'; import { COMMON_AUDIT_ARGS, COMMON_OUTDATED_ARGS } from '../constants.js'; import type { PackageManager } from '../types.js'; -import { yarnv1ToAuditResult } from './audit-result.js'; -import { yarnv1ToOutdatedResult } from './outdated-result.js'; +import { yarnClassicToAuditResult } from './audit-result.js'; +import { yarnClassicToOutdatedResult } from './outdated-result.js'; -export const yarnv1PackageManager: PackageManager = { +export const yarnClassicPackageManager: PackageManager = { slug: 'yarn-classic', name: 'Yarn v1', command: 'yarn', @@ -21,10 +21,10 @@ export const yarnv1PackageManager: PackageManager = { dependencyGroupToLong[groupDep], ], ignoreExitCode: true, - unifyResult: yarnv1ToAuditResult, + unifyResult: yarnClassicToAuditResult, }, outdated: { commandArgs: COMMON_OUTDATED_ARGS, - unifyResult: yarnv1ToOutdatedResult, + unifyResult: yarnClassicToOutdatedResult, }, }; diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.ts index f86ac8671..a8cb01d2a 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.ts @@ -1,11 +1,34 @@ +import { fromJsonLines } from '@code-pushup/utils'; import type { AuditResult, Vulnerability } from '../../runner/audit/types.js'; -import { getVulnerabilitiesTotal } from '../../runner/audit/utils.js'; -import type { Yarnv2AuditResultJson } from './types.js'; +import { + getVulnerabilitiesTotal, + summaryStatsFromVulnerabilities, +} from '../../runner/audit/utils.js'; +import type { + YarnBerry2or3AuditResultJson, + YarnBerry4AuditResultJson, +} from './types.js'; -export function yarnv2ToAuditResult(output: string): AuditResult { - const yarnv2Audit = JSON.parse(output) as Yarnv2AuditResultJson; +export function yarnBerryToAuditResult(output: string): AuditResult { + const json = fromJsonLines< + [YarnBerry2or3AuditResultJson] | YarnBerry4AuditResultJson + >(output); - const vulnerabilities = Object.values(yarnv2Audit.advisories).map( + if (json.length === 1 && 'advisories' in json[0] && 'metadata' in json[0]) { + return transformYarn2or3(json[0]); + } + + if (json.every(item => 'value' in item && 'children' in item)) { + return transformYarn4(json); + } + + throw new Error( + `Unknown output format from 'yarn npm audit --json':\n${output}`, + ); +} + +function transformYarn2or3(json: YarnBerry2or3AuditResultJson): AuditResult { + const vulnerabilities = Object.values(json.advisories).map( ({ module_name: name, severity, @@ -33,8 +56,28 @@ export function yarnv2ToAuditResult(output: string): AuditResult { return { vulnerabilities, summary: { - ...yarnv2Audit.metadata.vulnerabilities, - total: getVulnerabilitiesTotal(yarnv2Audit.metadata.vulnerabilities), + ...json.metadata.vulnerabilities, + total: getVulnerabilitiesTotal(json.metadata.vulnerabilities), }, }; } + +function transformYarn4(json: YarnBerry4AuditResultJson): AuditResult { + const vulnerabilities = json.map( + ({ value, children }): Vulnerability => ({ + name: value, + severity: children['Severity'], + title: children['Issue'], + url: children['URL'], + id: children['ID'], + versionRange: children['Vulnerable Versions'], + directDependency: + children['Dependents'].some(spec => spec.endsWith('@workspace:.')) || + '', + }), + ); + + const summary = summaryStatsFromVulnerabilities(vulnerabilities); + + return { vulnerabilities, summary }; +} diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.unit.test.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.unit.test.ts index cc3a2b23c..a466157f9 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/audit-result.unit.test.ts @@ -1,12 +1,15 @@ import { describe, expect, it } from 'vitest'; import type { AuditResult } from '../../runner/audit/types.js'; -import { yarnv2ToAuditResult } from './audit-result.js'; -import type { Yarnv2AuditResultJson } from './types.js'; +import { yarnBerryToAuditResult } from './audit-result.js'; +import type { + YarnBerry2or3AuditResultJson, + YarnBerry4AuditVulnerability, +} from './types.js'; -describe('yarnv2ToAuditResult', () => { +describe('yarnBerryToAuditResult', () => { it('should transform Yarn v2 audit to unified audit result', () => { expect( - yarnv2ToAuditResult( + yarnBerryToAuditResult( JSON.stringify({ advisories: { '123': { @@ -28,7 +31,7 @@ describe('yarnv2ToAuditResult', () => { info: 0, }, }, - } satisfies Yarnv2AuditResultJson), + } satisfies YarnBerry2or3AuditResultJson), ), ).toEqual({ vulnerabilities: [ @@ -46,25 +49,61 @@ describe('yarnv2ToAuditResult', () => { }); }); - it('should return empty report if no vulnerabilities found', () => { + it('should transform Yarn v4 audit to unified audit result', () => { + const vulnerabilities: YarnBerry4AuditVulnerability[] = [ + { + value: 'express', + children: { + ID: 1_096_820, + Issue: 'Express.js Open Redirect in malformed URLs', + URL: 'https://github.com/advisories/GHSA-rv95-896h-c2vc', + Severity: 'moderate', + 'Vulnerable Versions': '<4.19.2', + 'Tree Versions': ['3.21.2'], + Dependents: ['my-project@workspace:.'], + }, + }, + { + value: 'send', + children: { + ID: 1_100_526, + Issue: 'send vulnerable to template injection that can lead to XSS', + URL: 'https://github.com/advisories/GHSA-m6fv-jmcg-4jfg', + Severity: 'low', + 'Vulnerable Versions': '<0.19.0', + 'Tree Versions': ['0.13.0', '0.13.2'], + Dependents: ['express@npm:3.21.2', 'serve-static@npm:1.10.3'], + }, + }, + ]; expect( - yarnv2ToAuditResult( - JSON.stringify({ - advisories: {}, - metadata: { - vulnerabilities: { - critical: 0, - high: 0, - moderate: 0, - low: 0, - info: 0, - }, - }, - }), + yarnBerryToAuditResult( + vulnerabilities + .map(vulnerability => `${JSON.stringify(vulnerability)}\n`) + .join(''), ), - ).toStrictEqual({ - vulnerabilities: [], - summary: { critical: 0, high: 0, moderate: 0, low: 0, info: 0, total: 0 }, + ).toEqual({ + vulnerabilities: [ + { + name: 'express', + severity: 'moderate', + title: 'Express.js Open Redirect in malformed URLs', + url: 'https://github.com/advisories/GHSA-rv95-896h-c2vc', + id: 1_096_820, + versionRange: '<4.19.2', + directDependency: true, + }, + { + name: 'send', + severity: 'low', + title: 'send vulnerable to template injection that can lead to XSS', + url: 'https://github.com/advisories/GHSA-m6fv-jmcg-4jfg', + id: 1_100_526, + versionRange: '<0.19.0', + directDependency: '', + }, + ], + summary: { critical: 0, high: 0, moderate: 1, low: 1, info: 0, total: 2 }, }); }); }); diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.ts index 8bc52f71d..1be7e1e6b 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.ts @@ -1,8 +1,8 @@ import type { OutdatedResult } from '../../runner/outdated/types.js'; -import type { Yarnv2OutdatedResultJson } from './types.js'; +import type { YarnBerryOutdatedResultJson } from './types.js'; -export function yarnv2ToOutdatedResult(output: string): OutdatedResult { - const npmOutdated = JSON.parse(output) as Yarnv2OutdatedResultJson; +export function yarnBerryToOutdatedResult(output: string): OutdatedResult { + const npmOutdated = JSON.parse(output) as YarnBerryOutdatedResultJson; return npmOutdated.map(({ name, current, latest, type }) => ({ name, diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.unit.test.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.unit.test.ts index 87f20caf4..423e84ed4 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/outdated-result.unit.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { yarnv2ToOutdatedResult } from './outdated-result.js'; -import type { Yarnv2OutdatedResultJson } from './types.js'; +import { yarnBerryToOutdatedResult } from './outdated-result.js'; +import type { YarnBerryOutdatedResultJson } from './types.js'; -describe('yarnv2ToOutdatedResult', () => { +describe('yarnBerryToOutdatedResult', () => { it('should transform Yarn v2 outdated to unified outdated result', () => { const outdated = [ { @@ -17,14 +17,14 @@ describe('yarnv2ToOutdatedResult', () => { latest: '5.1.4', type: 'devDependencies', }, - ] satisfies Yarnv2OutdatedResultJson; + ] satisfies YarnBerryOutdatedResultJson; - expect(yarnv2ToOutdatedResult(JSON.stringify(outdated))).toStrictEqual( + expect(yarnBerryToOutdatedResult(JSON.stringify(outdated))).toStrictEqual( outdated, ); }); it('should transform no dependencies to empty array', () => { - expect(yarnv2ToOutdatedResult('[]')).toEqual([]); + expect(yarnBerryToOutdatedResult('[]')).toEqual([]); }); }); diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/types.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/types.ts index ef49ff2e0..7ddfd0569 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/types.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/types.ts @@ -1,8 +1,7 @@ -// Subset of Yarn v2+ audit JSON type import type { PackageAuditLevel } from '../../config.js'; import type { DependencyGroupLong } from '../../runner/outdated/types.js'; -export type Yarnv2AuditAdvisory = { +export type YarnBerry2or3AuditAdvisory = { module_name: string; severity: PackageAuditLevel; vulnerable_versions: string; @@ -12,13 +11,28 @@ export type Yarnv2AuditAdvisory = { findings: { paths: string[] }[]; // TODO indirect? }; -export type Yarnv2AuditResultJson = { - advisories: Record; +export type YarnBerry2or3AuditResultJson = { + advisories: Record; metadata: { vulnerabilities: Record }; }; +export type YarnBerry4AuditVulnerability = { + value: string; + children: { + ID: number; + Issue: string; + URL: string; + Severity: PackageAuditLevel; + 'Vulnerable Versions': string; + 'Tree Versions': string[]; + Dependents: string[]; + }; +}; + +export type YarnBerry4AuditResultJson = YarnBerry4AuditVulnerability[]; + // Subset of Yarn v2 outdated JSON type -export type Yarnv2VersionOverview = { +export type YarnBerryOutdatedPackage = { current: string; latest: string; name: string; @@ -26,4 +40,4 @@ export type Yarnv2VersionOverview = { workspace?: string; }; -export type Yarnv2OutdatedResultJson = Yarnv2VersionOverview[]; +export type YarnBerryOutdatedResultJson = YarnBerryOutdatedPackage[]; diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/yarn-modern.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/yarn-modern.ts index 0adca059d..fc6d20002 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/yarn-modern.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-modern/yarn-modern.ts @@ -2,17 +2,17 @@ import type { DependencyGroup } from '../../config.js'; import { COMMON_AUDIT_ARGS, COMMON_OUTDATED_ARGS } from '../constants.js'; import type { PackageManager } from '../types.js'; -import { yarnv2ToAuditResult } from './audit-result.js'; -import { yarnv2ToOutdatedResult } from './outdated-result.js'; +import { yarnBerryToAuditResult } from './audit-result.js'; +import { yarnBerryToOutdatedResult } from './outdated-result.js'; // see https://github.com/yarnpkg/berry/blob/master/packages/plugin-npm-cli/sources/npmAuditTypes.ts#L5 -const yarnv2EnvironmentOptions: Record = { +const yarnModernEnvironmentOptions: Record = { prod: 'production', dev: 'development', optional: '', }; -export const yarnv2PackageManager: PackageManager = { +export const yarnModernPackageManager: PackageManager = { slug: 'yarn-modern', name: 'yarn-modern', command: 'yarn', @@ -27,14 +27,14 @@ export const yarnv2PackageManager: PackageManager = { 'npm', ...COMMON_AUDIT_ARGS, '--environment', - yarnv2EnvironmentOptions[groupDep], + yarnModernEnvironmentOptions[groupDep], ], supportedDepGroups: ['prod', 'dev'], // Yarn v2 does not support audit for optional dependencies - unifyResult: yarnv2ToAuditResult, + unifyResult: yarnBerryToAuditResult, ignoreExitCode: true, }, outdated: { commandArgs: [...COMMON_OUTDATED_ARGS, '--workspace=.'], // filter out other packages in case of Yarn workspaces - unifyResult: yarnv2ToOutdatedResult, + unifyResult: yarnBerryToOutdatedResult, }, }; diff --git a/packages/plugin-js-packages/src/lib/runner/audit/types.ts b/packages/plugin-js-packages/src/lib/runner/audit/types.ts index 916d6db35..20cb43822 100644 --- a/packages/plugin-js-packages/src/lib/runner/audit/types.ts +++ b/packages/plugin-js-packages/src/lib/runner/audit/types.ts @@ -9,7 +9,7 @@ export type Vulnerability = { severity: PackageAuditLevel; versionRange: string; directDependency: string | true; // either name of direct dependency this one affects or true - fixInformation: string | false; // either guide on how to fix the vulnerability or false + fixInformation?: string | false; // either guide on how to fix the vulnerability or false }; export type AuditSummary = Record; export type AuditResult = { diff --git a/packages/plugin-js-packages/src/lib/runner/audit/utils.ts b/packages/plugin-js-packages/src/lib/runner/audit/utils.ts index 48a915b7c..5e502148f 100644 --- a/packages/plugin-js-packages/src/lib/runner/audit/utils.ts +++ b/packages/plugin-js-packages/src/lib/runner/audit/utils.ts @@ -1,7 +1,25 @@ -import type { PackageAuditLevel } from '../../config.js'; +import { objectFromEntries } from '@code-pushup/utils'; +import { type PackageAuditLevel, packageAuditLevels } from '../../config.js'; +import type { AuditSummary } from './types.js'; export function getVulnerabilitiesTotal( summary: Record, ): number { return Object.values(summary).reduce((acc, value) => acc + value, 0); } + +export function summaryStatsFromVulnerabilities( + vulnerabilities: { severity: PackageAuditLevel }[], +): AuditSummary { + const initial: AuditSummary = objectFromEntries( + ([...packageAuditLevels, 'total'] as const).map(key => [key, 0]), + ); + return vulnerabilities.reduce( + (acc, { severity }) => ({ + ...acc, + [severity]: acc[severity] + 1, + total: acc.total + 1, + }), + initial, + ); +} diff --git a/packages/plugin-js-packages/src/lib/runner/audit/utils.unit.test.ts b/packages/plugin-js-packages/src/lib/runner/audit/utils.unit.test.ts new file mode 100644 index 000000000..47cc4cdd9 --- /dev/null +++ b/packages/plugin-js-packages/src/lib/runner/audit/utils.unit.test.ts @@ -0,0 +1,23 @@ +import type { AuditSummary } from './types.js'; +import { summaryStatsFromVulnerabilities } from './utils.js'; + +describe('summaryStatsFromVulnerabilities', () => { + it('should count severity occurences and total', () => { + expect( + summaryStatsFromVulnerabilities([ + { severity: 'high' }, + { severity: 'moderate' }, + { severity: 'low' }, + { severity: 'moderate' }, + { severity: 'high' }, + ]), + ).toEqual({ + critical: 0, + high: 2, + moderate: 2, + low: 1, + info: 0, + total: 5, + } satisfies AuditSummary); + }); +});