@socketsecurity/cli-with-sentry 1.1.131 → 1.1.132
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/cli.js +1227 -144
- package/dist/cli.js.map +1 -1
- package/dist/constants.js +4 -4
- package/dist/constants.js.map +1 -1
- package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
- package/dist/manifest-scripts/socket-facts.init.gradle +479 -0
- package/dist/manifest-scripts/socket-facts.plugin.scala +444 -0
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/manifest/cmd-manifest-gradle.d.mts.map +1 -1
- package/dist/types/commands/manifest/cmd-manifest-kotlin.d.mts.map +1 -1
- package/dist/types/commands/manifest/cmd-manifest-maven.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +5 -6
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-maven-to-facts.d.mts +5 -6
- package/dist/types/commands/manifest/convert-maven-to-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts +7 -8
- package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/generate_auto_manifest.d.mts +8 -1
- package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
- package/dist/types/commands/manifest/run-manifest-facts.d.mts +20 -0
- package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/assemble.d.mts +16 -0
- package/dist/types/commands/manifest/scripts/assemble.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/build-tool.d.mts +3 -0
- package/dist/types/commands/manifest/scripts/build-tool.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/facts.d.mts +46 -0
- package/dist/types/commands/manifest/scripts/facts.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/records.d.mts +64 -0
- package/dist/types/commands/manifest/scripts/records.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts +10 -0
- package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/resolution-report-ivy.d.mts +7 -0
- package/dist/types/commands/manifest/scripts/resolution-report-ivy.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/resolution-report-maven.d.mts +8 -0
- package/dist/types/commands/manifest/scripts/resolution-report-maven.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts +41 -0
- package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/resolution-report.d.mts +12 -0
- package/dist/types/commands/manifest/scripts/resolution-report.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/run.d.mts +28 -0
- package/dist/types/commands/manifest/scripts/run.d.mts.map +1 -0
- package/dist/types/commands/manifest/scripts/sidecar.d.mts +27 -0
- package/dist/types/commands/manifest/scripts/sidecar.d.mts.map +1 -0
- package/dist/types/commands/scan/cmd-scan-create.d.mts.map +1 -1
- package/dist/types/commands/scan/handle-create-new-scan.d.mts.map +1 -1
- package/dist/types/commands/scan/perform-reachability-analysis.d.mts +4 -2
- package/dist/types/commands/scan/perform-reachability-analysis.d.mts.map +1 -1
- package/dist/utils.js +1 -78
- package/dist/utils.js.map +1 -1
- package/package.json +3 -2
- package/dist/types/commands/manifest/coana-manifest-facts.d.mts +0 -27
- package/dist/types/commands/manifest/coana-manifest-facts.d.mts.map +0 -1
- package/dist/types/utils/auto-manifest-config.d.mts +0 -55
- package/dist/types/utils/auto-manifest-config.d.mts.map +0 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// Invoke via:
|
|
2
|
+
// ./gradlew --init-script socket-facts.init.gradle socketFacts
|
|
3
|
+
|
|
4
|
+
import java.util.Collections
|
|
5
|
+
|
|
6
|
+
// Emits a flat line-protocol RECORDS file (NOT the final .socket.facts.json, and NOT to stdout). The
|
|
7
|
+
// TS assembler (utils/src/manifest-scripts/assemble.ts) reads the records and owns all SBOM
|
|
8
|
+
// construction — graph merge, content-addressed ids, contentHash. This script only RESOLVES and emits
|
|
9
|
+
// raw facts. See records.ts for the record grammar.
|
|
10
|
+
|
|
11
|
+
// `Project.findProperty` only exists since Gradle 2.13; fall back to hasProperty/property for older Gradle.
|
|
12
|
+
gradle.ext.socketProp = { proj, name -> proj.hasProperty(name) ? proj.property(name) : null }
|
|
13
|
+
|
|
14
|
+
// Synchronized collections so --parallel-enabled builds don't race; lives on gradle.ext so every
|
|
15
|
+
// collector and the root aggregator share one instance.
|
|
16
|
+
gradle.ext.socketFactsState = [
|
|
17
|
+
nodes : Collections.synchronizedMap([:]),
|
|
18
|
+
directIds : Collections.synchronizedSet([] as Set),
|
|
19
|
+
// `detail` is the build tool's failure message verbatim; Coana classifies the failure KIND from it.
|
|
20
|
+
failures : Collections.synchronizedList([]),
|
|
21
|
+
scannedConfigs : Collections.synchronizedSet([] as Set),
|
|
22
|
+
projectKeys : Collections.synchronizedSet([] as Set),
|
|
23
|
+
// Only populated with `-Psocket.withFiles=true`: reading `artifact.file` downloads the artifact.
|
|
24
|
+
paths : Collections.synchronizedMap([:]),
|
|
25
|
+
perSub : Collections.synchronizedMap([:]),
|
|
26
|
+
projectsInfo : Collections.synchronizedList([]),
|
|
27
|
+
// "group:name" -> the module's real artifact extension (e.g. jar), so an intra-project dep that
|
|
28
|
+
// resolves without selecting a published artifact still gets its true coordinate, not ext-less.
|
|
29
|
+
projectArtifactExt : Collections.synchronizedMap([:]),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
// Capture every project's (group:name) before collectors run so they can filter intra-project
|
|
33
|
+
// deps without an ordering dependency on other subprojects.
|
|
34
|
+
gradle.projectsEvaluated { g ->
|
|
35
|
+
def rootPath = g.rootProject.projectDir.toPath()
|
|
36
|
+
// '/'-normalized so values are stable across OSes and portable to another machine.
|
|
37
|
+
def rel = { java.io.File f ->
|
|
38
|
+
def r = rootPath.relativize(f.toPath()).toString().replace(File.separator, '/')
|
|
39
|
+
r.isEmpty() ? '.' : r
|
|
40
|
+
}
|
|
41
|
+
g.rootProject.allprojects.each { p ->
|
|
42
|
+
g.socketFactsState.projectKeys.add("${p.group ?: ''}:${p.name}")
|
|
43
|
+
// The module's real artifact extension (jar for java; '' for a project that builds no archive,
|
|
44
|
+
// e.g. a pure aggregator). Used to stamp intra-project deps that resolve without an artifact.
|
|
45
|
+
def artExt = ''
|
|
46
|
+
try {
|
|
47
|
+
def jt = p.tasks.findByName('jar')
|
|
48
|
+
if (jt != null) {
|
|
49
|
+
artExt = 'jar'
|
|
50
|
+
try { def e = jt.archiveExtension.getOrNull(); if (e) artExt = e } catch (Exception ig1) {
|
|
51
|
+
try { def e = jt.extension; if (e) artExt = e } catch (Exception ig2) {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (Exception ignore) {}
|
|
55
|
+
g.socketFactsState.projectArtifactExt["${p.group ?: ''}:${p.name}".toString()] = artExt
|
|
56
|
+
// Guarded: projects without the java `sourceSets` convention (pure aggregators, AGP-only
|
|
57
|
+
// Android modules) contribute empty lists rather than failing.
|
|
58
|
+
// `classesDirs` (Gradle 4+) falls back to the legacy single `classesDir` on older Gradle.
|
|
59
|
+
def sources = [] as Set
|
|
60
|
+
def targets = [] as Set
|
|
61
|
+
try {
|
|
62
|
+
if (p.hasProperty('sourceSets')) {
|
|
63
|
+
['main', 'test'].each { sn ->
|
|
64
|
+
def ss = p.sourceSets.findByName(sn)
|
|
65
|
+
if (ss != null) {
|
|
66
|
+
ss.allSource.srcDirs.each { d -> sources << d.absolutePath }
|
|
67
|
+
try {
|
|
68
|
+
ss.output.classesDirs.each { d -> targets << d.absolutePath }
|
|
69
|
+
} catch (Exception e1) {
|
|
70
|
+
try { targets << ss.output.classesDir.absolutePath } catch (Exception ignore) {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (Exception ignore) {}
|
|
76
|
+
g.socketFactsState.projectsInfo.add([
|
|
77
|
+
path : p.path,
|
|
78
|
+
group : (p.group ?: '').toString(),
|
|
79
|
+
name : p.name,
|
|
80
|
+
version: (p.version ?: '').toString(),
|
|
81
|
+
dir : rel(p.projectDir),
|
|
82
|
+
sources: (sources as List).sort(),
|
|
83
|
+
targets: (targets as List).sort(),
|
|
84
|
+
])
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
allprojects { project ->
|
|
89
|
+
def collectTask = project.tasks.create('socketFactsCollect') {
|
|
90
|
+
description = "Resolves ${project.path}'s configurations into the build-wide Socket facts accumulator"
|
|
91
|
+
// Dependency resolution depends on state Gradle's up-to-date tracking can't represent reliably.
|
|
92
|
+
outputs.upToDateWhen { false }
|
|
93
|
+
|
|
94
|
+
doLast {
|
|
95
|
+
def state = gradle.socketFactsState
|
|
96
|
+
// Per-RESOLUTION-ROOT accumulators, LOCAL to this collector: one (subproject, configuration)
|
|
97
|
+
// classpath resolved into its own coordinate-keyed tree. Reassigned per config below (the
|
|
98
|
+
// visit/upsertNode closures capture them); `failures`/`scannedConfigs` stay shared.
|
|
99
|
+
def nodes = [:]
|
|
100
|
+
def directIds = [] as Set
|
|
101
|
+
def failures = state.failures
|
|
102
|
+
def scannedConfigs = state.scannedConfigs
|
|
103
|
+
def projectKeys = state.projectKeys
|
|
104
|
+
def projectArtifactExt = state.projectArtifactExt
|
|
105
|
+
def paths = [:]
|
|
106
|
+
def withFiles = gradle.socketProp.call(project, 'socket.withFiles')?.toString()?.toLowerCase() == 'true'
|
|
107
|
+
// Optional `-Psocket.populateFilesFor=<file>` (newline-delimited GAVs): scope `--with-files`
|
|
108
|
+
// materialization to those GAVs. This run walks EVERY resolvable config (not told the SBOM's
|
|
109
|
+
// include/exclude), so without a scope it would force-download the build's entire dep universe.
|
|
110
|
+
// Matched at GAV level to tolerate untrustworthy-ext SBOMs (lockfile / version catalog hardcode
|
|
111
|
+
// ext=jar). Absent/empty/missing file -> materialize all. Parsed once under the state monitor.
|
|
112
|
+
def populateFilesFor = gradle.socketProp.call(project, 'socket.populateFilesFor')?.toString()
|
|
113
|
+
def populateGavs = null
|
|
114
|
+
if (withFiles && populateFilesFor != null && !populateFilesFor.trim().isEmpty()) {
|
|
115
|
+
synchronized (state) {
|
|
116
|
+
if (!state.containsKey('populateGavs')) {
|
|
117
|
+
def f = new File(populateFilesFor.trim())
|
|
118
|
+
if (f.exists()) {
|
|
119
|
+
def set = [] as Set
|
|
120
|
+
f.eachLine { line -> def t = line.trim(); if (!t.isEmpty()) { set << t } }
|
|
121
|
+
if (set.isEmpty()) {
|
|
122
|
+
// Empty file is more likely a wiring slip than a deliberate "fetch nothing"; materialize all.
|
|
123
|
+
state.populateGavs = null
|
|
124
|
+
println "[socket-facts] WARN populateFilesFor file empty; materializing files for all resolved artifacts"
|
|
125
|
+
} else {
|
|
126
|
+
state.populateGavs = set
|
|
127
|
+
println "[socket-facts] --with-files scoped to ${set.size()} SBOM artifact(s)"
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// Missing file is more likely a wiring bug than an intentional empty scope; materialize all.
|
|
131
|
+
state.populateGavs = null
|
|
132
|
+
println "[socket-facts] WARN populateFilesFor file not found; materializing files for all resolved artifacts"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
populateGavs = state.populateGavs
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Full Maven coordinate (absent segments dropped) — each ext/classifier variant is its own
|
|
140
|
+
// component; the id is the coordinate key reachability joins on (see mavenCoordinateKey).
|
|
141
|
+
def coordId = { coord ->
|
|
142
|
+
[coord.groupId, coord.artifactId, coord.ext, coord.classifier, coord.version].findAll { it }.join(':')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def isIntraProject = { String group, String name ->
|
|
146
|
+
projectKeys.contains("${group ?: ''}:${name}")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A first-party module dep can resolve with no published artifact (classes-dir variant, or
|
|
150
|
+
// variant-ambiguous moduleArtifacts), leaving it ext-less. Stamp the module's real artifact
|
|
151
|
+
// extension so it keeps a full, stable coordinate instead of an ext-less one.
|
|
152
|
+
def effExt = { String group, String name, String ext ->
|
|
153
|
+
if ((ext == null || ext.isEmpty()) && isIntraProject(group, name)) {
|
|
154
|
+
projectArtifactExt["${group ?: ''}:${name}".toString()] ?: ''
|
|
155
|
+
} else {
|
|
156
|
+
ext ?: ''
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// A node is created once; its prod flag accumulates (OR) across the configs that reach it.
|
|
161
|
+
def upsertNode = { Map coord, boolean isProd ->
|
|
162
|
+
def id = coordId(coord)
|
|
163
|
+
synchronized (nodes) {
|
|
164
|
+
def node = nodes[id]
|
|
165
|
+
if (node == null) {
|
|
166
|
+
node = [coord: coord, children: [] as Set, prod: false]
|
|
167
|
+
nodes[id] = node
|
|
168
|
+
}
|
|
169
|
+
if (isProd) {
|
|
170
|
+
node.prod = true
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
id
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Walk a resolved dependency and its transitive closure. We never touch `artifact.file` here —
|
|
177
|
+
// that forces Gradle to download the file (catastrophic on builds declaring distribution
|
|
178
|
+
// archives as deps); `artifact.extension`/`artifact.classifier` read already-resolved metadata.
|
|
179
|
+
// Intra-project deps (project(':lib')) ARE emitted and recursed, so an external the consumer
|
|
180
|
+
// pins/forces to a different version THROUGH the edge is captured (firstLevelModuleDependencies
|
|
181
|
+
// lists only declared deps, so a project dep's transitives are reachable only via the edge).
|
|
182
|
+
def visit
|
|
183
|
+
visit = { dep, boolean isProd, Map cache ->
|
|
184
|
+
if (cache.containsKey(dep)) {
|
|
185
|
+
return cache[dep]
|
|
186
|
+
}
|
|
187
|
+
// Pre-populate the cache to break cycles before we recurse.
|
|
188
|
+
def producedIds = [] as Set
|
|
189
|
+
cache[dep] = producedIds
|
|
190
|
+
|
|
191
|
+
// `moduleArtifacts` forces ARTIFACT-level variant selection, which can throw on a
|
|
192
|
+
// variant-ambiguous dep even when the dependency GRAPH resolves fine. Guard per-dep: on
|
|
193
|
+
// failure emit at GAV level and keep walking so one ambiguous artifact can't sink the config.
|
|
194
|
+
def artifacts
|
|
195
|
+
try {
|
|
196
|
+
artifacts = dep.moduleArtifacts
|
|
197
|
+
} catch (Exception e) {
|
|
198
|
+
artifacts = [] as Set
|
|
199
|
+
}
|
|
200
|
+
if (artifacts.isEmpty()) {
|
|
201
|
+
def ext = effExt(dep.moduleGroup, dep.moduleName, '')
|
|
202
|
+
// Skip a no-artifact first-party module (aggregator / build root): it builds no archive, so
|
|
203
|
+
// it's fully described by `projects` and is never a real artifact dependency.
|
|
204
|
+
if (!(ext.isEmpty() && isIntraProject(dep.moduleGroup, dep.moduleName))) {
|
|
205
|
+
producedIds << upsertNode([
|
|
206
|
+
groupId : dep.moduleGroup ?: '',
|
|
207
|
+
artifactId: dep.moduleName,
|
|
208
|
+
version : dep.moduleVersion ?: '',
|
|
209
|
+
classifier: '',
|
|
210
|
+
ext : ext,
|
|
211
|
+
], isProd)
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// Build the GAV scope key only when a scope is set: the no-scope path never reads it and
|
|
215
|
+
// this is the hottest line under --parallel.
|
|
216
|
+
def shouldMaterialize
|
|
217
|
+
if (!withFiles) {
|
|
218
|
+
shouldMaterialize = false
|
|
219
|
+
} else if (populateGavs == null) {
|
|
220
|
+
shouldMaterialize = true
|
|
221
|
+
} else {
|
|
222
|
+
def gav = "${dep.moduleGroup ?: ''}:${dep.moduleName}:${dep.moduleVersion ?: ''}".toString()
|
|
223
|
+
shouldMaterialize = populateGavs.contains(gav)
|
|
224
|
+
}
|
|
225
|
+
artifacts.each { a ->
|
|
226
|
+
// Directory variants (java-classes-directory etc.) carry no extension; for a first-party
|
|
227
|
+
// module fall back to its real artifact ext (never artifact.type, a Gradle variant attr).
|
|
228
|
+
def ext = effExt(dep.moduleGroup, dep.moduleName, a.extension)
|
|
229
|
+
// Skip a no-artifact first-party module (see the empty-artifacts branch above).
|
|
230
|
+
if (ext.isEmpty() && isIntraProject(dep.moduleGroup, dep.moduleName)) return
|
|
231
|
+
def aid = upsertNode([
|
|
232
|
+
groupId : dep.moduleGroup ?: '',
|
|
233
|
+
artifactId: dep.moduleName,
|
|
234
|
+
version : dep.moduleVersion ?: '',
|
|
235
|
+
classifier: a.classifier ?: '',
|
|
236
|
+
ext : ext,
|
|
237
|
+
], isProd)
|
|
238
|
+
producedIds << aid
|
|
239
|
+
// `a.file` downloads the artifact if not already cached, so scoping avoids fetching
|
|
240
|
+
// artifacts the SBOM doesn't reference. Per-artifact try/catch: a single download can
|
|
241
|
+
// fail without the whole config being unresolvable.
|
|
242
|
+
if (shouldMaterialize) {
|
|
243
|
+
try {
|
|
244
|
+
def path = a.file.absolutePath
|
|
245
|
+
synchronized (paths) {
|
|
246
|
+
def set = paths[aid]
|
|
247
|
+
if (set == null) { set = [] as Set; paths[aid] = set }
|
|
248
|
+
set << path
|
|
249
|
+
}
|
|
250
|
+
} catch (Exception e) {
|
|
251
|
+
println "[socket-facts] could not materialize ${aid}: ${e.message?.readLines()?.first()}"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def childIds = [] as Set
|
|
258
|
+
dep.children.each { child ->
|
|
259
|
+
childIds.addAll(visit(child, isProd, cache))
|
|
260
|
+
}
|
|
261
|
+
// A skipped no-artifact first-party module produced no node, so bubble its resolved children
|
|
262
|
+
// up to the parent (return childIds) — otherwise the module's transitives orphan (attached to
|
|
263
|
+
// nothing, not bubbled). The module itself is described by `projects`.
|
|
264
|
+
if (producedIds.isEmpty()) {
|
|
265
|
+
cache[dep] = childIds
|
|
266
|
+
return childIds
|
|
267
|
+
}
|
|
268
|
+
synchronized (nodes) {
|
|
269
|
+
producedIds.each { pid ->
|
|
270
|
+
// Drop self-edges: a project that depends on its own output (test → main) can resolve
|
|
271
|
+
// to the same coordinate as its root, which would otherwise emit a node listing itself.
|
|
272
|
+
nodes[pid].children.addAll(childIds.findAll { it != pid })
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
producedIds
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
def isTestConfig = { String name -> name.toLowerCase().contains('test') }
|
|
279
|
+
// Matches real compile/runtime classpaths (modern `*Classpath`, legacy `compile`/`runtime`),
|
|
280
|
+
// NOT a loose substring that would wrongly promote war-plugin `providedCompile`/`providedRuntime`
|
|
281
|
+
// (provided deps are dev). Sets the informational `dev` flag only — never gates analysis.
|
|
282
|
+
def isClasspathConfig = { String name ->
|
|
283
|
+
def n = name.toLowerCase()
|
|
284
|
+
n.contains('classpath') || n == 'compile' || n == 'runtime'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Config-name glob (`*`/`?` only) to a case-INSENSITIVE regex — Gradle config names are
|
|
288
|
+
// camelCase, so a case-sensitive `*Classpath` would miss the base `compileClasspath`.
|
|
289
|
+
def globToRegex = { String glob ->
|
|
290
|
+
def sb = new StringBuilder()
|
|
291
|
+
glob.each { String ch ->
|
|
292
|
+
switch (ch) {
|
|
293
|
+
case '*': sb << '.*'; break
|
|
294
|
+
case '?': sb << '.'; break
|
|
295
|
+
case '.': case '\\': case '^': case '$': case '|':
|
|
296
|
+
case '+': case '(': case ')':
|
|
297
|
+
case '[': case ']': case '{': case '}':
|
|
298
|
+
sb << '\\' << ch; break
|
|
299
|
+
default: sb << ch
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
java.util.regex.Pattern.compile(sb.toString(), java.util.regex.Pattern.CASE_INSENSITIVE)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// `-Psocket.includeConfigs`/`-Psocket.excludeConfigs`: comma-separated config-name globs. A
|
|
306
|
+
// config is walked when it matches some include (or there are none) AND matches no exclude.
|
|
307
|
+
def parsePatterns = { String s ->
|
|
308
|
+
def out = []
|
|
309
|
+
if (s != null && !s.trim().isEmpty()) {
|
|
310
|
+
s.split(',').each { raw ->
|
|
311
|
+
def p = raw.trim()
|
|
312
|
+
if (!p.isEmpty()) out << globToRegex(p)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
out
|
|
316
|
+
}
|
|
317
|
+
def includePatterns = parsePatterns(gradle.socketProp.call(project, 'socket.includeConfigs')?.toString())
|
|
318
|
+
def excludePatterns = parsePatterns(gradle.socketProp.call(project, 'socket.excludeConfigs')?.toString())
|
|
319
|
+
|
|
320
|
+
// Don't filter by canBeConsumed: Gradle defaults both role flags to true, so a plain resolvable
|
|
321
|
+
// config is also consumable, and excluding consumable configs would drop legitimate custom ones.
|
|
322
|
+
// `isCanBeResolved` only exists since Gradle 3.3; older Gradle has no split (all resolvable).
|
|
323
|
+
def isResolvable = { c -> c.metaClass.respondsTo(c, 'isCanBeResolved') ? c.canBeResolved : true }
|
|
324
|
+
def targetConfigs = project.configurations.findAll {
|
|
325
|
+
if (!isResolvable(it)) return false
|
|
326
|
+
def name = it.name
|
|
327
|
+
if (excludePatterns.any { p -> p.matcher(name).matches() }) return false
|
|
328
|
+
if (!includePatterns.isEmpty() && !includePatterns.any { p -> p.matcher(name).matches() }) return false
|
|
329
|
+
return true
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Deepest cause's message, in FULL — Coana classifies/templates it, so we don't categorize here.
|
|
333
|
+
def rawDetail = { problem ->
|
|
334
|
+
def deepest = null
|
|
335
|
+
def t = problem
|
|
336
|
+
int guard = 0
|
|
337
|
+
while (t != null && guard++ < 12) { if (t.message) { deepest = t.message }; t = t.cause }
|
|
338
|
+
return (deepest ?: 'unknown resolution failure').toString().trim()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
targetConfigs.each { cfg ->
|
|
342
|
+
// Recorded even if it resolves nothing or throws — Coana surfaces the full scanned set.
|
|
343
|
+
scannedConfigs.add(cfg.name)
|
|
344
|
+
// Informational only (OR-accumulated across configs); never gates analysis, so this name
|
|
345
|
+
// heuristic can only mis-bucket a dep in prod/dev views, never drop it.
|
|
346
|
+
def isProd = isClasspathConfig(cfg.name) && !isTestConfig(cfg.name)
|
|
347
|
+
// Fresh tree for THIS resolution root — captured by visit/upsertNode, so no union with siblings.
|
|
348
|
+
nodes = [:]
|
|
349
|
+
directIds = [] as Set
|
|
350
|
+
paths = [:]
|
|
351
|
+
// Per-config try/catch: AGP-style configs can fail variant ambiguity from an init-script
|
|
352
|
+
// context lacking the consumer attributes AGP sets internally.
|
|
353
|
+
try {
|
|
354
|
+
def lenient = cfg.resolvedConfiguration.lenientConfiguration
|
|
355
|
+
def cache = [:]
|
|
356
|
+
// No-arg getter only since Gradle 3.3; older Gradle has just the Spec-taking overload.
|
|
357
|
+
def firstLevel
|
|
358
|
+
try {
|
|
359
|
+
firstLevel = lenient.firstLevelModuleDependencies
|
|
360
|
+
} catch (MissingPropertyException e) {
|
|
361
|
+
firstLevel = lenient.getFirstLevelModuleDependencies({ true } as org.gradle.api.specs.Spec)
|
|
362
|
+
}
|
|
363
|
+
firstLevel.each { dep ->
|
|
364
|
+
directIds.addAll(visit(dep, isProd, cache))
|
|
365
|
+
}
|
|
366
|
+
// Unresolved deps drive the root aggregator's abort/warn decision but are NOT emitted as
|
|
367
|
+
// nodes — their selector-only coordinates would surface as half-formed phantom artifacts.
|
|
368
|
+
lenient.unresolvedModuleDependencies.each { dep ->
|
|
369
|
+
def sel = dep.selector
|
|
370
|
+
if (isIntraProject(sel.group, sel.name)) {
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
failures << [
|
|
374
|
+
coord : "${sel.group}:${sel.name}${sel.version ? ':' + sel.version : ''}".toString(),
|
|
375
|
+
detail: rawDetail(dep.problem),
|
|
376
|
+
config: cfg.name,
|
|
377
|
+
]
|
|
378
|
+
}
|
|
379
|
+
} catch (Exception e) {
|
|
380
|
+
println "[socket-facts] skipping ${project.path}:${cfg.name}: ${e.message?.readLines()?.first()}"
|
|
381
|
+
}
|
|
382
|
+
// Stash this resolution root's tree; the key only has to be unique per root (aggregator
|
|
383
|
+
// groups by subtree hash). Skip empty configs — they'd contribute nothing.
|
|
384
|
+
if (!nodes.isEmpty()) {
|
|
385
|
+
synchronized (state.perSub) {
|
|
386
|
+
state.perSub["${project.path}::${cfg.name}".toString()] =
|
|
387
|
+
[nodes: nodes, direct: directIds, paths: paths, projectPath: project.path, config: cfg.name, prod: isProd]
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
rootProject { rp ->
|
|
396
|
+
// Hoist property reads to configuration time: the configuration cache forbids `Task.project` from
|
|
397
|
+
// task actions. The Socket CLI disables the cache for this run, but hoisting is cheap insurance.
|
|
398
|
+
def recordsFileOverride = gradle.socketProp.call(rp, 'socket.recordsFile')?.toString()
|
|
399
|
+
def defaultRecordsFile = new File(rp.projectDir, '.socket.facts.records.tsv').absolutePath
|
|
400
|
+
// `sources`/`targets` are --with-files-only; a plain run emits only the graph fields.
|
|
401
|
+
def withFilesProjects = gradle.socketProp.call(rp, 'socket.withFiles')?.toString()?.toLowerCase() == 'true'
|
|
402
|
+
|
|
403
|
+
rp.tasks.create('socketFacts') {
|
|
404
|
+
group = 'socket'
|
|
405
|
+
description = 'Emits Socket facts records for the entire build'
|
|
406
|
+
outputs.upToDateWhen { false }
|
|
407
|
+
|
|
408
|
+
doLast {
|
|
409
|
+
def state = gradle.socketFactsState
|
|
410
|
+
|
|
411
|
+
// Backslash-escape so a value can never break line/field framing (see records.ts unescape).
|
|
412
|
+
def esc = { v ->
|
|
413
|
+
(v == null ? '' : v.toString())
|
|
414
|
+
.replace('\\', '\\\\').replace('\t', '\\t').replace('\n', '\\n').replace('\r', '\\r')
|
|
415
|
+
}
|
|
416
|
+
def lines = []
|
|
417
|
+
def rec = { List fields -> lines << fields.collect { esc(it) }.join('\t') }
|
|
418
|
+
|
|
419
|
+
rec(['meta', 'gradle', gradle.gradleVersion, System.getProperty('java.version')])
|
|
420
|
+
|
|
421
|
+
// One `project` record per build module (sources/targets only with --with-files).
|
|
422
|
+
def projectsInfo
|
|
423
|
+
synchronized (state.projectsInfo) { projectsInfo = new ArrayList(state.projectsInfo) }
|
|
424
|
+
projectsInfo.each { pi ->
|
|
425
|
+
rec(['project', pi.path, pi.group ?: '', pi.name, pi.version ?: '', pi.dir])
|
|
426
|
+
if (withFilesProjects) {
|
|
427
|
+
pi.sources.each { s -> rec(['projectSrc', pi.path, s]) }
|
|
428
|
+
pi.targets.each { t -> rec(['projectTgt', pi.path, t]) }
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// One resolution root per (subproject, configuration); the TS assembler merges them
|
|
433
|
+
// path-sensitively and content-addresses divergent subtrees.
|
|
434
|
+
def perSub
|
|
435
|
+
synchronized (state.perSub) { perSub = new LinkedHashMap(state.perSub) }
|
|
436
|
+
int rootIdx = 0
|
|
437
|
+
perSub.each { rootKey, tree ->
|
|
438
|
+
def rootId = (rootIdx++).toString()
|
|
439
|
+
rec(['root', rootId, tree.projectPath, tree.config, tree.prod ? '1' : '0'])
|
|
440
|
+
tree.nodes.each { coordId, node ->
|
|
441
|
+
def c = node.coord
|
|
442
|
+
rec(['node', rootId, coordId, c.groupId ?: '', c.artifactId ?: '', c.version ?: '', c.ext ?: '',
|
|
443
|
+
c.classifier ?: '', tree.direct.contains(coordId) ? '1' : '0'])
|
|
444
|
+
node.children.each { childId -> rec(['edge', rootId, coordId, childId]) }
|
|
445
|
+
def fs = tree.paths[coordId]
|
|
446
|
+
if (fs) { (fs as List).sort().each { p -> rec(['file', rootId, coordId, p]) } }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Scanned configs + raw failures (Coana owns the abort/warn policy and report rendering).
|
|
451
|
+
def scanned
|
|
452
|
+
synchronized (state.scannedConfigs) { scanned = (state.scannedConfigs as List).sort() }
|
|
453
|
+
scanned.each { name -> rec(['scanned', name]) }
|
|
454
|
+
def failures
|
|
455
|
+
synchronized (state.failures) { failures = new ArrayList(state.failures) }
|
|
456
|
+
failures.each { f -> rec(['failure', f.coord, f.detail, f.config]) }
|
|
457
|
+
|
|
458
|
+
def outFile = new File(recordsFileOverride ?: defaultRecordsFile)
|
|
459
|
+
outFile.parentFile?.mkdirs()
|
|
460
|
+
// Explicit UTF-8: the TS reader reads UTF-8, but `File.text =` would encode
|
|
461
|
+
// with the JVM default charset (Cp1252 / US-ASCII on Windows or a non-UTF-8
|
|
462
|
+
// locale on JDK <=17), corrupting non-ASCII paths and failure messages.
|
|
463
|
+
outFile.withWriter('UTF-8') { it.write(lines.join('\n') + '\n') }
|
|
464
|
+
println "Socket facts records written to: ${outFile.absolutePath}"
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
gradle.projectsEvaluated { g ->
|
|
470
|
+
def aggregator = g.rootProject.tasks.findByName('socketFacts')
|
|
471
|
+
if (aggregator) {
|
|
472
|
+
g.rootProject.allprojects.each { p ->
|
|
473
|
+
def collector = p.tasks.findByName('socketFactsCollect')
|
|
474
|
+
if (collector) {
|
|
475
|
+
aggregator.dependsOn(collector)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|