@socketsecurity/cli-with-sentry 1.1.100 → 1.1.102

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.
@@ -0,0 +1,353 @@
1
+ // Gradle init script that emits a single `.socket.facts.json` file at the
2
+ // build root describing the resolved compile/runtime dependency graph of
3
+ // every subproject combined.
4
+ //
5
+ // Schema matches the canonical SocketFacts shape consumed by depscan
6
+ // (`workspaces/lib/src/socket-facts/socket-facts-schema.ts`):
7
+ //
8
+ // { components: SF_Artifact[] }
9
+ //
10
+ // Each Maven SF_Artifact is `{ type: 'maven', namespace, name, version?,
11
+ // qualifiers? } & { id, direct?, dev?, tooling?, dependencies? }`.
12
+ // `qualifiers` is strict on `{ classifier?, ext? }` — anything else is
13
+ // dropped.
14
+ //
15
+ // Invoke via:
16
+ // ./gradlew --init-script socket-facts.init.gradle socketFacts
17
+ //
18
+ // Structure:
19
+ // - per-subproject `socketFactsCollect` tasks resolve that subproject's
20
+ // configurations and contribute to shared accumulators on gradle.ext
21
+ // - the root `socketFacts` task depends on every collector, then
22
+ // serializes the accumulated graph to a single JSON file at the build
23
+ // root
24
+ //
25
+ // Intra-project dependencies (i.e. `project(':lib')` style edges between
26
+ // subprojects in the same build) are dropped from the output entirely.
27
+ // Their reasoning: each subproject contributes its own external deps to
28
+ // the shared facts; the inter-project edges would just be noise that
29
+ // downstream consumers (coana mvn dependency:get) would try to resolve
30
+ // against Maven Central and fail. The externals each intra-project dep
31
+ // brings in are picked up via that subproject's own collector.
32
+
33
+ import java.util.Collections
34
+ import groovy.json.JsonOutput
35
+
36
+ // Must stay in sync with `DOT_SOCKET_DOT_FACTS_JSON` in
37
+ // src/constants.mts (TS side). Groovy can't import the TS constant, so
38
+ // the two strings are intentionally duplicated; if you change one,
39
+ // change the other.
40
+ ext.SOCKET_FACTS_FILENAME = '.socket.facts.json'
41
+
42
+ // Shared accumulators across all subprojects' contributions. Synchronized
43
+ // collections so --parallel-enabled builds don't race. The accumulator
44
+ // lives on `gradle.ext` so every subproject's collector and the root
45
+ // aggregator share the same instance.
46
+ gradle.ext.socketFactsState = [
47
+ // id -> [coord, children, prod, nonTooling]
48
+ nodes : Collections.synchronizedMap([:]),
49
+ // first-level dep ids
50
+ directIds : Collections.synchronizedSet([] as Set),
51
+ // selectors we've already logged as unresolved (deduped across configs)
52
+ reportedUnresolved : Collections.synchronizedSet([] as Set),
53
+ // "group:name" of every project in this build — used to filter
54
+ // intra-project deps. Populated once all projects are evaluated.
55
+ projectKeys : Collections.synchronizedSet([] as Set),
56
+ ]
57
+
58
+ // Capture every project's (group:name) once all projects are configured so
59
+ // per-subproject collectors can filter intra-project deps without an
60
+ // ordering dependency on other subprojects.
61
+ gradle.projectsEvaluated { g ->
62
+ g.rootProject.allprojects.each { p ->
63
+ g.socketFactsState.projectKeys.add("${p.group ?: ''}:${p.name}")
64
+ }
65
+ }
66
+
67
+ allprojects { project ->
68
+ def collectTask = project.tasks.create('socketFactsCollect') {
69
+ description = "Resolves ${project.path}'s configurations into the build-wide Socket facts accumulator"
70
+ // Dependency resolution depends on state Gradle's up-to-date tracking
71
+ // can't represent reliably.
72
+ outputs.upToDateWhen { false }
73
+
74
+ doLast {
75
+ def state = gradle.socketFactsState
76
+ def nodes = state.nodes
77
+ def directIds = state.directIds
78
+ def reportedUnresolved = state.reportedUnresolved
79
+ def projectKeys = state.projectKeys
80
+
81
+ // `id` omits ext so Gradle's variant artifacts (e.g.
82
+ // `java-classes-directory` and `jar` for the same project dep)
83
+ // dedupe into a single component. Classifier stays in the id since
84
+ // it identifies a distinct artifact (sources, javadoc, etc.).
85
+ def coordId = { coord ->
86
+ def parts = [coord.groupId, coord.artifactId]
87
+ if (coord.classifier) parts << coord.classifier
88
+ parts << coord.version
89
+ parts.join(':')
90
+ }
91
+
92
+ def isIntraProject = { String group, String name ->
93
+ projectKeys.contains("${group ?: ''}:${name}")
94
+ }
95
+
96
+ // Atomic upsert: bracket the read-modify-write under the nodes map's
97
+ // monitor so concurrent contributions don't lose flag updates.
98
+ def upsertNode = { Map coord, boolean isProd, boolean isNonTooling ->
99
+ def id = coordId(coord)
100
+ synchronized (nodes) {
101
+ def node = nodes[id]
102
+ if (node == null) {
103
+ node = [coord: coord, children: [] as Set, prod: false, nonTooling: false]
104
+ nodes[id] = node
105
+ } else if (!node.coord.ext && coord.ext) {
106
+ // Upgrade to the variant whose Gradle artifact has a real
107
+ // packaging extension. Compile classpath visits often arrive
108
+ // with no ext (a project dep exposes only its classes-directory
109
+ // variant there); the runtime classpath visit then fills in
110
+ // the canonical jar/aar.
111
+ node.coord = coord
112
+ }
113
+ if (isProd) {
114
+ node.prod = true
115
+ }
116
+ if (isNonTooling) {
117
+ node.nonTooling = true
118
+ }
119
+ }
120
+ id
121
+ }
122
+
123
+ // Walk a resolved dependency, emitting nodes for itself and its
124
+ // transitive closure. `cache` is keyed by ResolvedDependency identity
125
+ // and short-circuits revisits in diamond/cyclic graphs.
126
+ //
127
+ // We never touch `artifact.file` — that forces Gradle to *download*
128
+ // the underlying file (catastrophic on large builds that declare
129
+ // distribution archives as dependencies). `artifact.extension` and
130
+ // `artifact.classifier` read from metadata that resolution already
131
+ // needed.
132
+ //
133
+ // Intra-project deps (project(':lib') and friends) are dropped at
134
+ // visit time: we return an empty produced-id set, don't emit a node,
135
+ // and don't recurse into the dep's children. The transitives those
136
+ // intra-project deps expose are picked up via the consumer
137
+ // subproject's classpath directly (Gradle merges them) and via the
138
+ // intra-project's own collector.
139
+ def visit
140
+ visit = { dep, boolean isProd, boolean isNonTooling, Map cache ->
141
+ if (cache.containsKey(dep)) {
142
+ return cache[dep]
143
+ }
144
+ if (isIntraProject(dep.moduleGroup, dep.moduleName)) {
145
+ def empty = [] as Set
146
+ cache[dep] = empty
147
+ return empty
148
+ }
149
+ // Pre-populate the cache to break cycles before we recurse.
150
+ def producedIds = [] as Set
151
+ cache[dep] = producedIds
152
+
153
+ def artifacts = dep.moduleArtifacts
154
+ if (artifacts.isEmpty()) {
155
+ producedIds << upsertNode([
156
+ groupId : dep.moduleGroup ?: '',
157
+ artifactId: dep.moduleName,
158
+ version : dep.moduleVersion ?: '',
159
+ classifier: '',
160
+ ext : '',
161
+ ], isProd, isNonTooling)
162
+ } else {
163
+ artifacts.each { a ->
164
+ producedIds << upsertNode([
165
+ groupId : dep.moduleGroup ?: '',
166
+ artifactId: dep.moduleName,
167
+ version : dep.moduleVersion ?: '',
168
+ classifier: a.classifier ?: '',
169
+ // Use the file extension Gradle reports. For Gradle-internal
170
+ // directory variants (java-classes-directory etc.) the
171
+ // extension is empty — we let that through and emit no ext
172
+ // qualifier. Never fall back to artifact.type, which is
173
+ // Gradle's variant attribute, not Maven packaging.
174
+ ext : a.extension ?: '',
175
+ ], isProd, isNonTooling)
176
+ }
177
+ }
178
+
179
+ def childIds = [] as Set
180
+ dep.children.each { child ->
181
+ childIds.addAll(visit(child, isProd, isNonTooling, cache))
182
+ }
183
+ synchronized (nodes) {
184
+ producedIds.each { pid ->
185
+ nodes[pid].children.addAll(childIds)
186
+ }
187
+ }
188
+ producedIds
189
+ }
190
+
191
+ // Configuration selection by name pattern. We match the conventional
192
+ // suffixes used across Gradle plugins for resolvable classpath configs:
193
+ // Java (`compileClasspath`, `runtimeClasspath`,
194
+ // `testCompileClasspath`, `testRuntimeClasspath`), Kotlin Gradle Plugin
195
+ // (`jvmMainCompileClasspath`, `linuxX64MainRuntimeClasspath`, ...) and
196
+ // AGP per-variant (`debugCompileClasspath`, `releaseRuntimeClasspath`,
197
+ // `debugUnitTestRuntimeClasspath`, ...).
198
+ //
199
+ // Beyond classpaths we also walk other resolvable configurations
200
+ // (annotation processors, linter classpaths, etc.) so build-tooling
201
+ // deps land in the output too — tagged `tooling: true` so downstream
202
+ // reachability scanners can skip them.
203
+ //
204
+ // We exclude AGP's instrumented-test classpaths (`*AndroidTest*`)
205
+ // because their variant resolution requires consumer attributes
206
+ // (target SDK, device/host runtime) that an init-script-driven
207
+ // resolution doesn't set, and they produce ambiguity errors at
208
+ // resolution time. Unit-test classpaths (`*UnitTest*`) resolve fine.
209
+ def isClasspath = { String name ->
210
+ def lower = name.toLowerCase()
211
+ lower.endsWith('compileclasspath') || lower.endsWith('runtimeclasspath')
212
+ }
213
+ def isAndroidInstrumentedTest = { String name ->
214
+ name.toLowerCase().contains('androidtest')
215
+ }
216
+ def isTestConfig = { String name -> name.toLowerCase().contains('test') }
217
+
218
+ def targetConfigs = project.configurations.findAll {
219
+ it.canBeResolved && !isAndroidInstrumentedTest(it.name)
220
+ }
221
+
222
+ targetConfigs.each { cfg ->
223
+ def isProd = !isTestConfig(cfg.name)
224
+ def isNonTooling = isClasspath(cfg.name)
225
+ // Per-configuration try/catch: AGP-style configurations can fail
226
+ // with "variant ambiguity" when resolved from an init-script
227
+ // context that doesn't carry the consumer attributes AGP sets
228
+ // internally. We log and continue so a single ambiguous config
229
+ // doesn't sink the whole facts file.
230
+ try {
231
+ def lenient = cfg.resolvedConfiguration.lenientConfiguration
232
+ def cache = [:]
233
+ lenient.firstLevelModuleDependencies.each { dep ->
234
+ directIds.addAll(visit(dep, isProd, isNonTooling, cache))
235
+ }
236
+ lenient.unresolvedModuleDependencies.each { dep ->
237
+ if (isIntraProject(dep.selector.group, dep.selector.name)) {
238
+ return
239
+ }
240
+ def selectorKey = dep.selector.toString()
241
+ if (reportedUnresolved.add(selectorKey)) {
242
+ def reason = dep.problem?.message?.readLines()?.first() ?: 'unknown reason'
243
+ println "[socket-facts] unresolved: ${selectorKey} in ${project.path}: ${reason}"
244
+ }
245
+ def coord = [
246
+ groupId : dep.selector.group ?: '',
247
+ artifactId: dep.selector.name,
248
+ version : dep.selector.version ?: '',
249
+ classifier: '',
250
+ ext : '',
251
+ ]
252
+ directIds.add(upsertNode(coord, isProd, isNonTooling))
253
+ }
254
+ } catch (Exception e) {
255
+ println "[socket-facts] skipping ${project.path}:${cfg.name}: ${e.message?.readLines()?.first()}"
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ rootProject {
263
+ tasks.create('socketFacts') {
264
+ group = 'socket'
265
+ description = 'Aggregates a single Socket facts JSON for the entire build'
266
+ outputs.upToDateWhen { false }
267
+
268
+ doLast {
269
+ def state = gradle.socketFactsState
270
+ def nodes = state.nodes
271
+ def directIds = state.directIds
272
+
273
+ // Snapshot the accumulators under the same monitor used by writers in
274
+ // each subproject's socketFactsCollect doLast. Task dependencies
275
+ // (`aggregator.dependsOn(collector)`) already guarantee a
276
+ // happens-before edge between writes and this read, but we
277
+ // synchronize on `nodes` here so the read path is symmetric with the
278
+ // write path — no implicit reliance on Gradle's task-graph ordering
279
+ // semantics for memory visibility of plain HashMap/HashSet fields.
280
+ def components
281
+ synchronized (nodes) {
282
+ components = nodes.collect { id, node ->
283
+ [id: id, coord: node.coord, prod: node.prod, nonTooling: node.nonTooling, children: (node.children as List).sort()]
284
+ }
285
+ }
286
+
287
+ components = components.collect { snapshot ->
288
+ def id = snapshot.id
289
+ def coord = snapshot.coord
290
+ def component = [
291
+ type : 'maven',
292
+ namespace: coord.groupId,
293
+ name : coord.artifactId,
294
+ ]
295
+ if (coord.version) {
296
+ component.version = coord.version
297
+ }
298
+ def qualifiers = [:]
299
+ if (coord.classifier) {
300
+ qualifiers.classifier = coord.classifier
301
+ }
302
+ if (coord.ext) {
303
+ qualifiers.ext = coord.ext
304
+ }
305
+ if (!qualifiers.isEmpty()) {
306
+ component.qualifiers = qualifiers
307
+ }
308
+ component.id = id
309
+ if (directIds.contains(id)) {
310
+ component.direct = true
311
+ }
312
+ if (!snapshot.prod) {
313
+ component.dev = true
314
+ }
315
+ if (!snapshot.nonTooling) {
316
+ component.tooling = true
317
+ }
318
+ if (!snapshot.children.isEmpty()) {
319
+ component.dependencies = snapshot.children
320
+ }
321
+ component
322
+ }
323
+
324
+ if (components.isEmpty()) {
325
+ println "[socket-facts] no resolvable dependencies in build, skipping"
326
+ return
327
+ }
328
+
329
+ def outputDir = project.findProperty('socket.outputDirectory')
330
+ ? new File(project.findProperty('socket.outputDirectory').toString())
331
+ : project.projectDir
332
+ outputDir.mkdirs()
333
+ def fileName = project.findProperty('socket.outputFile') ?: SOCKET_FACTS_FILENAME
334
+ def outFile = new File(outputDir, fileName.toString())
335
+ outFile.text = JsonOutput.prettyPrint(JsonOutput.toJson([components: components]))
336
+ println "Socket facts file written to: ${outFile.absolutePath}"
337
+ }
338
+ }
339
+ }
340
+
341
+ // Wire every subproject's collector as a dependency of the root aggregator
342
+ // so the aggregator runs after all contributions have been made.
343
+ gradle.projectsEvaluated { g ->
344
+ def aggregator = g.rootProject.tasks.findByName('socketFacts')
345
+ if (aggregator) {
346
+ g.rootProject.allprojects.each { p ->
347
+ def collector = p.tasks.findByName('socketFactsCollect')
348
+ if (collector) {
349
+ aggregator.dependsOn(collector)
350
+ }
351
+ }
352
+ }
353
+ }