Skip to content

Commit

Permalink
feat(plugin-js-packages): implement runner for npm audit
Browse files Browse the repository at this point in the history
  • Loading branch information
Tlacenka committed Mar 18, 2024
1 parent 6348ba3 commit 6aa55a2
Show file tree
Hide file tree
Showing 8 changed files with 468 additions and 30 deletions.
2 changes: 1 addition & 1 deletion packages/plugin-js-packages/src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { executeRunner } from './lib/runner';

executeRunner();
await executeRunner();
27 changes: 11 additions & 16 deletions packages/plugin-js-packages/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { z } from 'zod';
import { IssueSeverity, issueSeveritySchema } from '@code-pushup/models';
import { defaultAuditLevelMapping } from './constants';

export const packageDependencies = ['prod', 'dev', 'optional'] as const;
export type PackageDependency = (typeof packageDependencies)[number];

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

const packageAuditLevelSchema = z.enum([
'info',
'low',
'moderate',
'high',
export const packageAuditLevels = [
'critical',
]);
'high',
'moderate',
'low',
'info',
] as const;
const packageAuditLevelSchema = z.enum(packageAuditLevels);
export type PackageAuditLevel = z.infer<typeof packageAuditLevelSchema>;

const defaultAuditLevelMapping: Record<PackageAuditLevel, IssueSeverity> = {
critical: 'error',
high: 'error',
moderate: 'warning',
low: 'warning',
info: 'info',
};

export function fillAuditLevelMapping(
mapping: Partial<Record<PackageAuditLevel, IssueSeverity>>,
): Record<PackageAuditLevel, IssueSeverity> {
Expand Down Expand Up @@ -66,5 +63,3 @@ export type JSPackagesPluginConfig = z.input<
export type FinalJSPackagesPluginConfig = z.infer<
typeof jsPackagesPluginConfigSchema
>;

export type PackageDependencyType = 'prod' | 'dev' | 'optional';
21 changes: 18 additions & 3 deletions packages/plugin-js-packages/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { MaterialIcon } from '@code-pushup/models';
import { PackageDependencyType, PackageManager } from './config';
import { IssueSeverity, MaterialIcon } from '@code-pushup/models';
import type {
PackageAuditLevel,
PackageDependency,
PackageManager,
} from './config';

export const defaultAuditLevelMapping: Record<
PackageAuditLevel,
IssueSeverity
> = {
critical: 'error',
high: 'error',
moderate: 'warning',
low: 'warning',
info: 'info',
};

export const pkgManagerNames: Record<PackageManager, string> = {
npm: 'NPM',
Expand Down Expand Up @@ -35,7 +50,7 @@ export const outdatedDocs: Record<PackageManager, string> = {
pnpm: 'https://pnpm.io/cli/outdated',
};

export const dependencyDocs: Record<PackageDependencyType, string> = {
export const dependencyDocs: Record<PackageDependency, string> = {
prod: 'https://classic.yarnpkg.com/docs/dependency-types#toc-dependencies',
dev: 'https://classic.yarnpkg.com/docs/dependency-types#toc-devdependencies',
optional:
Expand Down
8 changes: 4 additions & 4 deletions packages/plugin-js-packages/src/lib/js-packages-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Audit, Group, PluginConfig } from '@code-pushup/models';
import type { Audit, Group, PluginConfig } from '@code-pushup/models';
import { name, version } from '../../package.json';
import {
JSPackagesPluginConfig,
PackageCommand,
PackageDependencyType,
PackageDependency,
PackageManager,
jsPackagesPluginConfigSchema,
} from './config';
Expand Down Expand Up @@ -126,7 +126,7 @@ function createAudits(
function getAuditTitle(
pkgManager: PackageManager,
check: PackageCommand,
dependencyType: PackageDependencyType,
dependencyType: PackageDependency,
) {
return check === 'audit'
? `Vulnerabilities for ${pkgManagerNames[pkgManager]} ${dependencyType} dependencies.`
Expand All @@ -135,7 +135,7 @@ function getAuditTitle(

function getAuditDescription(
check: PackageCommand,
dependencyType: PackageDependencyType,
dependencyType: PackageDependency,
) {
return check === 'audit'
? `Runs security audit on ${dependencyType} dependencies.`
Expand Down
81 changes: 81 additions & 0 deletions packages/plugin-js-packages/src/lib/runner/audit/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { AuditOutput, Issue, IssueSeverity } from '@code-pushup/models';
import { objectToEntries } from '@code-pushup/utils';
import {
PackageAuditLevel,
PackageDependency,
packageAuditLevels,
} from '../../config';
import { NpmAuditResultJson, Vulnerabilities } from './types';

export function auditResultToAuditOutput(
result: NpmAuditResultJson,
dependenciesType: PackageDependency,
auditLevelMapping: Record<PackageAuditLevel, IssueSeverity>,
): AuditOutput {
const issues = vulnerabilitiesToIssues(
result.vulnerabilities,
auditLevelMapping,
);
return {
slug: `npm-audit-${dependenciesType}`,
score: result.metadata.vulnerabilities.total === 0 ? 1 : 0,
value: result.metadata.vulnerabilities.total,
displayValue: vulnerabilitiesToDisplayValue(
result.metadata.vulnerabilities,
),
...(issues.length > 0 && { details: { issues } }),
};
}

export function vulnerabilitiesToDisplayValue(
vulnerabilities: Record<PackageAuditLevel | 'total', number>,
): string {
if (vulnerabilities.total === 0) {
return 'passed';
}

const displayValue = packageAuditLevels
.map(level =>
vulnerabilities[level] > 0 ? `${vulnerabilities[level]} ${level}` : '',
)
.filter(text => text !== '')
.join(', ');
return `${displayValue} ${
vulnerabilities.total === 1 ? 'vulnerability' : 'vulnerabilities'
}`;
}

export function vulnerabilitiesToIssues(
vulnerabilities: Vulnerabilities,
auditLevelMapping: Record<PackageAuditLevel, IssueSeverity>,
): Issue[] {
if (Object.keys(vulnerabilities).length === 0) {
return [];
}

return objectToEntries(vulnerabilities).map<Issue>(([, detail]) => {
// Advisory details via can refer to another vulnerability
// For now, only direct context is supported
if (
Array.isArray(detail.via) &&
detail.via.length > 0 &&
typeof detail.via[0] === 'object'
) {
return {
message: `${detail.name} dependency has a vulnerability "${
detail.via[0].title
}" for versions ${detail.range}. Fix is ${
detail.fixAvailable ? '' : 'not '
}available. More information [here](${detail.via[0].url})`,
severity: auditLevelMapping[detail.severity],
};
}

return {
message: `${detail.name} dependency has a vulnerability for versions ${
detail.range
}. Fix is ${detail.fixAvailable ? '' : 'not '}available.`,
severity: auditLevelMapping[detail.severity],
};
});
}
Loading

0 comments on commit 6aa55a2

Please sign in to comment.