audit-ci
Version:
Audits NPM, Yarn, and PNPM projects in CI environments
1 lines • 90.3 kB
Source Map (JSON)
{"version":3,"sources":["../lib/colors.ts","../lib/npm-auditor.ts","../lib/common.ts","../lib/config.ts","../lib/nsp-record.ts","../lib/allowlist.ts","../lib/map-vulnerability.ts","../lib/model.ts","../lib/pnpm-auditor.ts","../lib/yarn-auditor.ts","../lib/yarn-version.ts","../lib/audit.ts","../lib/audit-ci.ts","../lib/bin.ts"],"sourcesContent":["export const blue = \"\\u001B[36m%s\\u001B[0m\";\nexport const green = \"\\u001B[32m%s\\u001B[0m\";\nexport const red = \"\\u001B[31m%s\\u001B[0m\";\nexport const yellow = \"\\u001B[33m%s\\u001B[0m\";\n","import type {\n GitHubAdvisoryId,\n NPMAuditReportV1,\n NPMAuditReportV2,\n} from \"audit-types\";\nimport { blue } from \"./colors.js\";\nimport { reportAudit, ReportConfig, runProgram } from \"./common.js\";\nimport {\n AuditCiConfig,\n AuditCiFullConfig,\n mapAuditCiConfigToAuditCiFullConfig,\n} from \"./config.js\";\nimport Model, { Summary } from \"./model.js\";\n\nasync function runNpmAudit(\n config: AuditCiFullConfig,\n): Promise<NPMAuditReportV1.AuditResponse | NPMAuditReportV2.AuditResponse> {\n const {\n directory,\n registry,\n _npm,\n \"skip-dev\": skipDevelopmentDependencies,\n \"extra-args\": extraArguments,\n } = config;\n const npmExec = _npm || \"npm\";\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let stdoutBuffer: any = {};\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n function outListener(data: any) {\n stdoutBuffer = { ...stdoutBuffer, ...data };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const stderrBuffer: any[] = [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n function errorListener(line: any) {\n stderrBuffer.push(line);\n }\n\n const arguments_ = [\"audit\", \"--json\"];\n if (registry) {\n arguments_.push(\"--registry\", registry);\n }\n if (skipDevelopmentDependencies) {\n arguments_.push(\"--production\");\n }\n if (extraArguments) {\n arguments_.push(...extraArguments);\n }\n const options = { cwd: directory };\n await runProgram(npmExec, arguments_, options, outListener, errorListener);\n if (stderrBuffer.length > 0) {\n throw new Error(\n `Invocation of npm audit failed:\\n${stderrBuffer.join(\"\\n\")}`,\n );\n }\n return stdoutBuffer;\n}\nexport function isV2Audit(\n parsedOutput: NPMAuditReportV1.Audit | NPMAuditReportV2.Audit,\n): parsedOutput is NPMAuditReportV2.Audit {\n return (\n \"auditReportVersion\" in parsedOutput &&\n parsedOutput.auditReportVersion === 2\n );\n}\n\nfunction printReport(\n parsedOutput: NPMAuditReportV1.Audit | NPMAuditReportV2.Audit,\n levels: AuditCiFullConfig[\"levels\"],\n reportType: \"full\" | \"important\" | \"summary\",\n outputFormat: \"text\" | \"json\",\n) {\n const printReportObject = (text: string, object: unknown) => {\n if (outputFormat === \"text\") {\n console.log(blue, text);\n }\n console.log(JSON.stringify(object, undefined, 2));\n };\n switch (reportType) {\n case \"full\": {\n printReportObject(\"NPM audit report JSON:\", parsedOutput);\n break;\n }\n case \"important\": {\n const relevantAdvisories = (() => {\n if (isV2Audit(parsedOutput)) {\n const advisories = parsedOutput.vulnerabilities;\n const relevantAdvisoryLevels = Object.keys(advisories).filter(\n (advisory) => {\n const severity = advisories[advisory].severity;\n return severity !== \"info\" && levels[severity];\n },\n );\n\n const relevantAdvisories: Record<string, NPMAuditReportV2.Advisory> =\n {};\n for (const advisory of relevantAdvisoryLevels) {\n relevantAdvisories[advisory] = advisories[advisory];\n }\n return relevantAdvisories;\n } else {\n const advisories = parsedOutput.advisories;\n const advisoryKeys = Object.keys(advisories) as GitHubAdvisoryId[];\n const relevantAdvisoryLevels = advisoryKeys.filter((advisory) => {\n const severity = advisories[advisory].severity;\n return severity !== \"info\" && levels[severity];\n });\n\n const relevantAdvisories: Record<\n GitHubAdvisoryId,\n NPMAuditReportV1.Advisory\n > = {};\n for (const advisory of relevantAdvisoryLevels) {\n relevantAdvisories[advisory] = advisories[advisory];\n }\n return relevantAdvisories;\n }\n })();\n\n const keyFindings = {\n advisories: relevantAdvisories,\n metadata: parsedOutput.metadata,\n };\n printReportObject(\"NPM audit report results:\", keyFindings);\n break;\n }\n case \"summary\": {\n printReportObject(\"NPM audit report summary:\", parsedOutput.metadata);\n break;\n }\n default: {\n throw new Error(\n `Invalid report type: ${reportType}. Should be \\`['important', 'full', 'summary']\\`.`,\n );\n }\n }\n}\n\nexport function report(\n parsedOutput: NPMAuditReportV1.Audit | NPMAuditReportV2.Audit,\n config: AuditCiFullConfig,\n reporter: (\n summary: Summary,\n config: ReportConfig,\n audit: NPMAuditReportV1.Audit | NPMAuditReportV2.Audit,\n ) => Summary,\n) {\n const {\n levels,\n \"report-type\": reportType,\n \"output-format\": outputFormat,\n } = config;\n printReport(parsedOutput, levels, reportType, outputFormat);\n const model = new Model(config);\n const summary = model.load(parsedOutput);\n return reporter(summary, config, parsedOutput);\n}\n\n/**\n * Audit your NPM project!\n *\n * @returns Returns the audit report summary on resolve, `Error` on rejection.\n */\nexport async function auditWithFullConfig(\n config: AuditCiFullConfig,\n reporter = reportAudit,\n) {\n const parsedOutput = await runNpmAudit(config);\n if (\"error\" in parsedOutput) {\n const { code, summary } = parsedOutput.error;\n throw new Error(`code ${code}: ${summary}`);\n } else if (\"message\" in parsedOutput) {\n throw new Error(parsedOutput.message);\n }\n return report(parsedOutput, config, reporter);\n}\n\n/**\n * Audit your NPM project!\n *\n * @returns Returns the audit report summary on resolve, `Error` on rejection.\n */\nexport async function audit(config: AuditCiConfig, reporter = reportAudit) {\n const fullConfig = mapAuditCiConfigToAuditCiFullConfig(config);\n return await auditWithFullConfig(fullConfig, reporter);\n}\n","import { GitHubAdvisoryId } from \"audit-types\";\nimport { SpawnOptionsWithoutStdio } from \"child_process\";\nimport { spawn } from \"cross-spawn\";\nimport escapeStringRegexp from \"escape-string-regexp\";\nimport eventStream from \"event-stream\";\nimport * as JSONStream from \"jsonstream-next\";\nimport ReadlineTransform from \"readline-transform\";\nimport Allowlist from \"./allowlist.js\";\nimport { blue, yellow } from \"./colors.js\";\nimport { AuditCiConfig } from \"./config.js\";\nimport { Summary } from \"./model.js\";\n\nexport function partition<T>(a: T[], fun: (parameter: T) => boolean) {\n const returnValue: { truthy: T[]; falsy: T[] } = { truthy: [], falsy: [] };\n for (const item of a) {\n if (fun(item)) {\n returnValue.truthy.push(item);\n } else {\n returnValue.falsy.push(item);\n }\n }\n return returnValue;\n}\n\nexport type ReportConfig = Pick<\n AuditCiConfig,\n \"show-found\" | \"show-not-found\" | \"output-format\"\n> & { allowlist: Allowlist };\n\nexport function reportAudit(summary: Summary, config: ReportConfig) {\n const {\n allowlist,\n \"show-not-found\": showNotFound,\n \"show-found\": showFound,\n \"output-format\": outputFormat,\n } = config;\n const {\n allowlistedModulesFound,\n allowlistedAdvisoriesFound,\n allowlistedModulesNotFound,\n allowlistedAdvisoriesNotFound,\n allowlistedPathsNotFound,\n failedLevelsFound,\n advisoriesFound,\n advisoryPathsFound,\n } = summary;\n\n if (outputFormat === \"text\") {\n if (allowlist.modules.length > 0) {\n console.log(\n blue,\n `Modules to allowlist: ${allowlist.modules.join(\", \")}.`,\n );\n }\n\n if (showFound) {\n if (allowlistedModulesFound.length > 0) {\n const found = allowlistedModulesFound.join(\", \");\n console.warn(yellow, `Found vulnerable allowlisted modules: ${found}.`);\n }\n if (allowlistedAdvisoriesFound.length > 0) {\n const found = allowlistedAdvisoriesFound.join(\", \");\n console.warn(\n yellow,\n `Found vulnerable allowlisted advisories: ${found}.`,\n );\n }\n }\n if (showNotFound) {\n if (allowlistedModulesNotFound.length > 0) {\n const found = allowlistedModulesNotFound\n .sort((a, b) => a.localeCompare(b))\n .join(\", \");\n const allowlistMessage =\n allowlistedModulesNotFound.length === 1\n ? `Consider not allowlisting module: ${found}.`\n : `Consider not allowlisting modules: ${found}.`;\n console.warn(yellow, allowlistMessage);\n }\n if (allowlistedAdvisoriesNotFound.length > 0) {\n const found = allowlistedAdvisoriesNotFound\n .sort((a, b) => a.localeCompare(b))\n .join(\", \");\n const allowlistMessage =\n allowlistedAdvisoriesNotFound.length === 1\n ? `Consider not allowlisting advisory: ${found}.`\n : `Consider not allowlisting advisories: ${found}.`;\n console.warn(yellow, allowlistMessage);\n }\n if (allowlistedPathsNotFound.length > 0) {\n const found = allowlistedPathsNotFound\n .sort((a, b) => a.localeCompare(b))\n .join(\", \");\n const allowlistMessage =\n allowlistedPathsNotFound.length === 1\n ? `Consider not allowlisting path: ${found}.`\n : `Consider not allowlisting paths: ${found}.`;\n console.warn(yellow, allowlistMessage);\n }\n }\n\n if (advisoryPathsFound.length > 0) {\n const found = advisoryPathsFound.join(\"\\n\");\n console.warn(yellow, `Found vulnerable advisory paths:`);\n console.log(found);\n }\n }\n\n if (failedLevelsFound.length > 0) {\n // Get the levels that have failed by filtering the keys with true values\n throw new Error(\n `Failed security audit due to ${failedLevelsFound.join(\n \", \",\n )} vulnerabilities.\\nVulnerable advisories are:\\n${advisoriesFound\n .map((element) => gitHubAdvisoryIdToUrl(element))\n .join(\"\\n\")}`,\n );\n }\n return summary;\n}\n\nfunction hasMessage(value: unknown): value is { message: unknown } {\n return typeof value === \"object\" && value != undefined && \"message\" in value;\n}\n\nfunction hasStatusCode(\n value: unknown,\n): value is { statusCode: unknown; message: unknown } {\n return (\n typeof value === \"object\" && value != undefined && \"statusCode\" in value\n );\n}\n\nexport function runProgram(\n command: string,\n arguments_: readonly string[],\n options: SpawnOptionsWithoutStdio,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n stdoutListener: (data: any) => void,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n stderrListener: (data: any) => void,\n) {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n const transform = new ReadlineTransform({ skipEmpty: true });\n const proc = spawn(command, arguments_, options);\n let recentMessage: string;\n let errorMessage: string;\n proc.stdout.setEncoding(\"utf8\");\n proc.stdout\n .pipe(\n transform.on(\"error\", (error: unknown) => {\n throw error;\n }),\n )\n .pipe(\n eventStream.mapSync((data: string) => {\n recentMessage = data;\n return data;\n }),\n )\n .pipe(\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error -- JSONStream.parse() accepts (pattern: any) when it should accept (pattern?: any)\n JSONStream.parse().on(\"error\", () => {\n errorMessage = recentMessage;\n throw new Error(errorMessage);\n }),\n )\n .pipe(\n eventStream.mapSync((data: unknown) => {\n if (!data) return;\n try {\n // due to response without error\n if (\n hasMessage(data) &&\n typeof data.message === \"string\" &&\n data.message.includes(\"ENOTFOUND\")\n ) {\n stderrListener(data.message);\n return;\n }\n // TODO: There are no tests that cover this case, not sure when this happens.\n if (hasStatusCode(data) && data.statusCode === 404) {\n stderrListener(data.message);\n return;\n }\n\n stdoutListener(data);\n } catch (error) {\n stderrListener(error);\n }\n }),\n );\n return new Promise<void>((resolve, reject) => {\n proc.on(\"close\", () => {\n if (errorMessage) {\n return reject(new Error(errorMessage));\n }\n return resolve();\n });\n proc.on(\"error\", (error) =>\n reject(errorMessage ? new Error(errorMessage) : error),\n );\n });\n}\n\nfunction wildcardToRegex(stringWithWildcard: string) {\n const regexString = stringWithWildcard\n .split(/\\*+/) // split at every wildcard (*) character\n .map((s) => escapeStringRegexp(s)) // escape the substrings to make sure that they aren't evaluated\n .join(\".*\"); // construct a regex matching anything at each wildcard location\n return new RegExp(`^${regexString}$`);\n}\n\nexport function matchString(template: string, string_: string) {\n return template.includes(\"*\")\n ? wildcardToRegex(template).test(string_)\n : template === string_;\n}\n\nexport function isGitHubAdvisoryId(id: unknown): id is GitHubAdvisoryId {\n return typeof id === \"string\" && id.startsWith(\"GHSA\");\n}\n\nexport function gitHubAdvisoryUrlToAdvisoryId(url: string): GitHubAdvisoryId {\n return url.split(\"/\")[4] as GitHubAdvisoryId;\n}\n\nexport function gitHubAdvisoryIdToUrl<T extends string>(\n id: T,\n): `https://github.com/advisories/${T}` {\n return `https://github.com/advisories/${id}`;\n}\n\nexport function deduplicate(array: readonly string[]) {\n return [...new Set(array)];\n}\n","import { existsSync, readFileSync } from \"fs\";\nimport jju from \"jju\";\n// eslint-disable-next-line unicorn/import-style\nimport * as path from \"path\";\nimport { hideBin } from \"yargs/helpers\";\nimport yargs from \"yargs\";\nimport Allowlist, { type AllowlistRecord } from \"./allowlist.js\";\nimport {\n mapVulnerabilityLevelInput,\n type VulnerabilityLevels,\n} from \"./map-vulnerability.js\";\n\nfunction mapReportTypeInput(\n config: Pick<AuditCiPreprocessedConfig, \"report-type\">,\n) {\n const { \"report-type\": reportType } = config;\n switch (reportType) {\n case \"full\":\n case \"important\":\n case \"summary\": {\n return reportType;\n }\n default: {\n throw new Error(\n `Invalid report type: ${reportType}. Should be \\`['important', 'full', 'summary']\\`.`,\n );\n }\n }\n}\n\nfunction mapExtraArgumentsInput(\n config: Pick<AuditCiPreprocessedConfig, \"extra-args\">,\n) {\n // These args will often be flags for another command, so we\n // want to have some way of escaping args that start with a -.\n // We'll look for and remove a single backslash at the start, if present.\n return config[\"extra-args\"].map((a) => a.replace(/^\\\\/, \"\"));\n}\n\n/**\n * The output of `Yargs`'s `parse` function.\n * This is the type of the `argv` object.\n */\ntype AuditCiPreprocessedConfig = {\n /** Exit for low or above vulnerabilities */\n l: boolean;\n /** Exit for moderate or above vulnerabilities */\n m: boolean;\n /** Exit for high or above vulnerabilities */\n h: boolean;\n /** Exit for critical or above vulnerabilities */\n c: boolean;\n /** Exit for low or above vulnerabilities */\n low: boolean;\n /** Exit for moderate or above vulnerabilities */\n moderate: boolean;\n /** Exit for high or above vulnerabilities */\n high: boolean;\n /** Exit for critical vulnerabilities */\n critical: boolean;\n /** Package manager */\n p: \"auto\" | \"npm\" | \"yarn\" | \"pnpm\";\n /** Show a full audit report */\n r: boolean;\n /** Show a full audit report */\n report: boolean;\n /** Show a summary audit report */\n s: boolean;\n /** Show a summary audit report */\n summary: boolean;\n /** Package manager */\n \"package-manager\": \"auto\" | \"npm\" | \"yarn\" | \"pnpm\";\n a: string[];\n allowlist: AllowlistRecord[];\n /** The directory containing the package.json to audit */\n d: string;\n /** The directory containing the package.json to audit */\n directory: string;\n /** show allowlisted advisories that are not found. */\n \"show-not-found\": boolean;\n /** Show allowlisted advisories that are found */\n \"show-found\": boolean;\n /** the registry to resolve packages by name and version */\n registry?: string;\n /** The format of the output of audit-ci */\n o: \"text\" | \"json\";\n /** The format of the output of audit-ci */\n \"output-format\": \"text\" | \"json\";\n /** how the audit report is displayed. */\n \"report-type\": \"full\" | \"important\" | \"summary\";\n /** The number of attempts audit-ci calls an unavailable registry before failing */\n \"retry-count\": number;\n /** Pass if no audit is performed due to the registry returning ENOAUDIT */\n \"pass-enoaudit\": boolean;\n /** skip devDependencies */\n \"skip-dev\": boolean;\n /** extra positional args for underlying audit command */\n \"extra-args\": string[];\n};\n\n// Rather than exporting a weird union type, we resolve the type to a simple object.\ntype ComplexConfig = Omit<\n AuditCiPreprocessedConfig,\n // Remove single-letter options from the base config to avoid confusion.\n | \"allowlist\"\n | \"a\"\n | \"p\"\n | \"o\"\n | \"d\"\n | \"s\"\n | \"r\"\n | \"l\"\n | \"m\"\n | \"h\"\n | \"c\"\n | \"low\"\n | \"moderate\"\n | \"high\"\n | \"critical\"\n> & {\n /** Package manager */\n \"package-manager\": \"npm\" | \"yarn\" | \"pnpm\";\n /** An object containing a list of modules, advisories, and module paths that should not break the build if their vulnerability is found. */\n allowlist: Allowlist;\n /** The vulnerability levels to fail on, if `moderate` is set `true`, `high` and `critical` should be as well. */\n levels: { [K in keyof VulnerabilityLevels]: VulnerabilityLevels[K] };\n /**\n * A path to npm, uses npm from `$PATH` if not specified\n * @internal\n */\n _npm?: string;\n /**\n * A path to pnpm, uses pnpm from `$PATH` if not specified\n * @internal\n */\n _pnpm?: string;\n /**\n * A path to yarn, uses yarn from `$PATH` if not specified\n * @internal\n */\n _yarn?: string;\n};\n\nexport type AuditCiFullConfig = {\n [K in keyof ComplexConfig]: ComplexConfig[K];\n};\n\ntype AuditCiConfigComplex = Omit<\n Partial<AuditCiFullConfig>,\n \"levels\" | \"allowlist\"\n> & {\n allowlist?: AllowlistRecord[];\n low?: boolean;\n moderate?: boolean;\n high?: boolean;\n critical?: boolean;\n};\n\nexport type AuditCiConfig = {\n [K in keyof AuditCiConfigComplex]: AuditCiConfigComplex[K];\n};\n\n/**\n * @param pmArgument the package manager (including the `auto` option)\n * @param directory the directory where the package manager files exist\n * @returns the non-`auto` package manager\n */\nfunction resolvePackageManagerType(\n pmArgument: \"auto\" | \"npm\" | \"yarn\" | \"pnpm\",\n directory: string,\n): \"npm\" | \"yarn\" | \"pnpm\" {\n switch (pmArgument) {\n case \"npm\":\n case \"pnpm\":\n case \"yarn\": {\n return pmArgument;\n }\n case \"auto\": {\n const getPath = (file: string) => path.resolve(directory, file);\n // TODO: Consider prioritizing `package.json#packageManager` for determining the package manager.\n const packageLockExists = existsSync(getPath(\"package-lock.json\"));\n if (packageLockExists) return \"npm\";\n const shrinkwrapExists = existsSync(getPath(\"npm-shrinkwrap.json\"));\n if (shrinkwrapExists) return \"npm\";\n const yarnLockExists = existsSync(getPath(\"yarn.lock\"));\n if (yarnLockExists) return \"yarn\";\n const pnpmLockExists = existsSync(getPath(\"pnpm-lock.yaml\"));\n if (pnpmLockExists) return \"pnpm\";\n throw new Error(\n \"Cannot establish package-manager type, missing package-lock.json, yarn.lock, and pnpm-lock.yaml.\",\n );\n }\n default: {\n throw new Error(`Unexpected package manager argument: ${pmArgument}`);\n }\n }\n}\n\nconst defaults = {\n low: false,\n moderate: false,\n high: false,\n critical: false,\n \"skip-dev\": false,\n \"pass-enoaudit\": false,\n \"retry-count\": 5,\n \"report-type\": \"important\" as const,\n report: false,\n directory: \"./\",\n \"package-manager\": \"auto\" as const,\n \"show-not-found\": true,\n \"show-found\": true,\n registry: undefined,\n summary: false,\n allowlist: [] as AllowlistRecord[],\n \"output-format\": \"text\" as const,\n \"extra-args\": [] as string[],\n};\n\nfunction mapArgvToAuditCiConfig(argv: AuditCiPreprocessedConfig) {\n const allowlist = Allowlist.mapConfigToAllowlist(argv);\n\n const {\n low,\n moderate,\n high,\n critical,\n \"package-manager\": packageManager,\n directory,\n } = argv;\n\n const resolvedPackageManager = resolvePackageManagerType(\n packageManager,\n directory,\n );\n\n const result: AuditCiFullConfig = {\n ...argv,\n \"package-manager\": resolvedPackageManager,\n levels: mapVulnerabilityLevelInput({\n low,\n moderate,\n high,\n critical,\n }),\n \"report-type\": mapReportTypeInput(argv),\n allowlist: allowlist,\n \"extra-args\": mapExtraArgumentsInput(argv),\n };\n return result;\n}\n\nexport function mapAuditCiConfigToAuditCiFullConfig(\n config: AuditCiConfig,\n): AuditCiFullConfig {\n const packageManager =\n config[\"package-manager\"] ?? defaults[\"package-manager\"];\n const directory = config.directory ?? defaults.directory;\n\n const resolvedPackageManager = resolvePackageManagerType(\n packageManager,\n directory,\n );\n\n const allowlist = Allowlist.mapConfigToAllowlist({\n allowlist: config.allowlist ?? defaults.allowlist,\n });\n\n const levels = mapVulnerabilityLevelInput({\n low: config.low ?? defaults.low,\n moderate: config.moderate ?? defaults.moderate,\n high: config.high ?? defaults.high,\n critical: config.critical ?? defaults.critical,\n });\n\n const fullConfig: AuditCiFullConfig = {\n \"skip-dev\": config[\"skip-dev\"] ?? defaults[\"skip-dev\"],\n \"pass-enoaudit\": config[\"pass-enoaudit\"] ?? defaults[\"pass-enoaudit\"],\n \"retry-count\": config[\"retry-count\"] ?? defaults[\"retry-count\"],\n \"report-type\": config[\"report-type\"] ?? defaults[\"report-type\"],\n \"package-manager\": resolvedPackageManager,\n directory,\n report: config.report ?? defaults.report,\n registry: config.registry ?? defaults.registry,\n \"show-not-found\": config[\"show-not-found\"] ?? defaults[\"show-not-found\"],\n \"show-found\": config[\"show-found\"] ?? defaults[\"show-found\"],\n summary: config.summary ?? defaults.summary,\n \"output-format\": config[\"output-format\"] ?? defaults[\"output-format\"],\n allowlist,\n levels,\n \"extra-args\": config[\"extra-args\"] ?? defaults[\"extra-args\"],\n };\n return fullConfig;\n}\n\nexport async function runYargs(): Promise<AuditCiFullConfig> {\n const { argv } = yargs(hideBin(process.argv))\n .config(\"config\", (configPath) =>\n // Supports JSON, JSONC, & JSON5\n jju.parse(readFileSync(configPath, \"utf8\"), {\n // When passing an allowlist using NSRecord syntax, yargs will throw an error\n // \"Invalid JSON config file\". We need to add this flag to prevent that.\n null_prototype: false,\n }),\n )\n .options({\n l: {\n alias: \"low\",\n default: defaults.low,\n describe: \"Exit for low vulnerabilities or higher\",\n type: \"boolean\",\n },\n m: {\n alias: \"moderate\",\n default: defaults.moderate,\n describe: \"Exit for moderate vulnerabilities or higher\",\n type: \"boolean\",\n },\n h: {\n alias: \"high\",\n default: defaults.high,\n describe: \"Exit for high vulnerabilities or higher\",\n type: \"boolean\",\n },\n c: {\n alias: \"critical\",\n default: defaults.critical,\n describe: \"Exit for critical vulnerabilities\",\n type: \"boolean\",\n },\n p: {\n alias: \"package-manager\",\n default: defaults[\"package-manager\"],\n describe: \"Choose a package manager\",\n choices: [\"auto\", \"npm\", \"yarn\", \"pnpm\"],\n },\n r: {\n alias: \"report\",\n default: defaults.report,\n describe: \"Show a full audit report\",\n type: \"boolean\",\n },\n s: {\n alias: \"summary\",\n default: defaults.summary,\n describe: \"Show a summary audit report\",\n type: \"boolean\",\n },\n a: {\n alias: \"allowlist\",\n default: [],\n describe:\n \"Allowlist module names (example), advisories (123), and module paths (123|example1>example2)\",\n type: \"array\",\n },\n d: {\n alias: \"directory\",\n default: \"./\",\n describe: \"The directory containing the package.json to audit\",\n type: \"string\",\n },\n o: {\n alias: \"output-format\",\n default: \"text\",\n describe: \"The format of the output of audit-ci\",\n choices: [\"text\", \"json\"],\n },\n \"show-found\": {\n default: defaults[\"show-found\"],\n describe: \"Show allowlisted advisories that are found\",\n type: \"boolean\",\n },\n \"show-not-found\": {\n default: defaults[\"show-not-found\"],\n describe: \"Show allowlisted advisories that are not found\",\n type: \"boolean\",\n },\n registry: {\n default: defaults.registry,\n describe: \"The registry to resolve packages by name and version\",\n type: \"string\",\n },\n \"report-type\": {\n default: defaults[\"report-type\"],\n describe: \"Format for the audit report results\",\n type: \"string\",\n choices: [\"important\", \"summary\", \"full\"],\n },\n \"retry-count\": {\n default: defaults[\"retry-count\"],\n describe:\n \"The number of attempts audit-ci calls an unavailable registry before failing\",\n type: \"number\",\n },\n \"pass-enoaudit\": {\n default: defaults[\"pass-enoaudit\"],\n describe:\n \"Pass if no audit is performed due to the registry returning ENOAUDIT\",\n type: \"boolean\",\n },\n \"skip-dev\": {\n default: defaults[\"skip-dev\"],\n describe: \"Skip devDependencies\",\n type: \"boolean\",\n },\n \"extra-args\": {\n default: [],\n describe: \"Pass additional arguments to the underlying audit command\",\n type: \"array\",\n },\n })\n .help(\"help\");\n\n // yargs doesn't support aliases + TypeScript\n const awaitedArgv = (await argv) as unknown as AuditCiPreprocessedConfig;\n const auditCiConfig = mapArgvToAuditCiConfig(awaitedArgv);\n return auditCiConfig;\n}\n","import type { GitHubAdvisoryId } from \"audit-types\";\n\nexport interface NSPContent {\n readonly active?: boolean;\n readonly notes?: string;\n readonly expiry?: string | number;\n}\n\nexport type NSPRecord = Record<string, NSPContent>;\nexport type GitHubNSPRecord = Record<GitHubAdvisoryId, NSPContent>;\n\n/**\n * Retrieves the allowlist id from the NSPRecord.\n *\n * @param nspRecord NSPRecord object.\n * @returns The advisory id.\n */\nexport function getAllowlistId(nspRecord: NSPRecord | GitHubNSPRecord): string {\n return Object.keys(nspRecord)[0];\n}\n\n/**\n * Retrieves the content for the NSPRecord.\n *\n * @param nspRecord NSPRecord object.\n * @returns The NSPContent object.\n */\nexport function getNSPContent(\n nspRecord: NSPRecord | GitHubNSPRecord,\n): NSPContent {\n const values = Object.values(nspRecord);\n if (values.length > 0) {\n return values[0];\n }\n throw new Error(\n `Empty NSPRecord is invalid. Here's an example of a valid NSPRecord:\n{\n \"allowlist\": [\n {\n \"vulnerable-module\": {\n \"active\": true,\n \"notes\": \"This is a note\",\n \"expiry\": \"2022-01-01\"\n }\n }\n ]\n}\n `,\n );\n}\n\n/**\n * Determines if the NSPRecord is active.\n *\n * @param nspRecord NSPRecord object.\n * @param now The current date. The default is initialized to the current date.\n * @returns True if the record is active, false otherwise.\n */\nexport function isNSPRecordActive(\n nspRecord: NSPRecord,\n now = new Date(),\n): boolean {\n const content = getNSPContent(nspRecord);\n if (!content.active) {\n return false;\n }\n\n if (content.expiry) {\n const expiryDate = new Date(content.expiry);\n if (expiryDate.getTime() > 0) {\n // Expiry is valid, check if we've passed it yet.\n return now.getTime() < expiryDate.getTime();\n }\n\n // Expiry isn't valid. For safety, disable the rule.\n return false;\n }\n\n return true;\n}\n","import type { GitHubAdvisoryId } from \"audit-types\";\nimport { isGitHubAdvisoryId } from \"./common.js\";\nimport {\n type NSPContent,\n type NSPRecord,\n type GitHubNSPRecord,\n getAllowlistId,\n isNSPRecordActive,\n} from \"./nsp-record.js\";\n\nexport type AllowlistRecord = string | NSPRecord;\n\nconst DEFAULT_NSP_CONTENT: Readonly<NSPContent> = {\n active: true,\n notes: undefined,\n expiry: undefined,\n};\n\n/**\n * Takes a string and converts it into a NSPRecord object. If a NSPRecord\n * is passed in, no modifications are made and the record is returned as is.\n *\n * @param recordOrId A string or NSPRecord object.\n * @returns Normalized NSPRecord object.\n */\nexport function normalizeAllowlistRecord(\n recordOrId: AllowlistRecord,\n): NSPRecord {\n return typeof recordOrId === \"string\"\n ? {\n [recordOrId]: DEFAULT_NSP_CONTENT,\n }\n : recordOrId;\n}\n\n/**\n * Removes duplicate allowlist items from an array based on the allowlist id.\n *\n * @param recordsOrIds An array containing allowlist string ids or NSPRecords.\n * @returns An array of NSPRecords with duplicates removed.\n */\nexport function dedupeAllowlistRecords(\n recordsOrIds: AllowlistRecord[],\n): NSPRecord[] {\n const map = new Map<string, NSPRecord>();\n for (const recordOrId of recordsOrIds) {\n const nspRecord = normalizeAllowlistRecord(recordOrId);\n const advisoryId = getAllowlistId(nspRecord);\n\n if (!map.has(advisoryId)) {\n map.set(advisoryId, nspRecord);\n }\n }\n\n return [...map.values()];\n}\n\nclass Allowlist {\n modules: string[];\n advisories: GitHubAdvisoryId[];\n paths: string[];\n moduleRecords: NSPRecord[];\n advisoryRecords: GitHubNSPRecord[];\n pathRecords: NSPRecord[];\n /**\n * @param input the allowlisted module names, advisories, and module paths\n */\n constructor(input?: AllowlistRecord[]) {\n this.modules = [];\n this.advisories = [];\n this.paths = [];\n this.moduleRecords = [];\n this.advisoryRecords = [];\n this.pathRecords = [];\n if (!input) {\n return;\n }\n for (const allowlist of input) {\n if (typeof allowlist === \"number\") {\n throw new TypeError(\n \"Unsupported number as allowlist. Perform codemod to update config to use GitHub advisory as identifiers: https://github.com/quinnturner/audit-ci-codemod with `npx @quinnturner/audit-ci-codemod`. See also: https://github.com/IBM/audit-ci/pull/217\",\n );\n }\n\n const allowlistNspRecord = normalizeAllowlistRecord(allowlist);\n if (!isNSPRecordActive(allowlistNspRecord)) {\n continue;\n }\n\n const allowlistId =\n typeof allowlist === \"string\"\n ? allowlist\n : getAllowlistId(allowlistNspRecord);\n\n if (allowlistId.includes(\">\") || allowlistId.includes(\"|\")) {\n this.paths.push(allowlistId);\n this.pathRecords.push(allowlistNspRecord);\n } else if (isGitHubAdvisoryId(allowlistId)) {\n this.advisories.push(allowlistId);\n this.advisoryRecords.push(allowlistNspRecord);\n } else {\n this.modules.push(allowlistId);\n this.moduleRecords.push(allowlistNspRecord);\n }\n }\n }\n\n static mapConfigToAllowlist(\n config: Readonly<{ allowlist: AllowlistRecord[] }>,\n ) {\n const { allowlist } = config;\n const deduplicatedAllowlist = dedupeAllowlistRecords(allowlist || []);\n const allowlistObject = new Allowlist(deduplicatedAllowlist);\n return allowlistObject;\n }\n}\n\nexport default Allowlist;\n","export type VulnerabilityLevels = {\n low: boolean;\n moderate: boolean;\n high: boolean;\n critical: boolean;\n};\n\nexport function mapVulnerabilityLevelInput({\n low,\n l,\n moderate,\n m,\n high,\n h,\n critical,\n c,\n}: Record<string, boolean>): {\n [K in keyof VulnerabilityLevels]: VulnerabilityLevels[K];\n} {\n if (low || l) {\n return { low: true, moderate: true, high: true, critical: true };\n }\n if (moderate || m) {\n return { low: false, moderate: true, high: true, critical: true };\n }\n if (high || h) {\n return { low: false, moderate: false, high: true, critical: true };\n }\n if (critical || c) {\n return { low: false, moderate: false, high: false, critical: true };\n }\n return { low: false, moderate: false, high: false, critical: false };\n}\n","import type { GitHubAdvisoryId, NPMAuditReportV2 } from \"audit-types\";\nimport Allowlist from \"./allowlist.js\";\nimport {\n gitHubAdvisoryUrlToAdvisoryId,\n matchString,\n partition,\n} from \"./common.js\";\nimport type { AuditCiFullConfig } from \"./config.js\";\nimport type { VulnerabilityLevels } from \"./map-vulnerability.js\";\nimport type { DeepReadonly, DeepWriteable } from \"./types.js\";\n\nconst SUPPORTED_SEVERITY_LEVELS = new Set([\n \"critical\",\n \"high\",\n \"moderate\",\n \"low\",\n]);\n\nconst prependPath = <N extends string, C extends string>(\n newItem: N,\n currentPath: C,\n): `${N}>${C}` => `${newItem}>${currentPath}`;\n\nconst isVia = <T>(via: T | string): via is T => {\n return typeof via !== \"string\";\n};\n\nexport interface Summary {\n advisoriesFound: GitHubAdvisoryId[];\n failedLevelsFound: (\"low\" | \"moderate\" | \"high\" | \"critical\")[];\n allowlistedAdvisoriesNotFound: string[];\n allowlistedModulesNotFound: string[];\n allowlistedPathsNotFound: string[];\n allowlistedAdvisoriesFound: GitHubAdvisoryId[];\n allowlistedModulesFound: string[];\n allowlistedPathsFound: string[];\n advisoryPathsFound: string[];\n}\n\ninterface ProcessedAdvisory {\n id: number;\n github_advisory_id: GitHubAdvisoryId;\n severity: \"critical\" | \"high\" | \"moderate\" | \"low\" | \"info\";\n module_name: string;\n url: `https://github.com/advisories/${GitHubAdvisoryId}`;\n findings: { paths: string[] }[];\n}\n\n// These are hre to simplify testing by requiring only the relevant parts of the\n// audit report.\ninterface PartialNPMAuditReportV1Audit {\n advisories: Readonly<Record<GitHubAdvisoryId, ProcessedAdvisory>>;\n}\ninterface PartialPNPMAuditReportAudit {\n advisories: Readonly<Record<GitHubAdvisoryId, ProcessedAdvisory>>;\n}\ninterface PartialNPMAuditReportV2Audit {\n vulnerabilities: Readonly<\n Record<\n string,\n Pick<NPMAuditReportV2.Advisory, \"name\" | \"isDirect\" | \"via\" | \"effects\"> &\n Partial<NPMAuditReportV2.Advisory>\n >\n >;\n}\n\nclass Model {\n failingSeverities: {\n [K in keyof VulnerabilityLevels]: VulnerabilityLevels[K];\n };\n allowlist: Allowlist;\n allowlistedModulesFound: string[];\n allowlistedAdvisoriesFound: GitHubAdvisoryId[];\n allowlistedPathsFound: `${GitHubAdvisoryId}|${string}`[];\n advisoriesFound: ProcessedAdvisory[];\n advisoryPathsFound: string[];\n\n constructor(config: Pick<AuditCiFullConfig, \"allowlist\" | \"levels\">) {\n const unsupported = Object.keys(config.levels).filter(\n (level) => !SUPPORTED_SEVERITY_LEVELS.has(level),\n );\n if (unsupported.length > 0) {\n throw new Error(\n `Unsupported severity levels found: ${unsupported.sort().join(\", \")}`,\n );\n }\n this.failingSeverities = config.levels;\n\n this.allowlist = config.allowlist;\n\n this.allowlistedModulesFound = [];\n this.allowlistedAdvisoriesFound = [];\n this.allowlistedPathsFound = [];\n this.advisoriesFound = [];\n this.advisoryPathsFound = [];\n }\n\n process(advisory: ProcessedAdvisory) {\n const {\n severity,\n module_name: moduleName,\n github_advisory_id: githubAdvisoryId,\n findings,\n } = advisory;\n if (severity !== \"info\" && !this.failingSeverities[severity]) {\n return;\n }\n\n if (this.allowlist.modules.includes(moduleName)) {\n if (!this.allowlistedModulesFound.includes(moduleName)) {\n this.allowlistedModulesFound.push(moduleName);\n }\n return;\n }\n\n if (this.allowlist.advisories.includes(githubAdvisoryId)) {\n if (!this.allowlistedAdvisoriesFound.includes(githubAdvisoryId)) {\n this.allowlistedAdvisoriesFound.push(githubAdvisoryId);\n }\n return;\n }\n\n const allowlistedPathsFoundSet = new Set<`${GitHubAdvisoryId}|${string}`>();\n\n const flattenedPaths = findings.flatMap((finding) => finding.paths);\n const flattenedAllowlist = flattenedPaths.map(\n (path) => `${githubAdvisoryId}|${path}` as const,\n );\n const { truthy, falsy } = partition(flattenedAllowlist, (path) =>\n this.allowlist.paths.some((allowedPath) =>\n matchString(allowedPath, path),\n ),\n );\n for (const path of truthy) {\n allowlistedPathsFoundSet.add(path);\n }\n\n this.allowlistedPathsFound.push(...allowlistedPathsFoundSet);\n\n const isAllowListed = falsy.length === 0;\n if (isAllowListed) {\n return;\n }\n\n this.advisoriesFound.push(advisory);\n this.advisoryPathsFound.push(...falsy);\n }\n\n load(\n parsedOutput:\n | PartialNPMAuditReportV2Audit\n | PartialNPMAuditReportV1Audit\n | PartialPNPMAuditReportAudit,\n ) {\n /** NPM 6 & PNPM */\n if (\"advisories\" in parsedOutput && parsedOutput.advisories) {\n for (const advisory of Object.values<\n DeepWriteable<\n | PartialNPMAuditReportV1Audit[\"advisories\"][GitHubAdvisoryId]\n | PartialPNPMAuditReportAudit[\"advisories\"][GitHubAdvisoryId]\n >\n >(parsedOutput.advisories)) {\n advisory.github_advisory_id = gitHubAdvisoryUrlToAdvisoryId(\n advisory.url,\n );\n // PNPM paths have a leading `.>`\n // \"paths\": [\n // \".>module-name\"\n //]\n for (const finding of advisory.findings) {\n finding.paths = finding.paths.map((path) => path.replace(\".>\", \"\"));\n }\n this.process(advisory);\n }\n return this.getSummary();\n }\n\n /** NPM 7+ */\n if (\"vulnerabilities\" in parsedOutput && parsedOutput.vulnerabilities) {\n const advisoryMap = new Map<\n number,\n ProcessedAdvisory & {\n findingsSet: Set<string>;\n }\n >();\n // First, let's deal with building a structure that's as close to NPM 6 as we can\n // without dealing with the findings.\n for (const vulnerability of Object.values<\n PartialNPMAuditReportV2Audit[\"vulnerabilities\"][GitHubAdvisoryId]\n >(parsedOutput.vulnerabilities)) {\n const { via: vias, isDirect } = vulnerability;\n // https://github.com/microsoft/TypeScript/issues/33591\n for (const via of vias as Array<string | NPMAuditReportV2.Via>) {\n if (!isVia(via)) {\n continue;\n }\n const { source, url, name, severity } = via;\n if (!advisoryMap.has(source)) {\n advisoryMap.set(source, {\n id: source,\n github_advisory_id: gitHubAdvisoryUrlToAdvisoryId(url),\n module_name: name,\n severity: severity,\n url: url,\n // This will eventually be an array.\n // However, to improve the performance of deduplication,\n // start with a set.\n findingsSet: new Set(isDirect ? [name] : []),\n findings: [],\n });\n }\n }\n }\n\n // Now, all we have to deal with is develop the 'findings' property by traversing\n // the audit tree.\n\n const visitedModules = new Map<string, string[]>();\n\n for (const vuln of Object.entries<\n DeepReadonly<\n PartialNPMAuditReportV2Audit[\"vulnerabilities\"][GitHubAdvisoryId]\n >\n >(parsedOutput.vulnerabilities)) {\n // Did this approach rather than destructuring within the forEach to type vulnerability\n const moduleName = vuln[0];\n const vulnerability = vuln[1];\n const { via: vias, isDirect } = vulnerability;\n\n if (vias.length === 0 || typeof vias[0] === \"string\") {\n continue;\n }\n\n const visited = new Set<string>();\n\n const recursiveMagic = (\n cVuln: DeepReadonly<\n PartialNPMAuditReportV2Audit[\"vulnerabilities\"][GitHubAdvisoryId]\n >,\n dependencyPath: string,\n ): string[] => {\n const visitedModule = visitedModules.get(cVuln.name);\n if (visitedModule) {\n return visitedModule.map((name) => {\n const resultWithExtraCarat = prependPath(name, dependencyPath);\n return resultWithExtraCarat.slice(\n 0,\n Math.max(0, resultWithExtraCarat.length - 1),\n );\n });\n }\n\n if (visited.has(cVuln.name)) {\n // maybe undefined and filter?\n return [dependencyPath];\n }\n visited.add(cVuln.name);\n const newPath = prependPath(cVuln.name, dependencyPath);\n if (cVuln.effects.length === 0) {\n return [newPath.slice(0, Math.max(0, newPath.length - 1))];\n }\n const result = cVuln.effects.flatMap((effect) =>\n recursiveMagic(parsedOutput.vulnerabilities[effect], newPath),\n );\n return result;\n };\n\n const result = recursiveMagic(vulnerability, \"\");\n if (isDirect) {\n result.push(moduleName);\n }\n const advisories = (\n (vias as Array<string | NPMAuditReportV2.Via>).filter(\n (via) => typeof via !== \"string\",\n ) as NPMAuditReportV2.Via[]\n )\n .map((via) => via.source)\n // Filter boolean makes the next line non-nullable.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n .map((id) => advisoryMap.get(id)!)\n .filter(Boolean);\n for (const advisory of advisories) {\n for (const path of result) {\n advisory.findingsSet.add(path);\n }\n }\n // Optimization to prevent extra traversals.\n visitedModules.set(moduleName, result);\n }\n for (const [, advisory] of advisoryMap) {\n advisory.findings = [{ paths: [...advisory.findingsSet] }];\n // @ts-expect-error don't care about findingSet anymore\n delete advisory.findingsSet;\n this.process(advisory);\n }\n }\n return this.getSummary();\n }\n\n getSummary(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n advisoryMapper: (advisory: any) => GitHubAdvisoryId = (a) =>\n a.github_advisory_id,\n ) {\n // Clean up the data structures for more consistent output.\n this.advisoriesFound.sort();\n this.advisoryPathsFound = [...new Set(this.advisoryPathsFound)].sort();\n this.allowlistedAdvisoriesFound.sort();\n this.allowlistedModulesFound.sort();\n this.allowlistedPathsFound.sort();\n\n const foundSeverities = new Set<\"low\" | \"moderate\" | \"high\" | \"critical\">();\n for (const { severity } of this.advisoriesFound) {\n if (severity !== \"info\") {\n foundSeverities.add(severity);\n }\n }\n const failedLevelsFound = [...foundSeverities].sort();\n\n const advisoriesFound = [\n ...new Set(this.advisoriesFound.map((a) => advisoryMapper(a))),\n ].sort();\n\n const allowlistedAdvisoriesNotFound = this.allowlist.advisories\n .filter((id) => !this.allowlistedAdvisoriesFound.includes(id))\n .sort();\n const allowlistedModulesNotFound = this.allowlist.modules\n .filter((id) => !this.allowlistedModulesFound.includes(id))\n .sort();\n const allowlistedPathsNotFound = this.allowlist.paths\n .filter(\n (id) =>\n !this.allowlistedPathsFound.some((foundPath) =>\n matchString(id, foundPath),\n ),\n )\n .sort();\n\n const summary: Summary = {\n advisoriesFound,\n failedLevelsFound,\n allowlistedAdvisoriesNotFound,\n allowlistedModulesNotFound,\n allowlistedPathsNotFound,\n allowlistedAdvisoriesFound: this.allowlistedAdvisoriesFound,\n allowlistedModulesFound: this.allowlistedModulesFound,\n allowlistedPathsFound: this.allowlistedPathsFound,\n advisoryPathsFound: this.advisoryPathsFound,\n };\n return summary;\n }\n}\n\nexport default Model;\n","import type { GitHubAdvisoryId, PNPMAuditReport } from \"audit-types\";\nimport { execSync } from \"child_process\";\nimport * as semver from \"semver\";\nimport { blue, yellow } from \"./colors.js\";\nimport { ReportConfig, reportAudit, runProgram } from \"./common.js\";\nimport {\n AuditCiConfig,\n AuditCiFullConfig,\n mapAuditCiConfigToAuditCiFullConfig,\n} from \"./config.js\";\nimport Model, { type Summary } from \"./model.js\";\n\nconst MINIMUM_PNPM_AUDIT_REGISTRY_VERSION = \"5.4.0\";\n\nasync function runPnpmAudit(\n config: AuditCiFullConfig,\n): Promise<PNPMAuditReport.AuditResponse> {\n const {\n directory,\n registry,\n _pnpm,\n \"skip-dev\": skipDevelopmentDependencies,\n \"extra-args\": extraArguments,\n } = config;\n const pnpmExec = _pnpm || \"pnpm\";\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const stdoutBuffer: any = {};\n function outListener(data: unknown) {\n // Object.assign is used here instead of the spread operator for minor performance gains.\n Object.assign(stdoutBuffer, data);\n }\n\n const stderrBuffer: unknown[] = [];\n function errorListener(line: unknown) {\n stderrBuffer.push(line);\n }\n\n const arguments_ = [\"audit\", \"--json\"];\n if (registry) {\n const pnpmVersion = getPnpmVersion(directory);\n\n if (pnpmAuditSupportsRegistry(pnpmVersion)) {\n arguments_.push(\"--registry\", registry);\n } else {\n console.warn(\n yellow,\n `Update PNPM to version >=${MINIMUM_PNPM_AUDIT_REGISTRY_VERSION} to use the --registry flag`,\n );\n }\n }\n if (skipDevelopmentDependencies) {\n arguments_.push(\"--prod\");\n }\n if (extraArguments) {\n arguments_.push(...extraArguments);\n }\n const options = { cwd: directory };\n await runProgram(pnpmExec, arguments_, options, outListener, errorListener);\n if (stderrBuffer.length > 0) {\n throw new Error(\n `Invocation of pnpm audit failed:\\n${stderrBuffer.join(\"\\n\")}`,\n );\n }\n return stdoutBuffer;\n}\n\nfunction printReport(\n parsedOutput: PNPMAuditReport.Audit,\n levels: AuditCiFullConfig[\"levels\"],\n reportType: \"full\" | \"important\" | \"summary\",\n outputFormat: \"text\" | \"json\",\n) {\n const printReportObject = (text: string, object: unknown) => {\n if (outputFormat === \"text\") {\n console.log(blue, text);\n }\n console.log(JSON.stringify(object, undefined, 2));\n };\n switch (reportType) {\n case \"full\": {\n printReportObject(\"PNPM audit report JSON:\", parsedOutput);\n break;\n }\n case \"important\": {\n const { advisories, metadata } = parsedOutput;\n\n const advisoryKeys = Object.keys(advisories) as GitHubAdvisoryId[];\n\n const relevantAdvisoryLevels = advisoryKeys.filter((advisory) => {\n const severity = advisories[advisory].severity;\n return severity !== \"info\" && levels[severity];\n });\n\n const relevantAdvisories: Record<string, PNPMAuditReport.Advisory> = {};\n for (const advisory of relevantAdvisoryLevels) {\n relevantAdvisories[advisory] = advisories[advisory];\n }\n\n const keyFindings = {\n advisories: relevantAdvisories,\n metadata: metadata,\n };\n printReportObject(\"PNPM audit report results:\", keyFindings);\n break;\n }\n case \"summary\": {\n printReportObject(\"PNPM audit report summary:\", parsedOutput.metadata);\n break;\n }\n default: {\n throw new Error(\n `Invalid report type: ${reportType}. Should be \\`['important', 'full', 'summary']\\`.`,\n );\n }\n }\n}\n\nexport function report(\n parsedOutput: PNPMAuditReport.Audit,\n config: AuditC