@socketsecurity/cli-with-sentry 1.1.130 → 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 +12 -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,444 @@
|
|
|
1
|
+
package socket
|
|
2
|
+
|
|
3
|
+
import sbt._
|
|
4
|
+
import sbt.Keys._
|
|
5
|
+
|
|
6
|
+
import scala.collection.mutable
|
|
7
|
+
import scala.reflect.ClassTag
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Emits a flat line-protocol RECORDS file at the build root (NOT the final
|
|
11
|
+
* `.socket.facts.json`, and NOT to stdout — sbt prints resolution noise to stdout
|
|
12
|
+
* with no way to silence it). The TS assembler (utils/src/manifest-scripts/assemble.ts)
|
|
13
|
+
* reads the records and owns all SBOM construction — graph merge, content-addressed
|
|
14
|
+
* ids, contentHash. This plugin only RESOLVES and emits raw facts. See records.ts for
|
|
15
|
+
* the record grammar.
|
|
16
|
+
*
|
|
17
|
+
* Must compile on Scala 2.10/sbt 0.13 and Scala 2.12/sbt 1.x (compiled by the sbt
|
|
18
|
+
* meta-build), hence string-named TaskKeys and reflection for version-specific
|
|
19
|
+
* ExclusionRule / ResolveException / ConfigRef shapes.
|
|
20
|
+
*/
|
|
21
|
+
object SocketFactsPlugin extends AutoPlugin {
|
|
22
|
+
override def trigger = allRequirements
|
|
23
|
+
|
|
24
|
+
object autoImport {
|
|
25
|
+
val socketFacts =
|
|
26
|
+
taskKey[Unit]("Emit Socket facts records for the whole build")
|
|
27
|
+
}
|
|
28
|
+
import autoImport._
|
|
29
|
+
|
|
30
|
+
override def projectSettings: Seq[Setting[_]] = Seq(
|
|
31
|
+
aggregate in socketFacts := false,
|
|
32
|
+
socketFacts := {
|
|
33
|
+
val st = state.value
|
|
34
|
+
val buildRoot = (baseDirectory in ThisBuild).value
|
|
35
|
+
val withFiles = boolProp("socket.withFiles")
|
|
36
|
+
val populateScope: Option[Set[String]] = readPopulateScope()
|
|
37
|
+
|
|
38
|
+
val extracted = Project.extract(st)
|
|
39
|
+
val allRefs = extracted.structure.allProjectRefs
|
|
40
|
+
|
|
41
|
+
// Prefer `updateFull` (coursier `update` returns empty callers); fall back to `update` (sbt 0.13).
|
|
42
|
+
val hasUpdateFull =
|
|
43
|
+
extracted.structure.settings.map(_.key.key).exists(_.label == "updateFull")
|
|
44
|
+
val updateTaskName = if (hasUpdateFull) "updateFull" else "update"
|
|
45
|
+
|
|
46
|
+
// One tree per resolution root (project, config); the TS assembler merges them path-sensitively.
|
|
47
|
+
val perSub = mutable.LinkedHashMap.empty[String, RootTree]
|
|
48
|
+
val failures = mutable.LinkedHashSet.empty[Failure]
|
|
49
|
+
val scannedConfigs = mutable.LinkedHashSet.empty[String]
|
|
50
|
+
val matcher = buildConfigMatcher()
|
|
51
|
+
|
|
52
|
+
// Real artifact ext per build module, so an ext-less inter-project dep gets its true coordinate.
|
|
53
|
+
val moduleExts = buildModuleExts(allRefs, extracted)
|
|
54
|
+
|
|
55
|
+
allRefs.foreach { ref =>
|
|
56
|
+
runUpdateResilient(updateTaskName, ref, extracted, st, failures).foreach { report =>
|
|
57
|
+
foldReport(report, ref, extracted, matcher, scannedConfigs, withFiles, populateScope, moduleExts).foreach {
|
|
58
|
+
case (rootKey, tree) => perSub(rootKey) = tree
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
val moduleDirs: Map[String, (Seq[String], Seq[String])] =
|
|
64
|
+
if (withFiles) buildModuleDirs(allRefs, extracted) else Map.empty
|
|
65
|
+
|
|
66
|
+
val rootPath = buildRoot.getCanonicalFile.toPath
|
|
67
|
+
def relOf(f: File): String = {
|
|
68
|
+
val r = rootPath.relativize(f.getCanonicalFile.toPath).toString.replace(java.io.File.separator, "/")
|
|
69
|
+
if (r.isEmpty) "." else r
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val sb = new StringBuilder
|
|
73
|
+
def rec(fields: String*): Unit = {
|
|
74
|
+
sb.append(fields.map(esc).mkString("\t")); sb.append('\n')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
rec("meta", "sbt", extracted.getOpt(sbtVersion).getOrElse(""), sys.props.getOrElse("java.version", ""))
|
|
78
|
+
|
|
79
|
+
// One `project` record per build module (sources/targets only with --with-files).
|
|
80
|
+
allRefs.foreach { ref =>
|
|
81
|
+
val mid = rootIdOf(extracted, ref)
|
|
82
|
+
val ver = if (mid.revision == null) "" else mid.revision
|
|
83
|
+
rec("project", ref.project, mid.organization, mid.name, ver, relOf(extracted.get(baseDirectory.in(ref))))
|
|
84
|
+
if (withFiles) {
|
|
85
|
+
moduleDirs.get(mid.organization + ":" + mid.name + ":" + ver).foreach {
|
|
86
|
+
case (sources, targets) =>
|
|
87
|
+
sources.foreach(s => rec("projectSrc", ref.project, s))
|
|
88
|
+
targets.foreach(t => rec("projectTgt", ref.project, t))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// One resolution root per (subproject, configuration); the TS assembler content-addresses
|
|
94
|
+
// divergent subtrees.
|
|
95
|
+
var rootIdx = 0
|
|
96
|
+
perSub.foreach {
|
|
97
|
+
case (_, tree) =>
|
|
98
|
+
val rootId = rootIdx.toString
|
|
99
|
+
rootIdx += 1
|
|
100
|
+
rec("root", rootId, tree.projectKey, tree.config, if (tree.prod) "1" else "0")
|
|
101
|
+
tree.nodes.foreach {
|
|
102
|
+
case (coordId, node) =>
|
|
103
|
+
val c = node.coord
|
|
104
|
+
rec("node", rootId, coordId, c.org, c.name, c.version, c.ext, c.classifier, if (node.direct) "1" else "0")
|
|
105
|
+
node.children.foreach(ch => rec("edge", rootId, coordId, ch))
|
|
106
|
+
node.targets.foreach(p => rec("file", rootId, coordId, p))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Scanned configs + raw failures (Coana owns the abort/warn policy and report rendering).
|
|
111
|
+
scannedConfigs.toList.sorted.foreach(c => rec("scanned", c))
|
|
112
|
+
failures.foreach(f => rec("failure", f.coord, f.detail, f.config))
|
|
113
|
+
|
|
114
|
+
val recordsFile = sys.props.get("socket.recordsFile").filter(_.nonEmpty) match {
|
|
115
|
+
case Some(p) => new File(p)
|
|
116
|
+
case None => new File(buildRoot, ".socket.facts.records.tsv")
|
|
117
|
+
}
|
|
118
|
+
Option(recordsFile.getParentFile).foreach(_.mkdirs())
|
|
119
|
+
IO.write(recordsFile, sb.toString)
|
|
120
|
+
println("Socket facts records written to: " + recordsFile.getAbsolutePath)
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
// ---- resolution ---------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
private def rootIdOf(extracted: Extracted, ref: ProjectRef): ModuleID = {
|
|
127
|
+
val sv = extracted.get(scalaVersion.in(ref))
|
|
128
|
+
val sbv = extracted.get(scalaBinaryVersion.in(ref))
|
|
129
|
+
CrossVersion.apply(sv, sbv)(extracted.get(projectID.in(ref)))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// GAV -> a build module's real artifact extension (jar for a normal project). coursier reports an
|
|
133
|
+
// inter-project dependency with no resolved artifact, so it would otherwise be ext-less and not
|
|
134
|
+
// match how depscan ingests it (ext=jar); stamping the real ext keeps a full, matching coordinate.
|
|
135
|
+
private def buildModuleExts(allRefs: Seq[ProjectRef], extracted: Extracted): Map[String, String] = {
|
|
136
|
+
allRefs.map { ref =>
|
|
137
|
+
val ext = extracted.getOpt(artifact.in(ref)).map(_.extension).filter(e => e != null && e.nonEmpty).getOrElse("jar")
|
|
138
|
+
gavKey(rootIdOf(extracted, ref)) -> ext
|
|
139
|
+
}.toMap
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Absolute source roots + compiled-output dirs per build module, keyed by GAV. --with-files only;
|
|
143
|
+
// absolute because reachability locates an internal module's code on THIS machine (no registry jar).
|
|
144
|
+
private def buildModuleDirs(
|
|
145
|
+
allRefs: Seq[ProjectRef],
|
|
146
|
+
extracted: Extracted
|
|
147
|
+
): Map[String, (Seq[String], Seq[String])] = {
|
|
148
|
+
allRefs.map { ref =>
|
|
149
|
+
val mid = rootIdOf(extracted, ref)
|
|
150
|
+
val ver = if (mid.revision == null) "" else mid.revision
|
|
151
|
+
val sources = Seq(Compile, Test)
|
|
152
|
+
.flatMap(c => extracted.getOpt(sourceDirectories.in(ref).in(c)).getOrElse(Nil))
|
|
153
|
+
.map(_.getAbsolutePath).distinct.sorted
|
|
154
|
+
val targets = Seq(Compile, Test)
|
|
155
|
+
.flatMap(c => extracted.getOpt(classDirectory.in(ref).in(c)))
|
|
156
|
+
.map(_.getAbsolutePath).distinct.sorted
|
|
157
|
+
(mid.organization + ":" + mid.name + ":" + ver) -> ((sources, targets))
|
|
158
|
+
}.toMap
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// On a hard resolve failure, record the failed modules and retry once with them excluded. Never throws.
|
|
162
|
+
private def runUpdateResilient(
|
|
163
|
+
taskName: String,
|
|
164
|
+
ref: ProjectRef,
|
|
165
|
+
extracted: Extracted,
|
|
166
|
+
state: State,
|
|
167
|
+
failures: mutable.LinkedHashSet[Failure]
|
|
168
|
+
): Option[UpdateReport] = {
|
|
169
|
+
val key = TaskKey[UpdateReport](taskName)
|
|
170
|
+
|
|
171
|
+
def runOn(st: State): Either[Seq[ModuleID], UpdateReport] =
|
|
172
|
+
Project.runTask(key.in(ref).scopedKey, st) match {
|
|
173
|
+
case Some((_, sbt.Value(rep))) => Right(rep)
|
|
174
|
+
case Some((_, sbt.Inc(inc))) => Left(extractFailedModules(inc))
|
|
175
|
+
case _ => Left(Nil)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
runOn(state) match {
|
|
179
|
+
case Right(rep) => Some(rep)
|
|
180
|
+
case Left(failed) =>
|
|
181
|
+
failed.foreach { m =>
|
|
182
|
+
failures += Failure(coordOf(m), "unresolved dependency", taskName)
|
|
183
|
+
}
|
|
184
|
+
if (failed.isEmpty) None
|
|
185
|
+
else
|
|
186
|
+
runOn(extracted.append(exclusionSettings(extracted, failed), state)) match {
|
|
187
|
+
case Right(rep) => Some(rep)
|
|
188
|
+
case Left(_) => None
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Reflection: ResolveException's package and the `failed` accessor differ between sbt 0.13 and 1.x.
|
|
194
|
+
private def extractFailedModules(inc: sbt.Incomplete): Seq[ModuleID] = {
|
|
195
|
+
val out = mutable.ListBuffer.empty[ModuleID]
|
|
196
|
+
def walk(i: sbt.Incomplete): Unit = {
|
|
197
|
+
i.directCause.foreach { ex =>
|
|
198
|
+
val cn = ex.getClass.getName
|
|
199
|
+
if (cn == "sbt.ResolveException" || cn == "sbt.librarymanagement.ResolveException") {
|
|
200
|
+
try {
|
|
201
|
+
val m = ex.getClass.getMethod("failed")
|
|
202
|
+
out ++= m.invoke(ex).asInstanceOf[Seq[ModuleID]]
|
|
203
|
+
} catch { case _: Throwable => }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
i.causes.foreach(walk)
|
|
207
|
+
}
|
|
208
|
+
walk(inc)
|
|
209
|
+
out.toList.distinct
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ExclusionRule constructor shape differs between sbt 0.13 (`sbt.ExclusionRule`) and 1.x (`sbt.SbtExclusionRule`).
|
|
213
|
+
private def exclusionSettings(extracted: Extracted, failed: Seq[ModuleID]): Seq[Setting[_]] = {
|
|
214
|
+
val cls: Class[_] =
|
|
215
|
+
try Class.forName("sbt.SbtExclusionRule")
|
|
216
|
+
catch { case _: ClassNotFoundException => classOf[sbt.ExclusionRule] }
|
|
217
|
+
|
|
218
|
+
def rule(m: ModuleID): Any =
|
|
219
|
+
if (cls.getName.contains("SbtExclusionRule")) {
|
|
220
|
+
cls.getConstructors.find(_.getParameterCount == 5) match {
|
|
221
|
+
case Some(ctor) =>
|
|
222
|
+
ctor.newInstance(m.organization, m.name, "*", Seq(), sbt.CrossVersion.Disabled)
|
|
223
|
+
case None =>
|
|
224
|
+
throw new IllegalStateException("No suitable SbtExclusionRule constructor")
|
|
225
|
+
}
|
|
226
|
+
} else ExclusionRule(organization = m.organization, name = m.name)
|
|
227
|
+
|
|
228
|
+
def castSeq[T](xs: Seq[Any])(implicit ct: ClassTag[T]): Seq[T] = xs.map(_.asInstanceOf[T])
|
|
229
|
+
|
|
230
|
+
extracted.structure.allProjectRefs.map { ref =>
|
|
231
|
+
excludeDependencies.in(ref) := {
|
|
232
|
+
val original = excludeDependencies.in(ref).value
|
|
233
|
+
castSeq(original ++ failed.map(rule))(ClassTag(cls))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Fold one project's UpdateReport into one tree per configuration (rootKey -> tree), keyed by coordinate.
|
|
239
|
+
private def foldReport(
|
|
240
|
+
report: UpdateReport,
|
|
241
|
+
ref: ProjectRef,
|
|
242
|
+
extracted: Extracted,
|
|
243
|
+
matcher: String => Boolean,
|
|
244
|
+
scannedConfigs: mutable.LinkedHashSet[String],
|
|
245
|
+
withFiles: Boolean,
|
|
246
|
+
populateScope: Option[Set[String]],
|
|
247
|
+
moduleExts: Map[String, String]
|
|
248
|
+
): mutable.LinkedHashMap[String, RootTree] = {
|
|
249
|
+
val perRoot = mutable.LinkedHashMap.empty[String, RootTree]
|
|
250
|
+
val rootGav = gavKey(rootIdOf(extracted, ref))
|
|
251
|
+
|
|
252
|
+
def emittable(m: ModuleReport): Boolean = !m.evicted
|
|
253
|
+
|
|
254
|
+
def inScope(m: ModuleID): Boolean = populateScope match {
|
|
255
|
+
case None => true
|
|
256
|
+
case Some(gavs) => gavs.contains(gavKey(m))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
report.configurations.foreach { cr =>
|
|
260
|
+
val cfg = confName(cr)
|
|
261
|
+
if (matcher(cfg)) {
|
|
262
|
+
scannedConfigs += cfg
|
|
263
|
+
val prod = isProdConf(cfg) && !isTestConf(cfg)
|
|
264
|
+
val nodes = mutable.LinkedHashMap.empty[String, Node]
|
|
265
|
+
// module GAV -> component ids (caller edges are module-level).
|
|
266
|
+
val midToIds = mutable.HashMap.empty[String, mutable.LinkedHashSet[String]]
|
|
267
|
+
|
|
268
|
+
cr.modules.foreach { m =>
|
|
269
|
+
if (emittable(m)) {
|
|
270
|
+
val ids = midToIds.getOrElseUpdate(gavKey(m.module), mutable.LinkedHashSet.empty[String])
|
|
271
|
+
variantsOf(m, moduleExts).foreach { case (coord, fileOpt) =>
|
|
272
|
+
val node = nodes.getOrElseUpdate(coord.id, new Node(coord))
|
|
273
|
+
ids += coord.id
|
|
274
|
+
if (withFiles && inScope(m.module)) fileOpt.foreach(f => node.targets += f.getAbsolutePath)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Caller edges within this config: a root caller marks the child direct, any other becomes its parent.
|
|
280
|
+
cr.modules.foreach { m =>
|
|
281
|
+
if (emittable(m)) {
|
|
282
|
+
midToIds.get(gavKey(m.module)).foreach { childIds =>
|
|
283
|
+
m.callers.foreach { c =>
|
|
284
|
+
val callerKey = gavKey(c.caller)
|
|
285
|
+
if (callerKey == rootGav) childIds.foreach(cid => nodes(cid).direct = true)
|
|
286
|
+
else
|
|
287
|
+
midToIds.get(callerKey).foreach { parentIds =>
|
|
288
|
+
// Drop self-edges (test → main resolving to the same coordinate), matching gradle.
|
|
289
|
+
parentIds.foreach(pid => childIds.foreach(cid => if (pid != cid) nodes(pid).children += cid))
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (nodes.nonEmpty) perRoot(ref.project + "::" + cfg) = new RootTree(ref.project, cfg, prod, nodes)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
perRoot
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private def variantsOf(m: ModuleReport, moduleExts: Map[String, String]): Seq[(Coord, Option[File])] = {
|
|
303
|
+
val mid = m.module
|
|
304
|
+
// `mid.revision` is normally set for a resolved module, but normalize null (Coord.id calls
|
|
305
|
+
// `.nonEmpty`, which NPEs on null) to match the other call sites (gavKey, module records).
|
|
306
|
+
val ver = if (mid.revision == null) "" else mid.revision
|
|
307
|
+
val arts = m.artifacts
|
|
308
|
+
if (arts == null || arts.isEmpty)
|
|
309
|
+
// No resolved artifact (e.g. an inter-project dep): stamp the module's real ext if it's a build
|
|
310
|
+
// module (matches depscan's ext=jar), else leave ext-less.
|
|
311
|
+
Seq((Coord(mid.organization, mid.name, ver, moduleExts.getOrElse(gavKey(mid), ""), ""), None))
|
|
312
|
+
else
|
|
313
|
+
arts.toList.map {
|
|
314
|
+
case (art, file) =>
|
|
315
|
+
(Coord(mid.organization, mid.name, ver, extOf(art), classifierOf(art)), Option(file))
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---- config selection ---------------------------------------------------
|
|
320
|
+
|
|
321
|
+
// With no includes the default is ALL configurations (captures build/tooling deps).
|
|
322
|
+
private def buildConfigMatcher(): String => Boolean = {
|
|
323
|
+
def parse(prop: String): List[java.util.regex.Pattern] =
|
|
324
|
+
sys.props.get(prop) match {
|
|
325
|
+
case Some(s) if s.trim.nonEmpty =>
|
|
326
|
+
s.split(",").map(_.trim).filter(_.nonEmpty).toList.map(globToRegex)
|
|
327
|
+
case _ => Nil
|
|
328
|
+
}
|
|
329
|
+
val includes = parse("socket.includeConfigs")
|
|
330
|
+
val excludes = parse("socket.excludeConfigs")
|
|
331
|
+
(name: String) => {
|
|
332
|
+
val included =
|
|
333
|
+
if (includes.isEmpty) true
|
|
334
|
+
else includes.exists(_.matcher(name).matches())
|
|
335
|
+
included && !excludes.exists(_.matcher(name).matches())
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private def globToRegex(glob: String): java.util.regex.Pattern = {
|
|
340
|
+
val sb = new StringBuilder
|
|
341
|
+
glob.foreach {
|
|
342
|
+
case '*' => sb.append(".*")
|
|
343
|
+
case '?' => sb.append('.')
|
|
344
|
+
case c if "\\.^$|+()[]{}".indexOf(c.toInt) >= 0 =>
|
|
345
|
+
sb.append('\\').append(c)
|
|
346
|
+
case c => sb.append(c)
|
|
347
|
+
}
|
|
348
|
+
java.util.regex.Pattern.compile(sb.toString, java.util.regex.Pattern.CASE_INSENSITIVE)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ConfigurationReport.configuration is a String on sbt 0.13, a ConfigRef on 1.x: read `.name` reflectively.
|
|
352
|
+
private def confName(cr: ConfigurationReport): String = {
|
|
353
|
+
val c: Any = cr.configuration
|
|
354
|
+
try c.getClass.getMethod("name").invoke(c).toString
|
|
355
|
+
catch { case _: Throwable => c.toString }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private def isTestConf(name: String): Boolean = name.toLowerCase.contains("test")
|
|
359
|
+
|
|
360
|
+
// Only feeds the informational `dev` flag, never gates analysis.
|
|
361
|
+
private def isProdConf(name: String): Boolean = {
|
|
362
|
+
val n = name.toLowerCase
|
|
363
|
+
n == "compile" || n == "runtime"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---- misc helpers --------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
private def boolProp(name: String): Boolean =
|
|
369
|
+
java.lang.Boolean.parseBoolean(sys.props.getOrElse(name, "false"))
|
|
370
|
+
|
|
371
|
+
private def readPopulateScope(): Option[Set[String]] = {
|
|
372
|
+
sys.props.get("socket.populateFilesFor").map(_.trim).filter(_.nonEmpty) match {
|
|
373
|
+
case None => None
|
|
374
|
+
case Some(path) =>
|
|
375
|
+
val f = new java.io.File(path)
|
|
376
|
+
if (!f.exists) {
|
|
377
|
+
println("[socket-facts] WARN populateFilesFor file not found; recording files for all resolved artifacts")
|
|
378
|
+
None
|
|
379
|
+
} else {
|
|
380
|
+
val src = scala.io.Source.fromFile(f, "UTF-8")
|
|
381
|
+
try {
|
|
382
|
+
val set = src.getLines().map(_.trim).filter(_.nonEmpty).toSet
|
|
383
|
+
if (set.isEmpty) {
|
|
384
|
+
println("[socket-facts] WARN populateFilesFor file empty; recording files for all resolved artifacts")
|
|
385
|
+
None
|
|
386
|
+
} else {
|
|
387
|
+
println("[socket-facts] --with-files scoped to " + set.size + " SBOM artifact(s)")
|
|
388
|
+
Some(set)
|
|
389
|
+
}
|
|
390
|
+
} finally src.close()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private def gavKey(m: ModuleID): String = {
|
|
396
|
+
def s(v: String): String = if (v == null) "" else v
|
|
397
|
+
s(m.organization) + ":" + s(m.name) + ":" + s(m.revision)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private def coordOf(m: ModuleID): String = {
|
|
401
|
+
val rev = m.revision
|
|
402
|
+
m.organization + ":" + m.name + (if (rev != null && rev.nonEmpty) ":" + rev else "")
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private def extOf(a: Artifact): String = {
|
|
406
|
+
val e = a.extension
|
|
407
|
+
if (e == null || e.isEmpty) "jar" else e
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private def classifierOf(a: Artifact): String =
|
|
411
|
+
a.classifier.getOrElse("")
|
|
412
|
+
|
|
413
|
+
// Backslash-escape so a value can never break line/field framing (see records.ts unescape).
|
|
414
|
+
private def esc(v: String): String = {
|
|
415
|
+
if (v == null) ""
|
|
416
|
+
else v.replace("\\", "\\\\").replace("\t", "\\t").replace("\n", "\\n").replace("\r", "\\r")
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private final case class Failure(coord: String, detail: String, config: String)
|
|
420
|
+
|
|
421
|
+
private final case class Coord(
|
|
422
|
+
org: String,
|
|
423
|
+
name: String,
|
|
424
|
+
version: String,
|
|
425
|
+
ext: String,
|
|
426
|
+
classifier: String
|
|
427
|
+
) {
|
|
428
|
+
val id: String = Seq(org, name, ext, classifier, version).filter(_.nonEmpty).mkString(":")
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private final class Node(val coord: Coord) {
|
|
432
|
+
val children = mutable.TreeSet.empty[String]
|
|
433
|
+
var direct = false
|
|
434
|
+
// External artifact's resolved jar(s); --with-files only.
|
|
435
|
+
val targets = mutable.TreeSet.empty[String]
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private final class RootTree(
|
|
439
|
+
val projectKey: String,
|
|
440
|
+
val config: String,
|
|
441
|
+
val prod: Boolean,
|
|
442
|
+
val nodes: mutable.LinkedHashMap[String, Node]
|
|
443
|
+
)
|
|
444
|
+
}
|