diff --git a/src/commands/manifest/scripts/assemble.mts b/src/commands/manifest/scripts/assemble.mts index f4501a310..5634146c1 100644 --- a/src/commands/manifest/scripts/assemble.mts +++ b/src/commands/manifest/scripts/assemble.mts @@ -54,7 +54,11 @@ export function assembleFacts( const { directByRoot, finalNodes } = mergeByCoordinate(perRoot) const tool = (parsed.tool || 'gradle') as SocketFactsSbomMetadata['tool'] - const components = buildComponents(finalNodes) + const projectsByGav = new Map() + for (const p of parsed.projects.values()) { + projectsByGav.set(gav(p.group, p.name, p.version), p) + } + const components = buildComponents(finalNodes, projectsByGav) const projects = opts.emitProjects === false ? [] @@ -77,6 +81,7 @@ export function assembleFacts( artifactPaths: buildArtifactPaths( finalNodes, [...parsed.projects.values()], + projectsByGav, perRoot, fileExists, ), @@ -173,6 +178,7 @@ function mergeByCoordinate(perRoot: Map): { function buildComponents( finalNodes: Map, + projectsByGav: Map, ): SocketFactsSbomComponent[] { return [...finalNodes.keys()].sort().map(id => { const fn = finalNodes.get(id)! @@ -200,6 +206,9 @@ function buildComponents( if (!fn.prod) { comp.dev = true } + if (projectsByGav.has(gav(c.group, c.name, c.version ?? ''))) { + comp.firstParty = true + } if (fn.children.size) { comp.dependencies = [...fn.children].sort() } @@ -303,19 +312,10 @@ function buildClasspathByProject( function buildArtifactPaths( finalNodes: Map, projects: RawProject[], + projectsByGav: Map, perRoot: Map, fileExists: (path: string) => boolean, ): ResolvedArtifactPaths { - const projectsByGav = new Map< - string, - { sources: string[]; targets: string[] } - >() - for (const p of projects) { - projectsByGav.set(gav(p.group, p.name, p.version), { - sources: p.sources, - targets: p.targets, - }) - } const targetsByCoord = new Map() const targetsByGav = new Map() const sourcesByCoord = new Map() diff --git a/src/commands/manifest/scripts/assemble.test.mts b/src/commands/manifest/scripts/assemble.test.mts index af78da6d9..52a01a073 100644 --- a/src/commands/manifest/scripts/assemble.test.mts +++ b/src/commands/manifest/scripts/assemble.test.mts @@ -116,4 +116,37 @@ describe('records → assemble → sidecar', () => { 'g:lib:jar:1', ]) }) + it('marks only components with the exact coordinate of a build module as firstParty', () => { + const records = [ + 'meta\tmaven\t3.9.6\t17', + 'project\t:a\tg\ta\t1.0-SNAPSHOT\ta', + 'project\t:b\tg\tb\t1.0-SNAPSHOT\tb', + 'root\tr1\t:a\truntimeClasspath\t1', + 'node\tr1\tg:ext:jar:2\tg\text\t2\tjar\t\t1', + 'root\tr2\t:b\truntimeClasspath\t1', + 'node\tr2\tg:a:jar:1.0-SNAPSHOT\tg\ta\t1.0-SNAPSHOT\tjar\t\t1', + 'node\tr2\tg:ext:jar:2\tg\text\t2\tjar\t\t0', + 'edge\tr2\tg:a:jar:1.0-SNAPSHOT\tg:ext:jar:2', + 'node\tr2\tg:b:jar:0.9\tg\tb\t0.9\tjar\t\t1', + ].join('\n') + const { artifactPaths, facts } = assembleFacts(parseRecords(records)) + + expect(facts.components.map(c => [c.id, c.firstParty ?? 'absent'])).toEqual( + [ + ['g:a:jar:1.0-SNAPSHOT', true], + ['g:b:jar:0.9', 'absent'], + ['g:ext:jar:2', 'absent'], + ], + ) + + const acc: SidecarAccumulator = new Map() + accumulateSidecar(acc, facts, artifactPaths, '/abs/.socket.facts.json') + const bucket = serializeSidecar(acc)['/abs/.socket.facts.json']! + expect( + bucket.components.find(c => c.id === 'g:a:jar:1.0-SNAPSHOT')?.firstParty, + ).toBe(true) + for (const project of bucket.projects) { + expect(project).not.toHaveProperty('firstParty') + } + }) }) diff --git a/src/commands/manifest/scripts/facts.mts b/src/commands/manifest/scripts/facts.mts index 1ff25965f..aab42c0d2 100644 --- a/src/commands/manifest/scripts/facts.mts +++ b/src/commands/manifest/scripts/facts.mts @@ -27,6 +27,8 @@ export type SocketFactsSbomComponent = AnyPURL & { id: string direct?: boolean | undefined dev?: boolean | undefined + // A module of the scanned build itself (same GAV as a projects[] entry). + firstParty?: true | undefined dependencies?: string[] | undefined }