Skip to content

Commit 9b9663d

Browse files
balrokCarl Mai
authored and
Carl Mai
committed
implement 'requireAllLicensesAllowed' config option to change how dependency licenses are matched against allowedLicenses
The default behavior is that a dependency is fine when any of its licenses are found inside allowedLicenses. This may miss dependencies, which contain multiple licenses. When 'requireAllLicensesAllowed' is set to true, it will only approve a dependency when all of its discovered licenses are found in the allowedLicenses. This may report false-positives for dependencies which are dual-licensed. But in general I think a false-positive is better than missing a license violation. This fixes #285
1 parent 135292c commit 9b9663d

File tree

5 files changed

+249
-59
lines changed

5 files changed

+249
-59
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ licenseReport {
8585
// This is for the allowed-licenses-file in checkLicense Task
8686
// Accepts File, URL or String path to local or remote file
8787
allowedLicensesFile = new File("$projectDir/config/allowed-licenses.json")
88+
89+
// When false is set, a dependency is good, if any of its licenses are matched with allowedLicenses
90+
// When true is set, a dependency is good, if all of its licenses are matched with allowedLicenses
91+
// default is false, but true is recommended.
92+
requireAllLicensesAllowed = false
8893
}
8994
```
9095

src/main/groovy/com/github/jk1/license/LicenseReportExtension.groovy

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class LicenseReportExtension {
4141
public String[] excludeGroups
4242
public String[] excludes
4343
public Object allowedLicensesFile
44+
public boolean requireAllLicensesAllowed
4445

4546
LicenseReportExtension(Project project) {
4647
unionParentPomLicenses = true
@@ -55,6 +56,7 @@ class LicenseReportExtension {
5556
excludes = []
5657
importers = []
5758
filters = []
59+
requireAllLicensesAllowed = false
5860
}
5961

6062
@Nested
@@ -104,6 +106,8 @@ class LicenseReportExtension {
104106
snapshot += excludes
105107
snapshot << 'unionParentPomLicenses'
106108
snapshot += unionParentPomLicenses
109+
snapshot << "requireAllLicensesAllowed"
110+
snapshot += requireAllLicensesAllowed
107111
snapshot.join("!")
108112
}
109113

src/main/groovy/com/github/jk1/license/check/LicenseChecker.groovy

+59-37
Original file line numberDiff line numberDiff line change
@@ -18,70 +18,92 @@ package com.github.jk1.license.check
1818
import groovy.json.JsonOutput
1919
import org.gradle.api.GradleException
2020

21+
/**
22+
* This class compares the found licences with the allowed licenses and creates a report for any missing license
23+
*/
2124
class LicenseChecker {
22-
23-
void checkAllDependencyLicensesAreAllowed(
24-
Object allowedLicensesFile, File projectLicensesDataFile, File notPassedDependenciesOutputFile) {
25+
static void checkAllDependencyLicensesAreAllowed(
26+
Object allowedLicensesFile,
27+
File projectLicensesDataFile,
28+
boolean requireAllLicensesAllowed,
29+
File notPassedDependenciesOutputFile) {
2530
List<Dependency> allDependencies = LicenseCheckerFileReader.importDependencies(projectLicensesDataFile)
31+
removeNullLicenses(allDependencies)
2632
List<AllowedLicense> allowedLicenses = LicenseCheckerFileReader.importAllowedLicenses(allowedLicensesFile)
27-
List<Dependency> notPassedDependencies = searchForNotAllowedDependencies(allDependencies, allowedLicenses)
33+
List<Tuple2<Dependency, List<ModuleLicense>>> notPassedDependencies = getNotAllowedLicenses(allDependencies, allowedLicenses)
34+
if (!requireAllLicensesAllowed) {
35+
// when we do not check for all Licenses allowed, we can filter out all dependencies here which had a partial match:
36+
// this means, when the size of notPassedLicenses differs, at least one license matched with our allowed-list
37+
notPassedDependencies = notPassedDependencies.findAll { it.get(0).moduleLicenses == null || it.get(1).size() == it.get(0).moduleLicenses.size() }
38+
}
2839
generateNotPassedDependenciesFile(notPassedDependencies, notPassedDependenciesOutputFile)
2940

3041
if (!notPassedDependencies.isEmpty()) {
31-
throw new GradleException("Some library licenses are not allowed.\n" +
32-
"Read [$notPassedDependenciesOutputFile.path] for more information.")
42+
throw new GradleException("Some library licenses are not allowed:\n" +
43+
"$notPassedDependenciesOutputFile.text\n\n" +
44+
"Read [$notPassedDependenciesOutputFile.path] for more information.")
3345
}
3446
}
3547

36-
private List<Dependency> searchForNotAllowedDependencies(
37-
List<Dependency> dependencies, List<AllowedLicense> allowedLicenses) {
38-
return dependencies.findAll { !isDependencyHasAllowedLicense(it, allowedLicenses) }
39-
}
40-
41-
private void generateNotPassedDependenciesFile(
42-
List<Dependency> notPassedDependencies, File notPassedDependenciesOutputFile) {
43-
notPassedDependenciesOutputFile.text =
44-
JsonOutput.prettyPrint(JsonOutput.toJson(
45-
["dependenciesWithoutAllowedLicenses": notPassedDependencies.collect { toAllowedLicenseList(it) }.flatten()]))
48+
/**
49+
* removes 'null'-licenses from dependencies which have at least one more license
50+
*/
51+
private static void removeNullLicenses(List<Dependency> dependencies) {
52+
for (Dependency dependency : dependencies) {
53+
if (dependency.moduleLicenses.any { it.moduleLicense == null } && !dependency.moduleLicenses.every { it.moduleLicense == null }) {
54+
dependency.moduleLicenses = dependency.moduleLicenses.findAll { it.moduleLicense != null }
55+
}
56+
}
4657
}
4758

48-
private boolean isDependencyHasAllowedLicense(Dependency dependency, List<AllowedLicense> allowedLicenses) {
49-
for(allowedLicense in allowedLicenses) {
50-
if (isDependencyMatchesAllowedLicense(dependency, allowedLicense)) return true
59+
private static List<Tuple2<Dependency, List<ModuleLicense>>> getNotAllowedLicenses(List<Dependency> dependencies, List<AllowedLicense> allowedLicenses) {
60+
List<Tuple2<Dependency, List<ModuleLicense>>> result = new ArrayList<>()
61+
for (Dependency dependency : dependencies) {
62+
List<AllowedLicense> perDependencyAllowedLicenses = allowedLicenses.findAll { isDependencyNameMatchesAllowedLicense(dependency, it) && isDependencyVersionMatchesAllowedLicense(dependency, it) }
63+
// allowedLicense matches anything, so we don't need to further check
64+
if (perDependencyAllowedLicenses.any { it.moduleLicense == null || it.moduleLicense == ".*" }) {
65+
continue
66+
}
67+
def notAllowedLicenses = dependency.moduleLicenses.findAll { !isDependencyLicenseMatchesAllowedLicense(it, perDependencyAllowedLicenses) }
68+
if (!notAllowedLicenses.isEmpty()) {
69+
result.add(Tuple2.of(dependency, notAllowedLicenses))
70+
}
5171
}
52-
return false
72+
return result
5373
}
5474

55-
private boolean isDependencyMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
56-
return isDependencyNameMatchesAllowedLicense(dependency, allowedLicense) &&
57-
isDependencyLicenseMatchesAllowedLicense(dependency, allowedLicense) &&
58-
isDependencyVersionMatchesAllowedLicense(dependency, allowedLicense)
75+
private static void generateNotPassedDependenciesFile(
76+
List<Tuple2<Dependency, List<ModuleLicense>>> notPassedDependencies, File notPassedDependenciesOutputFile) {
77+
notPassedDependenciesOutputFile.text =
78+
JsonOutput.prettyPrint(JsonOutput.toJson(
79+
["dependenciesWithoutAllowedLicenses": notPassedDependencies.collect { toAllowedLicenseList(it.get(0), it.get(1)) }.flatten()]))
5980
}
6081

61-
private boolean isDependencyNameMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
82+
private static boolean isDependencyNameMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
6283
return dependency.moduleName ==~ allowedLicense.moduleName || allowedLicense.moduleName == null ||
63-
dependency.moduleName == allowedLicense.moduleName
84+
dependency.moduleName == allowedLicense.moduleName
6485
}
6586

66-
private boolean isDependencyVersionMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
87+
private static boolean isDependencyVersionMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
6788
return dependency.moduleVersion ==~ allowedLicense.moduleVersion || allowedLicense.moduleVersion == null ||
68-
dependency.moduleVersion == allowedLicense.moduleVersion
89+
dependency.moduleVersion == allowedLicense.moduleVersion
6990
}
7091

71-
private boolean isDependencyLicenseMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
72-
if (allowedLicense.moduleLicense == null || allowedLicense.moduleLicense == ".*") return true
92+
private static boolean isDependencyLicenseMatchesAllowedLicense(ModuleLicense moduleLicense, List<AllowedLicense> allowedLicenses) {
93+
for (AllowedLicense allowedLicense : allowedLicenses) {
94+
if (allowedLicense.moduleLicense == null || allowedLicense.moduleLicense == ".*") return true
7395

74-
for (moduleLicenses in dependency.moduleLicenses)
75-
if (moduleLicenses.moduleLicense ==~ allowedLicense.moduleLicense ||
76-
moduleLicenses.moduleLicense == allowedLicense.moduleLicense) return true
96+
if (moduleLicense.moduleLicense ==~ allowedLicense.moduleLicense ||
97+
moduleLicense.moduleLicense == allowedLicense.moduleLicense) return true
98+
}
7799
return false
78100
}
79101

80-
private List<AllowedLicense> toAllowedLicenseList(Dependency dependency) {
81-
if (dependency.moduleLicenses.isEmpty()) {
82-
return [ new AllowedLicense(dependency.moduleName, dependency.moduleVersion, null) ]
102+
private static List<AllowedLicense> toAllowedLicenseList(Dependency dependency, List<ModuleLicense> moduleLicenses) {
103+
if (moduleLicenses.isEmpty()) {
104+
return [new AllowedLicense(dependency.moduleName, dependency.moduleVersion, null)]
83105
} else {
84-
return dependency.moduleLicenses.collect { new AllowedLicense(dependency.moduleName, dependency.moduleVersion, it.moduleLicense) }
106+
return moduleLicenses.findAll { it }.collect { new AllowedLicense(dependency.moduleName, dependency.moduleVersion, it.moduleLicense) }
85107
}
86108
}
87109
}

src/main/groovy/com/github/jk1/license/task/CheckLicenseTask.groovy

+10-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ class CheckLicenseTask extends DefaultTask {
5353
return new File("${config.absoluteOutputDir}/${PROJECT_JSON_FOR_LICENSE_CHECKING_FILE}")
5454
}
5555

56+
@Input
57+
boolean isRequireAllLicensesAllowed() {
58+
return config.requireAllLicensesAllowed
59+
}
60+
5661
@OutputFile
5762
File getNotPassedDependenciesFile() {
5863
new File("${config.absoluteOutputDir}/$NOT_PASSED_DEPENDENCIES_FILE")
@@ -64,6 +69,10 @@ class CheckLicenseTask extends DefaultTask {
6469
LicenseChecker licenseChecker = new LicenseChecker()
6570
LOGGER.info("Check licenses if they are allowed to use.")
6671
licenseChecker.checkAllDependencyLicensesAreAllowed(
67-
getAllowedLicenseFile(), getProjectDependenciesData(), notPassedDependenciesFile)
72+
getAllowedLicenseFile(),
73+
getProjectDependenciesData(),
74+
isRequireAllLicensesAllowed(),
75+
notPassedDependenciesFile
76+
)
6877
}
6978
}

0 commit comments

Comments
 (0)