File: /home/frenchy/www/french-american.org/current/node_modules/snyk-gradle-plugin/lib/init.gradle
import groovy.json.JsonOutput
import java.util.regex.Pattern
import java.util.regex.Matcher
// Snyk dependency resolution script for Gradle.
// Tested on Gradle versions from 2.14 to 5.4.1
// This script does the following: for all the projects in the build file,
// generate a merged configuration of all the available configurations,
// and then list the dependencies as a tree.
// It's the responsibility of the caller to pick the project(s) they are
// interested in from the results.
// CLI usages:
// gradle -q -I init.gradle snykResolvedDepsJson
// gradle -q -I init.gradle snykResolvedDepsJson -Pconfiguration=specificConf -PonlySubProject=sub-project
// gradle -q -I init.gradle snykResolvedDepsJson -Pconfiguration=confNameRegex -PconfAttr=buildtype:debug,usage:java-runtime
// (-q to have clean output, -P supplies args as per https://stackoverflow.com/a/48370451)
// confAttr parameter (supported only in Gradle 3+) is used to perform attribute-based dependency variant matching
// (important for Android: https://developer.android.com/studio/build/dependencies#variant_aware)
// Its value is a comma-separated list of key:value pairs. The "key" is a case-insensitive substring
// of the class name of the attribute (e.g. "buildtype" would match com.android.build.api.attributes.BuildTypeAttr),
// the value should be a case-insensitive stringified value of the attribute
// Output format:
//
// Since Gradle is chatty and often prints a "Welcome" banner even with -q option,
// the only output lines that matter are:
// - prefixed "SNYKECHO ": should be immediately printed as debug information by the caller
// - prefixed "JSONDEPS ": JSON representation of the dependencies trees for all projects in the following format
// interface JsonDepsScriptResult {
// defaultProject: string;
// projects: ProjectsDict;
// allSubProjectNames: string[];
// }
// interface ProjectsDict {
// [project: string]: GradleProjectInfo;
// }
// interface GradleProjectInfo {
// depDict: DepDict;
// targetFile: string;
// }
// export interface DepDict {
// [name: string]: DepTree;
// }
// interface DepTree {
// name: string;
// version: string;
// dependencies?: DepDict;
// }
// We are attaching this task to every project, as this is the only reliable way to run it
// when we start with a subproject build.gradle. As a consequence, we need to make sure we
// only ever run it once, for the "starting" project.
def snykMergedDepsConfExecuted = false
allprojects { everyProj ->
task snykResolvedDepsJson {
def onlyProj = project.hasProperty('onlySubProject') ? onlySubProject : null
def confNameFilter = (project.hasProperty('configuration')
? Pattern.compile(configuration, Pattern.CASE_INSENSITIVE)
: /.*/
)
def confAttrSpec = (project.hasProperty('confAttr')
? confAttr.toLowerCase().split(',').collect { it.split(':') }
: null
)
// currentChain is a protection against dependency cycles, which are perfectly normal in Java/Gradle world
// This function converts a Gradle dependency tree into DepTree structure used by Snyk CLI
def depsToDict
depsToDict = { Iterable deps, Set currentChain ->
def res = [:]
deps.each { d ->
def depName = d.moduleGroup + ':' + d.moduleName
if (!currentChain.contains(depName)) {
def row = ['name': depName, 'version': d.moduleVersion, 'conf': d.configuration]
currentChain.add(depName)
def subDeps = depsToDict(d.children, currentChain)
currentChain.remove(depName)
if (subDeps.size() > 0) {
row['dependencies'] = subDeps
}
// In Gradle 2, there can be several instances of the same dependency present at each level,
// each for a different configuration. In this case, we need to merge the dependencies.
if (res.containsKey(depName)) {
if (subDeps.size() > 0) {
if (!res[depName].containsKey('dependencies')) {
res[depName]['dependencies'] = [:]
}
res[depName]['dependencies'].putAll(subDeps)
}
} else {
res[depName] = row
}
}
}
return res
}
def matchesAttributeFilter
matchesAttributeFilter = { conf ->
if (!conf.hasProperty('attributes')) {
// Gradle before version 3 does not support attributes
return true
}
def matches = true
def attrs = conf.attributes
attrs.keySet().each({ attr ->
def attrValueAsString = attrs.getAttribute(attr).toString().toLowerCase()
confAttrSpec.each({ keyValueFilter ->
// attr.name is a class name, e.g. com.android.build.api.attributes.BuildTypeAttr
if (attr.name.toLowerCase().contains(keyValueFilter[0])
&& attrValueAsString != keyValueFilter[1]) {
matches = false
}
})
})
return matches
}
def isMatchingConfiguration
isMatchingConfiguration = {
it.name != 'snykMergedDepsConf' && it.name =~ confNameFilter && matchesAttributeFilter(conf)
}
doLast { task ->
if (!snykMergedDepsConfExecuted) {
println('SNYKECHO snykResolvedDepsJson task is executing via doLast')
def projectsDict = [:]
def defaultProjectName = task.project.name
def result = [
'defaultProject': defaultProjectName,
'projects': projectsDict,
'allSubProjectNames': allprojects.collect { it.name }
]
def shouldScanProject = {
onlyProj == null ||
(onlyProj == '.' && it.name == defaultProjectName) ||
it.name == onlyProj
}
// First pass: scan all configurations that match the attribute filter and collect all attributes
// from them, to use unambiguous values of the attributes on the merged configuration.
//
// Why we need to scan all sub-projects: if a project A depends on B, and only B has some
// configurations with attribute C, we still might need attribute C in our configuration
// when resolving the project A, so that it selects a concrete variant of dependency B.
def allConfigurationAttributes = [:] // Map<Attribute<?>, Set<?>>
def attributesAsStrings = [:] // Map<String, Set<string>>
rootProject.allprojects.each { proj ->
proj.configurations.findAll({ it.name != 'snykMergedDepsConf' && it.name =~ confNameFilter && matchesAttributeFilter(it) }).each { conf ->
if (!conf.hasProperty('attributes')) {
// Gradle before version 3 does not support attributes
return
}
def attrs = conf.attributes
attrs.keySet().each({ attr ->
def value = attrs.getAttribute(attr)
if (!allConfigurationAttributes.containsKey(attr)) {
allConfigurationAttributes[attr] = new HashSet()
attributesAsStrings[attr.name] = new HashSet()
}
allConfigurationAttributes[attr].add(value)
attributesAsStrings[attr.name].add(value.toString())
})
}
}
// These will be used to suggest attribute filtering to the user if the scan fails
// due to ambiguous resolution of dependency variants
println("JSONATTRS " + JsonOutput.toJson(attributesAsStrings))
rootProject.allprojects.findAll(shouldScanProject).each { proj ->
println('SNYKECHO processing project: ' + proj.name)
def snykConf = null
def mergeableConfs = proj.configurations
.findAll({ it.name != 'snykMergedDepsConf' && it.name =~ confNameFilter })
// Drop all the configrations that don't match the attribute filter
if (confAttrSpec != null) {
mergeableConfs = mergeableConfs.findAll(matchesAttributeFilter)
}
if (mergeableConfs.size() == 0 && proj.configurations.size() > 0) {
throw new RuntimeException('Matching configurations not found: ' + confNameFilter +
', available configurations for project ' + proj + ': '
+ proj.configurations.collect { it.name })
} else if (mergeableConfs.size() == 1) {
// We use the only matching configuration, with its attributes.
snykConf = mergeableConfs.first()
} else if (mergeableConfs.size() > 1) {
println('SNYKECHO constructing merged configuration from ' + mergeableConfs.collect { conf -> conf.name })
// We create a new, "merged" configuration here.
snykConf = proj.configurations.create('snykMergedDepsConf')
mergeableConfs.each { snykConf.extendsFrom(it) }
// Copy all the unambiguous build attributes into the merged configuration
// Gradle before version 3 does not support attributes
if (snykConf.hasProperty('attributes')) {
allConfigurationAttributes.each({ attr, valueSet ->
if (valueSet.size() == 1) {
snykConf.attributes.attribute(attr, valueSet.head())
}
})
}
}
if (snykConf != null) {
println('SNYKECHO resolving configuration ' + snykConf.name)
def gradleDeps = snykConf.resolvedConfiguration.firstLevelModuleDependencies
println('SNYKECHO converting the dependency graph to the DepTree format')
projectsDict[proj.name] = [
'targetFile': findProject(proj.path).buildFile.toString(),
'depDict': depsToDict(gradleDeps, new HashSet())
]
} else {
projectsDict[proj.name] = [
'targetFile': findProject(proj.path).buildFile.toString()
]
}
}
println("JSONDEPS " + JsonOutput.toJson(result))
snykMergedDepsConfExecuted = true
}
}
}
}