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/).

### Added
- `socket fix --allow-overrides` fixes a vulnerability that a parent package's version range blocks by writing an override or resolution that forces the fixed version under that parent, in npm, pnpm, Yarn Berry and Rush projects.
- Socket facts for multi-module Maven, Gradle and sbt builds now attribute each direct dependency to the subproject build files that pull it in, so the dashboard shows which module brought it in.

### Changed
- Updated the Coana CLI to v `15.12.1`.
Expand Down
43 changes: 42 additions & 1 deletion src/commands/manifest/scripts/assemble.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { existsSync } from 'node:fs'

import {
type ResolvedArtifactPaths,
type SocketFactsManifestReference,
type SocketFactsSbom,
type SocketFactsSbomComponent,
type SocketFactsSbomMetadata,
type SocketFactsSbomProject,
mavenCoordinateKey,
projectClasspathKey,
} from './facts.mts'
import constants from '../../../constants.mts'

import type { ParsedRecords, RawCoord, RawProject } from './records.mts'
import type { ResolutionReport } from './resolution-report.mts'
Expand Down Expand Up @@ -58,7 +60,11 @@ export function assembleFacts(
for (const p of parsed.projects.values()) {
projectsByGav.set(gav(p.group, p.name, p.version), p)
}
const components = buildComponents(finalNodes, projectsByGav)
const components = buildComponents(
finalNodes,
projectsByGav,
buildManifestFilesByCoord(parsed, directByRoot, perRoot),
)
const projects =
opts.emitProjects === false
? []
Expand Down Expand Up @@ -176,9 +182,40 @@ function mergeByCoordinate(perRoot: Map<string, PerRoot>): {
return { finalNodes, directByRoot }
}

function buildManifestFilesByCoord(
parsed: ParsedRecords,
directByRoot: Map<string, Set<string>>,
perRoot: Map<string, PerRoot>,
): Map<string, SocketFactsManifestReference[]> {
const buildFilesByCoord = new Map<string, Set<string>>()
for (const [rootId, ids] of directByRoot) {
const projectKey = perRoot.get(rootId)?.projectKey ?? ''
const buildFiles = parsed.projects.get(projectKey)?.buildFiles ?? []
for (const id of ids) {
let set = buildFilesByCoord.get(id)
if (!set) {
set = new Set()
buildFilesByCoord.set(id, set)
}
for (const f of buildFiles) {
set.add(f)
}
}
}
return new Map(
[...buildFilesByCoord].map(({ 0: id, 1: buildFiles }) => [
id,
[constants.DOT_SOCKET_DOT_FACTS_JSON, ...[...buildFiles].sort()].map(
file => ({ file }),
),
]),
)
}

function buildComponents(
finalNodes: Map<string, MergedNode>,
projectsByGav: Map<string, RawProject>,
manifestFilesByCoord: Map<string, SocketFactsManifestReference[]>,
): SocketFactsSbomComponent[] {
return [...finalNodes.keys()].sort().map(id => {
const fn = finalNodes.get(id)!
Expand Down Expand Up @@ -212,6 +249,10 @@ function buildComponents(
if (fn.children.size) {
comp.dependencies = [...fn.children].sort()
}
const manifestFiles = manifestFilesByCoord.get(id)
if (manifestFiles) {
comp.manifestFiles = manifestFiles
}
return comp
})
}
Expand Down
37 changes: 37 additions & 0 deletions src/commands/manifest/scripts/assemble.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,41 @@ describe('records → assemble → sidecar', () => {
expect(project).not.toHaveProperty('firstParty')
}
})
it('marks direct dependencies with the facts file and the build files of the subprojects they are direct in', () => {
const records = [
'meta\tmaven\t3.9.6\t17',
'project\ta\tg\ta\t1\ta',
'projectBuild\ta\ta/pom.xml',
'project\tb\tg\tb\t1\tb',
'projectBuild\tb\tb/pom.xml',
// No build file of its own, e.g. configured from the root build.
'project\tc\tg\tc\t1\tc',
'root\tr1\ta\truntimeClasspath\t1',
'node\tr1\tg:ext:jar:2\tg\text\t2\tjar\t\t1',
'node\tr1\tg:dep:jar:3\tg\tdep\t3\tjar\t\t0',
'edge\tr1\tg:ext:jar:2\tg:dep:jar:3',
'root\tr2\tb\ttestRuntimeClasspath\t0',
'node\tr2\tg:a:jar:1\tg\ta\t1\tjar\t\t1',
'node\tr2\tg:ext:jar:2\tg\text\t2\tjar\t\t1',
'edge\tr2\tg:a:jar:1\tg:ext:jar:2',
'root\tr3\tc\truntimeClasspath\t1',
'node\tr3\tg:solo:jar:1\tg\tsolo\t1\tjar\t\t1',
].join('\n')
const { facts } = assembleFacts(parseRecords(records))

expect(
Object.fromEntries(
facts.components.map(c => [c.id, c.manifestFiles ?? 'absent']),
),
).toEqual({
'g:a:jar:1': [{ file: '.socket.facts.json' }, { file: 'b/pom.xml' }],
'g:dep:jar:3': 'absent',
'g:ext:jar:2': [
{ file: '.socket.facts.json' },
{ file: 'a/pom.xml' },
{ file: 'b/pom.xml' },
],
'g:solo:jar:1': [{ file: '.socket.facts.json' }],
})
})
})
8 changes: 8 additions & 0 deletions src/commands/manifest/scripts/facts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export type SocketFactsSbomComponent = AnyPURL & {
// A module of the scanned build itself (same GAV as a projects[] entry).
firstParty?: true | undefined
dependencies?: string[] | undefined
// Direct dependencies only: the facts file plus the build files of the subprojects
// pulling it in directly, which need not declare it (e.g. a parent POM does).
manifestFiles?: SocketFactsManifestReference[] | undefined
}

// Relative to the facts file's directory.
export type SocketFactsManifestReference = {
file: string
}

export type SocketFactsSbomProject = AnyPURL & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public void run(MavenSession session, List<MavenProject> reactor, File rootDir,
String ws = SocketSupport.workspace(rootDir.toPath(), module.getBasedir().toPath());
if (SocketSupport.isExcludedPath(ws, excludes)) continue;
rec(lines, "project", ws, module.getGroupId(), module.getArtifactId(), module.getVersion(), ws);
File pom = module.getFile();
if (pom != null && pom.isFile()) rec(lines, "projectBuild", ws, SocketSupport.relativePath(rootDir.toPath(), pom.toPath()));
if (opts.withFiles) {
for (String s : collectSources(module)) rec(lines, "projectSrc", ws, s);
for (String t : collectTargets(module)) rec(lines, "projectTgt", ws, t);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public static String workspace(Path rootDir, Path projectDir) {
return rootDir.equals(projectDir) ? "." : rootDir.relativize(projectDir).toString();
}

public static String relativePath(Path rootDir, Path file) {
return rootDir.relativize(file).toString().replace(File.separatorChar, '/');
}

/**
* Full Maven coordinate {@code groupId:artifactId:type:classifier:version} with empty segments
* dropped — the per-root node key the assembler uses. {@code type} is the Maven packaging (the
Expand Down
9 changes: 9 additions & 0 deletions src/commands/manifest/scripts/records.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
// project projectKey group name version dir
// projectSrc projectKey path (--with-files only)
// projectTgt projectKey path (--with-files only)
// projectBuild projectKey path (build-root-relative)
// root rootId projectKey config prod(0|1)
// node rootId coordId group name version ext classifier direct(0|1)
// edge rootId parentCoordId childCoordId
Expand Down Expand Up @@ -58,6 +59,8 @@ export type RawProject = {
dir: string
sources: string[]
targets: string[]
// The project's own build files, build-root-relative.
buildFiles: string[]
}

export type ParsedRecords = {
Expand Down Expand Up @@ -131,6 +134,7 @@ export function parseRecords(text: string): ParsedRecords {
dir: '',
sources: [],
targets: [],
buildFiles: [],
}
result.projects.set(key, p)
}
Expand Down Expand Up @@ -166,6 +170,11 @@ export function parseRecords(text: string): ParsedRecords {
project(f[1] ?? '').targets.push(f[2])
}
break
case 'projectBuild':
if (f[2]) {
project(f[1] ?? '').buildFiles.push(f[2])
}
break
case 'root': {
const r = root(f[1] ?? '')
r.projectKey = f[2] ?? ''
Expand Down
9 changes: 9 additions & 0 deletions src/commands/manifest/scripts/socket-facts.init.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ gradle.projectsEvaluated { g ->
}
}
} catch (Exception ignore) {}
// `buildFile` is the configured script even when absent on disk (a project configured from the
// root or a convention plugin); only a script that exists is the project's own build file.
def buildFiles = []
try {
def bf = p.buildFile
if (bf != null && bf.isFile()) buildFiles << rel(bf)
} catch (Exception ignore) {}
g.socketFactsState.projectsInfo.add([
path : p.path,
group : (p.group ?: '').toString(),
Expand All @@ -122,6 +129,7 @@ gradle.projectsEvaluated { g ->
dir : rel(p.projectDir),
sources: (sources as List).sort(),
targets: (targets as List).sort(),
buildFiles: buildFiles,
])
}
}
Expand Down Expand Up @@ -491,6 +499,7 @@ rootProject { rp ->
synchronized (state.projectsInfo) { projectsInfo = new ArrayList(state.projectsInfo) }
projectsInfo.each { pi ->
rec(['project', pi.path, pi.group ?: '', pi.name, pi.version ?: '', pi.dir])
pi.buildFiles.each { f -> rec(['projectBuild', pi.path, f]) }
if (withFilesProjects) {
pi.sources.each { s -> rec(['projectSrc', pi.path, s]) }
pi.targets.each { t -> rec(['projectTgt', pi.path, t]) }
Expand Down
17 changes: 17 additions & 0 deletions src/commands/manifest/scripts/socket-facts.plugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ object SocketFactsPlugin extends AutoPlugin {
val moduleDirs: Map[String, (Seq[String], Seq[String])] =
if (withFiles) buildModuleDirs(allRefs, extracted) else Map.empty

// Where sbt recorded the project's own settings as defined (the root build.sbt for a subproject
// defined there); positions outside the build are sbt defaults, plugins or our injected base.
val buildFilesByRef: Map[ProjectRef, Seq[String]] =
extracted.structure.settings
.flatMap { setting =>
(setting.key.scope.project, setting.pos) match {
case (Select(ref: ProjectRef), pos: FilePosition) =>
val f = new File(pos.path)
if (f.isAbsolute && f.isFile && f.getCanonicalFile.toPath.startsWith(rootCanonPath)) Some(ref -> relOf(f))
else None
case _ => None
}
}
.groupBy(_._1)
.map { case (ref, pairs) => ref -> pairs.map(_._2).distinct.sorted }

val sb = new StringBuilder
def rec(fields: String*): Unit = {
sb.append(fields.map(esc).mkString("\t")); sb.append('\n')
Expand All @@ -90,6 +106,7 @@ object SocketFactsPlugin extends AutoPlugin {
val mid = rootIdOf(extracted, ref)
val ver = if (mid.revision == null) "" else mid.revision
rec("project", ref.project, mid.organization, mid.name, ver, relOf(extracted.get(baseDirectory.in(ref))))
buildFilesByRef.getOrElse(ref, Nil).foreach(f => rec("projectBuild", ref.project, f))
if (withFiles) {
moduleDirs.get(mid.organization + ":" + mid.name + ":" + ver).foreach {
case (sources, targets) =>
Expand Down
Loading