Skip to content

Commit ab2402b

Browse files
committed
feat(plugin-js-packages): add Yarn v2 audit, improve plugin title
1 parent 1feaf05 commit ab2402b

File tree

9 files changed

+229
-61
lines changed

9 files changed

+229
-61
lines changed

packages/plugin-js-packages/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ This plugin checks for known vulnerabilities and outdated dependencies.
1010
It supports the following package managers:
1111

1212
- [NPM](https://docs.npmjs.com/)
13-
- [Yarn v1](https://classic.yarnpkg.com/docs/)[Yarn v2+](https://yarnpkg.com/getting-started)
13+
- [Yarn v1](https://classic.yarnpkg.com/docs/)
14+
- [Yarn v2+](https://yarnpkg.com/getting-started)
15+
- In order to check outdated dependencies for Yarn v2+, you need to install [`yarn-plugin-outdated`](https://github.com/mskelton/yarn-plugin-outdated).
1416
- [PNPM](https://pnpm.io/pnpm-cli)
1517

1618
> ![NOTE]
17-
> As of now, in order to check outdated dependencies for Yarn v2+, you need to install [`yarn-plugin-outdated`](https://github.com/mskelton/yarn-plugin-outdated).
19+
> As of now, Yarn v2 does not support security audit of optional dependencies. Only production and dev dependencies audits will be included in the report.
1820
1921
## Getting started
2022

@@ -100,7 +102,7 @@ The plugin accepts the following parameters:
100102

101103
### Audits and group
102104

103-
This plugin provides a group per check for a convenient declaration in your config.
105+
This plugin provides a group per check for a convenient declaration in your config. Each group contains audits for all supported groups of dependencies (`prod`, `dev` and `optional`).
104106

105107
```ts
106108
// ...

packages/plugin-js-packages/src/lib/js-packages-plugin.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ export async function jsPackagesPlugin(
5050

5151
return {
5252
slug: 'js-packages',
53-
title: 'Plugin for JS packages',
53+
title: 'JS Packages',
5454
icon: pkgManagerIcons[pkgManager],
5555
description:
56-
'This plugin runs audit to uncover vulnerabilities and lists outdated dependencies. It supports npm, yarn classic and berry, pnpm package managers.',
56+
'This plugin runs audit to uncover vulnerabilities and lists outdated dependencies. It supports npm, yarn classic, yarn modern, and pnpm package managers.',
5757
docsUrl: pkgManagerDocs[pkgManager],
5858
packageName: name,
5959
version,
@@ -75,9 +75,17 @@ function createGroups(
7575
docsUrl: auditDocs[pkgManager],
7676
refs: [
7777
// eslint-disable-next-line no-magic-numbers
78-
{ slug: `${pkgManager}-audit-prod`, weight: 8 },
78+
{ slug: `${pkgManager}-audit-prod`, weight: 3 },
7979
{ slug: `${pkgManager}-audit-dev`, weight: 1 },
80-
{ slug: `${pkgManager}-audit-optional`, weight: 1 },
80+
// Yarn v2 does not support audit for optional dependencies
81+
...(pkgManager === 'yarn-modern'
82+
? []
83+
: [
84+
{
85+
slug: `${pkgManager}-audit-optional`,
86+
weight: 1,
87+
},
88+
]),
8189
],
8290
},
8391
outdated: {
@@ -87,7 +95,7 @@ function createGroups(
8795
docsUrl: outdatedDocs[pkgManager],
8896
refs: [
8997
// eslint-disable-next-line no-magic-numbers
90-
{ slug: `${pkgManager}-outdated-prod`, weight: 8 },
98+
{ slug: `${pkgManager}-outdated-prod`, weight: 3 },
9199
{ slug: `${pkgManager}-outdated-dev`, weight: 1 },
92100
{ slug: `${pkgManager}-outdated-optional`, weight: 1 },
93101
],
@@ -114,12 +122,17 @@ function createAudits(
114122
description: getAuditDescription(check, 'dev'),
115123
docsUrl: dependencyDocs.dev,
116124
},
117-
{
118-
slug: `${pkgManager}-${check}-optional`,
119-
title: getAuditTitle(pkgManager, check, 'optional'),
120-
description: getAuditDescription(check, 'optional'),
121-
docsUrl: dependencyDocs.optional,
122-
},
125+
// Yarn v2 does not support audit for optional dependencies
126+
...(pkgManager === 'yarn-modern' && check === 'audit'
127+
? []
128+
: [
129+
{
130+
slug: `${pkgManager}-${check}-optional`,
131+
title: getAuditTitle(pkgManager, check, 'optional'),
132+
description: getAuditDescription(check, 'optional'),
133+
docsUrl: dependencyDocs.optional,
134+
},
135+
]),
123136
]);
124137
}
125138

packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('jsPackagesPlugin', () => {
1616
).resolves.toStrictEqual(
1717
expect.objectContaining({
1818
slug: 'js-packages',
19-
title: 'Plugin for JS packages',
19+
title: 'JS Packages',
2020
audits: expect.any(Array),
2121
groups: expect.any(Array),
2222
runner: expect.any(Object),
@@ -69,7 +69,7 @@ describe('jsPackagesPlugin', () => {
6969
expect.objectContaining<Partial<Group>>({
7070
slug: 'yarn-classic-audit',
7171
refs: [
72-
{ slug: 'yarn-classic-audit-prod', weight: 8 },
72+
{ slug: 'yarn-classic-audit-prod', weight: 3 },
7373
{ slug: 'yarn-classic-audit-dev', weight: 1 },
7474
{ slug: 'yarn-classic-audit-optional', weight: 1 },
7575
],

packages/plugin-js-packages/src/lib/runner/audit/constants.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
} from '../../config';
66
import { dependencyGroupToLong } from '../../constants';
77
import { AuditResult } from './types';
8-
import { npmToAuditResult, yarnv1ToAuditResult } from './unify-type';
8+
import {
9+
npmToAuditResult,
10+
yarnv1ToAuditResult,
11+
yarnv2ToAuditResult,
12+
} from './unify-type';
913

1014
/* eslint-disable no-magic-numbers */
1115
export const auditScoreModifiers: Record<PackageAuditLevel, number> = {
@@ -23,10 +27,7 @@ export const normalizeAuditMapper: Record<
2327
> = {
2428
npm: npmToAuditResult,
2529
'yarn-classic': yarnv1ToAuditResult,
26-
// eslint-disable-next-line @typescript-eslint/naming-convention
27-
'yarn-modern': () => {
28-
throw new Error('Yarn v2+ audit is not supported yet.');
29-
},
30+
'yarn-modern': yarnv2ToAuditResult,
3031
pnpm: () => {
3132
throw new Error('PNPM audit is not supported yet.');
3233
},
@@ -38,12 +39,24 @@ const npmDependencyOptions: Record<DependencyGroup, string[]> = {
3839
optional: ['--include=optional', '--omit=dev'],
3940
};
4041

42+
// Yarn v2 does not currently audit optional dependencies
43+
// see https://github.com/yarnpkg/berry/blob/master/packages/plugin-npm-cli/sources/npmAuditTypes.ts#L5
44+
const yarnv2EnvironmentOptions: Record<DependencyGroup, string> = {
45+
prod: 'production',
46+
dev: 'development',
47+
optional: '',
48+
};
49+
4150
export const auditArgs = (
4251
groupDep: DependencyGroup,
4352
): Record<PackageManager, string[]> => ({
4453
npm: [...npmDependencyOptions[groupDep], '--json', '--audit-level=none'],
45-
'yarn-classic': ['--json', `--groups ${dependencyGroupToLong[groupDep]}`],
46-
// TODO: Add once the package managers are supported.
47-
'yarn-modern': [],
54+
'yarn-classic': ['--json', '--groups', dependencyGroupToLong[groupDep]],
55+
'yarn-modern': [
56+
'--json',
57+
'--environment',
58+
yarnv2EnvironmentOptions[groupDep],
59+
],
60+
// TODO: Add once PNPM is supported.
4861
pnpm: [],
4962
});

packages/plugin-js-packages/src/lib/runner/audit/transform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function vulnerabilitiesToIssues(
6969
return [];
7070
}
7171

72-
return Object.values(vulnerabilities).map((detail): Issue => {
72+
return vulnerabilities.map((detail): Issue => {
7373
const versionRange =
7474
detail.versionRange === '*'
7575
? '**all** versions'

packages/plugin-js-packages/src/lib/runner/audit/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,21 @@ export type Yarnv1AuditResultJson = [
8181
...Yarnv1AuditAdvisory[],
8282
Yarnv1AuditSummary,
8383
];
84+
85+
// Subset of Yarn v2+ audit JSON type
86+
/* eslint-disable @typescript-eslint/naming-convention */
87+
export type Yarnv2AuditAdvisory = {
88+
module_name: string;
89+
severity: PackageAuditLevel;
90+
vulnerable_versions: string;
91+
recommendation: string;
92+
title: string;
93+
url: string;
94+
findings: { paths: string[] }[]; // TODO indirect?
95+
};
96+
/* eslint-enable @typescript-eslint/naming-convention */
97+
98+
export type Yarnv2AuditResultJson = {
99+
advisories: Record<string, Yarnv2AuditAdvisory>;
100+
metadata: { vulnerabilities: Record<PackageAuditLevel, number> };
101+
};

packages/plugin-js-packages/src/lib/runner/audit/unify-type.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Yarnv1AuditAdvisory,
1111
Yarnv1AuditResultJson,
1212
Yarnv1AuditSummary,
13+
Yarnv2AuditResultJson,
1314
} from './types';
1415

1516
export function npmToAuditResult(output: string): AuditResult {
@@ -108,21 +109,27 @@ export function yarnv1ToAuditResult(output: string): AuditResult {
108109

109110
const vulnerabilities = yarnv1Advisory.map(
110111
({ data: { resolution, advisory } }): Vulnerability => {
111-
const directDependency = resolution.path.slice(
112-
0,
113-
resolution.path.indexOf('>'),
114-
);
112+
const { id, path } = resolution;
113+
const directDependency = path.slice(0, path.indexOf('>'));
114+
115+
const {
116+
module_name: name,
117+
title,
118+
url,
119+
severity,
120+
vulnerable_versions: versionRange,
121+
recommendation: fixInformation,
122+
} = advisory;
115123

116124
return {
117-
name: advisory.module_name,
118-
title: advisory.title,
119-
id: resolution.id,
120-
url: advisory.url,
121-
severity: advisory.severity,
122-
versionRange: advisory.vulnerable_versions,
123-
directDependency:
124-
advisory.module_name === directDependency ? true : directDependency,
125-
fixInformation: advisory.recommendation,
125+
name,
126+
title,
127+
id,
128+
url,
129+
severity,
130+
versionRange,
131+
directDependency: name === directDependency ? true : directDependency,
132+
fixInformation,
126133
};
127134
},
128135
);
@@ -153,3 +160,40 @@ function validateYarnv1Result(
153160

154161
return [vulnerabilities, summary];
155162
}
163+
164+
export function yarnv2ToAuditResult(output: string): AuditResult {
165+
const yarnv2Audit = JSON.parse(output) as Yarnv2AuditResultJson;
166+
167+
const vulnerabilities = Object.values(yarnv2Audit.advisories).map(
168+
({
169+
module_name: name,
170+
severity,
171+
title,
172+
url,
173+
vulnerable_versions: versionRange,
174+
recommendation: fixInformation,
175+
findings,
176+
}): Vulnerability => {
177+
// TODO missing example of an indirect dependency to verify this
178+
const directDep = findings[0]?.paths[0];
179+
return {
180+
name,
181+
severity,
182+
title,
183+
url,
184+
versionRange,
185+
fixInformation,
186+
directDependency:
187+
directDep != null && directDep !== name ? directDep : true,
188+
};
189+
},
190+
);
191+
192+
const total = Object.values(yarnv2Audit.metadata.vulnerabilities).reduce(
193+
(acc, value) => acc + value,
194+
0,
195+
);
196+
const summary = { ...yarnv2Audit.metadata.vulnerabilities, total };
197+
198+
return { vulnerabilities, summary };
199+
}

packages/plugin-js-packages/src/lib/runner/audit/unify-type.unit.test.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import {
77
NpmVulnerability,
88
Yarnv1AuditAdvisory,
99
Yarnv1AuditSummary,
10+
Yarnv2AuditResultJson,
1011
} from './types';
1112
import {
1213
npmToAdvisory,
1314
npmToAuditResult,
1415
npmToFixInformation,
1516
yarnv1ToAuditResult,
17+
yarnv2ToAuditResult,
1618
} from './unify-type';
1719

1820
describe('npmToAuditResult', () => {
@@ -265,14 +267,7 @@ describe('yarnv1ToAuditResult', () => {
265267
url: 'https://github.com/advisories',
266268
},
267269
],
268-
summary: {
269-
critical: 0,
270-
high: 0,
271-
moderate: 1,
272-
low: 0,
273-
info: 0,
274-
total: 1,
275-
},
270+
summary: { critical: 0, high: 0, moderate: 1, low: 0, info: 0, total: 1 },
276271
});
277272
});
278273

@@ -286,3 +281,69 @@ describe('yarnv1ToAuditResult', () => {
286281
);
287282
});
288283
});
284+
285+
describe('yarnv2ToAuditResult', () => {
286+
it('should transform Yarn v2 audit to unified audit result', () => {
287+
expect(
288+
yarnv2ToAuditResult(
289+
JSON.stringify({
290+
advisories: {
291+
'123': {
292+
module_name: 'nx',
293+
severity: 'high',
294+
title: 'DoS',
295+
url: 'https://github.com/advisories',
296+
recommendation: 'Update nx to 17.0.0',
297+
vulnerable_versions: '<17.0.0',
298+
findings: [{ paths: ['nx'] }],
299+
},
300+
},
301+
metadata: {
302+
vulnerabilities: {
303+
critical: 0,
304+
high: 1,
305+
moderate: 0,
306+
low: 0,
307+
info: 0,
308+
},
309+
},
310+
} satisfies Yarnv2AuditResultJson),
311+
),
312+
).toEqual<AuditResult>({
313+
vulnerabilities: [
314+
{
315+
name: 'nx',
316+
severity: 'high',
317+
title: 'DoS',
318+
url: 'https://github.com/advisories',
319+
fixInformation: 'Update nx to 17.0.0',
320+
versionRange: '<17.0.0',
321+
directDependency: true,
322+
},
323+
],
324+
summary: { critical: 0, high: 1, moderate: 0, low: 0, info: 0, total: 1 },
325+
});
326+
});
327+
328+
it('should return empty report if no vulnerabilities found', () => {
329+
expect(
330+
yarnv2ToAuditResult(
331+
JSON.stringify({
332+
advisories: {},
333+
metadata: {
334+
vulnerabilities: {
335+
critical: 0,
336+
high: 0,
337+
moderate: 0,
338+
low: 0,
339+
info: 0,
340+
},
341+
},
342+
}),
343+
),
344+
).toStrictEqual<AuditResult>({
345+
vulnerabilities: [],
346+
summary: { critical: 0, high: 0, moderate: 0, low: 0, info: 0, total: 0 },
347+
});
348+
});
349+
});

0 commit comments

Comments
 (0)