Skip to content
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

chore(deps): Add audit-types #251

Merged
merged 2 commits into from Apr 28, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/allowlist.ts
@@ -1,8 +1,10 @@
import type { GitHubAdvisoryId } from "audit-types";
import { isGitHubAdvisoryId } from "./common";
import { AuditCiPreprocessedConfig } from "./config";

class Allowlist {
modules: string[];
advisories: string[];
advisories: GitHubAdvisoryId[];
paths: string[];
/**
* @param input the allowlisted module names, advisories, and module paths
Expand All @@ -23,7 +25,7 @@ class Allowlist {

if (allowlist.includes(">") || allowlist.includes("|")) {
this.paths.push(allowlist);
} else if (allowlist.startsWith("GHSA")) {
} else if (isGitHubAdvisoryId(allowlist)) {
this.advisories.push(allowlist);
} else {
this.modules.push(allowlist);
Expand Down
9 changes: 7 additions & 2 deletions lib/common.ts
@@ -1,3 +1,4 @@
import { GitHubAdvisoryId } from "audit-types";
import { SpawnOptionsWithoutStdio } from "child_process";
import { spawn } from "cross-spawn";
import escapeStringRegexp from "escape-string-regexp";
Expand Down Expand Up @@ -168,8 +169,12 @@ export function matchString(template: string, string_: string) {
: template === string_;
}

export function gitHubAdvisoryUrlToAdvisoryId(url: string) {
return url.split("/")[4];
export function isGitHubAdvisoryId(id: string): id is GitHubAdvisoryId {
return id.startsWith("GHSA");
}

export function gitHubAdvisoryUrlToAdvisoryId(url: string): GitHubAdvisoryId {
return url.split("/")[4] as GitHubAdvisoryId;
}

export function gitHubAdvisoryIdToUrl<T extends string>(
Expand Down
270 changes: 137 additions & 133 deletions lib/model.ts
@@ -1,3 +1,10 @@
import type {
GitHubAdvisoryId,
NPMAuditReportV1,
NPMAuditReportV2,
PNPMAuditReport,
YarnAudit,
} from "audit-types";
import Allowlist from "./allowlist";
import {
gitHubAdvisoryUrlToAdvisoryId,
Expand All @@ -6,6 +13,7 @@ import {
} from "./common";
import { AuditCiConfig } from "./config";
import { VulnerabilityLevels } from "./map-vulnerability";
import type { DeepWriteable } from "./types";

const SUPPORTED_SEVERITY_LEVELS = new Set([
"critical",
Expand All @@ -18,26 +26,35 @@ const prependPath = (newItem: string, currentPath: string) =>
`${newItem}>${currentPath}`;

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

interface ProcessedAdvisory {
id: number;
github_advisory_id: GitHubAdvisoryId;
severity: string;
module_name: string;
url: string;
findings: { paths: string[] }[];
}

class Model {
failingSeverities: {
[K in keyof VulnerabilityLevels]: VulnerabilityLevels[K];
};
allowlist: Allowlist;
allowlistedModulesFound: string[];
allowlistedAdvisoriesFound: string[];
allowlistedAdvisoriesFound: GitHubAdvisoryId[];
allowlistedPathsFound: string[];
advisoriesFound: any[];
advisoriesFound: ProcessedAdvisory[];
advisoryPathsFound: string[];

constructor(config: AuditCiConfig) {
Expand All @@ -60,7 +77,7 @@ class Model {
this.advisoryPathsFound = [];
}

process(advisory) {
process(advisory: ProcessedAdvisory) {
if (!this.failingSeverities[advisory.severity]) {
return;
}
Expand Down Expand Up @@ -107,162 +124,149 @@ class Model {
this.advisoryPathsFound.push(...falsy);
}

load(parsedOutput) {
/** NPM 6 */

if (parsedOutput.advisories) {
for (const advisory of Object.values(parsedOutput.advisories)) {
const advisoryAny = advisory as any;
// eslint-disable-next-line no-param-reassign, prefer-destructuring
advisoryAny.github_advisory_id = gitHubAdvisoryUrlToAdvisoryId(
advisoryAny.url
load(
parsedOutput:
| NPMAuditReportV2.Audit
| NPMAuditReportV1.Audit
| YarnAudit.AuditAdvisory
| PNPMAuditReport.Audit
) {
/** NPM 6 & PNPM */

if ("advisories" in parsedOutput && parsedOutput.advisories) {
for (const advisory of Object.values<
DeepWriteable<NPMAuditReportV1.Advisory | PNPMAuditReport.Advisory>
>(parsedOutput.advisories)) {
advisory.github_advisory_id = gitHubAdvisoryUrlToAdvisoryId(
advisory.url
);
// PNPM paths have a leading `.>`
// "paths": [
// ".>module-name"
//]
for (const finding of advisoryAny.findings) {
const findingAny = finding as any;
findingAny.paths = findingAny.paths.map((path) =>
path.replace(".>", "")
);
for (const finding of advisory.findings) {
finding.paths = finding.paths.map((path) => path.replace(".>", ""));
}
this.process(advisory);
}
return this.getSummary();
}

/** NPM 7+ & PNPM */

// This is incomplete. Rather than filling it out, plan on consuming an external dependency to manage.
type NPM7Vulnerability = {
name: string;
via: (string | object)[];
isDirect: boolean;
effects: string[];
};

const advisoryMap = new Map<
string,
{
id: number;
github_advisory_id: string;
severity: string;
module_name: string;
url: string;
findingsSet: Set<string>;
findings: { paths: string[] }[];
}
>();
// First, let's deal with building a structure that's as close to NPM 6 as we can
// without dealing with the findings.
for (const vulnerability of Object.values<NPM7Vulnerability>(
parsedOutput.vulnerabilities
)) {
const { via: vias, isDirect } = vulnerability;
for (const via of vias.filter(
(via) => typeof via !== "string"
) as any[]) {
if (!advisoryMap.has(via.source)) {
advisoryMap.set(via.source, {
id: via.source,
github_advisory_id: gitHubAdvisoryUrlToAdvisoryId(via.url),
module_name: via.name,
severity: via.severity,
url: via.url,
// This will eventually be an array.
// However, to improve the performance of deduplication,
// start with a set.
findingsSet: new Set(
[isDirect ? via.name : undefined].filter(Boolean)
),
findings: [],
});
/** NPM 7+ */
if ("vulnerabilities" in parsedOutput && parsedOutput.vulnerabilities) {
const advisoryMap = new Map<
number,
ProcessedAdvisory & {
findingsSet: Set<string>;
}
>();
// First, let's deal with building a structure that's as close to NPM 6 as we can
// without dealing with the findings.
for (const vulnerability of Object.values<NPMAuditReportV2.Advisory>(
parsedOutput.vulnerabilities
)) {
const { via: vias, isDirect } = vulnerability;
for (const via of vias.filter((via) => typeof via !== "string")) {
if (!advisoryMap.has(via.source)) {
advisoryMap.set(via.source, {
id: via.source,
github_advisory_id: gitHubAdvisoryUrlToAdvisoryId(via.url),
module_name: via.name,
severity: via.severity,
url: via.url,
// This will eventually be an array.
// However, to improve the performance of deduplication,
// start with a set.
findingsSet: new Set(isDirect ? [via.name] : []),
findings: [],
});
}
}
}
}

// Now, all we have to deal with is develop the 'findings' property by traversing
// the audit tree.
// Now, all we have to deal with is develop the 'findings' property by traversing
// the audit tree.

const visitedModules = new Map<string, string[]>();
const visitedModules = new Map<string, string[]>();

for (const vuln of Object.entries<NPM7Vulnerability>(
parsedOutput.vulnerabilities
)) {
// Did this approach rather than destructuring within the forEach to type vulnerability
const moduleName = vuln[0];
const vulnerability = vuln[1];
const { via: vias, isDirect } = vulnerability;
for (const vuln of Object.entries<NPMAuditReportV2.Advisory>(
parsedOutput.vulnerabilities
)) {
// Did this approach rather than destructuring within the forEach to type vulnerability
const moduleName = vuln[0];
const vulnerability = vuln[1];
const { via: vias, isDirect } = vulnerability;

if (vias.length === 0 || typeof vias[0] === "string") {
continue;
}

const visited = new Set<string>();

const recursiveMagic = (
cVuln: NPM7Vulnerability,
dependencyPath: string
): string[] => {
const visitedModule = visitedModules.get(cVuln.name);
if (visitedModule) {
return visitedModule.map((name) => {
const resultWithExtraCarat = prependPath(name, dependencyPath);
return resultWithExtraCarat.slice(
0,
Math.max(0, resultWithExtraCarat.length - 1)
);
});
if (vias.length === 0 || typeof vias[0] === "string") {
continue;
}

if (visited.has(cVuln.name)) {
// maybe undefined and filter?
return [dependencyPath];
const visited = new Set<string>();

const recursiveMagic = (
cVuln: NPMAuditReportV2.Advisory,
dependencyPath: string
): string[] => {
const visitedModule = visitedModules.get(cVuln.name);
if (visitedModule) {
return visitedModule.map((name) => {
const resultWithExtraCarat = prependPath(name, dependencyPath);
return resultWithExtraCarat.slice(
0,
Math.max(0, resultWithExtraCarat.length - 1)
);
});
}

if (visited.has(cVuln.name)) {
// maybe undefined and filter?
return [dependencyPath];
}
visited.add(cVuln.name);
const newPath = prependPath(cVuln.name, dependencyPath);
if (cVuln.effects.length === 0) {
return [newPath.slice(0, Math.max(0, newPath.length - 1))];
}
const result = cVuln.effects.flatMap((effect) =>
recursiveMagic(parsedOutput.vulnerabilities[effect], newPath)
);
return result;
};

const result = recursiveMagic(vulnerability, "");
if (isDirect) {
result.push(moduleName);
}
visited.add(cVuln.name);
const newPath = prependPath(cVuln.name, dependencyPath);
if (cVuln.effects.length === 0) {
return [newPath.slice(0, Math.max(0, newPath.length - 1))];
const advisories = (
vias.filter((via) => typeof via !== "string") as any[]
)
.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) {
advisory.findingsSet.add(path);
}
}
const result = cVuln.effects.flatMap((effect) =>
recursiveMagic(parsedOutput.vulnerabilities[effect], newPath)
);
return result;
};

const result = recursiveMagic(vulnerability, "");
if (isDirect) {
result.push(moduleName);
// Optimization to prevent extra traversals.
visitedModules.set(moduleName, result);
}
const advisories = (
vias.filter((via) => typeof via !== "string") as any[]
)
.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) {
advisory.findingsSet.add(path);
}
for (const [, advisory] of advisoryMap) {
advisory.findings = [{ paths: [...advisory.findingsSet] }];
// @ts-expect-error don't care about findingSet anymore
delete advisory.findingsSet;
this.process(advisory);
}
// Optimization to prevent extra traversals.
visitedModules.set(moduleName, result);
}

for (const [, advisory] of advisoryMap) {
advisory.findings = [{ paths: [...advisory.findingsSet] }];
// @ts-expect-error don't care about findingSet anymore
delete advisory.findingsSet;
this.process(advisory);
}

return this.getSummary();
}

getSummary(advisoryMapper = (a) => a.github_advisory_id) {
getSummary(
advisoryMapper: (advisory) => GitHubAdvisoryId = (a) => a.github_advisory_id
) {
const foundSeverities = new Set<string>();
for (const { severity } of this.advisoriesFound)
foundSeverities.add(severity);
Expand Down