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
+1 -1
View File
@@ -279,7 +279,7 @@ function printScorecardBlock(
if (scorecard) {
for (const dependency of scorecard.dependencies) {
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()
})
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
.object({
projectKey: z.object({
@@ -160,7 +191,7 @@ export const ScorecardSchema = z.object({
ecosystem: z.string(),
packageName: z.string(),
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 DepsDevProject = z.infer<typeof DepsDevProjectSchema>
export type Scorecard = z.infer<typeof ScorecardSchema>
export type ScorecardApi = z.infer<typeof ScorecardApiSchema>
+55 -63
View File
@@ -2,7 +2,8 @@ import {
Change,
DepsDevProject,
DepsDevProjectSchema,
Scorecard
Scorecard,
ScorecardApi
} from './schemas'
import {isSPDXValid, octokitClient} from './utils'
import {PackageURL} from 'packageurl-js'
@@ -16,77 +17,68 @@ export async function getScorecardLevels(
const ecosystem = change.ecosystem
const packageName = change.name
const version = change.version
try {
const depsDevResponse: DepsDevProject = await getDepsDevData(
ecosystem,
packageName,
version
)
data.dependencies.push({
ecosystem,
packageName,
version,
depsDevData: depsDevResponse
})
} catch (error: any) {
core.debug(`Error querying for depsDevData: ${error.message}`)
data.dependencies.push({
ecosystem,
packageName,
version,
depsDevData: null
})
//Get the project repository
let repositoryUrl = change.source_repository_url
// If GitHub API doesn't have the repository URL, query deps.dev for it.
if (
repositoryUrl === null ||
repositoryUrl === undefined ||
repositoryUrl === ''
) {
// Call the deps.dev API to get the repository URL from there
repositoryUrl = await getProjectUrl(ecosystem, packageName, version)
}
// 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
}
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(
ecosystem: String,
packageName: String,
version: any
): 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}`)
const url = `${apiRoot}/${repositoryUrl}`
const response = await fetch(url)
if (response.ok) {
scorecardResponse = await response.json()
}
return DepsDevProjectSchema.parse({})
return scorecardResponse
}
async function getDepsDevProjectData(
projectKeyId: string
): Promise<DepsDevProject> {
try {
core.debug(`Getting deps.dev project data for ${projectKeyId}`)
const url = `${depsDevAPIRoot}/v3alpha/projects/${encodeURIComponent(projectKeyId)}`
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
//core.debug(`Got deps.dev project data: ${JSON.stringify(data)}`)
return DepsDevProjectSchema.parse(data)
} else {
throw new Error(
`Failed to fetch project data with status code: ${response.status}`
)
async function getProjectUrl(
ecosystem: string,
packageName: string,
version: string
): Promise<string> {
core.debug(`Getting deps.dev data for ${packageName} ${version}`)
const depsDevAPIRoot = 'https://api.deps.dev'
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
if (data.relatedProjects.length > 0) {
return data.relatedProjects[0].projectKey.id
}
} 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) {
core.debug('Adding scorecard to summary')
core.debug(
`Overall score ${dependency.depsDevData?.scorecard.overallScore}`
)
core.debug(`Overall score ${dependency.scorecard?.score}`)
// Set the icon based on the overall score value
let overallIcon = ''
if (
dependency.depsDevData?.scorecard.overallScore !== undefined &&
dependency.depsDevData?.scorecard.overallScore !== null
dependency.scorecard?.score !== undefined &&
dependency.scorecard?.score !== null
) {
overallIcon =
dependency.depsDevData?.scorecard.overallScore <
config.warn_on_openssf_scorecard_level
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
? ':warning:'
: ':green_circle:'
}
@@ -280,13 +277,15 @@ export function addScorecardToSummary(
//Add a row for the dependency
core.summary.addRaw(
`<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
)
if (dependency.depsDevData?.scorecard.checks !== undefined) {
//Add details table in the last column
if (dependency.scorecard?.checks !== undefined) {
let detailsTable =
'<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 =
parseFloat(check.score) < config.warn_on_openssf_scorecard_level
? ':warning:'