Skip to content

Commit b9c8960

Browse files
committed
fixup: add tests
1 parent 1593f6f commit b9c8960

File tree

8 files changed

+350
-46
lines changed

8 files changed

+350
-46
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/* eslint-disable @typescript-eslint/restrict-template-expressions */
2+
import sinon from 'sinon';
3+
import path from 'path';
4+
import { expect } from 'chai';
5+
import { fetchCodeQLResults } from './fetch-codeql-results';
6+
7+
describe('fetch-codeql-results', function () {
8+
let octokit: any;
9+
10+
beforeEach(function () {
11+
octokit = {
12+
git: {
13+
getRef: sinon
14+
.stub()
15+
.resolves({ data: { object: { type: 'tag', sha: '1'.repeat(40) } } }),
16+
getTag: sinon
17+
.stub()
18+
.resolves({
19+
data: { object: { type: 'commit', sha: '2'.repeat(40) } },
20+
}),
21+
},
22+
codeScanning: {
23+
listRecentAnalyses: sinon
24+
.stub()
25+
.callsFake(({ owner, repo, page }: any) => {
26+
const key = `${owner}/${repo}#${page}`;
27+
const data = {
28+
'mongodb-js/mongodb-connection-string-url#0': [
29+
{ commit_sha: '2'.repeat(40), id: 1000 },
30+
],
31+
'mongodb-js/mongodb-connection-string-url#1': [
32+
{ commit_sha: '2'.repeat(40), id: 1001 },
33+
],
34+
'mongodb-js/example-repo#0': [
35+
{ commit_sha: '3'.repeat(40), id: 1002 },
36+
],
37+
'mongodb-js/example-repo#1': [],
38+
}[key];
39+
if (!data)
40+
throw new Error(`No analysis list entry for ${key} prepared`);
41+
return { data };
42+
}),
43+
getAnalysis: sinon
44+
.stub()
45+
.callsFake(({ owner, repo, analysis_id }: any) => {
46+
const key = `${owner}/${repo}#${analysis_id}`;
47+
const data = {
48+
'mongodb-js/mongodb-connection-string-url#1000': {
49+
version: '1',
50+
runs: [
51+
{
52+
results: [],
53+
tool: {
54+
driver: { name: 'toolname', semanticVersion: '1.2.3' },
55+
},
56+
versionControlProvenance: [{ revisionId: '2'.repeat(40) }],
57+
},
58+
],
59+
},
60+
'mongodb-js/mongodb-connection-string-url#1001': {
61+
version: '1',
62+
runs: [
63+
{
64+
results: [],
65+
tool: {
66+
driver: { name: 'toolname', semanticVersion: '1.2.3' },
67+
},
68+
versionControlProvenance: [{ revisionId: '2'.repeat(40) }],
69+
},
70+
],
71+
},
72+
'mongodb-js/example-repo#1002': {
73+
version: '1',
74+
runs: [
75+
{
76+
results: [
77+
{
78+
properties: {
79+
'github/alertUrl': 'https://example.com/alert1',
80+
},
81+
},
82+
],
83+
tool: {
84+
driver: { name: 'toolname', semanticVersion: '1.2.3' },
85+
},
86+
versionControlProvenance: [
87+
{
88+
revisionId: '3'.repeat(40),
89+
},
90+
],
91+
},
92+
],
93+
},
94+
}[key];
95+
if (!data) throw new Error(`No analysis entry for ${key} prepared`);
96+
return { data };
97+
}),
98+
},
99+
request: sinon.stub().callsFake(({ url }) => {
100+
const data = {
101+
'https://example.com/alert1': {
102+
state: 'dismissed',
103+
dismissed_reason: 'false positive',
104+
dismissed_comment: 'totally fine',
105+
rule: {
106+
id: 'rule1234',
107+
description: 'foobar',
108+
security_severity_level: 'high',
109+
},
110+
},
111+
}[url];
112+
if (!data) throw new Error(`No URL response for ${url} prepared`);
113+
return { data };
114+
}),
115+
};
116+
});
117+
118+
it('fetches CodeQL results for a repository', async function () {
119+
const dir = path.resolve(__dirname, '../../test/fixtures/example-repo');
120+
const result: any = await fetchCodeQLResults(octokit, {
121+
dependencyFiles: [path.join(dir, 'dependencies.json')],
122+
excludeRepos: [],
123+
currentRepo: {
124+
owner: 'mongodb-js',
125+
repo: 'example-repo',
126+
commit: '3'.repeat(40),
127+
},
128+
});
129+
result.properties['mongodb/creationParams'].timestamp =
130+
'2024-05-23T12:03:47.796Z';
131+
expect(result).to.deep.equal({
132+
runs: [
133+
{
134+
results: [],
135+
versionControlProvenance: [
136+
{ revisionId: '2222222222222222222222222222222222222222' },
137+
],
138+
},
139+
{
140+
results: [],
141+
versionControlProvenance: [
142+
{ revisionId: '2222222222222222222222222222222222222222' },
143+
],
144+
},
145+
{
146+
results: [
147+
{
148+
properties: {
149+
'github/alertUrl': 'https://example.com/alert1',
150+
'mongodb/alertState': {
151+
state: 'dismissed',
152+
dismissed_reason: 'false positive',
153+
dismissed_comment: 'totally fine',
154+
repos: {
155+
revisionId: '3333333333333333333333333333333333333333',
156+
repos: [
157+
{
158+
owner: 'mongodb-js',
159+
repo: 'example-repo',
160+
commit: '3333333333333333333333333333333333333333',
161+
},
162+
],
163+
},
164+
rule: {
165+
id: 'rule1234',
166+
description: 'foobar',
167+
security_severity_level: 'high',
168+
},
169+
},
170+
},
171+
},
172+
],
173+
versionControlProvenance: [
174+
{ revisionId: '3333333333333333333333333333333333333333' },
175+
],
176+
},
177+
],
178+
version: '1',
179+
properties: {
180+
'mongodb/creationParams': {
181+
fromRepo: {
182+
owner: 'mongodb-js',
183+
repo: 'example-repo',
184+
commit: '3333333333333333333333333333333333333333',
185+
},
186+
excludeRepos: ['mongodb-js/example-repo'],
187+
timestamp: '2024-05-23T12:03:47.796Z',
188+
},
189+
},
190+
});
191+
});
192+
});

packages/sbom-tools/src/commands/fetch-codeql-results.ts

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,11 @@ type ResolvedCommitInformation = {
2222
type UnresolvedRepoInformation = Omit<ResolvedCommitInformation, 'commit'> &
2323
Partial<ResolvedCommitInformation> & { packageVersion?: string };
2424

25-
const octokit = new Octokit({
26-
auth: process.env.GITHUB_TOKEN,
27-
request: { fetch },
28-
});
29-
3025
// Get CodeQL SARIF reports for a single commit in a single repository
31-
async function getSingleCommitSarif({
32-
owner,
33-
repo,
34-
commit,
35-
}: ResolvedCommitInformation): Promise<unknown[]> {
26+
async function getSingleCommitSarif(
27+
octokit: Octokit,
28+
{ owner, repo, commit }: ResolvedCommitInformation
29+
): Promise<unknown[]> {
3630
const reportIds = new Set<number>();
3731
for (let page = 0; ; page++) {
3832
const { data } = await octokit.codeScanning.listRecentAnalyses({
@@ -76,14 +70,14 @@ function repoForPackageJSON(
7670
: packageJson.repository?.url;
7771
if (!repoUrl)
7872
throw new Error(
79-
`Could not find repostiory information for package.json file at ${atPath}`
73+
`Could not find repository information for package.json file at ${atPath}`
8074
);
8175
const { owner, repo } =
8276
repoUrl.match(/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/.]+)(?:.git)?$/)
8377
?.groups ?? {};
8478
if (!owner || !repo)
8579
throw new Error(
86-
`Could not parse repostiory information for package.json file at ${atPath}`
80+
`Could not parse repository information for package.json file at ${atPath}`
8781
);
8882
return { owner, repo };
8983
}
@@ -119,6 +113,7 @@ declare class AggregateError {
119113

120114
// Look up the commit associated with a given tag
121115
async function resolveVersionSpecifier(
116+
octokit: Octokit,
122117
repo: UnresolvedRepoInformation
123118
): Promise<ResolvedCommitInformation> {
124119
if (repo.commit) {
@@ -195,57 +190,58 @@ async function getCurrentRepo(): Promise<ResolvedCommitInformation> {
195190
return { ...repo, commit };
196191
}
197192

198-
async function fetchCodeQLResults({
199-
dependencyFiles,
200-
excludeRepos,
201-
currentRepo,
202-
sarifDest,
203-
}: {
204-
dependencyFiles: string[];
205-
excludeRepos: string[];
206-
currentRepo?: Partial<ResolvedCommitInformation>;
207-
sarifDest: string;
208-
}) {
193+
export async function fetchCodeQLResults(
194+
octokit: Octokit,
195+
{
196+
dependencyFiles,
197+
excludeRepos,
198+
currentRepo,
199+
}: {
200+
dependencyFiles: string[];
201+
excludeRepos: string[];
202+
currentRepo?: Partial<ResolvedCommitInformation>;
203+
}
204+
): Promise<unknown> {
209205
if (!dependencyFiles?.length) {
210206
throw new Error('Missing required argument: --dependencies');
211207
}
212-
if (!sarifDest) {
213-
throw new Error('Missing required argument: --sarif-dest');
214-
}
215208

209+
// Add the current repository we're in to the list of repos to be scanned
216210
let resolvedCurrentRepo: ResolvedCommitInformation;
217211
if (!currentRepo?.owner || !currentRepo.repo || !currentRepo.commit) {
218212
resolvedCurrentRepo = { ...(await getCurrentRepo()), ...currentRepo };
219213
} else {
220214
resolvedCurrentRepo = currentRepo as ResolvedCommitInformation;
221215
}
222216
let repos = await listFirstPartyDependencies(dependencyFiles);
217+
// Make sure the only entry for the current repo is the one we explicitly add
223218
excludeRepos.push(`${resolvedCurrentRepo.owner}/${resolvedCurrentRepo.repo}`);
224219
repos = repos.filter(
225220
(repo) => !excludeRepos.includes(`${repo.owner}/${repo.repo}`)
226221
);
227222
repos.push(resolvedCurrentRepo);
228223
repos = deduplicateArray(repos);
229224
let resolvedRepos = await Promise.all(
230-
repos.map(async (repo) => await resolveVersionSpecifier(repo))
225+
repos.map(async (repo) => await resolveVersionSpecifier(octokit, repo))
231226
);
227+
// scan each [owner, repo, commit] triple only once, even if it appears for e.g. multiple packages
232228
resolvedRepos = deduplicateArray(resolvedRepos, ['owner', 'repo', 'commit']);
233229

234230
const sarifs = (
235231
await Promise.all(
236232
resolvedRepos.map(async (repo) => {
237233
try {
238-
const reports = await getSingleCommitSarif(repo);
234+
const reports = await getSingleCommitSarif(octokit, repo);
239235
if (reports.length === 0) {
240236
throw new Error('Could not find any reports');
241237
}
242238
return reports;
243239
} catch (err: unknown) {
244-
// @ts-expect-error 'cause' unsupported
245240
throw new Error(
246241
`Failed to get SARIF for repository ${JSON.stringify(
247242
repo
248243
)}: ${String(err)}`,
244+
// @ts-expect-error 'cause' unsupported
249245
{ cause: err }
250246
);
251247
}
@@ -316,7 +312,7 @@ async function fetchCodeQLResults({
316312
timestamp: new Date().toISOString(),
317313
},
318314
};
319-
await fs.writeFile(sarifDest, JSON.stringify(finalReport, null, 2));
315+
return finalReport;
320316
}
321317

322318
function commaSeparatedList(value: string) {
@@ -348,10 +344,17 @@ export const command = new Command('fetch-codeql-results')
348344
)
349345
.option('--sarif-dest <file>', 'JSON SARIF file output')
350346
.action(async (options) => {
351-
await fetchCodeQLResults({
352-
dependencyFiles: options.dependencies,
347+
const octokit = new Octokit({
348+
auth: process.env.GITHUB_TOKEN,
349+
request: { fetch },
350+
});
351+
if (!options.sarifDest) {
352+
throw new Error('Missing required argument: --sarif-dest');
353+
}
354+
const finalReport = await fetchCodeQLResults(octokit, {
355+
dependencyFiles: options.despendencies,
353356
excludeRepos: options.excludeRepos,
354357
currentRepo: options.currentRepo,
355-
sarifDest: options.sarifDest,
356358
});
359+
await fs.writeFile(options.sarifDest, JSON.stringify(finalReport, null, 2));
357360
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import path from 'path';
2+
import { expect } from 'chai';
3+
import { promises as fs } from 'fs';
4+
import { sarifToMarkdown } from './sarif-to-markdown';
5+
6+
describe('sarif-to-markdown', function () {
7+
it('converts a SARIF JSON file to a simplified markdown representation of that file', async function () {
8+
const sarif = JSON.parse(
9+
await fs.readFile(
10+
path.resolve(
11+
__dirname,
12+
'..',
13+
'..',
14+
'test',
15+
'fixtures',
16+
'mock-sarif.json'
17+
),
18+
'utf8'
19+
)
20+
);
21+
const expectedMd = await fs.readFile(
22+
path.resolve(__dirname, '..', '..', 'test', 'fixtures', 'mock-sarif.md'),
23+
'utf8'
24+
);
25+
expect(sarifToMarkdown({ sarif })).to.equal(expectedMd);
26+
});
27+
});

0 commit comments

Comments
 (0)