Skip to content

Commit ee77207

Browse files
authored
Merge branch 'dependency-review' into copilot/sub-pr-21-one-more-time
2 parents 410f95e + 3991ca0 commit ee77207

File tree

11 files changed

+114
-120
lines changed

11 files changed

+114
-120
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ dist/
44
data/
55
.vscode/
66
.DS_Store
7-
component-detection
7+
component-detection
8+
tmp-branch-search-cache/

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ Flags:
6868
```bash
6969
--branch-scan # Fetch SBOMs for non-default branches
7070
--branch-limit <n> # Max number of non-default branches per repo (default 10)
71-
--dependency-review # Fetch dependency review diffs (enabled by default)
7271
--diff-base <branch> # Override base branch for diffs (default: repository default)
7372
```
7473

@@ -86,7 +85,7 @@ If a branch SBOM or diff retrieval fails, the error is recorded but does not sto
8685

8786
#### Handling Missing Dependency Review Snapshots
8887

89-
If the Dependency Review API returns a 404 for a branch diff (commonly due to a missing dependency snapshot on either the base or head commit), the toolkit can optionally attempt to generate and submit a snapshot using Component Detection and Dependency Submission. This is vendored-in and forked from the public [Component Detection Dependency Submission Action](https://github.com/your-org/component-detection-dependency-submission-action).
88+
If the Dependency Review API returns a 404 for a branch diff (commonly due to a missing dependency snapshot on either the base or head commit), the toolkit can optionally attempt to generate and submit a snapshot using Component Detection and Dependency Submission. This is vendored-in and forked from the public [Component Detection Dependency Submission Action](https://github.com/advanced-security/component-detection-dependency-submission-action).
9089

9190
Enable automatic submission + retry with:
9291

@@ -172,7 +171,7 @@ npm run start -- --sbom-cache sboms --purl-file queries.txt
172171
npm run start -- --sync-sboms --org my-org --sbom-cache sboms
173172
```
174173

175-
1. Later offline search (no API calls; uses previously written per‑repo JSON):
174+
2. Later offline search (no API calls; uses previously written per‑repo JSON):
176175

177176
```bash
178177
npm run start -- --sbom-cache sboms --purl pkg:npm/[email protected]
@@ -442,7 +441,7 @@ npm install
442441
npm run build
443442
```
444443
445-
1. Run the test harness script:
444+
2. Run the test harness script:
446445
447446
```bash
448447
node dist/test-fixture-match.js

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
"start": "node dist/cli.js",
1313
"dev": "tsx src/cli.ts",
1414
"lint": "eslint . --ext .ts --max-warnings=0",
15-
"test": "node dist/test-fixture-match.js",
16-
"test:branch-search": "node dist/test-branch-search.js"
15+
"test": "node dist/test-fixture-match.js && node dist/test-branch-search.js"
1716
},
1817
"engines": {
1918
"node": ">=18.0.0"
@@ -28,7 +27,7 @@
2827
"@octokit/plugin-throttling": "^11.0.3",
2928
"chalk": "^5.6.2",
3029
"cross-fetch": "^4.1.0",
31-
"inquirer": "^12.11.0",
30+
"inquirer": "^12.11.1",
3231
"octokit": "^5.0.5",
3332
"p-limit": "^7.2.0",
3433
"packageurl-js": "^2.0.1",

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async function main() {
5858
if (!args.enterprise && !args.org && !args.repo) throw new Error("Provide --enterprise, --org or --repo with --sync-sboms");
5959
if (args.enterprise && args.org) throw new Error("Specify only one of --enterprise or --org");
6060
if (args.repo && (args.enterprise || args.org)) throw new Error("Specify only one of --enterprise, --org, or --repo");
61-
if (!args.sbomCache) throw new Error("--sync-sboms requires --sbom-cache to write updated SBOMs to disk");
61+
if (syncing && !args.sbomCache) throw new Error("--sync-sboms requires --sbom-cache to write updated SBOMs to disk");
6262
} else {
6363
const malwareOnly = !!args["sync-malware"] && !args.sbomCache && !args.purl && !args["purl-file"] && !args["match-malware"] && !args.uploadSarif && !args.interactive;
6464
if (!malwareOnly && !args.sbomCache) throw new Error("Offline mode requires --sbom-cache unless running --sync-malware by itself");

src/componentDetection.ts

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Octokit } from "octokit"
1+
import { Octokit } from "@octokit/core";
22
import {
33
PackageCache,
44
Package,
@@ -11,16 +11,30 @@ import path from 'path';
1111
import { tmpdir } from 'os';
1212
import { StringDecoder } from 'node:string_decoder';
1313

14-
export default class ComponentDetection {
15-
public static componentDetectionPath = process.platform === "win32" ? './component-detection.exe' : './component-detection';
16-
public static outputPath = path.join(tmpdir(), `component-detection-output-${Date.now()}.json`);
14+
export default class ComponentDetection {
15+
public componentDetectionPath: string = process.platform === "win32" ? './component-detection.exe' : './component-detection';
16+
public outputPath: string;
17+
octokit: Octokit;
18+
baseUrl: string;
1719

18-
// This is the default entry point for this class.
19-
// If executablePath is provided, use it directly and skip download.
20-
static async scanAndGetManifests(path: string, executablePath?: string): Promise<Manifest[] | undefined> {
20+
constructor(octokit: Octokit, baseUrl: string, executablePath?: string) {
21+
this.octokit = octokit;
22+
this.baseUrl = baseUrl;
2123
if (executablePath) {
2224
this.componentDetectionPath = executablePath;
23-
} else {
25+
}
26+
27+
// Set the output path
28+
this.outputPath = (() => {
29+
const tmpDir = fs.mkdtempSync(path.join(tmpdir(), 'component-detection-'));
30+
return path.join(tmpDir, 'output.json');
31+
})();
32+
}
33+
34+
// This is the default entry point for this class.
35+
// If executablePath is provided, use it directly and skip download.
36+
async scanAndGetManifests(path: string): Promise<Manifest[] | undefined> {
37+
if (!this.componentDetectionPath) {
2438
await this.downloadLatestRelease();
2539
}
2640

@@ -34,7 +48,7 @@ export default class ComponentDetection {
3448
return await this.getManifestsFromResults(this.outputPath, path);
3549
}
3650
// Get the latest release from the component-detection repo, download the tarball, and extract it
37-
public static async downloadLatestRelease() {
51+
public async downloadLatestRelease() {
3852
try {
3953
const statResult = fs.statSync(this.componentDetectionPath);
4054
if (statResult && statResult.isFile()) {
@@ -54,14 +68,14 @@ export default class ComponentDetection {
5468

5569
// Write the blob to a file
5670
console.debug(`Writing binary to file ${this.componentDetectionPath}`);
57-
await fs.writeFileSync(this.componentDetectionPath, buffer, { mode: 0o777, flag: 'w' });
71+
await fs.writeFileSync(this.componentDetectionPath, buffer, { mode: 0o755, flag: 'w' });
5872
} catch (error: any) {
5973
console.error(error);
6074
}
6175
}
6276

6377
// Run the component-detection CLI on the path specified
64-
public static runComponentDetection(path: string): Promise<boolean> {
78+
public runComponentDetection(path: string): Promise<boolean> {
6579
console.debug(`Running component-detection on ${path}`);
6680

6781
console.debug(`Writing to output file: ${this.outputPath}`);
@@ -102,7 +116,7 @@ export default class ComponentDetection {
102116
});
103117
}
104118

105-
public static async getManifestsFromResults(file: string, path: string): Promise<Manifest[] | undefined> {
119+
public async getManifestsFromResults(file: string, path: string): Promise<Manifest[] | undefined> {
106120
console.debug(`Reading results from ${file}`);
107121
const results = await fs.readFileSync(file, 'utf8');
108122
const json: any = JSON.parse(results);
@@ -112,7 +126,7 @@ export default class ComponentDetection {
112126
return this.processComponentsToManifests(json.componentsFound, dependencyGraphs);
113127
}
114128

115-
public static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[] {
129+
public processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[] {
116130
// Parse the result file and add the packages to the package cache
117131
const packageCache = new PackageCache();
118132
const packages: Array<ComponentDetectionPackage> = [];
@@ -193,7 +207,7 @@ export default class ComponentDetection {
193207
return manifests;
194208
}
195209

196-
private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>, dependencyGraphs: DependencyGraphs): void {
210+
private addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>, dependencyGraphs: DependencyGraphs): void {
197211
packages.forEach((pkg: ComponentDetectionPackage) => {
198212
pkg.locationsFoundAt.forEach((location: any) => {
199213
// Use the normalized path (remove leading slash if present)
@@ -250,7 +264,7 @@ export default class ComponentDetection {
250264
}
251265

252266
try {
253-
const packageUrl = `${packageUrlJson.Scheme}:${packageUrlJson.Type}/`;
267+
let packageUrl = `${packageUrlJson.Scheme}:${packageUrlJson.Type}/`;
254268
if (packageUrlJson.Namespace) {
255269
packageUrl += `${packageUrlJson.Namespace.replaceAll("@", "%40")}/`;
256270
}
@@ -274,28 +288,23 @@ export default class ComponentDetection {
274288
}
275289
}
276290

277-
private static async getLatestReleaseURL(): Promise<string> {
278-
let githubToken = process.env.GITHUB_TOKEN || "";
279-
280-
const githubAPIURL = process.env.GITHUB_API_URL || 'https://api.github.com';
281-
282-
let ghesMode = process.env.GITHUB_API_URL != githubAPIURL;
283-
// If the we're running in GHES, then use an empty string as the token
284-
if (ghesMode) {
285-
githubToken = "";
291+
private async getLatestReleaseURL(): Promise<string> {
292+
let octokit: Octokit = this.octokit;
293+
294+
if (this.baseUrl !== 'https://api.github.com') {
295+
octokit = new Octokit({
296+
auth: "", request: { fetch: fetch }, log: {
297+
debug: console.debug,
298+
info: console.info,
299+
warn: console.warn,
300+
error: console.error
301+
},
302+
});
286303
}
287-
const octokit = new Octokit({
288-
auth: githubToken, baseUrl: githubAPIURL, request: { fetch: fetch }, log: {
289-
debug: console.debug,
290-
info: console.info,
291-
warn: console.warn,
292-
error: console.error
293-
},
294-
});
295304

296305
const owner = "microsoft";
297306
const repo = "component-detection";
298-
console.debug("Attempting to download latest release from " + githubAPIURL);
307+
console.debug(`Attempting to download latest release from ${owner}/${repo}`);
299308

300309
try {
301310
const latestRelease = await octokit.request("GET /repos/{owner}/{repo}/releases/latest", { owner, repo });
@@ -325,7 +334,7 @@ export default class ComponentDetection {
325334
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
326335
* @returns A new DependencyGraphs object with relative path keys.
327336
*/
328-
public static normalizeDependencyGraphPaths(
337+
public normalizeDependencyGraphPaths(
329338
dependencyGraphs: DependencyGraphs,
330339
filePathInput: string
331340
): DependencyGraphs {

src/componentSubmission.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface SubmitOpts {
2525
componentDetectionBinPath?: string; // optional path to component-detection executable
2626
}
2727

28-
export async function getLanguageIntersection(octokit: any, owner: string, repo: string, languages: string[] | undefined, quiet: boolean = false): Promise<string[]> {
28+
export async function getLanguageIntersection(octokit: Octokit, owner: string, repo: string, languages: string[] | undefined, quiet: boolean = false): Promise<string[]> {
2929
const langResp = await octokit.request('GET /repos/{owner}/{repo}/languages', { owner, repo });
3030
const repoLangs = Object.keys(langResp.data || {});
3131
const wanted = languages;
@@ -63,10 +63,15 @@ export async function submitSnapshotIfPossible(opts: SubmitOpts): Promise<boolea
6363
throw new Error('Octokit instance is required in opts.octokit');
6464
}
6565

66+
const tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'cd-submission-'));
67+
6668
try {
6769
const intersect = await getLanguageIntersection(opts.octokit, opts.owner, opts.repo, opts.languages);
6870
// Create temp dir and sparse checkout only manifest files according to selected languages
69-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cd-submission-'));
71+
if (!intersect.length) {
72+
// No matching languages, skip submission
73+
return false;
74+
}
7075
console.debug(chalk.green(`Sparse checkout into ${tmp} for languages: ${intersect.join(', ')}`));
7176

7277
const sha = await sparseCheckout(opts.owner, opts.repo, opts.branch, tmp, intersect, opts.baseUrl);
@@ -76,14 +81,15 @@ export async function submitSnapshotIfPossible(opts: SubmitOpts): Promise<boolea
7681
if (!opts.quiet) console.error(chalk.red(`Failed to determine SHA for ${opts.owner}/${opts.repo} on branch ${opts.branch}`));
7782
return false;
7883
}
79-
await run(opts.octokit, tmp, opts.owner, opts.repo, sha, opts.branch, opts.componentDetectionBinPath);
84+
return await run(opts.octokit, tmp, opts.owner, opts.repo, sha, opts.branch, opts.componentDetectionBinPath);
8085

8186
} catch (e) {
8287
if (!opts.quiet) console.error(chalk.red(`Component Detection failed: ${(e as Error).message}`));
8388
return false;
89+
} finally {
90+
// Clean up temp dir
91+
await fs.promises.rm(tmp, { recursive: true, force: true });
8492
}
85-
86-
return true;
8793
}
8894

8995
function buildSparsePatterns(langs: string[]): string[] {
@@ -124,8 +130,10 @@ function buildSparsePatterns(langs: string[]): string[] {
124130
add('**/*.sln');
125131
}
126132
}
127-
// Always include root lockfiles just in case
128-
add('package.json'); add('package-lock.json'); add('yarn.lock'); add('pnpm-lock.yaml');
133+
// Include root lockfiles only if JavaScript/TypeScript is among selected languages
134+
if (langs.some(l => ['javascript', 'typescript', 'node', 'js', 'ts'].includes(l.toLowerCase()))) {
135+
add('package.json'); add('package-lock.json'); add('yarn.lock'); add('pnpm-lock.yaml');
136+
}
129137
return Array.from(set);
130138
}
131139

@@ -142,12 +150,11 @@ async function execGit(args: string[], opts: { cwd: string, quiet?: boolean }):
142150
});
143151
}
144152

145-
export async function run(octokit: Octokit, tmpDir: string, owner: string, repo: string, sha: string, ref: string, componentDetectionBinPath?: string) {
153+
export async function run(octokit: Octokit, tmpDir: string, owner: string, repo: string, sha: string, ref: string, componentDetectionBinPath?: string): Promise<boolean> {
154+
155+
const componentDetection = new ComponentDetection(octokit, '', componentDetectionBinPath);
146156

147-
let manifests = await ComponentDetection.scanAndGetManifests(
148-
tmpDir,
149-
componentDetectionBinPath
150-
);
157+
let manifests = await componentDetection.scanAndGetManifests(tmpDir);
151158

152159
// Get detector configuration inputs
153160
const detectorName = "Component Detection in GitHub SBOM Toolkit: advanced-security/github-sbom-toolkit";
@@ -161,9 +168,11 @@ export async function run(octokit: Octokit, tmpDir: string, owner: string, repo:
161168
url: detectorUrl,
162169
};
163170

171+
const date = new Date().toISOString();
172+
164173
const job: Job = {
165174
correlator: 'github-sbom-toolkit',
166-
id: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString()
175+
id: `${owner}-${repo}-${ref}-${date}-${Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString()}`
167176
};
168177

169178
let snapshot = new Snapshot(detector, undefined, job);
@@ -176,20 +185,22 @@ export async function run(octokit: Octokit, tmpDir: string, owner: string, repo:
176185
snapshot.addManifest(manifest);
177186
});
178187

179-
submitSnapshot(octokit, snapshot, { owner, repo });
188+
return await submitSnapshot(octokit, snapshot, { owner, repo });
180189
}
181190

182191
/**
183192
* submitSnapshot submits a snapshot to the Dependency Submission API - vendored in from @github/dependency-submission-toolkit, to make it work at the CLI, vs in Actions.
184193
*
185-
* @param {Snapshot} snapshot
186-
* @param {Repo} repo
194+
* @param {Octokit} octokit - The Octokit instance for GitHub API requests
195+
* @param {Snapshot} snapshot - The dependency snapshot to submit
196+
* @param {Repo} repo - The repository owner and name
197+
* @returns {Promise<boolean>} true if submission was successful, false otherwise
187198
*/
188199
export async function submitSnapshot(
189200
octokit: Octokit,
190201
snapshot: Snapshot,
191202
repo: { owner: string; repo: string }
192-
) {
203+
): Promise<boolean> {
193204
console.debug('Submitting snapshot...')
194205
console.debug(snapshot.prettyJSON())
195206

@@ -198,7 +209,7 @@ export async function submitSnapshot(
198209
'POST /repos/{owner}/{repo}/dependency-graph/snapshots',
199210
{
200211
headers: {
201-
accept: 'application/vnd.github.foo-bar-preview+json'
212+
accept: 'application/vnd.github+json'
202213
},
203214
owner: repo.owner,
204215
repo: repo.repo,
@@ -211,10 +222,12 @@ export async function submitSnapshot(
211222
`Snapshot successfully created at ${response.data.created_at.toString()}` +
212223
` with id ${response.data.id}`
213224
)
225+
return true
214226
} else {
215227
console.error(
216228
`Snapshot creation failed with result: "${result}: ${response.data.message}"`
217229
)
230+
return false
218231
}
219232
} catch (error) {
220233
if (error instanceof RequestError) {
@@ -231,6 +244,6 @@ export async function submitSnapshot(
231244
console.error(error.message)
232245
if (error.stack) console.error(error.stack)
233246
}
234-
throw new Error(`Failed to submit snapshot: ${error}`)
247+
return false
235248
}
236249
}

0 commit comments

Comments
 (0)