Skip to content

Commit 67407f7

Browse files
authored
fix(report): handle Export PDF fail when scorecard results fail with 404 (#473)
1 parent 42af3ea commit 67407f7

4 files changed

Lines changed: 84 additions & 18 deletions

File tree

src/analysis/extractScannerData.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import fs from "node:fs";
44
// Import Third-party Dependencies
55
import { getScoreColor, getVCSRepositoryPathAndPlatform } from "@nodesecure/utils";
66
import { getManifest, getFlags } from "@nodesecure/flags/web";
7-
import * as scorecard from "@nodesecure/ossf-scorecard-sdk";
87
import { Extractors, type Payload, type Dependency, type DependencyVersion, type DependencyLinks } from "@nodesecure/scanner";
98
import type { RC } from "@nodesecure/rc";
109

1110
// Import Internal Dependencies
1211
import * as localStorage from "../localStorage.ts";
12+
import { fetchScorecardScore } from "./fetch.ts";
1313

1414
// CONSTANTS
1515
const kFlagsList = Object.values(getManifest());
@@ -54,10 +54,10 @@ export interface BuildScannerStatsOptions {
5454
reportConfig?: RC["report"];
5555
}
5656

57-
export async function buildStatsFromScannerDependencies(
57+
export function buildStatsFromScannerDependencies(
5858
payloadFiles: string[] | Payload["dependencies"] = [],
5959
options: BuildScannerStatsOptions = Object.create(null)
60-
): Promise<ReportStat> {
60+
): ReportStat {
6161
const { reportConfig } = options;
6262

6363
const config = reportConfig ?? localStorage.getConfig().report!;
@@ -180,22 +180,26 @@ export async function buildStatsFromScannerDependencies(
180180
return acc;
181181
}, {});
182182

183-
const givenPackages = Object.values(stats.packages).filter((pkg) => pkg.isGiven);
183+
stats.packages_count.all = Object.keys(stats.packages).length;
184+
stats.packages_count.internal = stats.packages_count.all - stats.packages_count.external;
185+
stats.scorecards = {};
186+
187+
return stats;
188+
}
184189

190+
export async function buildGivenPackagesScorecards(stats: ReportStat): Promise<ReportStat["scorecards"]> {
191+
const givenPackages = Object.values(stats.packages).filter((pkg) => pkg.isGiven);
192+
const scorecards: ReportStat["scorecards"] = {};
185193
await Promise.all(givenPackages.map(async(pkg) => {
186194
const { fullName } = pkg;
187-
const { score } = await scorecard.result(fullName, { resolveOnVersionControl: false });
195+
const score = await fetchScorecardScore(fullName);
188196
const [repo, platform] = getVCSRepositoryPathAndPlatform(pkg.links?.repository) ?? [];
189-
stats.scorecards[fullName] = {
197+
scorecards[fullName] = {
190198
score,
191199
color: getScoreColor(score),
192200
visualizerUrl: repo ? `${kScorecardVisualizerUrl}/${platform}/${repo}` : "#"
193201
};
194202
}));
195203

196-
stats.packages_count.all = Object.keys(stats.packages).length;
197-
stats.packages_count.internal = stats.packages_count.all - stats.packages_count.external;
198-
199-
return stats;
204+
return scorecards;
200205
}
201-

src/analysis/fetch.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import path from "node:path";
33

44
// Import Third-party Dependencies
55
import kleur from "kleur";
6+
import * as scorecard from "@nodesecure/ossf-scorecard-sdk";
7+
import { isHTTPError } from "@openally/httpie";
68

79
// Import Internal Dependencies
810
import { buildStatsFromScannerDependencies } from "./extractScannerData.ts";
@@ -11,6 +13,9 @@ import * as localStorage from "../localStorage.ts";
1113
import * as utils from "../utils/index.ts";
1214
import * as CONSTANTS from "../constants.ts";
1315

16+
// CONSTANTS
17+
const kNotFoundStatusCode = 404;
18+
1419
export async function fetchPackagesAndRepositoriesData(
1520
verbose = true
1621
) {
@@ -98,3 +103,24 @@ async function fetchRepositoriesStats(
98103
jsonFiles.filter((value) => value !== null)
99104
);
100105
}
106+
107+
const scoresCache = new Map<string, number>();
108+
109+
export async function fetchScorecardScore(fullName: string) {
110+
if (scoresCache.has(fullName)) {
111+
return scoresCache.get(fullName);
112+
}
113+
try {
114+
const { score } = await scorecard.result(fullName, { resolveOnVersionControl: false });
115+
scoresCache.set(fullName, score);
116+
117+
return score;
118+
}
119+
catch (e) {
120+
if (isHTTPError(e) && e.statusCode === kNotFoundStatusCode) {
121+
scoresCache.set(fullName, 0);
122+
}
123+
124+
return 0;
125+
}
126+
}

src/api/report.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { type Payload } from "@nodesecure/scanner";
88
import { type RC } from "@nodesecure/rc";
99

1010
// Import Internal Dependencies
11-
import { buildStatsFromScannerDependencies } from "../analysis/extractScannerData.ts";
11+
import { buildStatsFromScannerDependencies, buildGivenPackagesScorecards } from "../analysis/extractScannerData.ts";
1212
import { HTML, PDF } from "../reporting/index.ts";
1313

1414
export interface ReportLocationOptions {
@@ -63,12 +63,13 @@ export async function report(
6363
throw new Error("At least one reporter must be enabled (pdf or html)");
6464
}
6565

66-
const [pkgStats, finalReportLocation] = await Promise.all([
67-
buildStatsFromScannerDependencies(scannerDependencies, {
68-
reportConfig
69-
}),
70-
reportLocation(reportOutputLocation, { includesPDF, savePDFOnDisk, saveHTMLOnDisk })
71-
]);
66+
const pkgStats = buildStatsFromScannerDependencies(scannerDependencies, {
67+
reportConfig
68+
});
69+
70+
pkgStats.scorecards = await buildGivenPackagesScorecards(pkgStats);
71+
72+
const finalReportLocation = await reportLocation(reportOutputLocation, { includesPDF, savePDFOnDisk, saveHTMLOnDisk });
7273

7374
let reportHTMLPath: string | undefined;
7475
try {

test/api/report.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,41 @@ describe("(API) report", { concurrency: 1 }, () => {
8181
}
8282
});
8383

84+
test(`it should successfully generate a PDF and should not save
85+
PDF or HTML for packages that don't have a scorecard`, async() => {
86+
const reportOutputLocation = await fs.mkdtemp(
87+
path.join(os.tmpdir(), "test-runner-report-pdf-")
88+
);
89+
90+
const payload = await from("@pyroscope/nodejs");
91+
92+
const generatedPDF = await report(
93+
payload.dependencies,
94+
structuredClone({
95+
...kReportPayload,
96+
npm: {
97+
organizationPrefix: "@pyroscope",
98+
packages: ["nodejs"]
99+
}
100+
}),
101+
{ reportOutputLocation }
102+
);
103+
try {
104+
assert.ok(Buffer.isBuffer(generatedPDF));
105+
assert.ok(isPDF(generatedPDF));
106+
107+
const files = (await fs.readdir(reportOutputLocation, { withFileTypes: true }))
108+
.flatMap((dirent) => (dirent.isFile() ? [dirent.name] : []));
109+
assert.deepEqual(
110+
files,
111+
[]
112+
);
113+
}
114+
finally {
115+
await fs.rm(reportOutputLocation, { force: true, recursive: true });
116+
}
117+
});
118+
84119
test("should save HTML when saveHTMLOnDisk is truthy", async() => {
85120
const reportOutputLocation = await fs.mkdtemp(
86121
path.join(os.tmpdir(), "test-runner-report-pdf-")

0 commit comments

Comments
 (0)