@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.
- package/CHANGELOG.md +13 -2
- package/dist/cli.js +267 -40
- package/dist/cli.js.map +1 -1
- package/dist/constants.js +8 -4
- package/dist/constants.js.map +1 -1
- package/dist/socket-facts.init.gradle +353 -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/convert-gradle-to-facts.d.mts +7 -0
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts.map +1 -0
- package/dist/types/commands/manifest/convert_gradle_to_maven.d.mts.map +1 -1
- package/dist/types/commands/manifest/generate_auto_manifest.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.map +1 -1
- package/dist/types/constants.d.mts +4 -0
- package/dist/types/constants.d.mts.map +1 -1
- package/dist/types/utils/dlx.d.mts +6 -0
- package/dist/types/utils/dlx.d.mts.map +1 -1
- package/dist/types/utils/socket-json.d.mts +1 -0
- package/dist/types/utils/socket-json.d.mts.map +1 -1
- package/dist/utils.js +215 -43
- package/dist/utils.js.map +1 -1
- package/package.json +2 -3
|
@@ -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
|
+
}
|