Skip to content
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
26 changes: 15 additions & 11 deletions src/analysis/extractScannerData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import fs from "node:fs";
// Import Third-party Dependencies
import { getScoreColor, getVCSRepositoryPathAndPlatform } from "@nodesecure/utils";
import { getManifest, getFlags } from "@nodesecure/flags/web";
import * as scorecard from "@nodesecure/ossf-scorecard-sdk";
import { Extractors, type Payload, type Dependency, type DependencyVersion, type DependencyLinks } from "@nodesecure/scanner";
import type { RC } from "@nodesecure/rc";

// Import Internal Dependencies
import * as localStorage from "../localStorage.ts";
import { fetchScorecardScore } from "./fetch.ts";

// CONSTANTS
const kFlagsList = Object.values(getManifest());
Expand Down Expand Up @@ -54,10 +54,10 @@ export interface BuildScannerStatsOptions {
reportConfig?: RC["report"];
}

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

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

const givenPackages = Object.values(stats.packages).filter((pkg) => pkg.isGiven);
stats.packages_count.all = Object.keys(stats.packages).length;
stats.packages_count.internal = stats.packages_count.all - stats.packages_count.external;
stats.scorecards = {};

return stats;
}

export async function buildGivenPackagesScorecards(stats: ReportStat): Promise<ReportStat["scorecards"]> {
const givenPackages = Object.values(stats.packages).filter((pkg) => pkg.isGiven);
const scorecards: ReportStat["scorecards"] = {};
await Promise.all(givenPackages.map(async(pkg) => {
const { fullName } = pkg;
const { score } = await scorecard.result(fullName, { resolveOnVersionControl: false });
const score = await fetchScorecardScore(fullName);
const [repo, platform] = getVCSRepositoryPathAndPlatform(pkg.links?.repository) ?? [];
stats.scorecards[fullName] = {
scorecards[fullName] = {
score,
color: getScoreColor(score),
visualizerUrl: repo ? `${kScorecardVisualizerUrl}/${platform}/${repo}` : "#"
};
}));

stats.packages_count.all = Object.keys(stats.packages).length;
stats.packages_count.internal = stats.packages_count.all - stats.packages_count.external;

return stats;
return scorecards;
}

26 changes: 26 additions & 0 deletions src/analysis/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import path from "node:path";

// Import Third-party Dependencies
import kleur from "kleur";
import * as scorecard from "@nodesecure/ossf-scorecard-sdk";
import { isHTTPError } from "@openally/httpie";

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

// CONSTANTS
const kNotFoundStatusCode = 404;

export async function fetchPackagesAndRepositoriesData(
verbose = true
) {
Expand Down Expand Up @@ -98,3 +103,24 @@ async function fetchRepositoriesStats(
jsonFiles.filter((value) => value !== null)
);
}

const scoresCache = new Map<string, number>();

export async function fetchScorecardScore(fullName: string) {
if (scoresCache.has(fullName)) {
return scoresCache.get(fullName);
}
try {
const { score } = await scorecard.result(fullName, { resolveOnVersionControl: false });
scoresCache.set(fullName, score);

return score;
}
catch (e) {
if (isHTTPError(e) && e.statusCode === kNotFoundStatusCode) {
scoresCache.set(fullName, 0);
}

return 0;
}
}
15 changes: 8 additions & 7 deletions src/api/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { type Payload } from "@nodesecure/scanner";
import { type RC } from "@nodesecure/rc";

// Import Internal Dependencies
import { buildStatsFromScannerDependencies } from "../analysis/extractScannerData.ts";
import { buildStatsFromScannerDependencies, buildGivenPackagesScorecards } from "../analysis/extractScannerData.ts";
import { HTML, PDF } from "../reporting/index.ts";

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

const [pkgStats, finalReportLocation] = await Promise.all([
buildStatsFromScannerDependencies(scannerDependencies, {
reportConfig
}),
reportLocation(reportOutputLocation, { includesPDF, savePDFOnDisk, saveHTMLOnDisk })
]);
const pkgStats = buildStatsFromScannerDependencies(scannerDependencies, {
reportConfig
});

pkgStats.scorecards = await buildGivenPackagesScorecards(pkgStats);

const finalReportLocation = await reportLocation(reportOutputLocation, { includesPDF, savePDFOnDisk, saveHTMLOnDisk });

let reportHTMLPath: string | undefined;
try {
Expand Down
35 changes: 35 additions & 0 deletions test/api/report.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,41 @@ describe("(API) report", { concurrency: 1 }, () => {
}
});

test(`it should successfully generate a PDF and should not save
PDF or HTML for packages that don't have a scorecard`, async() => {
const reportOutputLocation = await fs.mkdtemp(
path.join(os.tmpdir(), "test-runner-report-pdf-")
);

const payload = await from("@pyroscope/nodejs");

const generatedPDF = await report(
payload.dependencies,
structuredClone({
...kReportPayload,
npm: {
organizationPrefix: "@pyroscope",
packages: ["nodejs"]
}
}),
{ reportOutputLocation }
);
try {
assert.ok(Buffer.isBuffer(generatedPDF));
assert.ok(isPDF(generatedPDF));

const files = (await fs.readdir(reportOutputLocation, { withFileTypes: true }))
.flatMap((dirent) => (dirent.isFile() ? [dirent.name] : []));
assert.deepEqual(
files,
[]
);
}
finally {
await fs.rm(reportOutputLocation, { force: true, recursive: true });
}
});

test("should save HTML when saveHTMLOnDisk is truthy", async() => {
const reportOutputLocation = await fs.mkdtemp(
path.join(os.tmpdir(), "test-runner-report-pdf-")
Expand Down