Refactor scorecard API implementation

This commit is contained in:
Justin Hutchings
2024-03-06 14:43:49 +00:00
parent a1258f2a2e
commit 111227a118
6 changed files with 212 additions and 143 deletions
Generated Vendored
+113 -67
View File
@@ -750,10 +750,10 @@ function printNullLicenses(changes) {
} }
function printScorecardBlock(scorecard, config) { function printScorecardBlock(scorecard, config) {
core.group('Scorecard', () => __awaiter(this, void 0, void 0, function* () { core.group('Scorecard', () => __awaiter(this, void 0, void 0, function* () {
var _a, _b; var _a;
if (scorecard) { if (scorecard) {
for (const dependency of scorecard.dependencies) { for (const dependency of scorecard.dependencies) {
core.info(`${dependency.ecosystem}/${dependency.packageName}: OpenSSF Scorecard Score: ${(_b = (_a = dependency === null || dependency === void 0 ? void 0 : dependency.depsDevData) === null || _a === void 0 ? void 0 : _a.scorecard) === null || _b === void 0 ? void 0 : _b.overallScore}`); core.info(`${dependency.ecosystem}/${dependency.packageName}: OpenSSF Scorecard Score: ${(_a = dependency === null || dependency === void 0 ? void 0 : dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score}`);
} }
} }
})); }));
@@ -839,7 +839,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ScorecardSchema = exports.DepsDevProjectSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0; exports.ScorecardSchema = exports.DepsDevProjectSchema = exports.ScorecardApiSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
const z = __importStar(__nccwpck_require__(3301)); const z = __importStar(__nccwpck_require__(3301));
exports.SEVERITIES = ['critical', 'high', 'moderate', 'low']; exports.SEVERITIES = ['critical', 'high', 'moderate', 'low'];
exports.SCOPES = ['unknown', 'runtime', 'development']; exports.SCOPES = ['unknown', 'runtime', 'development'];
@@ -931,6 +931,34 @@ exports.ComparisonResponseSchema = z.object({
changes: z.array(exports.ChangeSchema), changes: z.array(exports.ChangeSchema),
snapshot_warnings: z.string() snapshot_warnings: z.string()
}); });
exports.ScorecardApiSchema = z.object({
date: z.string(),
repo: z
.object({
name: z.string(),
commit: z.string()
})
.nullish(),
scorecard: z
.object({
version: z.string(),
commit: z.string()
})
.nullish(),
checks: z
.array(z.object({
name: z.string(),
documentation: z.object({
shortDescription: z.string(),
url: z.string()
}),
score: z.string(),
reason: z.string(),
details: z.array(z.string())
}))
.nullish(),
score: z.number().nullish()
});
exports.DepsDevProjectSchema = z exports.DepsDevProjectSchema = z
.object({ .object({
projectKey: z.object({ projectKey: z.object({
@@ -985,7 +1013,7 @@ exports.ScorecardSchema = z.object({
ecosystem: z.string(), ecosystem: z.string(),
packageName: z.string(), packageName: z.string(),
version: z.string().nullish(), version: z.string().nullish(),
depsDevData: exports.DepsDevProjectSchema scorecard: exports.ScorecardApiSchema.nullish()
})) }))
}); });
@@ -1031,7 +1059,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getScorecardLevels = void 0; exports.getScorecardLevels = void 0;
const schemas_1 = __nccwpck_require__(8774);
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
function getScorecardLevels(changes) { function getScorecardLevels(changes) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
@@ -1040,72 +1067,63 @@ function getScorecardLevels(changes) {
const ecosystem = change.ecosystem; const ecosystem = change.ecosystem;
const packageName = change.name; const packageName = change.name;
const version = change.version; const version = change.version;
try { //Get the project repository
const depsDevResponse = yield getDepsDevData(ecosystem, packageName, version); let repositoryUrl = change.source_repository_url;
data.dependencies.push({ // If GitHub API doesn't have the repository URL, query deps.dev for it.
ecosystem, if (repositoryUrl === null ||
packageName, repositoryUrl === undefined ||
version, repositoryUrl === '') {
depsDevData: depsDevResponse // Call the deps.dev API to get the repository URL from there
}); repositoryUrl = yield getProjectUrl(ecosystem, packageName, version);
} }
catch (error) { // Get the scorecard API response from the scorecards API
core.debug(`Error querying for depsDevData: ${error.message}`); let scorecardApi = null;
data.dependencies.push({ if (repositoryUrl !== null &&
ecosystem, repositoryUrl !== undefined &&
packageName, repositoryUrl !== '') {
version, try {
depsDevData: null scorecardApi = yield getScorecard(repositoryUrl);
}); }
catch (error) {
core.debug(`Error querying for scorecard: ${error.message}`);
}
} }
data.dependencies.push({
ecosystem,
packageName,
version,
scorecard: scorecardApi
});
} }
return data; return data;
}); });
} }
exports.getScorecardLevels = getScorecardLevels; exports.getScorecardLevels = getScorecardLevels;
const depsDevAPIRoot = 'https://api.deps.dev'; function getScorecard(repositoryUrl) {
function getDepsDevData(ecosystem, packageName, version) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { const apiRoot = 'https://api.securityscorecards.dev/';
core.debug(`Getting deps.dev data for ${packageName} ${version}`); let scorecardResponse = {};
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`; const url = `${apiRoot}/${repositoryUrl}`;
const response = yield fetch(url); const response = yield fetch(url);
if (response.ok) { if (response.ok) {
const data = yield response.json(); scorecardResponse = yield response.json();
const projects = data.relatedProjects;
for (const project of projects) {
return yield getDepsDevProjectData(project.projectKey.id);
}
}
else {
throw new Error(`Failed to fetch data with status code: ${response.status}`);
}
} }
catch (error) { return scorecardResponse;
core.debug(`Error fetching data: ${error.message}`);
}
return schemas_1.DepsDevProjectSchema.parse({});
}); });
} }
function getDepsDevProjectData(projectKeyId) { function getProjectUrl(ecosystem, packageName, version) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { core.debug(`Getting deps.dev data for ${packageName} ${version}`);
core.debug(`Getting deps.dev project data for ${projectKeyId}`); const depsDevAPIRoot = 'https://api.deps.dev';
const url = `${depsDevAPIRoot}/v3alpha/projects/${encodeURIComponent(projectKeyId)}`; const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`;
const response = yield fetch(url); const response = yield fetch(url);
if (response.ok) { if (response.ok) {
const data = yield response.json(); const data = yield response.json();
//core.debug(`Got deps.dev project data: ${JSON.stringify(data)}`) if (data.relatedProjects.length > 0) {
return schemas_1.DepsDevProjectSchema.parse(data); return data.relatedProjects[0].projectKey.id;
}
else {
throw new Error(`Failed to fetch project data with status code: ${response.status}`);
} }
} }
catch (error) { return '';
core.debug(`Error fetching project data: ${error.message}`);
}
return schemas_1.DepsDevProjectSchema.parse({});
}); });
} }
@@ -1328,23 +1346,23 @@ function addScorecardToSummary(scorecard, config) {
core.summary.addRaw(`<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>`, true); core.summary.addRaw(`<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>`, true);
for (const dependency of scorecard.dependencies) { for (const dependency of scorecard.dependencies) {
core.debug('Adding scorecard to summary'); core.debug('Adding scorecard to summary');
core.debug(`Overall score ${(_a = dependency.depsDevData) === null || _a === void 0 ? void 0 : _a.scorecard.overallScore}`); core.debug(`Overall score ${(_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score}`);
// Set the icon based on the overall score value // Set the icon based on the overall score value
let overallIcon = ''; let overallIcon = '';
if (((_b = dependency.depsDevData) === null || _b === void 0 ? void 0 : _b.scorecard.overallScore) !== undefined && if (((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) !== undefined &&
((_c = dependency.depsDevData) === null || _c === void 0 ? void 0 : _c.scorecard.overallScore) !== null) { ((_c = dependency.scorecard) === null || _c === void 0 ? void 0 : _c.score) !== null) {
overallIcon = overallIcon =
((_d = dependency.depsDevData) === null || _d === void 0 ? void 0 : _d.scorecard.overallScore) < ((_d = dependency.scorecard) === null || _d === void 0 ? void 0 : _d.score) < config.warn_on_openssf_scorecard_level
config.warn_on_openssf_scorecard_level
? ':warning:' ? ':warning:'
: ':green_circle:'; : ':green_circle:';
} }
//Add a row for the dependency //Add a row for the dependency
core.summary.addRaw(`<tr><td>${dependency.ecosystem}/${dependency.packageName}</td><td>${dependency.version}</td> core.summary.addRaw(`<tr><td>${dependency.ecosystem}/${dependency.packageName}</td><td>${dependency.version}</td>
<td>${overallIcon} ${((_e = dependency.depsDevData) === null || _e === void 0 ? void 0 : _e.scorecard.overallScore) === undefined || ((_f = dependency.depsDevData) === null || _f === void 0 ? void 0 : _f.scorecard.overallScore) === null ? 'Unknown' : (_g = dependency.depsDevData) === null || _g === void 0 ? void 0 : _g.scorecard.overallScore}</td>`, false); <td>${overallIcon} ${((_e = dependency.scorecard) === null || _e === void 0 ? void 0 : _e.score) === undefined || ((_f = dependency.scorecard) === null || _f === void 0 ? void 0 : _f.score) === null ? 'Unknown' : (_g = dependency.scorecard) === null || _g === void 0 ? void 0 : _g.score}</td>`, false);
if (((_h = dependency.depsDevData) === null || _h === void 0 ? void 0 : _h.scorecard.checks) !== undefined) { //Add details table in the last column
if (((_h = dependency.scorecard) === null || _h === void 0 ? void 0 : _h.checks) !== undefined) {
let detailsTable = '<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>'; let detailsTable = '<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>';
for (const check of ((_j = dependency.depsDevData) === null || _j === void 0 ? void 0 : _j.scorecard.checks) || []) { for (const check of ((_j = dependency.scorecard) === null || _j === void 0 ? void 0 : _j.checks) || []) {
const icon = parseFloat(check.score) < config.warn_on_openssf_scorecard_level const icon = parseFloat(check.score) < config.warn_on_openssf_scorecard_level
? ':warning:' ? ':warning:'
: ':green_circle:'; : ':green_circle:';
@@ -49997,7 +50015,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ScorecardSchema = exports.DepsDevProjectSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0; exports.ScorecardSchema = exports.DepsDevProjectSchema = exports.ScorecardApiSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
const z = __importStar(__nccwpck_require__(3301)); const z = __importStar(__nccwpck_require__(3301));
exports.SEVERITIES = ['critical', 'high', 'moderate', 'low']; exports.SEVERITIES = ['critical', 'high', 'moderate', 'low'];
exports.SCOPES = ['unknown', 'runtime', 'development']; exports.SCOPES = ['unknown', 'runtime', 'development'];
@@ -50089,6 +50107,34 @@ exports.ComparisonResponseSchema = z.object({
changes: z.array(exports.ChangeSchema), changes: z.array(exports.ChangeSchema),
snapshot_warnings: z.string() snapshot_warnings: z.string()
}); });
exports.ScorecardApiSchema = z.object({
date: z.string(),
repo: z
.object({
name: z.string(),
commit: z.string()
})
.nullish(),
scorecard: z
.object({
version: z.string(),
commit: z.string()
})
.nullish(),
checks: z
.array(z.object({
name: z.string(),
documentation: z.object({
shortDescription: z.string(),
url: z.string()
}),
score: z.string(),
reason: z.string(),
details: z.array(z.string())
}))
.nullish(),
score: z.number().nullish()
});
exports.DepsDevProjectSchema = z exports.DepsDevProjectSchema = z
.object({ .object({
projectKey: z.object({ projectKey: z.object({
@@ -50143,7 +50189,7 @@ exports.ScorecardSchema = z.object({
ecosystem: z.string(), ecosystem: z.string(),
packageName: z.string(), packageName: z.string(),
version: z.string().nullish(), version: z.string().nullish(),
depsDevData: exports.DepsDevProjectSchema scorecard: exports.ScorecardApiSchema.nullish()
})) }))
}); });
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -279,7 +279,7 @@ function printScorecardBlock(
if (scorecard) { if (scorecard) {
for (const dependency of scorecard.dependencies) { for (const dependency of scorecard.dependencies) {
core.info( core.info(
`${dependency.ecosystem}/${dependency.packageName}: OpenSSF Scorecard Score: ${dependency?.depsDevData?.scorecard?.overallScore}` `${dependency.ecosystem}/${dependency.packageName}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}`
) )
} }
} }
+33 -1
View File
@@ -102,6 +102,37 @@ export const ComparisonResponseSchema = z.object({
snapshot_warnings: z.string() snapshot_warnings: z.string()
}) })
export const ScorecardApiSchema = z.object({
date: z.string(),
repo: z
.object({
name: z.string(),
commit: z.string()
})
.nullish(),
scorecard: z
.object({
version: z.string(),
commit: z.string()
})
.nullish(),
checks: z
.array(
z.object({
name: z.string(),
documentation: z.object({
shortDescription: z.string(),
url: z.string()
}),
score: z.string(),
reason: z.string(),
details: z.array(z.string())
})
)
.nullish(),
score: z.number().nullish()
})
export const DepsDevProjectSchema = z export const DepsDevProjectSchema = z
.object({ .object({
projectKey: z.object({ projectKey: z.object({
@@ -160,7 +191,7 @@ export const ScorecardSchema = z.object({
ecosystem: z.string(), ecosystem: z.string(),
packageName: z.string(), packageName: z.string(),
version: z.string().nullish(), version: z.string().nullish(),
depsDevData: DepsDevProjectSchema scorecard: ScorecardApiSchema.nullish()
}) })
) )
}) })
@@ -173,3 +204,4 @@ export type Severity = z.infer<typeof SeveritySchema>
export type Scope = (typeof SCOPES)[number] export type Scope = (typeof SCOPES)[number]
export type DepsDevProject = z.infer<typeof DepsDevProjectSchema> export type DepsDevProject = z.infer<typeof DepsDevProjectSchema>
export type Scorecard = z.infer<typeof ScorecardSchema> export type Scorecard = z.infer<typeof ScorecardSchema>
export type ScorecardApi = z.infer<typeof ScorecardApiSchema>
+55 -63
View File
@@ -2,7 +2,8 @@ import {
Change, Change,
DepsDevProject, DepsDevProject,
DepsDevProjectSchema, DepsDevProjectSchema,
Scorecard Scorecard,
ScorecardApi
} from './schemas' } from './schemas'
import {isSPDXValid, octokitClient} from './utils' import {isSPDXValid, octokitClient} from './utils'
import {PackageURL} from 'packageurl-js' import {PackageURL} from 'packageurl-js'
@@ -16,77 +17,68 @@ export async function getScorecardLevels(
const ecosystem = change.ecosystem const ecosystem = change.ecosystem
const packageName = change.name const packageName = change.name
const version = change.version const version = change.version
try {
const depsDevResponse: DepsDevProject = await getDepsDevData( //Get the project repository
ecosystem, let repositoryUrl = change.source_repository_url
packageName, // If GitHub API doesn't have the repository URL, query deps.dev for it.
version if (
) repositoryUrl === null ||
data.dependencies.push({ repositoryUrl === undefined ||
ecosystem, repositoryUrl === ''
packageName, ) {
version, // Call the deps.dev API to get the repository URL from there
depsDevData: depsDevResponse repositoryUrl = await getProjectUrl(ecosystem, packageName, version)
})
} catch (error: any) {
core.debug(`Error querying for depsDevData: ${error.message}`)
data.dependencies.push({
ecosystem,
packageName,
version,
depsDevData: null
})
} }
// Get the scorecard API response from the scorecards API
let scorecardApi: ScorecardApi | null = null
if (
repositoryUrl !== null &&
repositoryUrl !== undefined &&
repositoryUrl !== ''
) {
try {
scorecardApi = await getScorecard(repositoryUrl)
} catch (error: any) {
core.debug(`Error querying for scorecard: ${error.message}`)
}
}
data.dependencies.push({
ecosystem,
packageName,
version,
scorecard: scorecardApi
})
} }
return data return data
} }
const depsDevAPIRoot = 'https://api.deps.dev' async function getScorecard(repositoryUrl: string): Promise<ScorecardApi> {
const apiRoot = 'https://api.securityscorecards.dev/'
let scorecardResponse: ScorecardApi = {} as ScorecardApi
async function getDepsDevData( const url = `${apiRoot}/${repositoryUrl}`
ecosystem: String, const response = await fetch(url)
packageName: String, if (response.ok) {
version: any scorecardResponse = await response.json()
): Promise<DepsDevProject> {
try {
core.debug(`Getting deps.dev data for ${packageName} ${version}`)
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
const projects = data.relatedProjects
for (const project of projects) {
return await getDepsDevProjectData(project.projectKey.id)
}
} else {
throw new Error(
`Failed to fetch data with status code: ${response.status}`
)
}
} catch (error: any) {
core.debug(`Error fetching data: ${error.message}`)
} }
return DepsDevProjectSchema.parse({}) return scorecardResponse
} }
async function getDepsDevProjectData( async function getProjectUrl(
projectKeyId: string ecosystem: string,
): Promise<DepsDevProject> { packageName: string,
try { version: string
core.debug(`Getting deps.dev project data for ${projectKeyId}`) ): Promise<string> {
const url = `${depsDevAPIRoot}/v3alpha/projects/${encodeURIComponent(projectKeyId)}` core.debug(`Getting deps.dev data for ${packageName} ${version}`)
const response = await fetch(url) const depsDevAPIRoot = 'https://api.deps.dev'
if (response.ok) { const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`
const data = await response.json() const response = await fetch(url)
//core.debug(`Got deps.dev project data: ${JSON.stringify(data)}`) if (response.ok) {
return DepsDevProjectSchema.parse(data) const data = await response.json()
} else { if (data.relatedProjects.length > 0) {
throw new Error( return data.relatedProjects[0].projectKey.id
`Failed to fetch project data with status code: ${response.status}`
)
} }
} catch (error: any) {
core.debug(`Error fetching project data: ${error.message}`)
} }
return DepsDevProjectSchema.parse({}) return ''
} }
+9 -10
View File
@@ -260,19 +260,16 @@ export function addScorecardToSummary(
) )
for (const dependency of scorecard.dependencies) { for (const dependency of scorecard.dependencies) {
core.debug('Adding scorecard to summary') core.debug('Adding scorecard to summary')
core.debug( core.debug(`Overall score ${dependency.scorecard?.score}`)
`Overall score ${dependency.depsDevData?.scorecard.overallScore}`
)
// Set the icon based on the overall score value // Set the icon based on the overall score value
let overallIcon = '' let overallIcon = ''
if ( if (
dependency.depsDevData?.scorecard.overallScore !== undefined && dependency.scorecard?.score !== undefined &&
dependency.depsDevData?.scorecard.overallScore !== null dependency.scorecard?.score !== null
) { ) {
overallIcon = overallIcon =
dependency.depsDevData?.scorecard.overallScore < dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
config.warn_on_openssf_scorecard_level
? ':warning:' ? ':warning:'
: ':green_circle:' : ':green_circle:'
} }
@@ -280,13 +277,15 @@ export function addScorecardToSummary(
//Add a row for the dependency //Add a row for the dependency
core.summary.addRaw( core.summary.addRaw(
`<tr><td>${dependency.ecosystem}/${dependency.packageName}</td><td>${dependency.version}</td> `<tr><td>${dependency.ecosystem}/${dependency.packageName}</td><td>${dependency.version}</td>
<td>${overallIcon} ${dependency.depsDevData?.scorecard.overallScore === undefined || dependency.depsDevData?.scorecard.overallScore === null ? 'Unknown' : dependency.depsDevData?.scorecard.overallScore}</td>`, <td>${overallIcon} ${dependency.scorecard?.score === undefined || dependency.scorecard?.score === null ? 'Unknown' : dependency.scorecard?.score}</td>`,
false false
) )
if (dependency.depsDevData?.scorecard.checks !== undefined) {
//Add details table in the last column
if (dependency.scorecard?.checks !== undefined) {
let detailsTable = let detailsTable =
'<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>' '<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>'
for (const check of dependency.depsDevData?.scorecard.checks || []) { for (const check of dependency.scorecard?.checks || []) {
const icon = const icon =
parseFloat(check.score) < config.warn_on_openssf_scorecard_level parseFloat(check.score) < config.warn_on_openssf_scorecard_level
? ':warning:' ? ':warning:'