Skip to content

Commit

Permalink
[plugin-npm-cli]: Add ability to exclude packages, or ignore specific…
Browse files Browse the repository at this point in the history
… advisories in `yarn npm audit` (#4356)

* [plugin-npm-cli]: Add ability to exclude packages from `yarn npm audit`

This patch adds a `--exclude` flag to the `yarn npm audit` command in the
`nmp-cli` plugin. This flag can be passed multiple times, and any package
listed will be removed from the list of packages audited.

* [plugin-npm-cli] Add ability to ignore advisories in `yarn npm audit`

This patch adds a `--ignore` flag to `yarn npm audit`, which is an array
of ID's to ignore from the audit report.

In addition, the ID is presented in the tree output (as well as the JSON).

* Version bump

* chore: Fix types

* [plugin-npm-cli] Add configuration options for --exclude and --ignore

Adds configuration options to specify packages to exclude from `yarn npm audit`
and to specify advisories to ignore from the results.

* Update audit.ts

* Update audit.ts

* [plugin-npm-cli] Update docs

* [plugin-npm-cli] Add support for glob patterns in --exclude and --ignore

* [plugin-npm-cli] Add some unit tests and stubs for integration tests

Co-authored-by: Maël Nison <[email protected]>
  • Loading branch information
hughdavenport and arcanis authored May 22, 2022
1 parent cc7caeb commit 0a2261d
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 6 deletions.
40 changes: 40 additions & 0 deletions .pnp.cjs

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions .yarn/versions/2e7530b2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
releases:
"@yarnpkg/cli": patch
"@yarnpkg/plugin-npm-cli": patch

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export {};

describe(`Commands`, () => {
describe(`npm audit`, () => {
// TODO
// test ignore as flag
// test exclude as flag
// test ignore as config
// test exclude as config
// test combinations
// test json
// test environment
// test severity
// test recursive
test.todo(`it should report vulnerable packages`);
test.todo(`it should exclude packages`);
test.todo(`it should only exclude excluded packages`);
test.todo(`it should ignore advisories`);
test.todo(`it should only ignore ignored advisories`);
test.todo(`it should return results as JSON`);
test.todo(`it should only use the specified environment`);
test.todo(`it should only use the specified severity level`);
test.todo(`it should recurse packages to audit`);
});
});
20 changes: 20 additions & 0 deletions packages/gatsby/static/configuration/yarnrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,26 @@
"enum": ["public", "restricted"],
"examples": ["public"]
},
"npmAuditExcludePackages": {
"_package": "@yarnpkg/plugin-npm-cli",
"description": "Array of glob patterns of packages to exclude from `yarn npm audit`. Doesn't need to be defined, in which case no packages will be excluded. Can also be augmented by the `--exclude` flag.",
"type": "array",
"items": {
"type": "string"
},
"default": [],
"examples": ["known_insecure_package"]
},
"npmAuditIgnoreAdvisories": {
"_package": "@yarnpkg/plugin-npm-cli",
"description": "Array of glob patterns of advisory ID's to ignore from `yarn npm audit` results. Doesn't need to be defined, in which case no advisories will be ignored. Can also be augmented by the `--ignore` flag.",
"type": "array",
"items": {
"type": "string"
},
"default": [],
"examples": ["1234567"]
},
"npmPublishRegistry": {
"_package": "@yarnpkg/plugin-npm",
"description": "Defines the registry that must be used when pushing packages. Doesn't need to be defined, in which case the value of `npmRegistryServer` will be used. Overridden by `publishConfig.registry`.",
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-npm-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@yarnpkg/fslib": "workspace:^",
"clipanion": "^3.2.0-rc.10",
"enquirer": "^2.3.6",
"micromatch": "^4.0.2",
"semver": "^7.1.2",
"tslib": "^1.13.0",
"typanion": "^3.3.0"
Expand All @@ -19,6 +20,7 @@
},
"devDependencies": {
"@npm/types": "^1.0.1",
"@types/micromatch": "^4.0.1",
"@types/semver": "^7.1.0",
"@yarnpkg/cli": "workspace:^",
"@yarnpkg/core": "workspace:^",
Expand Down
61 changes: 61 additions & 0 deletions packages/plugin-npm-cli/sources/commands/npm/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {BaseCommand, WorkspaceRequiredError}
import {Configuration, Project, MessageName, treeUtils, LightReport, StreamReport} from '@yarnpkg/core';
import {npmConfigUtils, npmHttpUtils} from '@yarnpkg/plugin-npm';
import {Command, Option, Usage} from 'clipanion';
import micromatch from 'micromatch';
import * as t from 'typanion';

import * as npmAuditTypes from '../../npmAuditTypes';
Expand All @@ -24,6 +25,10 @@ export default class AuditCommand extends BaseCommand {
If the \`--json\` flag is set, Yarn will print the output exactly as received from the registry. Regardless of this flag, the process will exit with a non-zero exit code if a report is found for the selected packages.
If certain packages produce false positives for a particular environment, the \`--exclude\` flag can be used to exclude any number of packages from the audit. This can also be set in the configuration file with the \`npmAuditExcludePackages\` option.
If particular advisories are needed to be ignored, the \`--ignore\` flag can be used with Advisory ID's to ignore any number of advisories in the audit report. This can also be set in the configuration file with the \`npmAuditIgnoreAdvisories\` option.
To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why <package>\` to get more information as to who depends on them.
`,
examples: [[
Expand All @@ -44,6 +49,12 @@ export default class AuditCommand extends BaseCommand {
], [
`Output moderate (or more severe) vulnerabilities`,
`yarn npm audit --severity moderate`,
], [
`Exclude certain packages`,
`yarn npm audit --exclude package1 --exclude package2`,
], [
`Ignore specific advisories`,
`yarn npm audit --ignore 1234567 --ignore 7654321`,
]],
});

Expand All @@ -69,6 +80,14 @@ export default class AuditCommand extends BaseCommand {
validator: t.isEnum(npmAuditTypes.Severity),
});

excludes = Option.Array(`--exclude`, [], {
description: `Array of glob patterns of packages to exclude from audit`,
});

ignores = Option.Array(`--ignore`, [], {
description: `Array of glob patterns of advisory ID's to ignore in the audit report`,
});

async execute() {
const configuration = await Configuration.find(this.context.cwd, this.context.plugins);
const {project, workspace} = await Project.find(configuration, this.context.cwd);
Expand All @@ -91,6 +110,33 @@ export default class AuditCommand extends BaseCommand {
}
}

const excludedPackages = Array.from(new Set([
...configuration.get(`npmAuditExcludePackages`),
...this.excludes,
]));

if (excludedPackages) {
for (const pkg of Object.keys(requires)) {
if (micromatch.isMatch(pkg, excludedPackages)) {
delete requires[pkg];
}
}

for (const pkg of Object.keys(dependencies)) {
if (micromatch.isMatch(pkg, excludedPackages)) {
delete dependencies[pkg];
}
}

for (const key of Object.keys(dependencies)) {
for (const pkg of Object.keys(dependencies[key].requires)) {
if (micromatch.isMatch(pkg, excludedPackages)) {
delete dependencies[key].requires[pkg];
}
}
}
}

const body = {
requires,
dependencies,
Expand All @@ -116,6 +162,21 @@ export default class AuditCommand extends BaseCommand {
if (httpReport.hasErrors())
return httpReport.exitCode();

const ignoredAdvisories = Array.from(new Set([
...configuration.get(`npmAuditIgnoreAdvisories`),
...this.ignores,
]));

if (ignoredAdvisories) {
for (const advisory of Object.keys(result.advisories)) {
if (micromatch.isMatch(advisory, ignoredAdvisories)) {
const entry = result.advisories[advisory];
result.metadata.vulnerabilities[entry.severity] -= 1;
delete result.advisories[advisory];
}
}
}

const hasError = npmAuditUtils.isError(result.metadata.vulnerabilities, this.severity);
if (!this.json && hasError) {
treeUtils.emitTree(npmAuditUtils.getReportTree(result, this.severity), {
Expand Down
14 changes: 14 additions & 0 deletions packages/plugin-npm-cli/sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import npmWhoami from './commands/npm/whoami';
declare module '@yarnpkg/core' {
interface ConfigurationValueMap {
npmPublishAccess: string | null;
npmAuditExcludePackages: Array<string>;
npmAuditIgnoreAdvisories: Array<string>;
}
}

Expand All @@ -23,6 +25,18 @@ const plugin: Plugin = {
type: SettingsType.STRING,
default: null,
},
npmAuditExcludePackages: {
description: `Array of glob patterns of packages to exclude from npm audit`,
type: SettingsType.STRING,
default: [],
isArray: true,
},
npmAuditIgnoreAdvisories: {
description: `Array of glob patterns of advisory IDs to exclude from npm audit`,
type: SettingsType.STRING,
default: [],
isArray: true,
},
},
commands: [
npmAudit,
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-npm-cli/sources/npmAuditTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface AuditAdvisory {
recommendation: string;
references: string;
access: string;
severity: string;
severity: Severity;
cwe: string;
metadata: {
module_type: string;
Expand Down
14 changes: 9 additions & 5 deletions packages/plugin-npm-cli/sources/npmAuditUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function setDifference<T>(x: Set<T>, y: Set<T>): Set<T> {
// - are present in the lockfile
// - are a transitive dependency of some top-level devDependency
// - are not a transitive dependency of some top-level production dependency
export function getTransitiveDevDependencies(project: Project, workspace: Workspace, {all}: {all: boolean}): Set<DescriptorHash> {
function getTransitiveDevDependencies(project: Project, workspace: Workspace, {all}: {all: boolean}): Set<DescriptorHash> {
// Determine workspaces in scope
const workspaces = all
? project.workspaces
Expand Down Expand Up @@ -103,7 +103,7 @@ export function getTransitiveDevDependencies(project: Project, workspace: Worksp
return setDifference(developmentDependencies, productionDependencies);
}

export function transformDescriptorIterableToRequiresObject(descriptors: Iterable<Descriptor>) {
function transformDescriptorIterableToRequiresObject(descriptors: Iterable<Descriptor>) {
const data: {[key: string]: string} = {};

for (const descriptor of descriptors)
Expand All @@ -112,17 +112,17 @@ export function transformDescriptorIterableToRequiresObject(descriptors: Iterabl
return data;
}

export function getSeverityInclusions(severity?: npmAuditTypes.Severity): Set<npmAuditTypes.Severity> {
function getSeverityInclusions(severity?: npmAuditTypes.Severity): Set<npmAuditTypes.Severity> {
if (typeof severity === `undefined`)
return new Set();
return new Set(allSeverities);

const severityIndex = allSeverities.indexOf(severity);
const severities = allSeverities.slice(severityIndex);

return new Set(severities);
}

export function filterVulnerabilities(vulnerabilities: npmAuditTypes.AuditVulnerabilities, severity?: npmAuditTypes.Severity) {
function filterVulnerabilities(vulnerabilities: npmAuditTypes.AuditVulnerabilities, severity?: npmAuditTypes.Severity) {
const inclusions = getSeverityInclusions(severity);

const filteredVulnerabilities: Partial<npmAuditTypes.AuditVulnerabilities> = {};
Expand Down Expand Up @@ -157,6 +157,10 @@ export function getReportTree(result: npmAuditTypes.AuditResponse, severity?: np
label: advisory.module_name,
value: formatUtils.tuple(formatUtils.Type.RANGE, advisory.findings.map(finding => finding.version).join(`, `)),
children: {
ID: {
label: `ID`,
value: formatUtils.tuple(formatUtils.Type.NUMBER, advisory.id),
},
Issue: {
label: `Issue`,
value: formatUtils.tuple(formatUtils.Type.NO_HINT, advisory.title),
Expand Down
Loading

0 comments on commit 0a2261d

Please sign in to comment.