Generation of .socket.facts.json and the wire contracts around it.
Generation of .socket.facts.json for JVM projects has crossed the same
ownership boundary twice: socket-cli delegated it to a reachability partner in
June 2026 and reversed the delegation 26 days later, because the partner
re-resolved the build later in the pipeline and diverged from the scan-time
build on dynamically-versioned projects. Both sides now carry emitters and
schemas that describe the same bytes, so this package holds one copy of each and
both consumers import it instead of the code moving back and forth again.
This package never decides whether a build-tool invocation is trustworthy.
socket.json lets a repository supply the build binary and its options, which
makes that an attacker-controlled input and the consumer's call. So
runFactsGeneration takes an invocation that is already resolved and already
vetted — an absolute bin, an explicit opts array, an explicit env, an
explicit cwd — and throws when any of them is missing rather than falling back
to a PATH lookup, a wrapper probe, or process.env. It runs the invocation; it
does not assemble one.
The property everything here protects is that resolution happens once, at scan time, against the developer's real build. A resolver that reads a cache or parses a manifest statically passes a literal-version fixture and diverges on a real project, so the conformance suite is built out of selectors — a wildcard, a bounded range, a SNAPSHOT, and a project version generated per run — whose answers appear in no committed file, and it diffs what this package emitted against what the build tool itself reports resolving. There is no static fallback: a build tool that cannot run is a loud failure, never a degraded success.
sdxgen is the other intended consumer, and the dependency runs sdxgen → facts: its JVM lane converts these facts to CycloneDX, which lets it drop its own duplicated Gradle init script and replace best-effort sbt regex parsing with real plugin-driven resolution. Component identity here is purl-shaped and the edge list is addressable so that conversion is comfortable, but the converter belongs on sdxgen's side, not this one.
npm install @socketsecurity/factsRun a build tool's emitter against an invocation you have already resolved:
import { runFactsGeneration } from '@socketsecurity/facts'
const { artifactPaths, code, facts, report } = await runFactsGeneration({
tool: 'gradle',
// Absolute, and chosen by your trust policy — not looked up here.
bin: '/repo/gradlew',
// Already filtered by your trust policy.
opts: [],
// Explicit. GRADLE_OPTS, JAVA_TOOL_OPTIONS, MAVEN_OPTS, SBT_OPTS, and
// _JAVA_OPTIONS are stripped by default; pass envPolicy: 'as-given' to keep them.
env: { JAVA_HOME: process.env.JAVA_HOME, PATH: process.env.PATH },
cwd: '/repo',
withFiles: true,
})Share the wire contracts instead of re-declaring them:
import {
assertResolvedPathsSidecar,
validateSocketFactsSbom,
} from '@socketsecurity/facts/contract'
// Throws with what / where / saw-vs-wanted / fix, including on an additive
// field, which a strict consumer rejects wholesale.
const sidecar = assertResolvedPathsSidecar(payload, 'reachability sidecar read')Verify emitted facts against the build's own answer:
import {
compareFactsToGroundTruth,
conformanceViolations,
parseGradleDependencyTree,
} from '@socketsecurity/facts/conformance'
// `gradle dependencies --configuration runtimeClasspath` output.
const truth = parseGradleDependencyTree(report)
const divergence = conformanceViolations(
compareFactsToGroundTruth(facts, truth),
)Resolve the emitter assets rather than guessing where they live:
import {
assertMavenExtensionBuilt,
gradleInitScriptPath,
sbtPluginSourcePath,
} from '@socketsecurity/facts/assets'Contributor commands
pnpm install
pnpm run check
pnpm testBuild the Maven core extension, which needs a JDK and is not part of
pnpm run build:
pnpm run build:maven-extensionThe dynamic-version conformance suite needs Gradle, Maven, and a JDK. It skips
loudly when they are absent; set SOCKET_FACTS_REQUIRE_COMPAT=1 to turn that
skip into a failure on a machine that should have them:
SOCKET_FACTS_REQUIRE_COMPAT=1 pnpm test test/repo/compatMIT