Skip to content

feat: Print path for vulnerable advisories #230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 13, 2022
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
48 changes: 38 additions & 10 deletions lib/common.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { SpawnOptionsWithoutStdio } from "child_process";
import { spawn } from "cross-spawn";
import escapeStringRegexp from "escape-string-regexp";
import * as eventStream from "event-stream";
import * as JSONStream from "JSONStream";
import { SpawnOptionsWithoutStdio } from "child_process";
import ReadlineTransform from "readline-transform";
import { blue, yellow } from "./colors";
import { AuditCiConfig } from "./config";
import { Summary } from "./model";

export function partition<T>(a: T[], fun: (parameter: T) => boolean) {
const returnValue: { truthy: T[]; falsy: T[] } = { truthy: [], falsy: [] };
for (const item of a) {
if (fun(item)) {
returnValue.truthy.push(item);
} else {
returnValue.falsy.push(item);
}
}
return returnValue;
}

export function reportAudit(summary, config: AuditCiConfig) {
export function reportAudit(summary: Summary, config: AuditCiConfig) {
const {
allowlist,
"show-not-found": showNotFound,
Expand All @@ -22,6 +35,7 @@ export function reportAudit(summary, config: AuditCiConfig) {
allowlistedPathsNotFound,
failedLevelsFound,
advisoriesFound,
advisoryPathsFound,
} = summary;

if (o === "text") {
Expand All @@ -48,7 +62,7 @@ export function reportAudit(summary, config: AuditCiConfig) {
if (showNotFound) {
if (allowlistedModulesNotFound.length > 0) {
const found = allowlistedModulesNotFound
.sort((a, b) => a - b)
.sort((a, b) => a.localeCompare(b))
.join(", ");
const allowlistMessage =
allowlistedModulesNotFound.length === 1
Expand All @@ -58,7 +72,7 @@ export function reportAudit(summary, config: AuditCiConfig) {
}
if (allowlistedAdvisoriesNotFound.length > 0) {
const found = allowlistedAdvisoriesNotFound
.sort((a, b) => a - b)
.sort((a, b) => a.localeCompare(b))
.join(", ");
const allowlistMessage =
allowlistedAdvisoriesNotFound.length === 1
Expand All @@ -67,24 +81,32 @@ export function reportAudit(summary, config: AuditCiConfig) {
console.warn(yellow, allowlistMessage);
}
if (allowlistedPathsNotFound.length > 0) {
const found = allowlistedPathsNotFound.sort((a, b) => a - b).join(", ");
const found = allowlistedPathsNotFound
.sort((a, b) => a.localeCompare(b))
.join(", ");
const allowlistMessage =
allowlistedPathsNotFound.length === 1
? `Consider not allowlisting path: ${found}.`
: `Consider not allowlisting paths: ${found}.`;
console.warn(yellow, allowlistMessage);
}
}

if (advisoryPathsFound.length > 0) {
const found = advisoryPathsFound.join("\n");
console.warn(yellow, `Found vulnerable advisory paths:`);
console.log(found);
}
}

if (failedLevelsFound.length > 0) {
// Get the levels that have failed by filtering the keys with true values
throw new Error(
`Failed security audit due to ${failedLevelsFound.join(
", "
)} vulnerabilities.\nVulnerable advisories are: ${advisoriesFound.join(
", "
)}`
)} vulnerabilities.\nVulnerable advisories are:\n${advisoriesFound
.map((element) => gitHubAdvisoryIdToUrl(element))
.join("\n")}`
);
}
return summary;
Expand Down Expand Up @@ -132,8 +154,8 @@ export function runProgram(
});
}

function wildcardToRegex(string_: string) {
const regexString = string_
function wildcardToRegex(stringWithWildcard: string) {
const regexString = stringWithWildcard
.split(/\*+/) // split at every wildcard (*) character
.map((s) => escapeStringRegexp(s)) // escape the substrings to make sure that they aren't evaluated
.join(".*"); // construct a regex matching anything at each wildcard location
Expand All @@ -149,3 +171,9 @@ export function matchString(template: string, string_: string) {
export function gitHubAdvisoryUrlToAdvisoryId(url: string) {
return url.split("/")[4];
}

export function gitHubAdvisoryIdToUrl<T extends string>(
id: T
): `https://github.com/advisories/${T}` {
return `https://github.com/advisories/${id}`;
}
6 changes: 4 additions & 2 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
VulnerabilityLevels,
} from "./map-vulnerability";

function mapReportTypeInput(config) {
function mapReportTypeInput(
config: Pick<AuditCiPreprocessedConfig, "report-type">
) {
const { "report-type": reportType } = config;
switch (reportType) {
case "full":
Expand Down Expand Up @@ -251,7 +253,7 @@ export async function runYargs(): Promise<AuditCiConfig> {
h,
c,
}),
"report-type": mapReportTypeInput(argv),
"report-type": mapReportTypeInput(awaitedArgv),
a: allowlist,
allowlist: allowlist,
};
Expand Down
62 changes: 36 additions & 26 deletions lib/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Allowlist from "./allowlist";
import { gitHubAdvisoryUrlToAdvisoryId, matchString } from "./common";
import {
gitHubAdvisoryUrlToAdvisoryId,
matchString,
partition,
} from "./common";
import { AuditCiConfig } from "./config";
import { VulnerabilityLevels } from "./map-vulnerability";

Expand All @@ -10,20 +14,20 @@ const SUPPORTED_SEVERITY_LEVELS = new Set([
"low",
]);

function partition<T>(a: T[], fun: (parameter: T) => boolean) {
const returnValue: { truthy: T[]; falsy: T[] } = { truthy: [], falsy: [] };
for (const item of a) {
if (fun(item)) {
returnValue.truthy.push(item);
} else {
returnValue.falsy.push(item);
}
}
return returnValue;
}

const prependPath = (newItem, currentPath) => `${newItem}>${currentPath}`;

export interface Summary {
advisoriesFound: string[];
failedLevelsFound: string[];
allowlistedAdvisoriesNotFound: string[];
allowlistedModulesNotFound: string[];
allowlistedPathsNotFound: string[];
allowlistedAdvisoriesFound: string[];
allowlistedModulesFound: string[];
allowlistedPathsFound: string[];
advisoryPathsFound: string[];
}

class Model {
failingSeverities: {
[K in keyof VulnerabilityLevels]: VulnerabilityLevels[K];
Expand Down Expand Up @@ -91,10 +95,9 @@ class Model {
allowlistedPathsFoundSet.add(path);
}

const isAllowListed = falsy.length === 0;

this.allowlistedPathsFound.push(...allowlistedPathsFoundSet);

const isAllowListed = falsy.length === 0;
if (isAllowListed) {
return;
}
Expand Down Expand Up @@ -170,8 +173,7 @@ class Model {
// Now, all we have to deal with is develop the 'findings' property by traversing
// the audit tree.

/** @type {Map<string, string[]>} */
const visitedModules = new Map();
const visitedModules = new Map<string, string[]>();

for (const vuln of Object.entries<NPM7Vulnerability>(
parsedOutput.vulnerabilities
Expand All @@ -190,9 +192,10 @@ class Model {
const recursiveMagic = (
cVuln: NPM7Vulnerability,
dependencyPath: string
) => {
if (visitedModules.has(cVuln.name)) {
return visitedModules.get(cVuln.name).map((name) => {
): string[] => {
const visitedModule = visitedModules.get(cVuln.name);
if (visitedModule) {
return visitedModule.map((name) => {
const resultWithExtraCarat = prependPath(name, dependencyPath);
return resultWithExtraCarat.slice(
0,
Expand All @@ -210,7 +213,6 @@ class Model {
if (cVuln.effects.length === 0) {
return [newPath.slice(0, Math.max(0, newPath.length - 1))];
}
/** @type {string[]} */
const result = cVuln.effects.flatMap((effect) =>
recursiveMagic(parsedOutput.vulnerabilities[effect], newPath)
);
Expand All @@ -221,11 +223,17 @@ class Model {
if (isDirect) {
result.push(moduleName);
}
for (const advisory of (
const advisories = (
vias.filter((via) => typeof via !== "string") as any[]
).map((via) => via.source)) {
)
.map((via) => via.source)
// Filter boolean makes the next line non-nullable.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.map((id) => advisoryMap.get(id)!)
.filter(Boolean);
for (const advisory of advisories) {
for (const path of result) {
advisoryMap.get(advisory)?.findingsSet.add(path);
advisory.findingsSet.add(path);
}
}
// Optimization to prevent extra traversals.
Expand All @@ -242,7 +250,7 @@ class Model {
}

getSummary(advisoryMapper = (a) => a.github_advisory_id) {
const foundSeverities = new Set();
const foundSeverities = new Set<string>();
for (const { severity } of this.advisoriesFound)
foundSeverities.add(severity);
const failedLevelsFound = [...foundSeverities];
Expand All @@ -265,7 +273,7 @@ class Model {
)
);

return {
const summary: Summary = {
advisoriesFound,
failedLevelsFound,
allowlistedAdvisoriesNotFound,
Expand All @@ -274,7 +282,9 @@ class Model {
allowlistedAdvisoriesFound: this.allowlistedAdvisoriesFound,
allowlistedModulesFound: this.allowlistedModulesFound,
allowlistedPathsFound: this.allowlistedPathsFound,
advisoryPathsFound: this.advisoryPathsFound,
};
return summary;
}
}

Expand Down
1 change: 1 addition & 0 deletions test/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function summaryWithDefault(additions = {}) {
allowlistedPathsNotFound: [],
failedLevelsFound: [],
advisoriesFound: [],
advisoryPathsFound: [],
};
return { ...summary, ...additions };
}
Expand Down
24 changes: 24 additions & 0 deletions test/model.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe("Model", () => {
summaryWithDefault({
failedLevelsFound: ["critical"],
advisoriesFound: ["GHSA-28xh-wpgr-7fm8"],
advisoryPathsFound: ["GHSA-28xh-wpgr-7fm8|open"],
})
);
});
Expand Down Expand Up @@ -146,6 +147,13 @@ describe("Model", () => {
summaryWithDefault({
failedLevelsFound: ["critical", "low"],
advisoriesFound: ["GHSA-1", "GHSA-2", "GHSA-5", "GHSA-6", "GHSA-7"],
advisoryPathsFound: [
"GHSA-1|M_A",
"GHSA-2|M_B",
"GHSA-5|M_E",
"GHSA-6|M_F",
"GHSA-7|M_G",
],
})
);
});
Expand Down Expand Up @@ -223,6 +231,13 @@ describe("Model", () => {
allowlistedModulesFound: ["M_A", "M_D"],
failedLevelsFound: ["critical", "low", "moderate"],
advisoriesFound: ["GHSA-2", "GHSA-3", "GHSA-5", "GHSA-6", "GHSA-7"],
advisoryPathsFound: [
"GHSA-2|M_B",
"GHSA-3|M_C",
"GHSA-5|M_E",
"GHSA-6|M_F",
"GHSA-7|M_G",
],
})
);
});
Expand Down Expand Up @@ -308,6 +323,12 @@ describe("Model", () => {
allowlistedAdvisoriesFound: ["GHSA-2", "GHSA-3", "GHSA-6"],
failedLevelsFound: ["critical", "high", "low"],
advisoriesFound: ["GHSA-1", "GHSA-4", "GHSA-5", "GHSA-7"],
advisoryPathsFound: [
"GHSA-1|M_A",
"GHSA-4|M_D",
"GHSA-5|M_E",
"GHSA-7|M_G",
],
})
);
});
Expand Down Expand Up @@ -352,6 +373,7 @@ describe("Model", () => {
summaryWithDefault({
failedLevelsFound: ["critical", "low"],
advisoriesFound: ["GHSA-1", "GHSA-2"],
advisoryPathsFound: ["GHSA-1|M_A", "GHSA-2|M_B_1", "GHSA-2|M_B_2"],
})
);
});
Expand Down Expand Up @@ -417,6 +439,7 @@ describe("Model", () => {
summaryWithDefault({
failedLevelsFound: ["moderate"],
advisoriesFound: ["GHSA-123"],
advisoryPathsFound: ["GHSA-123|package3>"],
})
);
});
Expand Down Expand Up @@ -482,6 +505,7 @@ describe("Model", () => {
summaryWithDefault({
failedLevelsFound: ["moderate"],
advisoriesFound: ["GHSA-123"],
advisoryPathsFound: ["GHSA-123|package3>"],
})
);
});
Expand Down
Loading