Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +2 to +3
Copy link
Member

@merceyz merceyz Nov 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should have been marked as minor as this added a feature.


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