Edit File: legacy-init.gradle
import groovy.json.JsonOutput import java.util.regex.Pattern import java.util.regex.Matcher import org.gradle.util.GradleVersion // Snyk dependency resolution script for Gradle. // Tested on Gradle versions from v2.14 to v6.8.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 { // depGraph: DepGraph; // snykGraph: { [name: string]: SnykGraph }; // targetFile: string; // } // export interface SnykGraph { // name: string; // version: string; // parentIds: string[]; // } class SnykGraph { def nodes def rootId SnykGraph(rootId) { this.nodes = [:] this.rootId = rootId } def setNode(key, value) { if (!key) { return } if (this.nodes.get(key)) { return this.nodes.get(key) } if (!value) { return } def vertex = ['name': value.name, 'version': value.version, 'parentIds': [] as Set] this.nodes.put(key, vertex) return vertex } def setEdge(parentId, childId) { if (!parentId || !childId || parentId == childId) { return } // root-node will be the graphlib root that first-level deps will be attached to if (parentId != this.rootId) { def parentNode = this.setNode(parentId, null) if (!parentNode) { return } } def childNode = this.setNode(childId, null) if (!childNode || childNode.parentIds.contains(parentId)) { return } childNode.parentIds.add(parentId) } } def loadGraph(Iterable deps, SnykGraph graph, parentId, currentChain) { deps.each { dep -> dep.each { d -> def childId = "${d.moduleGroup}:${d.moduleName}@${d.moduleVersion}" if (!graph.nodes.get(childId)) { def childDependency = ['name': "${d.moduleGroup}:${d.moduleName}", 'version': d.moduleVersion] graph.setNode(childId, childDependency) } // 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 (!currentChain.contains(childId) && d.children) { currentChain.add(childId) loadGraph(d.children, graph, childId, currentChain) } graph.setEdge(parentId, childId) } } } def getSnykGraph(Iterable deps) { def rootId = 'root-node' def graph = new SnykGraph(rootId) def currentChain = new HashSet() loadGraph(deps, graph, rootId, currentChain) return graph.nodes } def configsSuccessfullyResolved(configurations) { def resolvedConfigurations = []; configurations.each({ configuration -> try { configuration.resolvedConfiguration.firstLevelModuleDependencies; resolvedConfigurations.add(configuration); } catch(Exception ex) { // Swallowing it } }) return resolvedConfigurations; } // 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 snykDepsConfExecuted = 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 ) 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 } doLast { task -> if (!snykDepsConfExecuted) { println('SNYKECHO snykResolvedDepsJson task is executing via doLast') def projectsDict = [:] def defaultProjectName = task.project.name def allSubProjectNames = [] allprojects.each { project -> if(project.name != defaultProjectName) { allSubProjectNames.add(project.name) } } def result = [ 'defaultProject': defaultProjectName, 'projects': projectsDict, 'allSubProjectNames': allSubProjectNames ] 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 =~ 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 projectConfigs = null def filteredProjectConfigs = null // Gradle v3.0+ contains concepts as attributes, config canBeResolved, that does not exist in legacy versions final GradleVersion gradleVersionInUse = GradleVersion.current(); final GradleVersion gradleVersion3 = GradleVersion.version('3.0'); final isGradleVersion3Plus = gradleVersionInUse >= gradleVersion3; if(isGradleVersion3Plus) { // We are looking for a configuration that `canBeResolved`, because it's a configuration for which // we can compute a dependency graph and that contains all the necessary information for resolution to happen. if (confAttrSpec != null) { // Drop all the configrations that don't match the attribute filter filteredProjectConfigs = proj.configurations.findAll({ it.canBeResolved == true && it.canBeConsumed == false && it.name =~ confNameFilter && matchesAttributeFilter(it) }) if(filteredProjectConfigs.size() == 0) { filteredProjectConfigs = proj.configurations .findAll({ it.canBeResolved == true && it.canBeConsumed == true && it.name =~ confNameFilter && matchesAttributeFilter(it) }) } projectConfigs = configsSuccessfullyResolved(filteredProjectConfigs) } else { // if we cannot find dependencies that can be only resolved but not consumable // we try to find configs that are simultaneously resolvable and consumable // to prevent dependency resolution conflicts (e.g. Cannot choose between the following variants) // we avoid the coexistence of (canBeResolved: true, canBeConsumed: false) and (canBeResolved: true, canBeConsumed: true) configs filteredProjectConfigs = proj.configurations.findAll({ it.canBeResolved == true && it.canBeConsumed == false && it.name =~ confNameFilter }) if(filteredProjectConfigs.size() == 0) { filteredProjectConfigs = proj.configurations .findAll({ it.canBeResolved == true && it.canBeConsumed == true && it.name =~ confNameFilter }) } projectConfigs = configsSuccessfullyResolved(filteredProjectConfigs) } } else { def configsFilteredByConfName = proj.configurations.findAll({ it.name =~ confNameFilter }) projectConfigs = configsSuccessfullyResolved(configsFilteredByConfName) } if (projectConfigs.size() == 0 && proj.configurations.size() > 0) { throw new RuntimeException('Matching configurations not found: ' + confNameFilter + ', available configurations for project ' + proj + ': ' + proj.configurations.collect { it.name }) } if (projectConfigs != null) { println('SNYKECHO resolving configuration ' + projectConfigs.name) def gradleFirstLevelDeps = projectConfigs.resolvedConfiguration.firstLevelModuleDependencies.findAll({ it.size() > 0 }) println('SNYKECHO converting gradle graph to snyk-graph format') projectsDict[proj.name] = [ 'targetFile': findProject(proj.path).buildFile.toString(), 'snykGraph': getSnykGraph(gradleFirstLevelDeps), 'projectVersion': proj.version.toString() ] } else { projectsDict[proj.name] = [ 'targetFile': findProject(proj.path).buildFile.toString() ] } } println('JSONDEPS ' + JsonOutput.toJson(result)) snykDepsConfExecuted = true } } } }
Back to File Manager