Skip to content

Commit cb795e2

Browse files
committed
implement 'LicenseChecker' 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 'AllRequiredLicenseChecker' is set, 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 jk1#285
1 parent 135292c commit cb795e2

File tree

7 files changed

+371
-95
lines changed

7 files changed

+371
-95
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+
// (default) OneRequiredLicenseChecker: a dependency is good, if any of its licenses are matched with allowedLicenses
90+
// AllRequiredLicenseChecker: a dependency is good, if all of its (non-null) licenses are matched with allowedLicenses
91+
// any class implementing LicenseChecker can be provided here
92+
licenseChecker = new com.github.jk1.license.check.OneRequiredLicenseChecker()
8893
}
8994
```
9095

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

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.github.jk1.license
1717

18+
import com.github.jk1.license.check.LicenseChecker
19+
import com.github.jk1.license.check.OneRequiredLicenseChecker
1820
import com.github.jk1.license.filter.DependencyFilter
1921
import com.github.jk1.license.importer.DependencyDataImporter
2022
import com.github.jk1.license.render.ReportRenderer
@@ -41,6 +43,7 @@ class LicenseReportExtension {
4143
public String[] excludeGroups
4244
public String[] excludes
4345
public Object allowedLicensesFile
46+
public LicenseChecker licenseChecker
4447

4548
LicenseReportExtension(Project project) {
4649
unionParentPomLicenses = true
@@ -55,6 +58,7 @@ class LicenseReportExtension {
5558
excludes = []
5659
importers = []
5760
filters = []
61+
licenseChecker = new OneRequiredLicenseChecker()
5862
}
5963

6064
@Nested
@@ -104,6 +108,8 @@ class LicenseReportExtension {
104108
snapshot += excludes
105109
snapshot << 'unionParentPomLicenses'
106110
snapshot += unionParentPomLicenses
111+
snapshot << "licenseChecker"
112+
snapshot += licenseChecker.class.name
107113
snapshot.join("!")
108114
}
109115

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2018 Evgeny Naumenko <[email protected]>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.jk1.license.check
17+
18+
/**
19+
* All licenses of a dependency must be found inside allowedLicenses to pass.
20+
*/
21+
class AllRequiredLicenseChecker implements LicenseChecker {
22+
@Override
23+
List<Tuple2<Dependency, List<ModuleLicense>>> checkAllDependencyLicensesAreAllowed(List<AllowedLicense> allowedLicenses, List<Dependency> allDependencies) {
24+
removeNullLicenses(allDependencies)
25+
List<Tuple2<Dependency, List<ModuleLicense>>> result = new ArrayList<>()
26+
for (Dependency dependency : (allDependencies)) {
27+
List<AllowedLicense> perDependencyAllowedLicenses = allowedLicenses.findAll { isDependencyNameMatchesAllowedLicense(dependency, it) && isDependencyVersionMatchesAllowedLicense(dependency, it) }
28+
// allowedLicense matches anything, so we don't need to further check
29+
if (perDependencyAllowedLicenses.any { it.moduleLicense == null || it.moduleLicense == ".*" }) {
30+
continue
31+
}
32+
List<ModuleLicense> notAllowedLicenses = dependency.moduleLicenses.findAll { !isDependencyLicenseMatchesAllowedLicense(it, perDependencyAllowedLicenses) }
33+
if (!notAllowedLicenses.isEmpty()) {
34+
result.add(new Tuple2(dependency, notAllowedLicenses))
35+
}
36+
}
37+
return result
38+
}
39+
40+
private static boolean isDependencyNameMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
41+
return dependency.moduleName ==~ allowedLicense.moduleName || allowedLicense.moduleName == null || dependency.moduleName == allowedLicense.moduleName
42+
}
43+
44+
private static boolean isDependencyVersionMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
45+
return dependency.moduleVersion ==~ allowedLicense.moduleVersion || allowedLicense.moduleVersion == null || dependency.moduleVersion == allowedLicense.moduleVersion
46+
}
47+
48+
private static boolean isDependencyLicenseMatchesAllowedLicense(ModuleLicense moduleLicense, List<AllowedLicense> allowedLicenses) {
49+
for (AllowedLicense allowedLicense : allowedLicenses) {
50+
if (allowedLicense.moduleLicense == null || allowedLicense.moduleLicense == ".*") return true
51+
52+
if (moduleLicense.moduleLicense ==~ allowedLicense.moduleLicense || moduleLicense.moduleLicense == allowedLicense.moduleLicense) return true
53+
}
54+
return false
55+
}
56+
57+
/**
58+
* removes 'null'-licenses from dependencies which have at least one more license
59+
*/
60+
private static void removeNullLicenses(List<Dependency> dependencies) {
61+
for (Dependency dependency : dependencies) {
62+
if (dependency.moduleLicenses.any { it.moduleLicense == null } && !dependency.moduleLicenses.every {
63+
it.moduleLicense == null
64+
}) {
65+
dependency.moduleLicenses = dependency.moduleLicenses.findAll { it.moduleLicense != null }
66+
}
67+
}
68+
}
69+
}

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

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

21-
class LicenseChecker {
21+
/**
22+
* This class compares the found licences with the allowed licenses and creates a report for any missing license
23+
*/
24+
interface LicenseChecker {
25+
List<Tuple2<Dependency, List<ModuleLicense>>> checkAllDependencyLicensesAreAllowed(
26+
List<AllowedLicense> allowedLicenses,
27+
List<Dependency> allDependencies)
2228

23-
void checkAllDependencyLicensesAreAllowed(
24-
Object allowedLicensesFile, File projectLicensesDataFile, File notPassedDependenciesOutputFile) {
25-
List<Dependency> allDependencies = LicenseCheckerFileReader.importDependencies(projectLicensesDataFile)
26-
List<AllowedLicense> allowedLicenses = LicenseCheckerFileReader.importAllowedLicenses(allowedLicensesFile)
27-
List<Dependency> notPassedDependencies = searchForNotAllowedDependencies(allDependencies, allowedLicenses)
28-
generateNotPassedDependenciesFile(notPassedDependencies, notPassedDependenciesOutputFile)
29+
default void checkAllDependencyLicensesAreAllowed(
30+
Object allowedLicensesFile, File projectLicensesDataFile, File notPassedDependenciesOutputFile) {
31+
def notPassedDependencies = checkAllDependencyLicensesAreAllowed(
32+
parseAllowedLicenseFile(allowedLicensesFile), getProjectDependencies(projectLicensesDataFile))
2933

34+
generateNotPassedDependenciesFile(notPassedDependencies, notPassedDependenciesOutputFile)
3035
if (!notPassedDependencies.isEmpty()) {
31-
throw new GradleException("Some library licenses are not allowed.\n" +
32-
"Read [$notPassedDependenciesOutputFile.path] for more information.")
33-
}
34-
}
35-
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()]))
46-
}
47-
48-
private boolean isDependencyHasAllowedLicense(Dependency dependency, List<AllowedLicense> allowedLicenses) {
49-
for(allowedLicense in allowedLicenses) {
50-
if (isDependencyMatchesAllowedLicense(dependency, allowedLicense)) return true
36+
throw new GradleException("Some library licenses are not allowed:\n" +
37+
"$notPassedDependenciesOutputFile.text\n\n" +
38+
"Read [$notPassedDependenciesOutputFile.path] for more information.")
5139
}
52-
return false
53-
}
54-
55-
private boolean isDependencyMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
56-
return isDependencyNameMatchesAllowedLicense(dependency, allowedLicense) &&
57-
isDependencyLicenseMatchesAllowedLicense(dependency, allowedLicense) &&
58-
isDependencyVersionMatchesAllowedLicense(dependency, allowedLicense)
5940
}
6041

61-
private boolean isDependencyNameMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
62-
return dependency.moduleName ==~ allowedLicense.moduleName || allowedLicense.moduleName == null ||
63-
dependency.moduleName == allowedLicense.moduleName
42+
default List<AllowedLicense> parseAllowedLicenseFile(File allowedLicenseFile) {
43+
return LicenseCheckerFileReader.importAllowedLicenses(allowedLicenseFile)
6444
}
6545

66-
private boolean isDependencyVersionMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
67-
return dependency.moduleVersion ==~ allowedLicense.moduleVersion || allowedLicense.moduleVersion == null ||
68-
dependency.moduleVersion == allowedLicense.moduleVersion
46+
default List<Dependency> getProjectDependencies(File depenenciesFile) {
47+
return LicenseCheckerFileReader.importDependencies(depenenciesFile)
6948
}
7049

71-
private boolean isDependencyLicenseMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
72-
if (allowedLicense.moduleLicense == null || allowedLicense.moduleLicense == ".*") return true
7350

74-
for (moduleLicenses in dependency.moduleLicenses)
75-
if (moduleLicenses.moduleLicense ==~ allowedLicense.moduleLicense ||
76-
moduleLicenses.moduleLicense == allowedLicense.moduleLicense) return true
77-
return false
51+
default void generateNotPassedDependenciesFile(List<Tuple2<Dependency, List<ModuleLicense>>> notPassedDependencies, File notPassedDependenciesOutputFile) {
52+
notPassedDependenciesOutputFile.text = JsonOutput.prettyPrint(
53+
JsonOutput.toJson([
54+
"dependenciesWithoutAllowedLicenses": notPassedDependencies.collect {
55+
toAllowedLicenseList(it.getV1(), it.getV2())
56+
}.flatten()
57+
]))
7858
}
7959

80-
private List<AllowedLicense> toAllowedLicenseList(Dependency dependency) {
81-
if (dependency.moduleLicenses.isEmpty()) {
82-
return [ new AllowedLicense(dependency.moduleName, dependency.moduleVersion, null) ]
60+
default List<AllowedLicense> toAllowedLicenseList(Dependency dependency, List<ModuleLicense> moduleLicenses) {
61+
if (moduleLicenses.isEmpty()) {
62+
return [new AllowedLicense(dependency.moduleName, dependency.moduleVersion, null)]
8363
} else {
84-
return dependency.moduleLicenses.collect { new AllowedLicense(dependency.moduleName, dependency.moduleVersion, it.moduleLicense) }
64+
return moduleLicenses.findAll { it }.collect { new AllowedLicense(dependency.moduleName, dependency.moduleVersion, it.moduleLicense) }
8565
}
8666
}
8767
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2018 Evgeny Naumenko <[email protected]>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.jk1.license.check
17+
18+
/**
19+
* A Dependency, which has at least one license inside allowedLicenses, will pass.
20+
*/
21+
class OneRequiredLicenseChecker implements LicenseChecker {
22+
23+
@Override
24+
List<Tuple2<Dependency, List<ModuleLicense>>> checkAllDependencyLicensesAreAllowed(List<AllowedLicense> allowedLicenses, List<Dependency> allDependencies) {
25+
List<Dependency> notPassedDependencies = allDependencies.findAll { !isDependencyHasAllowedLicense(it, allowedLicenses) }
26+
return notPassedDependencies.collect { new Tuple2(it, it.moduleLicenses.isEmpty() ? null : it.moduleLicenses) }
27+
}
28+
29+
private boolean isDependencyHasAllowedLicense(Dependency dependency, List<AllowedLicense> allowedLicenses) {
30+
for (allowedLicense in allowedLicenses) {
31+
if (isDependencyMatchesAllowedLicense(dependency, allowedLicense)) return true
32+
}
33+
return false
34+
}
35+
36+
private boolean isDependencyMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
37+
return isDependencyNameMatchesAllowedLicense(dependency, allowedLicense) &&
38+
isDependencyLicenseMatchesAllowedLicense(dependency, allowedLicense) &&
39+
isDependencyVersionMatchesAllowedLicense(dependency, allowedLicense)
40+
}
41+
42+
private boolean isDependencyNameMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
43+
return dependency.moduleName ==~ allowedLicense.moduleName || allowedLicense.moduleName == null ||
44+
dependency.moduleName == allowedLicense.moduleName
45+
}
46+
47+
private boolean isDependencyVersionMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
48+
return dependency.moduleVersion ==~ allowedLicense.moduleVersion || allowedLicense.moduleVersion == null ||
49+
dependency.moduleVersion == allowedLicense.moduleVersion
50+
}
51+
52+
private boolean isDependencyLicenseMatchesAllowedLicense(Dependency dependency, AllowedLicense allowedLicense) {
53+
if (allowedLicense.moduleLicense == null || allowedLicense.moduleLicense == ".*") return true
54+
55+
for (moduleLicenses in dependency.moduleLicenses)
56+
if (moduleLicenses.moduleLicense ==~ allowedLicense.moduleLicense ||
57+
moduleLicenses.moduleLicense == allowedLicense.moduleLicense) return true
58+
return false
59+
}
60+
}

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

+7-2
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+
LicenseChecker getLicenseChecker() {
58+
return config.licenseChecker
59+
}
60+
5661
@OutputFile
5762
File getNotPassedDependenciesFile() {
5863
new File("${config.absoluteOutputDir}/$NOT_PASSED_DEPENDENCIES_FILE")
@@ -61,9 +66,9 @@ class CheckLicenseTask extends DefaultTask {
6166
@TaskAction
6267
void checkLicense() {
6368
LOGGER.info("Startup CheckLicense for ${getProject().name}")
64-
LicenseChecker licenseChecker = new LicenseChecker()
69+
LicenseChecker licenseChecker = getLicenseChecker()
6570
LOGGER.info("Check licenses if they are allowed to use.")
6671
licenseChecker.checkAllDependencyLicensesAreAllowed(
67-
getAllowedLicenseFile(), getProjectDependenciesData(), notPassedDependenciesFile)
72+
getAllowedLicenseFile(), getProjectDependenciesData(), notPassedDependenciesFile)
6873
}
6974
}

0 commit comments

Comments
 (0)