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
13 changes: 11 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getCustom } from "./tools.js";
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'
import { discoverMavenModules } from './providers/java_maven.js'
import { discoverGradleSubprojects } from './providers/java_gradle.js'
import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js'
import {
discoverWorkspaceCrates,
discoverWorkspacePackages,
Expand All @@ -27,6 +28,7 @@ export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnal
export {
discoverMavenModules,
discoverGradleSubprojects,
discoverGoWorkspaceModules,
discoverWorkspacePackages,
discoverWorkspaceCrates,
validatePackageJson,
Expand Down Expand Up @@ -323,7 +325,7 @@ async function generateOneSbom(manifestPath, workspaceOpts) {
*
* @param {string} root - Resolved workspace root
* @param {Options} opts
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'gradle' | 'unknown', manifestPaths: string[] }>}
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'gradle' | 'gomodules' | 'unknown', manifestPaths: string[] }>}
* @private
*/
async function detectWorkspaceManifests(root, opts) {
Expand Down Expand Up @@ -352,6 +354,13 @@ async function detectWorkspaceManifests(root, opts) {
}
}

if (fs.existsSync(path.join(root, 'go.work'))) {
const manifestPaths = await discoverGoWorkspaceModules(root, opts)
if (manifestPaths.length > 0) {
return { ecosystem: 'gomodules', manifestPaths }
}
}

const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
|| fs.existsSync(path.join(root, 'yarn.lock'))
|| fs.existsSync(path.join(root, 'package-lock.json'))
Expand Down Expand Up @@ -547,7 +556,7 @@ async function stackAnalysisBatch(workspaceRoot, html = false, opts = {}) {
}

if (manifestPaths.length === 0) {
throw new Error(`No workspace manifests found at ${root}. Ensure Cargo.toml+Cargo.lock or package.json+lock file exist.`)
throw new Error(`No workspace manifests found at ${root}. Ensure a supported workspace root exists (Cargo.toml+Cargo.lock, go.work, or package.json+lock file).`)
}

const workspaceOpts = { ...opts, TRUSTIFY_DA_WORKSPACE_DIR: root }
Expand Down
54 changes: 54 additions & 0 deletions src/providers/golang_gomodules.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PackageURL } from 'packageurl-js'
import { readLicenseFile } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import { getCustom, getCustomPath, invokeCommand } from "../tools.js";
import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js'

import { getParser, getRequireQuery } from './gomod_parser.js'

Expand Down Expand Up @@ -427,3 +428,56 @@ function getLineSeparatorGolang() {
let reg = /\n|\r\n/
return reg
}

/**
* Discover all go.mod manifest paths in a Go workspace.
* Uses `go work edit -json` to get workspace members.
*
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain go.work)
* @param {import('../index.js').Options} [opts={}]
* @returns {Promise<string[]>} Paths to go.mod files (absolute)
*/
export async function discoverGoWorkspaceModules(workspaceRoot, opts = {}) {
const root = path.resolve(workspaceRoot)
const goWork = path.join(root, 'go.work')

if (!fs.existsSync(goWork)) {
return []
}

const goBin = getCustomPath('go', opts)
let output
try {
output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root })
} catch {
return []
}

let workspace
try {
workspace = JSON.parse(output.toString().trim())
} catch {
return []
}

const useEntries = workspace.Use || []
if (useEntries.length === 0) {
return []
}

const manifestPaths = []
for (const entry of useEntries) {
const diskPath = entry.DiskPath
if (!diskPath) {
continue
}
const moduleDir = path.resolve(root, diskPath)
const goMod = path.join(moduleDir, 'go.mod')
if (fs.existsSync(goMod)) {
manifestPaths.push(goMod)
}
}

const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts)
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
}
6 changes: 6 additions & 0 deletions test/providers/tst_manifests/golang/go_workspace/go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
go 1.22

use (
./module-a
./module-b
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/module-a

go 1.22
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/module-b

go 1.22
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go 1.22
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/existing

go 1.22
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
go 1.22

use (
./existing
./nonexistent
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
go 1.22

use (
./libs/core
./libs/util
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/libs/core

go 1.22
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/libs/util

go 1.22
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
go 1.22

use ./mymod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/mymod

go 1.22
83 changes: 83 additions & 0 deletions test/providers/workspace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'node:path'
import { expect } from 'chai'
import esmock from 'esmock'

import { discoverGoWorkspaceModules } from '../../src/providers/golang_gomodules.js'
import { discoverGradleSubprojects } from '../../src/providers/java_gradle.js'
import { discoverMavenModules } from '../../src/providers/java_maven.js'
import {
Expand Down Expand Up @@ -267,6 +268,7 @@ suite('discoverMavenModules', () => {
expect(result).to.be.an('array')
expect(result).to.have.lengthOf(0)
})

test('returns root pom only when mvn reports no modules', async () => {
const root = path.resolve('test/providers/tst_manifests/maven/maven_no_modules')
const result = await discoverMavenModules(root)
Expand Down Expand Up @@ -320,3 +322,84 @@ suite('discoverMavenModules', () => {
expect(result.some(p => p.includes('module-b'))).to.be.false
}).timeout(40000)
})

suite('discoverGoWorkspaceModules', () => {
test('returns empty when no go.work at root', async () => {
const result = await discoverGoWorkspaceModules('test/providers/tst_manifests/npm')
expect(result).to.be.an('array')
expect(result).to.have.lengthOf(0)
})

test('discovers modules from go.work with two modules', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace')
const result = await discoverGoWorkspaceModules(root)
expect(result).to.be.an('array')
expect(result).to.have.lengthOf(2)
expect(result.every(p => p.endsWith('go.mod'))).to.be.true
expect(result.some(p => p.includes('module-a'))).to.be.true
expect(result.some(p => p.includes('module-b'))).to.be.true
})

test('discovers modules from nested directories', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_nested')
const result = await discoverGoWorkspaceModules(root)
expect(result).to.have.lengthOf(2)
expect(result.some(p => p.includes(path.join('libs', 'core', 'go.mod')))).to.be.true
expect(result.some(p => p.includes(path.join('libs', 'util', 'go.mod')))).to.be.true
})

test('discovers single module', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_single')
const result = await discoverGoWorkspaceModules(root)
expect(result).to.have.lengthOf(1)
expect(result[0]).to.include(path.join('mymod', 'go.mod'))
})

test('skips modules whose directory does not exist', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_missing_module')
const result = await discoverGoWorkspaceModules(root)
expect(result).to.have.lengthOf(1)
expect(result[0]).to.include(path.join('existing', 'go.mod'))
})

test('returns empty when go command fails', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace')
const { discoverGoWorkspaceModules: discoverMocked } = await esmock('../../src/providers/golang_gomodules.js', {
'../../src/tools.js': {
getCustom: () => null,
getCustomPath: () => 'go',
invokeCommand: () => { throw new Error('go not found') },
},
})
const result = await discoverMocked(root)
expect(result).to.have.lengthOf(0)
})

test('returns empty when go output is invalid JSON', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace')
const { discoverGoWorkspaceModules: discoverMocked } = await esmock('../../src/providers/golang_gomodules.js', {
'../../src/tools.js': {
getCustom: () => null,
getCustomPath: () => 'go',
invokeCommand: () => Buffer.from('not json'),
},
})
const result = await discoverMocked(root)
expect(result).to.have.lengthOf(0)
})

test('returns empty when Use is null (no use directives)', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_empty')
const result = await discoverGoWorkspaceModules(root)
expect(result).to.have.lengthOf(0)
})

test('applies ignore patterns to discovered modules', async () => {
const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_nested')
const result = await discoverGoWorkspaceModules(root, {
workspaceDiscoveryIgnore: ['**/util/**'],
})
expect(result.some(p => p.includes(path.join('libs', 'core', 'go.mod')))).to.be.true
expect(result.some(p => p.includes(path.join('libs', 'util', 'go.mod')))).to.be.false
})
})
Loading