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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed
- Maven Socket facts honor artifact types that build extensions define, so they no longer list dependencies Maven doesn't resolve, or fetch files under the wrong extension.
- Maven Socket facts no longer drop dependencies whose scope Maven 3 doesn't define, such as Maven 4's `compile-only` or `none`.

## [1.15.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.15.0) - 2026-11-12

Expand Down
17 changes: 13 additions & 4 deletions src/commands/manifest/scripts/assemble.mts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ type MergedNode = {

type SubprojectGraph = {
children: Map<string, Set<string>>
// The nodes of the roots on a classpath: reachability analyses only these.
classpath: Set<string>
direct: Set<string>
}

type PerRoot = {
projectKey: string
prod: boolean
onClasspath: boolean
target?: RawTarget | undefined
nodes: Map<string, RootNode>
}
Expand Down Expand Up @@ -177,6 +180,7 @@ function buildPerRoot(parsed: ParsedRecords): Map<string, PerRoot> {
out.set(rootId, {
projectKey: r.projectKey,
prod: r.prod,
onClasspath: r.onClasspath,
target: r.target,
nodes,
})
Expand Down Expand Up @@ -259,13 +263,16 @@ function buildSubprojectGraphs(
perRoot: Map<string, PerRoot>,
): Map<string, SubprojectGraph> {
const graphs = new Map<string, SubprojectGraph>()
for (const { nodes, projectKey } of perRoot.values()) {
for (const { nodes, onClasspath, projectKey } of perRoot.values()) {
let g = graphs.get(projectKey)
if (!g) {
g = { children: new Map(), direct: new Set() }
g = { children: new Map(), classpath: new Set(), direct: new Set() }
graphs.set(projectKey, g)
}
for (const [coordId, node] of nodes) {
if (onClasspath) {
g.classpath.add(coordId)
}
let set = g.children.get(coordId)
if (!set) {
set = new Set()
Expand Down Expand Up @@ -480,8 +487,10 @@ function buildArtifactPaths(
}
return {
pathsById,
classpathByProject: byProject(projects, graphs, g => g.children.keys()),
directDependenciesByProject: byProject(projects, graphs, g => g.direct),
classpathByProject: byProject(projects, graphs, g => g.classpath),
directDependenciesByProject: byProject(projects, graphs, g =>
[...g.direct].filter(id => g.classpath.has(id)),
),
}
}

Expand Down
41 changes: 41 additions & 0 deletions src/commands/manifest/scripts/assemble.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,47 @@ describe('records → assemble → sidecar', () => {
expect(assemble).not.toThrow()
}
})
it('keeps the nodes of a root on no classpath in the SBOM but off every classpath', () => {
// Maven 4: `none` is declared, and resolved with its subtree, but on no
// path. Its child x is also on the classpath through lib.
const records = [
'meta\tmaven\t4.0.0\t21',
'project\ta\tg\ta\t1\ta',
'root\tr1\ta\tcompile\t1',
'node\tr1\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1',
'node\tr1\tg:x:jar:1\tg\tx\t1\tjar\t\t0',
'edge\tr1\tg:lib:jar:1\tg:x:jar:1',
'root\tr2\ta\tnone\t0',
'noClasspath\tr2',
'node\tr2\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1',
'node\tr2\tg:x:jar:1\tg\tx\t1\tjar\t\t0',
'node\tr2\tg:none:jar:1\tg\tnone\t1\tjar\t\t1',
'node\tr2\tg:none-dep:jar:1\tg\tnone-dep\t1\tjar\t\t0',
'edge\tr2\tg:lib:jar:1\tg:x:jar:1',
'edge\tr2\tg:none:jar:1\tg:x:jar:1',
'edge\tr2\tg:none:jar:1\tg:none-dep:jar:1',
].join('\n')

const { artifactPaths, facts } = assembleFacts(parseRecords(records))

expect(trees(facts)).toEqual({
a: [
'g:lib:jar:1(g:x:jar:1)',
'g:none:jar:1(g:x:jar:1, g:none-dep:jar:1)',
],
})
expect(facts.components.filter(c => c.dev).map(c => c.id)).toEqual([
'g:none-dep:jar:1',
'g:none:jar:1',
])
expect(artifactPaths.classpathByProject.get('a')).toEqual([
'g:lib:jar:1',
'g:x:jar:1',
])
expect(artifactPaths.directDependenciesByProject.get('a')).toEqual([
'g:lib:jar:1',
])
})
it("gives a dependency on a build project that project's id, and marks only it firstParty", () => {
const records = [
'meta\tgradle\t8.0\t17',
Expand Down
6 changes: 4 additions & 2 deletions src/commands/manifest/scripts/facts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@ export type ComponentFramework = {
export type ResolvedArtifactPaths = {
pathsById: Map<string, { sources: string[]; targets?: string[] | undefined }>
// Component ids on each project's resolved classpath (union over its
// configurations), keyed by project id.
// configurations), keyed by project id. A component on no classpath, such as
// a Maven 4 `none` dependency, is in the SBOM but not here.
classpathByProject: Map<string, string[]>
// Component ids of each project's direct dependencies, keyed by project id.
// Component ids of each project's direct dependencies on its classpath, keyed
// by project id.
directDependenciesByProject: Map<string, string[]>
// dotnet only: the same per target framework.
frameworksByProject?: Map<string, ProjectFramework[]> | undefined
Expand Down
Loading
Loading