Skip to content

Commit e87366f

Browse files
committed
fix(license): treat UNKNOWN-category dependencies as incompatible
Split the UNKNOWN category check in getCompatibility() so that dependencies with unrecognized licenses are flagged as incompatible when the project has a known license category, while keeping UNKNOWN project category returning UNKNOWN (no false positives). Add a distinct reason message for UNKNOWN-category dependencies to guide manual review. Implements TC-4707 Assisted-by: Claude Code
1 parent 3b9c5e2 commit e87366f

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 not recognized as a standard SPDX identifier."
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)