Skip to content

Commit d1d6800

Browse files
a-orenclaude
andauthored
fix(license): treat UNKNOWN-category dependencies as incompatible (#513)
## Summary - Split the UNKNOWN category check in `getCompatibility()` so dependencies with unrecognized licenses are flagged as incompatible when the project has a known license category - Added distinct reason message for UNKNOWN-category dependencies: "License not recognized as a standard SPDX identifier. Manual review recommended to verify compatibility." - Added parameterized tests covering all UNKNOWN/known category combinations and preserving existing behavior Implements [TC-4707](https://redhat.atlassian.net/browse/TC-4707) ## Test plan - [x] `getCompatibility(PERMISSIVE|WEAK_COPYLEFT|STRONG_COPYLEFT, UNKNOWN)` returns `INCOMPATIBLE` - [x] `getCompatibility(UNKNOWN, *)` returns `UNKNOWN` (no false positives) - [x] `getCompatibility(null, *)` returns `UNKNOWN` - [x] Known category pairs preserve existing behavior - [x] All 454 existing tests pass (6 pre-existing Python env failures unrelated) [TC-4707]: https://redhat.atlassian.net/browse/TC-4707?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ ## Summary by Sourcery Treat dependencies with UNKNOWN license category as incompatible when the project license category is known, and clarify reporting for such cases. Bug Fixes: - Ensure UNKNOWN-category dependency licenses are reported as incompatible when the project has a known license category. Enhancements: - Add a specific incompatibility reason message for dependencies with UNKNOWN license category in license check results. Tests: - Introduce parameterized tests covering UNKNOWN vs known license category combinations and verifying existing compatibility behavior for known categories. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1fba9c2 commit d1d6800

3 files changed

Lines changed: 139 additions & 6 deletions

File tree

src/main/java/io/github/guacsec/trustifyda/license/LicenseCheck.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,15 @@ public static CompletableFuture<LicenseSummary> runLicenseCheck(
145145
Compatibility status =
146146
LicenseUtils.getCompatibility(projectCategory, entry.category());
147147
if (status == Compatibility.INCOMPATIBLE) {
148+
String reason =
149+
entry.category() == LicenseCategory.UNKNOWN
150+
? "License category could not be determined."
151+
+ " Manual review recommended to verify compatibility."
152+
: "Dependency license(s) are incompatible with the project"
153+
+ " license.";
148154
incompatible.add(
149155
new IncompatibleDependency(
150-
purl,
151-
entry.licenses(),
152-
entry.category(),
153-
"Dependency license(s) are incompatible with the project license."));
156+
purl, entry.licenses(), entry.category(), reason));
154157
}
155158
}
156159

src/main/java/io/github/guacsec/trustifyda/license/LicenseUtils.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,12 @@ public static Compatibility getCompatibility(
191191
if (projectCategory == null || dependencyCategory == null) {
192192
return Compatibility.UNKNOWN;
193193
}
194-
if (projectCategory == LicenseCategory.UNKNOWN
195-
|| dependencyCategory == LicenseCategory.UNKNOWN) {
194+
if (projectCategory == LicenseCategory.UNKNOWN) {
196195
return Compatibility.UNKNOWN;
197196
}
197+
if (dependencyCategory == LicenseCategory.UNKNOWN) {
198+
return Compatibility.INCOMPATIBLE;
199+
}
198200
int projLevel = restrictiveness(projectCategory);
199201
int depLevel = restrictiveness(dependencyCategory);
200202

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2023-2025 Trustify Dependency Analytics Authors
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+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package io.github.guacsec.trustifyda.license;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import io.github.guacsec.trustifyda.api.v5.LicenseCategory;
22+
import io.github.guacsec.trustifyda.license.LicenseUtils.Compatibility;
23+
import java.util.stream.Stream;
24+
import org.junit.jupiter.api.Nested;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.Arguments;
27+
import org.junit.jupiter.params.provider.EnumSource;
28+
import org.junit.jupiter.params.provider.MethodSource;
29+
30+
/** Tests for {@link LicenseUtils#getCompatibility} with UNKNOWN category handling. */
31+
class License_Compatibility_Test {
32+
33+
@Nested
34+
class Unknown_Dependency_Category {
35+
36+
/** Known project + UNKNOWN dependency should be INCOMPATIBLE. */
37+
@ParameterizedTest
38+
@EnumSource(
39+
value = LicenseCategory.class,
40+
names = {"PERMISSIVE", "WEAK_COPYLEFT", "STRONG_COPYLEFT"})
41+
void known_project_and_unknown_dependency_returns_incompatible(
42+
LicenseCategory projectCategory) {
43+
// When
44+
Compatibility result =
45+
LicenseUtils.getCompatibility(projectCategory, LicenseCategory.UNKNOWN);
46+
47+
// Then
48+
assertThat(result).isEqualTo(Compatibility.INCOMPATIBLE);
49+
}
50+
}
51+
52+
@Nested
53+
class Unknown_Project_Category {
54+
55+
/** UNKNOWN project category should always return UNKNOWN. */
56+
@ParameterizedTest
57+
@EnumSource(LicenseCategory.class)
58+
void unknown_project_returns_unknown(LicenseCategory dependencyCategory) {
59+
// When
60+
Compatibility result =
61+
LicenseUtils.getCompatibility(LicenseCategory.UNKNOWN, dependencyCategory);
62+
63+
// Then
64+
assertThat(result).isEqualTo(Compatibility.UNKNOWN);
65+
}
66+
67+
/** Null project category should return UNKNOWN. */
68+
@ParameterizedTest
69+
@EnumSource(LicenseCategory.class)
70+
void null_project_returns_unknown(LicenseCategory dependencyCategory) {
71+
// When
72+
Compatibility result = LicenseUtils.getCompatibility(null, dependencyCategory);
73+
74+
// Then
75+
assertThat(result).isEqualTo(Compatibility.UNKNOWN);
76+
}
77+
}
78+
79+
@Nested
80+
class Known_Categories {
81+
82+
/** Existing compatibility logic for known categories remains unchanged. */
83+
@ParameterizedTest
84+
@MethodSource(
85+
"io.github.guacsec.trustifyda.license.License_Compatibility_Test#knownCategoryPairs")
86+
void known_categories_preserve_existing_behavior(
87+
LicenseCategory project, LicenseCategory dependency, Compatibility expected) {
88+
// When
89+
Compatibility result = LicenseUtils.getCompatibility(project, dependency);
90+
91+
// Then
92+
assertThat(result).isEqualTo(expected);
93+
}
94+
}
95+
96+
static Stream<Arguments> knownCategoryPairs() {
97+
return Stream.of(
98+
// Same restrictiveness → compatible
99+
Arguments.of(
100+
LicenseCategory.PERMISSIVE, LicenseCategory.PERMISSIVE, Compatibility.COMPATIBLE),
101+
Arguments.of(
102+
LicenseCategory.WEAK_COPYLEFT, LicenseCategory.WEAK_COPYLEFT, Compatibility.COMPATIBLE),
103+
Arguments.of(
104+
LicenseCategory.STRONG_COPYLEFT,
105+
LicenseCategory.STRONG_COPYLEFT,
106+
Compatibility.COMPATIBLE),
107+
// Less restrictive dependency → compatible
108+
Arguments.of(
109+
LicenseCategory.STRONG_COPYLEFT, LicenseCategory.PERMISSIVE, Compatibility.COMPATIBLE),
110+
Arguments.of(
111+
LicenseCategory.STRONG_COPYLEFT,
112+
LicenseCategory.WEAK_COPYLEFT,
113+
Compatibility.COMPATIBLE),
114+
Arguments.of(
115+
LicenseCategory.WEAK_COPYLEFT, LicenseCategory.PERMISSIVE, Compatibility.COMPATIBLE),
116+
// More restrictive dependency → incompatible
117+
Arguments.of(
118+
LicenseCategory.PERMISSIVE, LicenseCategory.WEAK_COPYLEFT, Compatibility.INCOMPATIBLE),
119+
Arguments.of(
120+
LicenseCategory.PERMISSIVE,
121+
LicenseCategory.STRONG_COPYLEFT,
122+
Compatibility.INCOMPATIBLE),
123+
Arguments.of(
124+
LicenseCategory.WEAK_COPYLEFT,
125+
LicenseCategory.STRONG_COPYLEFT,
126+
Compatibility.INCOMPATIBLE));
127+
}
128+
}

0 commit comments

Comments
 (0)