1 | import groovy.json.JsonOutput
|
2 | import java.util.regex.Pattern
|
3 | import java.util.regex.Matcher
|
4 | import org.gradle.util.GradleVersion
|
5 | import org.gradle.api.internal.artifacts.DefaultResolvedDependency
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | class GradleGraph {
|
60 | Map<String, Map> nodes = [:]
|
61 | String rootId
|
62 |
|
63 | GradleGraph(String rootId) {
|
64 | this.rootId = rootId
|
65 | }
|
66 |
|
67 | Map setNode(String key, Map value) {
|
68 | if (!key || !value) {
|
69 | return null
|
70 | }
|
71 |
|
72 | nodes.computeIfAbsent(key) { _ ->
|
73 | ['name': value.name, 'version': value.version, 'parentIds': [] as Set]
|
74 | }
|
75 | }
|
76 |
|
77 | void setEdge(String parentId, String childId) {
|
78 | if (isInvalidEdge(parentId, childId)) {
|
79 | return
|
80 | }
|
81 |
|
82 | def parentNode = nodes.computeIfAbsent(parentId) { _ -> createEmptyNode() }
|
83 | def childNode = nodes.computeIfAbsent(childId) { _ -> createEmptyNode() }
|
84 |
|
85 | if (!childNode.parentIds.contains(parentId)) {
|
86 | childNode.parentIds.add(parentId)
|
87 | }
|
88 | }
|
89 |
|
90 | private boolean isInvalidEdge(String parentId, String childId) {
|
91 | parentId == null || childId == null || parentId == childId || (parentId != rootId && !nodes.containsKey(parentId))
|
92 | }
|
93 |
|
94 | private Map createEmptyNode() {
|
95 | ['name': null, 'version': null, 'parentIds': [] as Set]
|
96 | }
|
97 | }
|
98 |
|
99 |
|
100 | class Sha1Map {
|
101 | def coordinates
|
102 | Sha1Map() {
|
103 | coordinates = [:]
|
104 | }
|
105 |
|
106 | def setCoordinate(key, value) {
|
107 | this.coordinates[key] = value
|
108 | }
|
109 | }
|
110 |
|
111 | def hash(File file) {
|
112 | def md = java.security.MessageDigest.getInstance('SHA-1')
|
113 | file.eachByte(1024 * 4) { buffer, len ->
|
114 | md.update(buffer, 0, len)
|
115 | }
|
116 | return md.digest().encodeHex().toString()
|
117 | }
|
118 |
|
119 | def loadGraph(Iterable deps, GradleGraph graph, parentId, currentChain) {
|
120 | deps.each { dep ->
|
121 | dep.each { d ->
|
122 | def childId = "${d.moduleGroup}:${d.moduleName}@${d.moduleVersion}"
|
123 | if (!graph.nodes.get(childId)) {
|
124 | def childDependency = ['name': "${d.moduleGroup}:${d.moduleName}", 'version': d.moduleVersion]
|
125 | graph.setNode(childId, childDependency)
|
126 | }
|
127 |
|
128 |
|
129 | if (!currentChain.contains(childId) && d.children) {
|
130 | currentChain.add(childId)
|
131 | loadGraph(d.children, graph, childId, currentChain)
|
132 | }
|
133 | graph.setEdge(parentId, childId)
|
134 | }
|
135 | }
|
136 | }
|
137 |
|
138 | def loadSha1MapGraph(Iterable deps, GradleGraph graph, parentId, currentChain, sha1Map) {
|
139 | deps.each { dep ->
|
140 | dep.each { d ->
|
141 | def childId = "${d.moduleGroup}:${d.moduleName}@${d.moduleVersion}"
|
142 | if (!graph.nodes.get(childId)) {
|
143 | def childDependency = ['name': "${d.moduleGroup}:${d.moduleName}", 'version': d.moduleVersion]
|
144 | graph.setNode(childId, childDependency)
|
145 |
|
146 | def moduleArtifacts = d.getModuleArtifacts()
|
147 | if (moduleArtifacts[0] && moduleArtifacts[0].getExtension()) {
|
148 |
|
149 | try {
|
150 | def fileHash = hash(moduleArtifacts[0].getFile())
|
151 | sha1Map.setCoordinate(fileHash, childId);
|
152 | } catch (Exception e) {
|
153 | debugLog("Failed to hash artifact ${moduleArtifacts[0]}")
|
154 | }
|
155 | }
|
156 | }
|
157 |
|
158 |
|
159 | if (!currentChain.contains(childId) && d.children) {
|
160 | currentChain.add(childId)
|
161 | loadSha1MapGraph(d.children, graph, childId, currentChain, sha1Map)
|
162 | }
|
163 | graph.setEdge(parentId, childId)
|
164 | }
|
165 | }
|
166 | }
|
167 |
|
168 | def getGradleGraph(Iterable deps) {
|
169 | def rootId = 'root-node'
|
170 | def graph = new GradleGraph(rootId)
|
171 | def currentChain = new HashSet()
|
172 | loadGraph(deps, graph, rootId, currentChain)
|
173 |
|
174 | return graph.nodes
|
175 | }
|
176 |
|
177 | def getGradleGraphWithSha1Map(Iterable deps, Sha1Map sha1Map) {
|
178 | if (!sha1Map) sha1Map = new Sha1Map()
|
179 | def rootId = 'root-node'
|
180 | def graph = new GradleGraph(rootId)
|
181 | def currentChain = new HashSet()
|
182 | loadSha1MapGraph(deps, graph, rootId, currentChain, sha1Map)
|
183 |
|
184 | return [graph: graph.nodes, sha1Map: sha1Map]
|
185 | }
|
186 |
|
187 | def debugLog(msg) {
|
188 | def debug = System.getenv('DEBUG') ?: ''
|
189 | if (debug.length() > 0) {
|
190 | println("SNYKECHO $msg")
|
191 | }
|
192 | }
|
193 |
|
194 | def matchesAttributeFilter(conf, confAttrSpec) {
|
195 | if (!conf.hasProperty('attributes')) {
|
196 |
|
197 | return true
|
198 | }
|
199 | def matches = true
|
200 | def attrs = conf.attributes
|
201 | attrs.keySet().each({ attr ->
|
202 | def attrValueAsString = attrs.getAttribute(attr).toString().toLowerCase()
|
203 | confAttrSpec.each({ keyValueFilter ->
|
204 |
|
205 | if (attr.name.toLowerCase().contains(keyValueFilter[0]) && attrValueAsString != keyValueFilter[1]) {
|
206 | matches = false
|
207 | }
|
208 | })
|
209 | })
|
210 | return matches
|
211 | }
|
212 |
|
213 | def findMatchingConfigs(confs, confNameFilter, confAttrSpec) {
|
214 | def matching = confs.findAll({ it.name =~ confNameFilter })
|
215 | if (confAttrSpec == null) {
|
216 |
|
217 | return matching
|
218 | }
|
219 | return matching.findAll({ matchesAttributeFilter(it, confAttrSpec) })
|
220 | }
|
221 |
|
222 | def findProjectConfigs(proj, confNameFilter, confAttrSpec) {
|
223 | def matching = findMatchingConfigs(proj.configurations, confNameFilter, confAttrSpec)
|
224 | if (GradleVersion.current() < GradleVersion.version('3.0')) {
|
225 | proj.configurations.each({ debugLog("conf.name=$it.name") })
|
226 | return matching
|
227 | }
|
228 | proj.configurations.each({ debugLog("conf.name=$it.name; conf.canBeResolved=$it.canBeResolved; conf.canBeConsumed=$it.canBeConsumed") })
|
229 |
|
230 |
|
231 |
|
232 | def resolvable = []
|
233 | matching.each({ it ->
|
234 | if (!it.canBeResolved) { return }
|
235 | try {
|
236 |
|
237 | it.resolvedConfiguration
|
238 | resolvable.add(it)
|
239 | } catch (Exception ex) {
|
240 |
|
241 | debugLog("Skipping config ${it.name} due to resolvedConfiguration error.")
|
242 | }
|
243 | })
|
244 | debugLog("resolvableConfigs=$resolvable")
|
245 | return resolvable
|
246 | }
|
247 | List getResolvedConfigs(resolvableConfigs){
|
248 | List resolvedConfigs = []
|
249 | resolvableConfigs.each({ config ->
|
250 | ResolvedConfiguration resConf = config.getResolvedConfiguration()
|
251 | debugLog("config `$config.name' resolution has errors: ${resConf.hasError()}")
|
252 | if (!resConf.hasError()) {
|
253 | resolvedConfigs.add(resConf)
|
254 | debugLog("Fully resolved config `$config.name' with deps: $resConf.firstLevelModuleDependencies")
|
255 | } else {
|
256 |
|
257 | LenientConfiguration lenientConf = resConf.getLenientConfiguration()
|
258 | debugLog("Partially resolved config `$config.name' with: $lenientConf.firstLevelModuleDependencies")
|
259 | debugLog("Couldn't resolve: ${lenientConf.getUnresolvedModuleDependencies()}")
|
260 | resolvedConfigs.add(lenientConf)
|
261 | }
|
262 | })
|
263 | return resolvedConfigs
|
264 | }
|
265 |
|
266 | String formatPath(path) {
|
267 | return path.replace(':', '/').replaceAll(~/(^\/+?)|(\/+$)/, '')
|
268 | }
|
269 |
|
270 | Boolean isRootPath(path){
|
271 | return path == rootProject.path
|
272 | }
|
273 |
|
274 |
|
275 |
|
276 |
|
277 | def snykDepsConfExecuted = false
|
278 | allprojects { Project currProj ->
|
279 | debugLog("Current project: $currProj.name")
|
280 | String onlyProj = project.hasProperty('onlySubProject') ? onlySubProject : null
|
281 | def confNameFilter = (project.hasProperty('configuration')
|
282 | ? Pattern.compile(configuration, Pattern.CASE_INSENSITIVE)
|
283 | : /.*/
|
284 | )
|
285 | def confAttrSpec = (project.hasProperty('confAttr')
|
286 | ? confAttr.toLowerCase().split(',').collect { it.split(':') }
|
287 | : null
|
288 | )
|
289 |
|
290 | task snykResolvedDepsJson {
|
291 | doLast { task ->
|
292 | if (snykDepsConfExecuted) {
|
293 | return
|
294 | }
|
295 |
|
296 | snykDepsConfExecuted = true
|
297 | debugLog('snykResolvedDepsJson task is executing via doLast')
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 | def allConfigurationAttributes = [:]
|
306 | def attributesAsStrings = [:]
|
307 | rootProject.allprojects.each { proj ->
|
308 | findMatchingConfigs(proj.configurations, confNameFilter, confAttrSpec)
|
309 | .each { conf ->
|
310 | def attrs = conf.attributes
|
311 | attrs.keySet().toList().each({ attr ->
|
312 | def value = attrs.getAttribute(attr)
|
313 | if (!allConfigurationAttributes.containsKey(attr)) {
|
314 | allConfigurationAttributes[attr] = new HashSet()
|
315 | attributesAsStrings[attr.name] = new HashSet()
|
316 | }
|
317 | allConfigurationAttributes[attr].add(value)
|
318 | attributesAsStrings[attr.name].add(value.toString())
|
319 | })
|
320 | }
|
321 | }
|
322 |
|
323 | String defaultProjectName = task.project.name
|
324 |
|
325 | String defaultProjectKey = isRootPath(task.project.path) ? defaultProjectName : formatPath(task.project.path)
|
326 |
|
327 | List allSubProjectNames = []
|
328 | allprojects
|
329 | .findAll({ it.path != task.project.path })
|
330 | .each({
|
331 | String projKey = formatPath(it.path)
|
332 | allSubProjectNames.add(projKey)
|
333 | })
|
334 | def shouldScanProject = {
|
335 | onlyProj == null ||
|
336 | (onlyProj == '.' && it.name == defaultProjectName) ||
|
337 | it.name == onlyProj ||
|
338 | formatPath(it.path) == onlyProj
|
339 | }
|
340 | def projectsDict = [:]
|
341 |
|
342 | debugLog("defaultProjectName=$defaultProjectName; defaultProjectKey=$defaultProjectKey; allSubProjectNames=$allSubProjectNames")
|
343 |
|
344 |
|
345 |
|
346 | def jsonAttrs = JsonOutput.toJson(attributesAsStrings)
|
347 | println("JSONATTRS $jsonAttrs")
|
348 |
|
349 | rootProject.allprojects.findAll(shouldScanProject).each { proj ->
|
350 | debugLog("processing project: name=$proj.name; path=$proj.path")
|
351 |
|
352 | def resolvableConfigs = findProjectConfigs(proj, confNameFilter, confAttrSpec)
|
353 | List resolvedConfigs = getResolvedConfigs(resolvableConfigs)
|
354 |
|
355 | if (resolvedConfigs.isEmpty() && !resolvableConfigs.isEmpty()) {
|
356 | throw new RuntimeException('Configurations: ' + resolvableConfigs.collect { it.name } +
|
357 | ' for project ' + proj + ' could not be resolved.')
|
358 | }
|
359 | List nonemptyFirstLevelDeps = []
|
360 | resolvedConfigs.each { nonemptyFirstLevelDeps.addAll(it.getFirstLevelModuleDependencies()) }
|
361 |
|
362 | debugLog("non-empty first level deps for project `$proj.name': $nonemptyFirstLevelDeps")
|
363 | debugLog('converting gradle graph to snyk-graph format')
|
364 |
|
365 | def projGraph = getGradleGraph(nonemptyFirstLevelDeps)
|
366 | String projKey = formatPath(proj.path)
|
367 |
|
368 | if (projKey == "") {
|
369 | debugLog("project path is empty (proj.path=$proj.path)! will use defaultProjectName=$defaultProjectName")
|
370 | projKey = defaultProjectKey
|
371 | }
|
372 |
|
373 | projectsDict[projKey] = [
|
374 | 'targetFile': findProject(proj.path).buildFile.toString(),
|
375 | 'gradleGraph': projGraph,
|
376 | 'projectVersion': proj.version.toString()
|
377 | ]
|
378 | }
|
379 |
|
380 | def result = [
|
381 | 'defaultProject': defaultProjectName,
|
382 | 'defaultProjectKey': defaultProjectKey,
|
383 | 'projects': projectsDict,
|
384 | 'allSubProjectNames': allSubProjectNames
|
385 | ]
|
386 | def jsonDeps = JsonOutput.toJson(result)
|
387 | println("JSONDEPS $jsonDeps")
|
388 | }
|
389 | }
|
390 |
|
391 | task snykNormalizedResolvedDepsJson {
|
392 |
|
393 | doLast { task ->
|
394 | if (snykDepsConfExecuted) {
|
395 | return
|
396 | }
|
397 |
|
398 | snykDepsConfExecuted = true
|
399 | debugLog('snykNormalizedResolvedDepsJson task is executing via doLast')
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 | def allConfigurationAttributes = [:]
|
408 | def attributesAsStrings = [:]
|
409 | rootProject.allprojects.each { proj ->
|
410 | findMatchingConfigs(proj.configurations, confNameFilter, confAttrSpec)
|
411 | .each { Configuration conf ->
|
412 | def attrs = conf.attributes
|
413 | attrs.keySet().each({ attr ->
|
414 | def value = attrs.getAttribute(attr)
|
415 | if (!allConfigurationAttributes.containsKey(attr)) {
|
416 | allConfigurationAttributes[attr] = new HashSet()
|
417 | attributesAsStrings[attr.name] = new HashSet()
|
418 | }
|
419 | allConfigurationAttributes[attr].add(value)
|
420 | attributesAsStrings[attr.name].add(value.toString())
|
421 | })
|
422 | }
|
423 | }
|
424 |
|
425 | String defaultProjectName = task.project.name
|
426 | String defaultProjectKey = isRootPath(task.project.path) ? defaultProjectName : formatPath(task.project.path)
|
427 | List allSubProjectNames = []
|
428 | allprojects
|
429 | .findAll({ it.path != task.project.path })
|
430 | .each({
|
431 | String projKey = formatPath(it.path)
|
432 | allSubProjectNames.add(projKey)
|
433 | })
|
434 | def shouldScanProject = {
|
435 | onlyProj == null ||
|
436 | (onlyProj == '.' && it.name == defaultProjectName) ||
|
437 | it.name == onlyProj ||
|
438 | formatPath(it.path) == onlyProj
|
439 | }
|
440 | def projectsDict = [:]
|
441 |
|
442 | debugLog("defaultProjectName=$defaultProjectName; allSubProjectNames=$allSubProjectNames")
|
443 |
|
444 |
|
445 |
|
446 | def jsonAttrs = JsonOutput.toJson(attributesAsStrings)
|
447 | println("JSONATTRS $jsonAttrs")
|
448 |
|
449 | def sha1Map = new Sha1Map()
|
450 |
|
451 | rootProject.allprojects.findAll(shouldScanProject).each { proj ->
|
452 | debugLog("processing project: name=$proj.name; path=$proj.path")
|
453 |
|
454 | def resolvableConfigs = findProjectConfigs(proj, confNameFilter, confAttrSpec)
|
455 | List resolvedConfigs = getResolvedConfigs(resolvableConfigs)
|
456 | if (!resolvableConfigs.isEmpty() && resolvedConfigs.isEmpty()) {
|
457 | throw new RuntimeException('Configurations: ' + resolvableConfigs.collect { it.name } +
|
458 | ' for project ' + proj + ' could not be resolved.')
|
459 | }
|
460 |
|
461 | List nonemptyFirstLevelDeps = []
|
462 | resolvedConfigs.each { nonemptyFirstLevelDeps.addAll(it.getFirstLevelModuleDependencies()) }
|
463 |
|
464 | debugLog("non-empty first level deps for project `$proj.name': $nonemptyFirstLevelDeps")
|
465 | debugLog('converting gradle graph to snyk-graph format')
|
466 |
|
467 | def projGraph = getGradleGraphWithSha1Map(nonemptyFirstLevelDeps, sha1Map)
|
468 | String projKey = formatPath(proj.path)
|
469 |
|
470 | if (projKey == "") {
|
471 | debugLog("project path is empty (proj.path=$proj.path)! will use defaultProjectName=$defaultProjectName")
|
472 | projKey = defaultProjectKey
|
473 | }
|
474 |
|
475 | projectsDict[projKey] = [
|
476 | 'targetFile': findProject(proj.path).buildFile.toString(),
|
477 | 'gradleGraph': projGraph.graph,
|
478 | 'projectVersion': proj.version.toString()
|
479 | ]
|
480 | }
|
481 |
|
482 | def result = [
|
483 | 'defaultProject': defaultProjectName,
|
484 | 'defaultProjectKey': defaultProjectKey,
|
485 | 'projects': projectsDict,
|
486 | 'allSubProjectNames': allSubProjectNames,
|
487 | 'sha1Map': sha1Map.coordinates
|
488 | ]
|
489 |
|
490 | def jsonDeps = JsonOutput.toJson(result)
|
491 | println("JSONDEPS $jsonDeps")
|
492 | }
|
493 | }
|
494 | }
|