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 @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `socket scan create --reach` keeps the private libraries and build modules of Socket facts files in the stored SBOM, with their dependencies under them.
- JVM dependencies whose jars cannot be fetched, or are gone from disk, no longer reach reachability analysis looking resolved without code.
- One unresolvable Maven dependency no longer strips the jar paths from every other dependency of its build, which made reachability analysis call their vulnerabilities unreachable. Jars that cannot be fetched are reported with their real cause, Maven 4 included.
- Maven dependencies with missing or invalid POMs are reported in every module that uses them, `system`-scope jars reach reachability analysis, and a failing dependency no longer fails scopes that `excludeConfigs` leaves out or the build when it lost a version conflict.

## [1.10.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.10.0) - 2026-10-22

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tech.coana.ext;

import org.apache.maven.eventspy.AbstractEventSpy;
import org.eclipse.aether.RepositoryEvent;
import tech.coana.socket.CollectionObserver;

import javax.inject.Named;
import javax.inject.Singleton;

/**
* Relays repository events to the facts engine while it collects. Every Maven chains its event
* spies into the session's repository listener, which an extension cannot replace: the session is
* read-only since Maven 3.1 and immutable in Maven 4.
*/
@Named("coana-facts-events")
@Singleton
public class CoanaFactsEventSpy extends AbstractEventSpy {

// Static: the container may hand the dispatcher and the participant different instances.
private static volatile CollectionObserver observer;

static void observe(CollectionObserver o) {
observer = o;
}

@Override
public void onEvent(Object event) {
CollectionObserver o = observer;
if (o != null && event instanceof RepositoryEvent) o.onEvent((RepositoryEvent) event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.eclipse.aether.RepositorySystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tech.coana.socket.CollectionObserver;
import tech.coana.socket.SocketFactsRecordsEngine;

import javax.inject.Inject;
Expand Down Expand Up @@ -59,11 +60,16 @@ public void afterSessionEnd(MavenSession session) throws MavenExecutionException
opts.excludeConfigs = opt(session, "socket.excludeConfigs");
opts.excludePaths = opt(session, "socket.excludePaths");
File rootDir = new File(session.getExecutionRootDirectory());
CollectionObserver observer = new CollectionObserver();
CoanaFactsEventSpy.observe(observer);
try {
new SocketFactsRecordsEngine(dependenciesResolver, repositorySystem, runtimeInformation.getMavenVersion(), LOG)
new SocketFactsRecordsEngine(
dependenciesResolver, repositorySystem, runtimeInformation.getMavenVersion(), observer, LOG)
.run(session, session.getProjects(), rootDir, opts);
} catch (IOException exception) {
throw new MavenExecutionException("Cannot write socket facts records", exception);
} finally {
CoanaFactsEventSpy.observe(null);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package tech.coana.socket;

import org.eclipse.aether.RepositoryEvent;
import org.eclipse.aether.RequestTrace;
import org.eclipse.aether.collection.CollectStepData;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyNode;

import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* What the repository events of dependency collection reveal beyond its result: the POMs Maven's
* default {@code ArtifactDescriptorPolicy} tolerated as missing or invalid (the node stays, its
* dependencies are lost, and no collection error is raised), build-wide, and the graph position of
* each collect step of the current module.
*/
public final class CollectionObserver {

// CollectStepData is maven-resolver 1.8.1+ (Maven 3.9+); older resolvers trace no collect step.
private static final boolean COLLECT_STEPS = classExists("org.eclipse.aether.collection.CollectStepData");

// The resolver caches a tolerated POM build-wide, so only the first module to reach one sees its
// event; that module's detail is kept.
private final Map<String, String> incompleteDescriptors = new LinkedHashMap<>();
private final Set<String> incompleteInModule = new HashSet<>();
private final Map<Object, DependencyNode> stepParents = new IdentityHashMap<>();

public synchronized void onEvent(RepositoryEvent event) {
if (event.getType() == RepositoryEvent.EventType.ARTIFACT_DESCRIPTOR_MISSING) {
incomplete(event, "is missing, no dependency information available");
} else if (event.getType() == RepositoryEvent.EventType.ARTIFACT_DESCRIPTOR_INVALID) {
incomplete(event, "is invalid, transitive dependencies (if any) will not be available");
}
// The depth-first collector hands each step its live node stack, so the parent is only right now.
if (COLLECT_STEPS) Steps.recordParent(event.getTrace(), stepParents);
}

private void incomplete(RepositoryEvent event, String problem) {
String gav = SocketFactsRecordsEngine.gav(event.getArtifact());
if (incompleteDescriptors.containsKey(gav) && !incompleteInModule.contains(gav)) return;
incompleteInModule.add(gav);
incompleteDescriptors.put(
gav,
"The POM for " + event.getArtifact() + " " + problem + ": "
+ SocketFactsRecordsEngine.rootMessage(event.getException()));
}

/** Starts the next module's collection. */
synchronized void reset() {
incompleteInModule.clear();
stepParents.clear();
}

/** GAV to failure detail of each POM the collection tolerated as missing or invalid. */
synchronized Map<String, String> incompleteDescriptors() {
return new LinkedHashMap<>(incompleteDescriptors);
}

/** The node whose children the collect step behind {@code trace} was adding, if it was observed. */
synchronized DependencyNode parentOf(RequestTrace trace) {
return COLLECT_STEPS ? Steps.parentOf(trace, stepParents) : null;
}

/** The dependency, before management, that the collect step behind {@code trace} was adding. */
synchronized Dependency dependencyOf(RequestTrace trace) {
return COLLECT_STEPS ? Steps.dependencyOf(trace) : null;
}

private static boolean classExists(String name) {
try {
Class.forName(name, false, RequestTrace.class.getClassLoader());
return true;
} catch (ClassNotFoundException | LinkageError e) {
return false;
}
}

// Only loaded when CollectStepData exists.
private static final class Steps {
static void recordParent(RequestTrace trace, Map<Object, DependencyNode> parents) {
RequestTrace at = stepTrace(trace);
if (at == null || parents.containsKey(at.getData())) return;
List<DependencyNode> path = ((CollectStepData) at.getData()).getPath();
if (!path.isEmpty()) parents.put(at.getData(), path.get(path.size() - 1));
}

static DependencyNode parentOf(RequestTrace trace, Map<Object, DependencyNode> parents) {
RequestTrace at = stepTrace(trace);
if (at == null) return null;
DependencyNode observed = parents.get(at.getData());
if (observed != null) return observed;
// The depth-first collector hands every step its one live node stack, so an unobserved step's path
// is only trusted when it is checkably its own: the breadth-first collector nests each step's
// trace in its parent's step, whose dependency is then the path's last node.
RequestTrace enclosing = stepTrace(at.getParent());
List<DependencyNode> path = ((CollectStepData) at.getData()).getPath();
if (enclosing == null || path.isEmpty()) return null;
DependencyNode last = path.get(path.size() - 1);
Dependency parent = ((CollectStepData) enclosing.getData()).getNode();
if (last.getDependency() == null
|| !last.getDependency().getArtifact().getGroupId().equals(parent.getArtifact().getGroupId())
|| !last.getDependency().getArtifact().getArtifactId().equals(parent.getArtifact().getArtifactId())) {
return null;
}
return last;
}

static Dependency dependencyOf(RequestTrace trace) {
RequestTrace at = stepTrace(trace);
return at == null ? null : ((CollectStepData) at.getData()).getNode();
}

private static RequestTrace stepTrace(RequestTrace trace) {
for (RequestTrace t = trace; t != null; t = t.getParent()) {
if (t.getData() instanceof CollectStepData) return t;
}
return null;
}
}
}
Loading
Loading