Skip to content

Commit 1ecd406

Browse files
committed
feat: add OSSF provider
1 parent 772481f commit 1ecd406

File tree

3 files changed

+168
-7
lines changed

3 files changed

+168
-7
lines changed

__tests__/__snapshots__/providers.test.js.snap

+103-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`GitHub Providers mappers Should map organization data correctly 1`] = `
3+
exports[`GitHub Provider mappers Should map organization data correctly 1`] = `
44
{
55
"advanced_security_enabled_for_new_repositories": false,
66
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
@@ -53,7 +53,7 @@ exports[`GitHub Providers mappers Should map organization data correctly 1`] = `
5353
}
5454
`;
5555

56-
exports[`GitHub Providers mappers Should map repository data correctly 1`] = `
56+
exports[`GitHub Provider mappers Should map repository data correctly 1`] = `
5757
{
5858
"allow_auto_merge": false,
5959
"allow_forking": true,
@@ -114,3 +114,104 @@ exports[`GitHub Providers mappers Should map repository data correctly 1`] = `
114114
"watchers_count": 80,
115115
}
116116
`;
117+
118+
exports[`OSSF Provider mappers Should map scorecard data correctly 1`] = `
119+
{
120+
"analysis_score": 6,
121+
"analysis_time": "2024-12-11T23:55:17Z",
122+
"binary_artifacts_details": undefined,
123+
"binary_artifacts_documentation": "Determines if the project has generated executable (binary) artifacts in the source repository.",
124+
"binary_artifacts_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#binary-artifacts",
125+
"binary_artifacts_reason": "no binaries found in the repo",
126+
"binary_artifacts_score": 10,
127+
"branch_protection_details": undefined,
128+
"branch_protection_documentation": "Determines if the default and release branches are protected with GitHub's branch protection settings.",
129+
"branch_protection_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#branch-protection",
130+
"branch_protection_reason": "branch protection is not maximal on development and all release branches",
131+
"branch_protection_score": 3,
132+
"ci_tests_details": undefined,
133+
"ci_tests_documentation": "Determines if the project runs tests before pull requests are merged.",
134+
"ci_tests_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#ci-tests",
135+
"ci_tests_reason": "13 out of 19 merged PRs checked by a CI test -- score normalized to 6",
136+
"ci_tests_score": 6,
137+
"cii_best_practices_details": undefined,
138+
"cii_best_practices_documentation": "Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.",
139+
"cii_best_practices_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#cii-best-practices",
140+
"cii_best_practices_reason": "no effort to earn an OpenSSF best practices badge detected",
141+
"cii_best_practices_score": 0,
142+
"code_review_details": undefined,
143+
"code_review_documentation": "Determines if the project requires human code review before pull requests (aka merge requests) are merged.",
144+
"code_review_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#code-review",
145+
"code_review_reason": "Found 9/30 approved changesets -- score normalized to 3",
146+
"code_review_score": 3,
147+
"contributors_details": undefined,
148+
"contributors_documentation": "Determines if the project has a set of contributors from multiple organizations (e.g., companies).",
149+
"contributors_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#contributors",
150+
"contributors_reason": "project has 21 contributing companies or organizations",
151+
"contributors_score": 10,
152+
"dangerous_workflow_details": undefined,
153+
"dangerous_workflow_documentation": "Determines if the project's GitHub Action workflows avoid dangerous patterns.",
154+
"dangerous_workflow_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#dangerous-workflow",
155+
"dangerous_workflow_reason": "no dangerous workflow patterns detected",
156+
"dangerous_workflow_score": 10,
157+
"dependency_update_tool_details": undefined,
158+
"dependency_update_tool_documentation": "Determines if the project uses a dependency update tool.",
159+
"dependency_update_tool_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#dependency-update-tool",
160+
"dependency_update_tool_reason": "no update tool detected",
161+
"dependency_update_tool_score": 0,
162+
"fuzzing_details": undefined,
163+
"fuzzing_documentation": "Determines if the project uses fuzzing.",
164+
"fuzzing_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#fuzzing",
165+
"fuzzing_reason": "project is not fuzzed",
166+
"fuzzing_score": 0,
167+
"license_details": "Info: project has a license file: LICENSE:0
168+
Info: FSF or OSI recognized license: MIT License: LICENSE:0",
169+
"license_documentation": "Determines if the project has defined a license.",
170+
"license_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#license",
171+
"license_reason": "license file detected",
172+
"license_score": 10,
173+
"maintained_details": undefined,
174+
"maintained_documentation": "Determines if the project is "actively maintained".",
175+
"maintained_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#maintained",
176+
"maintained_reason": "30 commit(s) and 21 issue activity found in the last 90 days -- score normalized to 10",
177+
"maintained_score": 10,
178+
"packaging_details": undefined,
179+
"packaging_documentation": "Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.",
180+
"packaging_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#packaging",
181+
"packaging_reason": "packaging workflow not detected",
182+
"packaging_score": -1,
183+
"pinned_dependencies_details": undefined,
184+
"pinned_dependencies_documentation": "Determines if the project has declared and pinned the dependencies of its build process.",
185+
"pinned_dependencies_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#pinned-dependencies",
186+
"pinned_dependencies_reason": "dependency not pinned by hash detected -- score normalized to 4",
187+
"pinned_dependencies_score": 4,
188+
"repo_commit": "e739f419e56442b754e4fea6dbcf98c1c8d00dda",
189+
"sast_details": undefined,
190+
"sast_documentation": "Determines if the project uses static code analysis.",
191+
"sast_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#sast",
192+
"sast_reason": "SAST tool is not run on all commits -- score normalized to 0",
193+
"sast_score": 0,
194+
"scorecard_commit": "ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4",
195+
"scorecard_version": "v5.0.0",
196+
"security_policy_details": undefined,
197+
"security_policy_documentation": "Determines if the project has published a security policy.",
198+
"security_policy_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#security-policy",
199+
"security_policy_reason": "security policy file detected",
200+
"security_policy_score": 10,
201+
"signed_releases_details": undefined,
202+
"signed_releases_documentation": "Determines if the project cryptographically signs release artifacts.",
203+
"signed_releases_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#signed-releases",
204+
"signed_releases_reason": "no releases found",
205+
"signed_releases_score": -1,
206+
"token_permissions_details": undefined,
207+
"token_permissions_documentation": "Determines if the project's workflows follow the principle of least privilege.",
208+
"token_permissions_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#token-permissions",
209+
"token_permissions_reason": "detected GitHub workflow tokens with excessive permissions",
210+
"token_permissions_score": 9,
211+
"vulnerabilities_details": undefined,
212+
"vulnerabilities_documentation": "Determines if the project has open, known unfixed vulnerabilities.",
213+
"vulnerabilities_documentation_url": "https://github.com/ossf/scorecard/blob/ea7e27ed41b76ab879c862fa0ca4cc9c61764ee4/docs/checks.md#vulnerabilities",
214+
"vulnerabilities_reason": "2 existing vulnerabilities detected",
215+
"vulnerabilities_score": 8,
216+
}
217+
`;

__tests__/providers.test.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
const { github } = require('../src/providers')
1+
const { github, ossf } = require('../src/providers')
22
const {
33
sampleGithubOrg,
4-
sampleGithubRepository
4+
sampleGithubRepository,
5+
sampleOSSFScorecardResult
56
} = require('../__fixtures__')
67

7-
describe('GitHub Providers', () => {
8+
describe('GitHub Provider', () => {
89
describe('fetchOrgByLogin', () => {
910
it.todo('Should fetch organization by login')
1011
it.todo('Should throw an error if the organization does not exist')
@@ -35,3 +36,17 @@ describe('GitHub Providers', () => {
3536
})
3637
})
3738
})
39+
40+
describe('OSSF Provider', () => {
41+
describe('performScorecardAnalysis', () => {
42+
it.todo('Should perform scorecard analysis')
43+
it.todo('Should throw an error if the repository does not exist')
44+
it.todo('Should throw an error if there are network issues')
45+
})
46+
describe('mappers', () => {
47+
it('Should map scorecard data correctly', () => {
48+
const mappedData = ossf.mappers.result(sampleOSSFScorecardResult)
49+
expect(mappedData).toMatchSnapshot()
50+
})
51+
})
52+
})

src/providers/index.js

+47-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const { Octokit } = require('octokit')
2-
const { ensureGithubToken } = require('../utils')
2+
const { logger, ensureGithubToken } = require('../utils')
33
const { simplifyObject } = require('@ulisesgascon/simplify-object')
4+
const util = require('util')
5+
const exec = util.promisify(require('node:child_process').exec)
6+
const { ossfScorecardSettings } = require('../config').getConfig()
7+
48
const debug = require('debug')('providers:github')
59

610
const fetchOrgByLogin = async (login) => {
@@ -54,6 +58,23 @@ const fetchRepoByFullName = async (fullName) => {
5458
return data
5559
}
5660

61+
const performScorecardAnalysis = async (repo) => {
62+
ensureGithubToken()
63+
logger.log(`Running OSSF Scorecard for repository (${repo.full_name})...`)
64+
65+
const start = new Date().getTime()
66+
const { stdout, stderr } = await exec(`docker run -e GITHUB_AUTH_TOKEN=${process.env.GITHUB_TOKEN} --rm ${ossfScorecardSettings.dockerImage} --repo=${repo.html_url} --show-details --format=json`)
67+
if (stderr) {
68+
console.error(stderr)
69+
throw new Error(`Error running OSSF Scorecard for repository (${repo.full_name})`)
70+
}
71+
const data = JSON.parse(stdout)
72+
const end = new Date().getTime()
73+
data.analysis_execution_time = (end - start)
74+
logger.log(`OSSF Scorecard finished successfully for repository (${repo.full_name}) in ${data.analysis_execution_time / 1000}s`)
75+
return data
76+
}
77+
5778
const github = {
5879
fetchOrgByLogin,
5980
fetchOrgReposListByLogin,
@@ -130,6 +151,30 @@ const github = {
130151
}
131152
}
132153

154+
const ossf = {
155+
performScorecardAnalysis,
156+
mappers: {
157+
result: (data) => {
158+
const output = {}
159+
output.analysis_score = data.score
160+
output.analysis_time = data.date
161+
output.repo_commit = data.repo?.commit
162+
output.scorecard_version = data.scorecard?.version
163+
output.scorecard_commit = data.scorecard?.commit
164+
data.checks?.forEach((check, i) => {
165+
const checkName = check.name.toLowerCase().replaceAll('-', '_')
166+
output[`${checkName}_reason`] = check.reason
167+
output[`${checkName}_score`] = check.score
168+
output[`${checkName}_documentation_url`] = check.documentation?.url
169+
output[`${checkName}_documentation`] = check.documentation?.short
170+
output[`${checkName}_details`] = check.details?.join('\n')
171+
})
172+
return output
173+
}
174+
}
175+
}
176+
133177
module.exports = {
134-
github
178+
github,
179+
ossf
135180
}

0 commit comments

Comments
 (0)