Merge pull request #121 from advanced-security/juxtin/direct-vs-transitive
Use explicitlyReferencedComponentIds to determine which packages are direct
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import ComponentDetection from "./componentDetection";
|
||||
import ComponentDetection, { DependencyGraphs } from "./componentDetection";
|
||||
import fs from "fs";
|
||||
|
||||
test("Downloads CLI", async () => {
|
||||
@@ -70,7 +70,7 @@ describe("ComponentDetection.makePackageUrl", () => {
|
||||
});
|
||||
|
||||
describe("ComponentDetection.processComponentsToManifests", () => {
|
||||
test("adds package as direct dependency when no top level referrers", () => {
|
||||
test("adds package as direct dependency when it is listed as an explicitlyReferencedComponentIds", () => {
|
||||
const componentsFound = [
|
||||
{
|
||||
component: {
|
||||
@@ -86,20 +86,29 @@ describe("ComponentDetection.processComponentsToManifests", () => {
|
||||
},
|
||||
isDevelopmentDependency: false,
|
||||
topLevelReferrers: [], // Empty = direct dependency
|
||||
locationsFoundAt: ["package.json"]
|
||||
locationsFoundAt: ["/package.json"]
|
||||
}
|
||||
];
|
||||
|
||||
const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
|
||||
const dependencyGraphs: DependencyGraphs = {
|
||||
"/package.json": {
|
||||
graph: { "test-package": null },
|
||||
explicitlyReferencedComponentIds: ["test-package 1.0.0 - npm"],
|
||||
developmentDependencies: [],
|
||||
dependencies: []
|
||||
}
|
||||
};
|
||||
|
||||
const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);
|
||||
|
||||
expect(manifests).toHaveLength(1);
|
||||
expect(manifests[0].name).toBe("package.json");
|
||||
expect(manifests[0].name).toBe("/package.json");
|
||||
expect(manifests[0].directDependencies()).toHaveLength(1);
|
||||
expect(manifests[0].indirectDependencies()).toHaveLength(0);
|
||||
expect(manifests[0].countDependencies()).toBe(1);
|
||||
});
|
||||
|
||||
test("adds package as indirect dependency when has top level referrers", () => {
|
||||
test("adds package as indirect dependency when it is not in explicitlyReferencedComponentIds", () => {
|
||||
const componentsFound = [
|
||||
{
|
||||
component: {
|
||||
@@ -126,56 +135,75 @@ describe("ComponentDetection.processComponentsToManifests", () => {
|
||||
}
|
||||
}
|
||||
],
|
||||
locationsFoundAt: ["package.json"]
|
||||
locationsFoundAt: ["/package.json"]
|
||||
}
|
||||
];
|
||||
|
||||
const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
|
||||
const dependencyGraphs: DependencyGraphs = {
|
||||
"/package.json": {
|
||||
graph: { "parent-package": null },
|
||||
explicitlyReferencedComponentIds: [],
|
||||
developmentDependencies: [],
|
||||
dependencies: []
|
||||
}
|
||||
};
|
||||
|
||||
const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);
|
||||
|
||||
expect(manifests).toHaveLength(1);
|
||||
expect(manifests[0].name).toBe("package.json");
|
||||
expect(manifests[0].name).toBe("/package.json");
|
||||
expect(manifests[0].directDependencies()).toHaveLength(0);
|
||||
expect(manifests[0].indirectDependencies()).toHaveLength(1);
|
||||
expect(manifests[0].countDependencies()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("adds package as direct dependency when top level referrer is itself", () => {
|
||||
const componentsFound = [
|
||||
{
|
||||
component: {
|
||||
name: "test-package",
|
||||
version: "1.0.0",
|
||||
packageUrl: {
|
||||
Scheme: "pkg",
|
||||
Type: "npm",
|
||||
Name: "test-package",
|
||||
Version: "1.0.0"
|
||||
},
|
||||
id: "test-package 1.0.0 - npm"
|
||||
},
|
||||
isDevelopmentDependency: false,
|
||||
topLevelReferrers: [
|
||||
{
|
||||
name: "test-package",
|
||||
version: "1.0.0",
|
||||
packageUrl: {
|
||||
Scheme: "pkg",
|
||||
Type: "npm",
|
||||
Name: "test-package",
|
||||
Version: "1.0.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
locationsFoundAt: ["package.json"]
|
||||
describe('normalizeDependencyGraphPaths', () => {
|
||||
test('converts absolute paths to relative paths based on filePath input', () => {
|
||||
// Simulate a repo at /repo and a scan root at /repo/packages
|
||||
const fakeCwd = '/workspaces';
|
||||
const filePathInput = 'my-super-cool-repo';
|
||||
const absBase = '/workspaces/my-super-cool-repo';
|
||||
const dependencyGraphs: DependencyGraphs = {
|
||||
'/workspaces/my-super-cool-repo/a/package.json': {
|
||||
graph: { 'foo': null },
|
||||
explicitlyReferencedComponentIds: [],
|
||||
developmentDependencies: [],
|
||||
dependencies: []
|
||||
},
|
||||
'/workspaces/my-super-cool-repo/b/package.json': {
|
||||
graph: { 'bar': null },
|
||||
explicitlyReferencedComponentIds: [],
|
||||
developmentDependencies: [],
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
|
||||
const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
|
||||
|
||||
expect(manifests).toHaveLength(1);
|
||||
expect(manifests[0].name).toBe("package.json");
|
||||
expect(manifests[0].directDependencies()).toHaveLength(1);
|
||||
expect(manifests[0].indirectDependencies()).toHaveLength(0);
|
||||
expect(manifests[0].countDependencies()).toBe(1);
|
||||
};
|
||||
// Patch process.cwd for this test
|
||||
const originalCwd = process.cwd;
|
||||
(process as any).cwd = () => fakeCwd;
|
||||
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, filePathInput);
|
||||
// Restore process.cwd
|
||||
(process as any).cwd = originalCwd;
|
||||
expect(Object.keys(normalized)).toContain('/a/package.json');
|
||||
expect(Object.keys(normalized)).toContain('/b/package.json');
|
||||
expect(normalized['/a/package.json'].graph).toEqual({ 'foo': null });
|
||||
expect(normalized['/b/package.json'].graph).toEqual({ 'bar': null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeDependencyGraphPaths with real output.json', () => {
|
||||
test('converts absolute paths in output.json to relative paths using current cwd and filePath', () => {
|
||||
const output = JSON.parse(fs.readFileSync('./output.json', 'utf8'));
|
||||
const dependencyGraphs = output.dependencyGraphs;
|
||||
// Use the same filePath as the action default (".")
|
||||
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, 'test');
|
||||
// Should contain /package.json and /package-lock.json as keys
|
||||
expect(Object.keys(normalized)).toContain('/package.json');
|
||||
expect(Object.keys(normalized)).toContain('/package-lock.json');
|
||||
// All keys should now be relative to the repo root (cwd) and start with '/'
|
||||
for (const key of Object.keys(normalized)) {
|
||||
expect(key.startsWith('/')).toBe(true);
|
||||
expect(key).not.toMatch(/^\w:\\|^\/\/|^\.{1,2}\//); // Not windows absolute, not network, not relative
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Package,
|
||||
Snapshot,
|
||||
Manifest,
|
||||
submitSnapshot
|
||||
submitSnapshot,
|
||||
} from '@github/dependency-submission-toolkit'
|
||||
import fetch from 'cross-fetch'
|
||||
import tar from 'tar'
|
||||
@@ -16,6 +16,7 @@ import * as exec from '@actions/exec';
|
||||
import dotenv from 'dotenv'
|
||||
import { Context } from '@actions/github/lib/context'
|
||||
import { unmockedModulePathPatterns } from './jest.config'
|
||||
import path from 'path';
|
||||
dotenv.config();
|
||||
|
||||
export default class ComponentDetection {
|
||||
@@ -70,10 +71,11 @@ export default class ComponentDetection {
|
||||
core.info("Getting manifests from results");
|
||||
const results = await fs.readFileSync(this.outputPath, 'utf8');
|
||||
var json: any = JSON.parse(results);
|
||||
return this.processComponentsToManifests(json.componentsFound);
|
||||
let dependencyGraphs: DependencyGraphs = this.normalizeDependencyGraphPaths(json.dependencyGraphs, core.getInput('filePath'));
|
||||
return this.processComponentsToManifests(json.componentsFound, dependencyGraphs);
|
||||
}
|
||||
|
||||
public static processComponentsToManifests(componentsFound: any[]): Manifest[] {
|
||||
public static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[] {
|
||||
// Parse the result file and add the packages to the package cache
|
||||
const packageCache = new PackageCache();
|
||||
const packages: Array<ComponentDetectionPackage> = [];
|
||||
@@ -126,6 +128,10 @@ export default class ComponentDetection {
|
||||
|
||||
try {
|
||||
const referrerPackage = packageCache.lookupPackage(referrerUrl);
|
||||
if (referrerPackage === pkg) {
|
||||
core.debug(`Skipping self-reference for package: ${pkg.id}`);
|
||||
return; // Skip self-references
|
||||
}
|
||||
if (referrerPackage) {
|
||||
referrerPackage.dependsOn(pkg);
|
||||
}
|
||||
@@ -139,12 +145,12 @@ export default class ComponentDetection {
|
||||
const manifests: Array<Manifest> = [];
|
||||
|
||||
// Check the locationsFoundAt for every package and add each as a manifest
|
||||
this.addPackagesToManifests(packages, manifests);
|
||||
this.addPackagesToManifests(packages, manifests, dependencyGraphs);
|
||||
|
||||
return manifests;
|
||||
}
|
||||
|
||||
private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>): void {
|
||||
private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>, dependencyGraphs: DependencyGraphs): void {
|
||||
packages.forEach((pkg: ComponentDetectionPackage) => {
|
||||
pkg.locationsFoundAt.forEach((location: any) => {
|
||||
if (!manifests.find((manifest: Manifest) => manifest.name == location)) {
|
||||
@@ -152,16 +158,27 @@ export default class ComponentDetection {
|
||||
manifests.push(manifest);
|
||||
}
|
||||
|
||||
// Filter out self-references from topLevelReferrers
|
||||
const nonSelfReferrers = pkg.topLevelReferrers.filter((referrer: any) => {
|
||||
if (!referrer.packageUrlString) return false;
|
||||
return referrer.packageUrlString !== pkg.packageUrlString;
|
||||
});
|
||||
const depGraphEntry = dependencyGraphs[location];
|
||||
if (!depGraphEntry) {
|
||||
core.warning(`No dependency graph entry found for manifest location: ${location}`);
|
||||
return; // Skip this location if not found in dependencyGraphs
|
||||
}
|
||||
|
||||
if (nonSelfReferrers.length == 0) {
|
||||
manifests.find((manifest: Manifest) => manifest.name == location)?.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
|
||||
const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
|
||||
if (directDependencies.includes(pkg.id)) {
|
||||
manifests
|
||||
.find((manifest: Manifest) => manifest.name == location)
|
||||
?.addDirectDependency(
|
||||
pkg,
|
||||
ComponentDetection.getDependencyScope(pkg)
|
||||
);
|
||||
} else {
|
||||
manifests.find((manifest: Manifest) => manifest.name == location)?.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
|
||||
manifests
|
||||
.find((manifest: Manifest) => manifest.name == location)
|
||||
?.addIndirectDependency(
|
||||
pkg,
|
||||
ComponentDetection.getDependencyScope(pkg)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -249,6 +266,29 @@ export default class ComponentDetection {
|
||||
throw new Error("Failed to download latest release");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
|
||||
* @param dependencyGraphs The DependencyGraphs object to normalize.
|
||||
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
|
||||
* @returns A new DependencyGraphs object with relative path keys.
|
||||
*/
|
||||
public static normalizeDependencyGraphPaths(
|
||||
dependencyGraphs: DependencyGraphs,
|
||||
filePathInput: string
|
||||
): DependencyGraphs {
|
||||
// Resolve the base directory from filePathInput (relative to cwd if not absolute)
|
||||
const baseDir = path.resolve(process.cwd(), filePathInput);
|
||||
const normalized: DependencyGraphs = {};
|
||||
for (const absPath in dependencyGraphs) {
|
||||
// Make the path relative to the baseDir
|
||||
let relPath = path.relative(baseDir, absPath).replace(/\\/g, '/');
|
||||
// Ensure leading slash to represent repo root
|
||||
if (!relPath.startsWith('/')) relPath = '/' + relPath;
|
||||
normalized[relPath] = dependencyGraphs[absPath];
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentDetectionPackage extends Package {
|
||||
@@ -261,6 +301,33 @@ class ComponentDetectionPackage extends Package {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Types for the dependencyGraphs section of output.json
|
||||
*/
|
||||
export type DependencyGraph = {
|
||||
/**
|
||||
* The dependency graph: keys are component IDs, values are either null (no dependencies) or an array of component IDs (dependencies)
|
||||
*/
|
||||
graph: Record<string, string[] | null>;
|
||||
/**
|
||||
* Explicitly referenced component IDs
|
||||
*/
|
||||
explicitlyReferencedComponentIds: string[];
|
||||
/**
|
||||
* Development dependencies
|
||||
*/
|
||||
developmentDependencies: string[];
|
||||
/**
|
||||
* Regular dependencies
|
||||
*/
|
||||
dependencies: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The top-level dependencyGraphs object: keys are manifest file paths, values are DependencyGraph objects
|
||||
*/
|
||||
export type DependencyGraphs = Record<string, DependencyGraph>;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
34
dist/componentDetection.d.ts
generated
vendored
34
dist/componentDetection.d.ts
generated
vendored
@@ -7,9 +7,41 @@ export default class ComponentDetection {
|
||||
static runComponentDetection(path: string): Promise<void>;
|
||||
private static getComponentDetectionParameters;
|
||||
static getManifestsFromResults(): Promise<Manifest[] | undefined>;
|
||||
static processComponentsToManifests(componentsFound: any[]): Manifest[];
|
||||
static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[];
|
||||
private static addPackagesToManifests;
|
||||
private static getDependencyScope;
|
||||
static makePackageUrl(packageUrlJson: any): string;
|
||||
private static getLatestReleaseURL;
|
||||
/**
|
||||
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
|
||||
* @param dependencyGraphs The DependencyGraphs object to normalize.
|
||||
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
|
||||
* @returns A new DependencyGraphs object with relative path keys.
|
||||
*/
|
||||
static normalizeDependencyGraphPaths(dependencyGraphs: DependencyGraphs, filePathInput: string): DependencyGraphs;
|
||||
}
|
||||
/**
|
||||
* Types for the dependencyGraphs section of output.json
|
||||
*/
|
||||
export type DependencyGraph = {
|
||||
/**
|
||||
* The dependency graph: keys are component IDs, values are either null (no dependencies) or an array of component IDs (dependencies)
|
||||
*/
|
||||
graph: Record<string, string[] | null>;
|
||||
/**
|
||||
* Explicitly referenced component IDs
|
||||
*/
|
||||
explicitlyReferencedComponentIds: string[];
|
||||
/**
|
||||
* Development dependencies
|
||||
*/
|
||||
developmentDependencies: string[];
|
||||
/**
|
||||
* Regular dependencies
|
||||
*/
|
||||
dependencies: string[];
|
||||
};
|
||||
/**
|
||||
* The top-level dependencyGraphs object: keys are manifest file paths, values are DependencyGraph objects
|
||||
*/
|
||||
export type DependencyGraphs = Record<string, DependencyGraph>;
|
||||
|
||||
54
dist/index.js
generated
vendored
54
dist/index.js
generated
vendored
@@ -36002,6 +36002,7 @@ const cross_fetch_1 = __importDefault(__nccwpck_require__(3304));
|
||||
const fs_1 = __importDefault(__nccwpck_require__(9896));
|
||||
const exec = __importStar(__nccwpck_require__(5236));
|
||||
const dotenv_1 = __importDefault(__nccwpck_require__(8889));
|
||||
const path_1 = __importDefault(__nccwpck_require__(6928));
|
||||
dotenv_1.default.config();
|
||||
class ComponentDetection {
|
||||
// This is the default entry point for this class.
|
||||
@@ -36056,10 +36057,11 @@ class ComponentDetection {
|
||||
core.info("Getting manifests from results");
|
||||
const results = yield fs_1.default.readFileSync(this.outputPath, 'utf8');
|
||||
var json = JSON.parse(results);
|
||||
return this.processComponentsToManifests(json.componentsFound);
|
||||
let dependencyGraphs = this.normalizeDependencyGraphPaths(json.dependencyGraphs, core.getInput('filePath'));
|
||||
return this.processComponentsToManifests(json.componentsFound, dependencyGraphs);
|
||||
});
|
||||
}
|
||||
static processComponentsToManifests(componentsFound) {
|
||||
static processComponentsToManifests(componentsFound, dependencyGraphs) {
|
||||
// Parse the result file and add the packages to the package cache
|
||||
const packageCache = new dependency_submission_toolkit_1.PackageCache();
|
||||
const packages = [];
|
||||
@@ -36103,6 +36105,10 @@ class ComponentDetection {
|
||||
}
|
||||
try {
|
||||
const referrerPackage = packageCache.lookupPackage(referrerUrl);
|
||||
if (referrerPackage === pkg) {
|
||||
core.debug(`Skipping self-reference for package: ${pkg.id}`);
|
||||
return; // Skip self-references
|
||||
}
|
||||
if (referrerPackage) {
|
||||
referrerPackage.dependsOn(pkg);
|
||||
}
|
||||
@@ -36115,10 +36121,10 @@ class ComponentDetection {
|
||||
// Create manifests
|
||||
const manifests = [];
|
||||
// Check the locationsFoundAt for every package and add each as a manifest
|
||||
this.addPackagesToManifests(packages, manifests);
|
||||
this.addPackagesToManifests(packages, manifests, dependencyGraphs);
|
||||
return manifests;
|
||||
}
|
||||
static addPackagesToManifests(packages, manifests) {
|
||||
static addPackagesToManifests(packages, manifests, dependencyGraphs) {
|
||||
packages.forEach((pkg) => {
|
||||
pkg.locationsFoundAt.forEach((location) => {
|
||||
var _a, _b;
|
||||
@@ -36126,17 +36132,19 @@ class ComponentDetection {
|
||||
const manifest = new dependency_submission_toolkit_1.Manifest(location, location);
|
||||
manifests.push(manifest);
|
||||
}
|
||||
// Filter out self-references from topLevelReferrers
|
||||
const nonSelfReferrers = pkg.topLevelReferrers.filter((referrer) => {
|
||||
if (!referrer.packageUrlString)
|
||||
return false;
|
||||
return referrer.packageUrlString !== pkg.packageUrlString;
|
||||
});
|
||||
if (nonSelfReferrers.length == 0) {
|
||||
(_a = manifests.find((manifest) => manifest.name == location)) === null || _a === void 0 ? void 0 : _a.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
|
||||
const depGraphEntry = dependencyGraphs[location];
|
||||
if (!depGraphEntry) {
|
||||
core.warning(`No dependency graph entry found for manifest location: ${location}`);
|
||||
return; // Skip this location if not found in dependencyGraphs
|
||||
}
|
||||
const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
|
||||
if (directDependencies.includes(pkg.id)) {
|
||||
(_a = manifests
|
||||
.find((manifest) => manifest.name == location)) === null || _a === void 0 ? void 0 : _a.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
|
||||
}
|
||||
else {
|
||||
(_b = manifests.find((manifest) => manifest.name == location)) === null || _b === void 0 ? void 0 : _b.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
|
||||
(_b = manifests
|
||||
.find((manifest) => manifest.name == location)) === null || _b === void 0 ? void 0 : _b.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -36216,6 +36224,26 @@ class ComponentDetection {
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
|
||||
* @param dependencyGraphs The DependencyGraphs object to normalize.
|
||||
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
|
||||
* @returns A new DependencyGraphs object with relative path keys.
|
||||
*/
|
||||
static normalizeDependencyGraphPaths(dependencyGraphs, filePathInput) {
|
||||
// Resolve the base directory from filePathInput (relative to cwd if not absolute)
|
||||
const baseDir = path_1.default.resolve(process.cwd(), filePathInput);
|
||||
const normalized = {};
|
||||
for (const absPath in dependencyGraphs) {
|
||||
// Make the path relative to the baseDir
|
||||
let relPath = path_1.default.relative(baseDir, absPath).replace(/\\/g, '/');
|
||||
// Ensure leading slash to represent repo root
|
||||
if (!relPath.startsWith('/'))
|
||||
relPath = '/' + relPath;
|
||||
normalized[relPath] = dependencyGraphs[absPath];
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
exports["default"] = ComponentDetection;
|
||||
ComponentDetection.componentDetectionPath = process.platform === "win32" ? './component-detection.exe' : './component-detection';
|
||||
|
||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user