Skip to content

Commit

Permalink
feat(plugin-js-packages): add Yarn v2 audit, improve plugin title
Browse files Browse the repository at this point in the history
  • Loading branch information
Tlacenka committed Mar 28, 2024
1 parent 1feaf05 commit ab2402b
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 61 deletions.
8 changes: 5 additions & 3 deletions packages/plugin-js-packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ This plugin checks for known vulnerabilities and outdated dependencies.
It supports the following package managers:

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

> ![NOTE]
> 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).
> 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.
## Getting started

Expand Down Expand Up @@ -100,7 +102,7 @@ The plugin accepts the following parameters:

### Audits and group

This plugin provides a group per check for a convenient declaration in your config.
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`).

```ts
// ...
Expand Down
35 changes: 24 additions & 11 deletions packages/plugin-js-packages/src/lib/js-packages-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ export async function jsPackagesPlugin(

return {
slug: 'js-packages',
title: 'Plugin for JS packages',
title: 'JS Packages',
icon: pkgManagerIcons[pkgManager],
description:
'This plugin runs audit to uncover vulnerabilities and lists outdated dependencies. It supports npm, yarn classic and berry, pnpm package managers.',
'This plugin runs audit to uncover vulnerabilities and lists outdated dependencies. It supports npm, yarn classic, yarn modern, and pnpm package managers.',
docsUrl: pkgManagerDocs[pkgManager],
packageName: name,
version,
Expand All @@ -75,9 +75,17 @@ function createGroups(
docsUrl: auditDocs[pkgManager],
refs: [
// eslint-disable-next-line no-magic-numbers
{ slug: `${pkgManager}-audit-prod`, weight: 8 },
{ slug: `${pkgManager}-audit-prod`, weight: 3 },
{ slug: `${pkgManager}-audit-dev`, weight: 1 },
{ slug: `${pkgManager}-audit-optional`, weight: 1 },
// Yarn v2 does not support audit for optional dependencies
...(pkgManager === 'yarn-modern'
? []
: [
{
slug: `${pkgManager}-audit-optional`,
weight: 1,
},
]),
],
},
outdated: {
Expand All @@ -87,7 +95,7 @@ function createGroups(
docsUrl: outdatedDocs[pkgManager],
refs: [
// eslint-disable-next-line no-magic-numbers
{ slug: `${pkgManager}-outdated-prod`, weight: 8 },
{ slug: `${pkgManager}-outdated-prod`, weight: 3 },
{ slug: `${pkgManager}-outdated-dev`, weight: 1 },
{ slug: `${pkgManager}-outdated-optional`, weight: 1 },
],
Expand All @@ -114,12 +122,17 @@ function createAudits(
description: getAuditDescription(check, 'dev'),
docsUrl: dependencyDocs.dev,
},
{
slug: `${pkgManager}-${check}-optional`,
title: getAuditTitle(pkgManager, check, 'optional'),
description: getAuditDescription(check, 'optional'),
docsUrl: dependencyDocs.optional,
},
// Yarn v2 does not support audit for optional dependencies
...(pkgManager === 'yarn-modern' && check === 'audit'
? []
: [
{
slug: `${pkgManager}-${check}-optional`,
title: getAuditTitle(pkgManager, check, 'optional'),
description: getAuditDescription(check, 'optional'),
docsUrl: dependencyDocs.optional,
},
]),
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('jsPackagesPlugin', () => {
).resolves.toStrictEqual(
expect.objectContaining({
slug: 'js-packages',
title: 'Plugin for JS packages',
title: 'JS Packages',
audits: expect.any(Array),
groups: expect.any(Array),
runner: expect.any(Object),
Expand Down Expand Up @@ -69,7 +69,7 @@ describe('jsPackagesPlugin', () => {
expect.objectContaining<Partial<Group>>({
slug: 'yarn-classic-audit',
refs: [
{ slug: 'yarn-classic-audit-prod', weight: 8 },
{ slug: 'yarn-classic-audit-prod', weight: 3 },
{ slug: 'yarn-classic-audit-dev', weight: 1 },
{ slug: 'yarn-classic-audit-optional', weight: 1 },
],
Expand Down
29 changes: 21 additions & 8 deletions packages/plugin-js-packages/src/lib/runner/audit/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
} from '../../config';
import { dependencyGroupToLong } from '../../constants';
import { AuditResult } from './types';
import { npmToAuditResult, yarnv1ToAuditResult } from './unify-type';
import {
npmToAuditResult,
yarnv1ToAuditResult,
yarnv2ToAuditResult,
} from './unify-type';

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

// Yarn v2 does not currently audit optional dependencies
// see https://github.com/yarnpkg/berry/blob/master/packages/plugin-npm-cli/sources/npmAuditTypes.ts#L5
const yarnv2EnvironmentOptions: Record<DependencyGroup, string> = {
prod: 'production',
dev: 'development',
optional: '',
};

export const auditArgs = (
groupDep: DependencyGroup,
): Record<PackageManager, string[]> => ({
npm: [...npmDependencyOptions[groupDep], '--json', '--audit-level=none'],
'yarn-classic': ['--json', `--groups ${dependencyGroupToLong[groupDep]}`],
// TODO: Add once the package managers are supported.
'yarn-modern': [],
'yarn-classic': ['--json', '--groups', dependencyGroupToLong[groupDep]],
'yarn-modern': [
'--json',
'--environment',
yarnv2EnvironmentOptions[groupDep],
],
// TODO: Add once PNPM is supported.
pnpm: [],
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function vulnerabilitiesToIssues(
return [];
}

return Object.values(vulnerabilities).map((detail): Issue => {
return vulnerabilities.map((detail): Issue => {
const versionRange =
detail.versionRange === '*'
? '**all** versions'
Expand Down
18 changes: 18 additions & 0 deletions packages/plugin-js-packages/src/lib/runner/audit/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,21 @@ export type Yarnv1AuditResultJson = [
...Yarnv1AuditAdvisory[],
Yarnv1AuditSummary,
];

// Subset of Yarn v2+ audit JSON type
/* eslint-disable @typescript-eslint/naming-convention */
export type Yarnv2AuditAdvisory = {
module_name: string;
severity: PackageAuditLevel;
vulnerable_versions: string;
recommendation: string;
title: string;
url: string;
findings: { paths: string[] }[]; // TODO indirect?
};
/* eslint-enable @typescript-eslint/naming-convention */

export type Yarnv2AuditResultJson = {
advisories: Record<string, Yarnv2AuditAdvisory>;
metadata: { vulnerabilities: Record<PackageAuditLevel, number> };
};
70 changes: 57 additions & 13 deletions packages/plugin-js-packages/src/lib/runner/audit/unify-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Yarnv1AuditAdvisory,
Yarnv1AuditResultJson,
Yarnv1AuditSummary,
Yarnv2AuditResultJson,
} from './types';

export function npmToAuditResult(output: string): AuditResult {
Expand Down Expand Up @@ -108,21 +109,27 @@ export function yarnv1ToAuditResult(output: string): AuditResult {

const vulnerabilities = yarnv1Advisory.map(
({ data: { resolution, advisory } }): Vulnerability => {
const directDependency = resolution.path.slice(
0,
resolution.path.indexOf('>'),
);
const { id, path } = resolution;
const directDependency = path.slice(0, path.indexOf('>'));

const {
module_name: name,
title,
url,
severity,
vulnerable_versions: versionRange,
recommendation: fixInformation,
} = advisory;

return {
name: advisory.module_name,
title: advisory.title,
id: resolution.id,
url: advisory.url,
severity: advisory.severity,
versionRange: advisory.vulnerable_versions,
directDependency:
advisory.module_name === directDependency ? true : directDependency,
fixInformation: advisory.recommendation,
name,
title,
id,
url,
severity,
versionRange,
directDependency: name === directDependency ? true : directDependency,
fixInformation,
};
},
);
Expand Down Expand Up @@ -153,3 +160,40 @@ function validateYarnv1Result(

return [vulnerabilities, summary];
}

export function yarnv2ToAuditResult(output: string): AuditResult {
const yarnv2Audit = JSON.parse(output) as Yarnv2AuditResultJson;

const vulnerabilities = Object.values(yarnv2Audit.advisories).map(
({
module_name: name,
severity,
title,
url,
vulnerable_versions: versionRange,
recommendation: fixInformation,
findings,
}): Vulnerability => {
// TODO missing example of an indirect dependency to verify this
const directDep = findings[0]?.paths[0];
return {
name,
severity,
title,
url,
versionRange,
fixInformation,
directDependency:
directDep != null && directDep !== name ? directDep : true,
};
},
);

const total = Object.values(yarnv2Audit.metadata.vulnerabilities).reduce(
(acc, value) => acc + value,
0,
);
const summary = { ...yarnv2Audit.metadata.vulnerabilities, total };

return { vulnerabilities, summary };
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
NpmVulnerability,
Yarnv1AuditAdvisory,
Yarnv1AuditSummary,
Yarnv2AuditResultJson,
} from './types';
import {
npmToAdvisory,
npmToAuditResult,
npmToFixInformation,
yarnv1ToAuditResult,
yarnv2ToAuditResult,
} from './unify-type';

describe('npmToAuditResult', () => {
Expand Down Expand Up @@ -265,14 +267,7 @@ describe('yarnv1ToAuditResult', () => {
url: 'https://github.com/advisories',
},
],
summary: {
critical: 0,
high: 0,
moderate: 1,
low: 0,
info: 0,
total: 1,
},
summary: { critical: 0, high: 0, moderate: 1, low: 0, info: 0, total: 1 },
});
});

Expand All @@ -286,3 +281,69 @@ describe('yarnv1ToAuditResult', () => {
);
});
});

describe('yarnv2ToAuditResult', () => {
it('should transform Yarn v2 audit to unified audit result', () => {
expect(
yarnv2ToAuditResult(
JSON.stringify({
advisories: {
'123': {
module_name: 'nx',
severity: 'high',
title: 'DoS',
url: 'https://github.com/advisories',
recommendation: 'Update nx to 17.0.0',
vulnerable_versions: '<17.0.0',
findings: [{ paths: ['nx'] }],
},
},
metadata: {
vulnerabilities: {
critical: 0,
high: 1,
moderate: 0,
low: 0,
info: 0,
},
},
} satisfies Yarnv2AuditResultJson),
),
).toEqual<AuditResult>({
vulnerabilities: [
{
name: 'nx',
severity: 'high',
title: 'DoS',
url: 'https://github.com/advisories',
fixInformation: 'Update nx to 17.0.0',
versionRange: '<17.0.0',
directDependency: true,
},
],
summary: { critical: 0, high: 1, moderate: 0, low: 0, info: 0, total: 1 },
});
});

it('should return empty report if no vulnerabilities found', () => {
expect(
yarnv2ToAuditResult(
JSON.stringify({
advisories: {},
metadata: {
vulnerabilities: {
critical: 0,
high: 0,
moderate: 0,
low: 0,
info: 0,
},
},
}),
),
).toStrictEqual<AuditResult>({
vulnerabilities: [],
summary: { critical: 0, high: 0, moderate: 0, low: 0, info: 0, total: 0 },
});
});
});
Loading

0 comments on commit ab2402b

Please sign in to comment.