Skip to content

Commit 72477cf

Browse files
authored
Merge branch 'dependency-review' into copilot/sub-pr-21-another-one
2 parents 988b8ef + 8e6e341 commit 72477cf

File tree

12 files changed

+98
-112
lines changed

12 files changed

+98
-112
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: 2 additions & 3 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

@@ -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-
var 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: 18 additions & 9 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);
@@ -81,6 +86,9 @@ export async function submitSnapshotIfPossible(opts: SubmitOpts): Promise<boolea
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
}
8593
}
8694

@@ -122,8 +130,10 @@ function buildSparsePatterns(langs: string[]): string[] {
122130
add('**/*.sln');
123131
}
124132
}
125-
// Always include root lockfiles just in case
126-
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+
}
127137
return Array.from(set);
128138
}
129139

@@ -142,10 +152,9 @@ async function execGit(args: string[], opts: { cwd: string, quiet?: boolean }):
142152

143153
export async function run(octokit: Octokit, tmpDir: string, owner: string, repo: string, sha: string, ref: string, componentDetectionBinPath?: string): Promise<boolean> {
144154

145-
let manifests = await ComponentDetection.scanAndGetManifests(
146-
tmpDir,
147-
componentDetectionBinPath
148-
);
155+
const componentDetection = new ComponentDetection(octokit, '', componentDetectionBinPath);
156+
157+
let manifests = await componentDetection.scanAndGetManifests(tmpDir);
149158

150159
// Get detector configuration inputs
151160
const detectorName = "Component Detection in GitHub SBOM Toolkit: advanced-security/github-sbom-toolkit";
@@ -198,7 +207,7 @@ export async function submitSnapshot(
198207
'POST /repos/{owner}/{repo}/dependency-graph/snapshots',
199208
{
200209
headers: {
201-
accept: 'application/vnd.github.foo-bar-preview+json'
210+
accept: 'application/vnd.github+json'
202211
},
203212
owner: repo.owner,
204213
repo: repo.repo,

src/malwareMatcher.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,12 @@ export function matchMalware(advisories: MalwareAdvisoryNode[], sboms: Repositor
157157
if (change.changeType !== 'added' && change.changeType !== 'updated') continue;
158158
let p: string | undefined = (change as { purl?: string }).purl;
159159
if (!p && change.packageURL && change.packageURL.startsWith('pkg:')) p = change.packageURL;
160-
if (!p && change.ecosystem && change.name && change.newVersion) {
160+
if (!p && change.ecosystem && change.name && change.version) {
161161
// Dependency review ecosystems are lower-case purl types already (e.g. npm, maven, pip, gem)
162-
p = `pkg:${change.ecosystem}/${change.name}${change.newVersion ? '@' + change.newVersion : ''}`;
162+
p = `pkg:${change.ecosystem}/${change.name}${change.version ? '@' + change.version : ''}`;
163163
}
164164
if (!p) continue;
165-
out.push({ purl: p, name: change.name, ecosystem: change.ecosystem, version: change.newVersion, __branch: branchName });
165+
out.push({ purl: p, name: change.name, ecosystem: change.ecosystem, version: change.version, __branch: branchName });
166166
}
167167
}
168168
return out;

src/sbomCollector.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ export class SbomCollector {
4848
if (!options.loadFromDir && !options.enterprise && !options.org && !options.repo) {
4949
throw new Error("One of enterprise/org/repo or loadFromDir must be specified");
5050
}
51+
// Validate repo format if provided
52+
if (options.repo) {
53+
if (typeof options.repo !== "string" || !options.repo.includes("/")) {
54+
throw new Error('If specifying "repo", it must be in the format "org/repo".');
55+
}
56+
const [orgPart, repoPart] = options.repo.split("/");
57+
if (!orgPart || !repoPart) {
58+
throw new Error('If specifying "repo", it must be in the format "org/repo" with both parts non-empty.');
59+
}
60+
}
5161
// Spread user options first then apply defaults via nullish coalescing so that
5262
// passing undefined does not erase defaults
5363
const o = { ...options };
@@ -528,7 +538,7 @@ export class SbomCollector {
528538
}
529539
}
530540
}
531-
return { latestCommitDate: new Date().toISOString(), base, head, retrievedAt: new Date().toISOString(), changes: [], error: reason };
541+
return { latestCommitDate: undefined, base, head, retrievedAt: new Date().toISOString(), changes: [], error: reason };
532542
}
533543
}
534544

@@ -627,7 +637,7 @@ export class SbomCollector {
627637
const candidatePurls: string[] = [];
628638
if ((change as { purl?: string }).purl) candidatePurls.push((change as { purl?: string }).purl as string);
629639
if (change.packageURL) candidatePurls.push(change.packageURL);
630-
applyQueries(candidatePurls, queries, found, diff.head, (change as any).newVersion);
640+
applyQueries(candidatePurls, queries, found, diff.head, (change as any).version);
631641
}
632642
}
633643
}

0 commit comments

Comments
 (0)