Skip to content

Commit

Permalink
chore(deps): Add audit-types (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
quinnturner committed Apr 28, 2022
1 parent 8c48d25 commit 7bd87e4
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 158 deletions.
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

0 comments on commit 7bd87e4

Please sign in to comment.