Skip to content

Commit 6aa55a2

Browse files
committed
feat(plugin-js-packages): implement runner for npm audit
1 parent 6348ba3 commit 6aa55a2

File tree

8 files changed

+468
-30
lines changed

8 files changed

+468
-30
lines changed
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import { executeRunner } from './lib/runner';
22

3-
executeRunner();
3+
await executeRunner();

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

+11-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { z } from 'zod';
22
import { IssueSeverity, issueSeveritySchema } from '@code-pushup/models';
3+
import { defaultAuditLevelMapping } from './constants';
4+
5+
export const packageDependencies = ['prod', 'dev', 'optional'] as const;
6+
export type PackageDependency = (typeof packageDependencies)[number];
37

48
const packageCommandSchema = z.enum(['audit', 'outdated']);
59
export type PackageCommand = z.infer<typeof packageCommandSchema>;
@@ -12,23 +16,16 @@ const packageManagerSchema = z.enum([
1216
]);
1317
export type PackageManager = z.infer<typeof packageManagerSchema>;
1418

15-
const packageAuditLevelSchema = z.enum([
16-
'info',
17-
'low',
18-
'moderate',
19-
'high',
19+
export const packageAuditLevels = [
2020
'critical',
21-
]);
21+
'high',
22+
'moderate',
23+
'low',
24+
'info',
25+
] as const;
26+
const packageAuditLevelSchema = z.enum(packageAuditLevels);
2227
export type PackageAuditLevel = z.infer<typeof packageAuditLevelSchema>;
2328

24-
const defaultAuditLevelMapping: Record<PackageAuditLevel, IssueSeverity> = {
25-
critical: 'error',
26-
high: 'error',
27-
moderate: 'warning',
28-
low: 'warning',
29-
info: 'info',
30-
};
31-
3229
export function fillAuditLevelMapping(
3330
mapping: Partial<Record<PackageAuditLevel, IssueSeverity>>,
3431
): Record<PackageAuditLevel, IssueSeverity> {
@@ -66,5 +63,3 @@ export type JSPackagesPluginConfig = z.input<
6663
export type FinalJSPackagesPluginConfig = z.infer<
6764
typeof jsPackagesPluginConfigSchema
6865
>;
69-
70-
export type PackageDependencyType = 'prod' | 'dev' | 'optional';

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

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1-
import { MaterialIcon } from '@code-pushup/models';
2-
import { PackageDependencyType, PackageManager } from './config';
1+
import { IssueSeverity, MaterialIcon } from '@code-pushup/models';
2+
import type {
3+
PackageAuditLevel,
4+
PackageDependency,
5+
PackageManager,
6+
} from './config';
7+
8+
export const defaultAuditLevelMapping: Record<
9+
PackageAuditLevel,
10+
IssueSeverity
11+
> = {
12+
critical: 'error',
13+
high: 'error',
14+
moderate: 'warning',
15+
low: 'warning',
16+
info: 'info',
17+
};
318

419
export const pkgManagerNames: Record<PackageManager, string> = {
520
npm: 'NPM',
@@ -35,7 +50,7 @@ export const outdatedDocs: Record<PackageManager, string> = {
3550
pnpm: 'https://pnpm.io/cli/outdated',
3651
};
3752

38-
export const dependencyDocs: Record<PackageDependencyType, string> = {
53+
export const dependencyDocs: Record<PackageDependency, string> = {
3954
prod: 'https://classic.yarnpkg.com/docs/dependency-types#toc-dependencies',
4055
dev: 'https://classic.yarnpkg.com/docs/dependency-types#toc-devdependencies',
4156
optional:

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { dirname, join } from 'node:path';
22
import { fileURLToPath } from 'node:url';
3-
import { Audit, Group, PluginConfig } from '@code-pushup/models';
3+
import type { Audit, Group, PluginConfig } from '@code-pushup/models';
44
import { name, version } from '../../package.json';
55
import {
66
JSPackagesPluginConfig,
77
PackageCommand,
8-
PackageDependencyType,
8+
PackageDependency,
99
PackageManager,
1010
jsPackagesPluginConfigSchema,
1111
} from './config';
@@ -126,7 +126,7 @@ function createAudits(
126126
function getAuditTitle(
127127
pkgManager: PackageManager,
128128
check: PackageCommand,
129-
dependencyType: PackageDependencyType,
129+
dependencyType: PackageDependency,
130130
) {
131131
return check === 'audit'
132132
? `Vulnerabilities for ${pkgManagerNames[pkgManager]} ${dependencyType} dependencies.`
@@ -135,7 +135,7 @@ function getAuditTitle(
135135

136136
function getAuditDescription(
137137
check: PackageCommand,
138-
dependencyType: PackageDependencyType,
138+
dependencyType: PackageDependency,
139139
) {
140140
return check === 'audit'
141141
? `Runs security audit on ${dependencyType} dependencies.`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { AuditOutput, Issue, IssueSeverity } from '@code-pushup/models';
2+
import { objectToEntries } from '@code-pushup/utils';
3+
import {
4+
PackageAuditLevel,
5+
PackageDependency,
6+
packageAuditLevels,
7+
} from '../../config';
8+
import { NpmAuditResultJson, Vulnerabilities } from './types';
9+
10+
export function auditResultToAuditOutput(
11+
result: NpmAuditResultJson,
12+
dependenciesType: PackageDependency,
13+
auditLevelMapping: Record<PackageAuditLevel, IssueSeverity>,
14+
): AuditOutput {
15+
const issues = vulnerabilitiesToIssues(
16+
result.vulnerabilities,
17+
auditLevelMapping,
18+
);
19+
return {
20+
slug: `npm-audit-${dependenciesType}`,
21+
score: result.metadata.vulnerabilities.total === 0 ? 1 : 0,
22+
value: result.metadata.vulnerabilities.total,
23+
displayValue: vulnerabilitiesToDisplayValue(
24+
result.metadata.vulnerabilities,
25+
),
26+
...(issues.length > 0 && { details: { issues } }),
27+
};
28+
}
29+
30+
export function vulnerabilitiesToDisplayValue(
31+
vulnerabilities: Record<PackageAuditLevel | 'total', number>,
32+
): string {
33+
if (vulnerabilities.total === 0) {
34+
return 'passed';
35+
}
36+
37+
const displayValue = packageAuditLevels
38+
.map(level =>
39+
vulnerabilities[level] > 0 ? `${vulnerabilities[level]} ${level}` : '',
40+
)
41+
.filter(text => text !== '')
42+
.join(', ');
43+
return `${displayValue} ${
44+
vulnerabilities.total === 1 ? 'vulnerability' : 'vulnerabilities'
45+
}`;
46+
}
47+
48+
export function vulnerabilitiesToIssues(
49+
vulnerabilities: Vulnerabilities,
50+
auditLevelMapping: Record<PackageAuditLevel, IssueSeverity>,
51+
): Issue[] {
52+
if (Object.keys(vulnerabilities).length === 0) {
53+
return [];
54+
}
55+
56+
return objectToEntries(vulnerabilities).map<Issue>(([, detail]) => {
57+
// Advisory details via can refer to another vulnerability
58+
// For now, only direct context is supported
59+
if (
60+
Array.isArray(detail.via) &&
61+
detail.via.length > 0 &&
62+
typeof detail.via[0] === 'object'
63+
) {
64+
return {
65+
message: `${detail.name} dependency has a vulnerability "${
66+
detail.via[0].title
67+
}" for versions ${detail.range}. Fix is ${
68+
detail.fixAvailable ? '' : 'not '
69+
}available. More information [here](${detail.via[0].url})`,
70+
severity: auditLevelMapping[detail.severity],
71+
};
72+
}
73+
74+
return {
75+
message: `${detail.name} dependency has a vulnerability for versions ${
76+
detail.range
77+
}. Fix is ${detail.fixAvailable ? '' : 'not '}available.`,
78+
severity: auditLevelMapping[detail.severity],
79+
};
80+
});
81+
}

0 commit comments

Comments
 (0)