Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions lib/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4280,11 +4280,21 @@ export async function createGoBom(path, options) {
}
if (gomodFiles.length) {
let shouldManuallyParse = false;
// Sort go.mod files by depth (shallowest first) to prioritize root modules
const sortedGomodFiles = gomodFiles.sort((a, b) => {
const relativePathA = relative(path, a);
const relativePathB = relative(path, b);
const depthA = relativePathA.split("/").length;
const depthB = relativePathB.split("/").length;
return depthA - depthB;
});

let rootParentComponent = null;
// Use the go list -deps and go mod why commands to generate a good quality BOM for non-docker invocations
if (
!hasAnyProjectType(["docker", "oci", "container", "os"], options, false)
) {
for (const f of gomodFiles) {
for (const f of sortedGomodFiles) {
const basePath = dirname(f);
// Ignore vendor packages
if (
Expand Down Expand Up @@ -4334,13 +4344,23 @@ export async function createGoBom(path, options) {
if (retMap.pkgList?.length) {
pkgList = pkgList.concat(retMap.pkgList);
}
// We treat the main module as our parent
// Prioritize the shallowest module as the root component
if (
retMap.parentComponent &&
Object.keys(retMap.parentComponent).length
) {
parentComponent = retMap.parentComponent;
parentComponent.type = "application";
if (!rootParentComponent) {
// First (shallowest) module becomes the root
rootParentComponent = retMap.parentComponent;
rootParentComponent.type = "application";
parentComponent = rootParentComponent;
} else {
// Subsequent modules become subcomponents
if (!parentComponent.components) {
parentComponent.components = [];
}
parentComponent.components.push(retMap.parentComponent);
}
}
if (DEBUG_MODE) {
console.log("Executing go mod graph in", basePath);
Expand Down Expand Up @@ -4426,11 +4446,14 @@ export async function createGoBom(path, options) {
parentComponent,
);
}
// Retain the parent component hierarchy
// Retain the parent component hierarchy, prioritizing the shallowest module
if (Object.keys(retMap.parentComponent).length) {
if (gomodFiles.length === 1) {
parentComponent = retMap.parentComponent;
if (!rootParentComponent) {
// First (shallowest) module becomes the root
rootParentComponent = retMap.parentComponent;
parentComponent = rootParentComponent;
} else {
// Subsequent modules become subcomponents
parentComponent.components = parentComponent.components || [];
parentComponent.components.push(retMap.parentComponent);
}
Expand All @@ -4456,7 +4479,7 @@ export async function createGoBom(path, options) {
dependencies,
parentComponent,
src: path,
filename: gomodFiles.join(", "),
filename: sortedGomodFiles.join(", "),
});
}
}
Expand All @@ -4468,7 +4491,7 @@ export async function createGoBom(path, options) {
"Manually parsing go.mod files. The resultant BOM would be incomplete.",
);
}
for (const f of gomodFiles) {
for (const f of sortedGomodFiles) {
if (DEBUG_MODE) {
console.log(`Parsing ${f}`);
}
Expand All @@ -4477,11 +4500,14 @@ export async function createGoBom(path, options) {
if (retMap?.pkgList?.length) {
pkgList = pkgList.concat(retMap.pkgList);
}
// Retain the parent component hierarchy
// Retain the parent component hierarchy, prioritizing the shallowest module
if (Object.keys(retMap.parentComponent).length) {
if (gomodFiles.length === 1) {
parentComponent = retMap.parentComponent;
if (!rootParentComponent) {
// First (shallowest) module becomes the root
rootParentComponent = retMap.parentComponent;
parentComponent = rootParentComponent;
} else {
// Subsequent modules become subcomponents
parentComponent.components = parentComponent.components || [];
parentComponent.components.push(retMap.parentComponent);
}
Expand All @@ -4506,7 +4532,7 @@ export async function createGoBom(path, options) {
src: path,
dependencies,
parentComponent,
filename: gomodFiles.join(", "),
filename: sortedGomodFiles.join(", "),
});
}
if (gopkgLockFiles.length) {
Expand Down
58 changes: 58 additions & 0 deletions lib/helpers/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,64 @@ test("parse go mod why dependencies", () => {
expect(pkg_name).toBeUndefined();
});

test("multimodule go.mod file ordering", async () => {
// Test that simulates the file ordering logic from createGoBom
const mockPath = "/workspace/project";
const mockGomodFiles = [
"/workspace/project/deep/nested/go.mod",
"/workspace/project/go.mod",
"/workspace/project/submodule/go.mod",
];

// Sort files by depth (shallowest first) - this is the fix we implemented
const sortedFiles = mockGomodFiles.sort((a, b) => {
const relativePathA = a.replace(`${mockPath}/`, "");
const relativePathB = b.replace(`${mockPath}/`, "");
const depthA = relativePathA.split("/").length;
const depthB = relativePathB.split("/").length;
return depthA - depthB;
});

// The root go.mod should be first (shallowest)
expect(sortedFiles[0]).toEqual("/workspace/project/go.mod");
expect(sortedFiles[1]).toEqual("/workspace/project/submodule/go.mod");
expect(sortedFiles[2]).toEqual("/workspace/project/deep/nested/go.mod");
});

test("parseGoModData for multiple modules with root priority", async () => {
// Test parsing multiple go.mod files to ensure proper component hierarchy
const rootModData = readFileSync("./test/data/multimodule-root.mod", {
encoding: "utf-8",
});
const subModData = readFileSync("./test/data/multimodule-sub.mod", {
encoding: "utf-8",
});
const deepModData = readFileSync("./test/data/multimodule-deep.mod", {
encoding: "utf-8",
});

const rootResult = await parseGoModData(rootModData, {});
const subResult = await parseGoModData(subModData, {});
const deepResult = await parseGoModData(deepModData, {});

// Root module should be identified correctly
expect(rootResult.parentComponent.name).toEqual(
"github.com/example/root-project",
);
expect(rootResult.parentComponent.type).toEqual("application");

// Sub modules should also be parsed correctly
expect(subResult.parentComponent.name).toEqual(
"github.com/example/root-project/submodule",
);
expect(deepResult.parentComponent.name).toEqual(
"github.com/example/root-project/deep/nested",
);

// In the fixed logic, the root should take priority over sub-modules
// This test verifies the parsing works correctly for each individual module
}, 10000);

test("parseGopkgData", async () => {
let dep_list = await parseGopkgData(null);
expect(dep_list).toEqual([]);
Expand Down
7 changes: 7 additions & 0 deletions test/data/multimodule-deep.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/example/root-project/deep/nested

go 1.21

require (
github.com/stretchr/testify v1.8.0
)
10 changes: 10 additions & 0 deletions test/data/multimodule-root.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/example/root-project

go 1.21

require (
github.com/gorilla/mux v1.8.0
github.com/example/root-project/submodule v0.0.0
)

replace github.com/example/root-project/submodule => ./submodule
7 changes: 7 additions & 0 deletions test/data/multimodule-sub.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/example/root-project/submodule

go 1.21

require (
github.com/pkg/errors v0.9.1
)
Loading