Skip to content

Commit d52522c

Browse files
fix: correct reliability of Central etc (JCS cache) analyzers on Java 25/Docker by making CLI classpath deterministic (#8117)
Signed-off-by: Chad Wilson <[email protected]> Co-authored-by: Jeremy Long <[email protected]>
1 parent 3deae1e commit d52522c

File tree

14 files changed

+230
-71
lines changed

14 files changed

+230
-71
lines changed

ant/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,6 @@ Copyright (c) 2013 - Jeremy Long. All Rights Reserved.
222222
<groupId>org.slf4j</groupId>
223223
<artifactId>slf4j-api</artifactId>
224224
</dependency>
225-
<dependency>
226-
<groupId>io.github.jeremylong</groupId>
227-
<artifactId>jcs3-slf4j</artifactId>
228-
</dependency>
229225
<dependency>
230226
<groupId>org.owasp</groupId>
231227
<artifactId>dependency-check-core</artifactId>

ant/src/main/java/org/owasp/dependencycheck/taskdefs/Purge.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818
package org.owasp.dependencycheck.taskdefs;
1919

20-
import io.github.jeremylong.jcs3.slf4j.Slf4jAdapter;
2120
import java.io.File;
2221
import java.io.IOException;
2322
import java.io.InputStream;
@@ -128,9 +127,6 @@ public final void execute() throws BuildException {
128127
* Hacky method of muting the noisy logging from JCS.
129128
*/
130129
private void muteNoisyLoggers() {
131-
System.setProperty("jcs.logSystem", "slf4j");
132-
Slf4jAdapter.muteLogging(true);
133-
134130
final String[] noisyLoggers = {
135131
"org.apache.hc"
136132
};

cli/pom.xml

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved.
6767
<archive>
6868
<manifest>
6969
<mainClass>org.owasp.dependencycheck.App</mainClass>
70+
<addClasspath>true</addClasspath>
7071
</manifest>
72+
<manifestEntries>
73+
<Premain-Class>org.owasp.dependencycheck.PluginLoader</Premain-Class>
74+
</manifestEntries>
7175
</archive>
7276
</configuration>
7377
</plugin>
@@ -79,6 +83,11 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved.
7983
<program>
8084
<mainClass>org.owasp.dependencycheck.App</mainClass>
8185
<id>dependency-check</id>
86+
<commandLineArguments>
87+
<commandLineArgument>-javaagent:@REPO@/${project.artifactId}-${project.version}.jar=@BASEDIR@/plugins</commandLineArgument>
88+
<commandLineArgument>-jar</commandLineArgument>
89+
<commandLineArgument>@REPO@/${project.artifactId}-${project.version}.jar</commandLineArgument>
90+
</commandLineArguments>
8291
</program>
8392
</programs>
8493
<assembleDirectory>${project.build.directory}/release</assembleDirectory>
@@ -88,10 +97,8 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved.
8897
</binFileExtensions>
8998
<repositoryLayout>flat</repositoryLayout>
9099
<repositoryName>lib</repositoryName>
91-
<useWildcardClassPath>true</useWildcardClassPath>
92-
<configurationDirectory>plugins/*</configurationDirectory>
93-
<includeConfigurationDirectoryInClasspath>true</includeConfigurationDirectoryInClasspath>
94-
<unixScriptTemplate>${project.basedir}/src/main/conf/unixBinTemplate</unixScriptTemplate>
100+
<unixScriptTemplate>${project.basedir}/src/main/conf/unixBinTemplate.sh</unixScriptTemplate>
101+
<windowsScriptTemplate>${project.basedir}/src/main/conf/windowsBinTemplate.bat</windowsScriptTemplate>
95102
<!--
96103
enable-native-access=ALL-UNNAMED
97104
Java 21+: Needed by Lucene indexes unless we do -Dorg.apache.lucene.store.MMapDirectory.enableMemorySegments=false
@@ -110,25 +117,6 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved.
110117
</execution>
111118
</executions>
112119
</plugin>
113-
<plugin>
114-
<groupId>org.apache.maven.plugins</groupId>
115-
<artifactId>maven-antrun-plugin</artifactId>
116-
<executions>
117-
<execution>
118-
<id>fix-windows-shell-script</id>
119-
<phase>package</phase>
120-
<goals>
121-
<goal>run</goal>
122-
</goals>
123-
<configuration>
124-
<!-- Hack/workaround for https://github.com/mojohaus/appassembler/issues/114 -->
125-
<target>
126-
<replace file="${project.build.directory}/release/bin/dependency-check.bat" token="%JAVACMD% %JAVA_OPTS%" value="&quot;%JAVACMD%&quot; %JAVA_OPTS%" failOnNoReplacements="true" />
127-
</target>
128-
</configuration>
129-
</execution>
130-
</executions>
131-
</plugin>
132120
<plugin>
133121
<groupId>org.apache.maven.plugins</groupId>
134122
<artifactId>maven-assembly-plugin</artifactId>
@@ -179,10 +167,6 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved.
179167
<groupId>org.slf4j</groupId>
180168
<artifactId>slf4j-api</artifactId>
181169
</dependency>
182-
<dependency>
183-
<groupId>io.github.jeremylong</groupId>
184-
<artifactId>jcs3-slf4j</artifactId>
185-
</dependency>
186170
<dependency>
187171
<!-- not visible in imports due to method chaining, but App code uses classes from this library -->
188172
<groupId>io.github.jeremylong</groupId>
@@ -202,5 +186,10 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved.
202186
</exclusion>
203187
</exclusions>
204188
</dependency>
189+
<dependency>
190+
<groupId>org.mockito</groupId>
191+
<artifactId>mockito-core</artifactId>
192+
<scope>test</scope>
193+
</dependency>
205194
</dependencies>
206195
</project>

cli/src/main/conf/unixBinTemplate renamed to cli/src/main/conf/unixBinTemplate.sh

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ fi
5353
# For Cygwin and MINGW, ensure paths are in UNIX format before anything is touched
5454
if $cygwin || $mingw; then
5555
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
56-
[ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
5756
fi
5857

5958
# If a specific java binary isn't specified search for the standard 'java' binary
@@ -81,20 +80,8 @@ then
8180
REPO="$BASEDIR"/@REPO@
8281
fi
8382

84-
CLASSPATH=@CLASSPATH@
85-
86-
ENDORSED_DIR=@ENDORSED_DIR@
87-
if [ -n "$ENDORSED_DIR" ] ; then
88-
CLASSPATH=$BASEDIR/$ENDORSED_DIR/*:$CLASSPATH
89-
fi
90-
91-
if [ -n "$CLASSPATH_PREFIX" ] ; then
92-
CLASSPATH=$CLASSPATH_PREFIX:$CLASSPATH
93-
fi
94-
9583
# For Cygwin and Mingw, switch paths to Windows format before running java
9684
if $cygwin || $mingw; then
97-
[ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
9885
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
9986
[ -n "$HOME" ] && HOME=`cygpath --path --windows "$HOME"`
10087
[ -n "$BASEDIR" ] && BASEDIR=`cygpath --path --windows "$BASEDIR"`
@@ -110,11 +97,9 @@ fi
11097
done
11198

11299
exec "$JAVACMD" $JAVA_OPTS $DEBUG @EXTRA_JVM_ARGUMENTS@ \
113-
-classpath "$CLASSPATH" \
114100
-Dapp.name="@APP_NAME@" \
115101
-Dapp.pid="$$" \
116102
-Dapp.repo="$REPO" \
117103
-Dapp.home="$BASEDIR" \
118104
-Dbasedir="$BASEDIR" \
119-
@MAINCLASS@ \
120105
@APP_ARGUMENTS@"$@"@UNIX_BACKGROUND@
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#LICENSE_HEADER#
2+
@echo off
3+
4+
set ERROR_CODE=0
5+
6+
:init
7+
@REM Decide how to startup depending on the version of windows
8+
9+
@REM -- Win98ME
10+
if NOT "%OS%"=="Windows_NT" goto Win9xArg
11+
12+
@REM set local scope for the variables with windows NT shell
13+
if "%OS%"=="Windows_NT" @setlocal
14+
15+
@REM -- 4NT shell
16+
if "%eval[2+2]" == "4" goto 4NTArgs
17+
18+
@REM -- Regular WinNT shell
19+
set CMD_LINE_ARGS=%*
20+
goto WinNTGetScriptDir
21+
22+
@REM The 4NT Shell from jp software
23+
:4NTArgs
24+
set CMD_LINE_ARGS=%$
25+
goto WinNTGetScriptDir
26+
27+
:Win9xArg
28+
@REM Slurp the command line arguments. This loop allows for an unlimited number
29+
@REM of arguments (up to the command line limit, anyway).
30+
set CMD_LINE_ARGS=
31+
:Win9xApp
32+
if %1a==a goto Win9xGetScriptDir
33+
set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1
34+
shift
35+
goto Win9xApp
36+
37+
:Win9xGetScriptDir
38+
set SAVEDIR=%CD%
39+
%0\
40+
cd %0\..\..
41+
set BASEDIR=%CD%
42+
cd %SAVEDIR%
43+
set SAVE_DIR=
44+
goto repoSetup
45+
46+
:WinNTGetScriptDir
47+
for %%i in ("%~dp0..") do set "BASEDIR=%%~fi"
48+
49+
:repoSetup
50+
set REPO=
51+
#ENV_SETUP#
52+
53+
if "%JAVACMD%"=="" set JAVACMD=#JAVA_BINARY#
54+
55+
if "%REPO%"=="" set REPO=%BASEDIR%\#REPO#
56+
57+
@REM Reaching here means variables are defined and arguments have been captured
58+
:endInit
59+
60+
"%JAVACMD%" %JAVA_OPTS% #EXTRA_JVM_ARGUMENTS# -Dapp.name="#APP_NAME#" -Dapp.repo="%REPO%" -Dapp.home="%BASEDIR%" -Dbasedir="%BASEDIR%" #APP_ARGUMENTS#%CMD_LINE_ARGS%
61+
if %ERRORLEVEL% NEQ 0 goto error
62+
goto end
63+
64+
:error
65+
if "%OS%"=="Windows_NT" @endlocal
66+
set ERROR_CODE=%ERRORLEVEL%
67+
68+
:end
69+
@REM set local scope for the variables with windows NT shell
70+
if "%OS%"=="Windows_NT" goto endNT
71+
72+
@REM For old DOS remove the set variables from ENV - we assume they were not set
73+
@REM before we started - at least we don't leave any baggage around
74+
set CMD_LINE_ARGS=
75+
goto postExec
76+
77+
:endNT
78+
@REM If error code is set to 1 then the endlocal was done already in :error.
79+
if %ERROR_CODE% EQU 0 @endlocal
80+
81+
82+
:postExec
83+
84+
if "%FORCE_EXIT_ON_ERROR%" == "on" (
85+
if %ERROR_CODE% NEQ 0 exit %ERROR_CODE%
86+
)
87+
88+
exit /B %ERROR_CODE%

cli/src/main/java/org/owasp/dependencycheck/App.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,6 @@ public class App {
8888
*/
8989
@SuppressWarnings("squid:S4823")
9090
public static void main(String[] args) {
91-
System.setProperty("jcs.logSystem", "slf4j");
92-
if (!LOGGER.isDebugEnabled()) {
93-
Slf4jAdapter.muteLogging(true);
94-
}
9591
final int exitCode;
9692
final App app = new App();
9793
exitCode = app.run(args);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.owasp.dependencycheck;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.lang.instrument.Instrumentation;
6+
import java.util.jar.JarFile;
7+
8+
/**
9+
* Java agent for loading plugin JARs from a specified directory into the system classpath
10+
* before the main application starts. This allows additional plugins to be available at runtime
11+
* by appending their JAR files to the system class loader search path; while allowing use of an
12+
* executable jar with deterministic classpath ordering.
13+
* <p/>
14+
* To use, specify this class as a Java agent and provide the plugins directory as the -javaagent argument
15+
*/
16+
public class PluginLoader {
17+
/**
18+
* Java agent entry point. Loads all JAR files from the specified plugins directory
19+
* and appends them to the system class loader search path.
20+
*
21+
* @param agentArg the path to the plugins directory containing JAR files to load, e.g `-javaagent:cli.jar=/usr/share/dependency-check/plugins`
22+
* @param inst the instrumentation instance provided by the JVM
23+
*/
24+
public static void premain(String agentArg, Instrumentation inst) {
25+
File pluginsDir = new File(agentArg);
26+
if (pluginsDir.isDirectory()) {
27+
File[] files = pluginsDir.listFiles((dir, name) -> name.endsWith(".jar"));
28+
for (File file : files == null ? new File[0] : files) {
29+
try (JarFile jar = new JarFile(file)) {
30+
inst.appendToSystemClassLoaderSearch(jar);
31+
} catch (IOException e) {
32+
System.err.printf("[WARN] Failed to read plugin jar file at %s. Jar will not be available on classpath: %s%n", file, e);
33+
e.printStackTrace(System.err);
34+
}
35+
}
36+
}
37+
}
38+
}

cli/src/main/resources/logback.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</appender>
1212

1313
<logger name="org.apache.lucene" level="ERROR" />
14-
<logger name="org.apache.commons.jcs" level="ERROR" />
14+
<logger name="org.apache.commons.jcs3" level="FATAL" />
1515
<logger name="org.apache.hc" level="ERROR" />
1616

1717
<root level="INFO">
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.owasp.dependencycheck;
2+
3+
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.io.TempDir;
6+
import org.mockito.Mockito;
7+
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.IOException;
10+
import java.io.PrintStream;
11+
import java.lang.instrument.Instrumentation;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.util.regex.Pattern;
15+
16+
import static org.hamcrest.MatcherAssert.assertThat;
17+
import static org.hamcrest.Matchers.matchesPattern;
18+
import static org.mockito.ArgumentMatchers.assertArg;
19+
import static org.mockito.Mockito.times;
20+
import static org.mockito.Mockito.verify;
21+
import static org.mockito.Mockito.verifyNoInteractions;
22+
23+
class PluginLoaderTest {
24+
25+
private final Instrumentation instrumentation = Mockito.mock(Instrumentation.class);
26+
27+
@TempDir
28+
Path tempDir;
29+
30+
@Test
31+
void shouldDoNothingIfDirectoryDoesntExist() {
32+
PluginLoader.premain("blah", instrumentation);
33+
verifyNoInteractions(instrumentation);
34+
}
35+
36+
@Test
37+
void shouldDoNothingIfDirectoryIsEmpty() {
38+
PluginLoader.premain(tempDir.toString(), instrumentation);
39+
verifyNoInteractions(instrumentation);
40+
}
41+
42+
@Test
43+
void shouldAddJarToClassPath() throws Exception {
44+
createEmptyValidJar();
45+
createEmptyValidJar();
46+
PluginLoader.premain(tempDir.toString(), instrumentation);
47+
verify(instrumentation, times(2))
48+
.appendToSystemClassLoaderSearch(assertArg(jar -> assertThat(jar.getName(), matchesPattern(".*/dummy.*\\.jar"))));
49+
}
50+
51+
@Test
52+
void shouldStopLoadingPluginsOnBadJarButSucceed() throws Exception {
53+
PrintStream originalErr = System.err;
54+
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
55+
System.setErr(new PrintStream(errContent));
56+
try {
57+
createEmptyBadJar();
58+
PluginLoader.premain(tempDir.toString(), instrumentation);
59+
assertThat(errContent.toString(), matchesPattern(Pattern.compile("\\[WARN\\] Failed to read plugin jar file at .*/dummy.*\\.jar\\. Jar will not be available on classpath.*zip file is empty.*", Pattern.DOTALL)));
60+
} finally {
61+
System.setErr(originalErr);
62+
}
63+
}
64+
65+
private Path createEmptyBadJar() throws IOException {
66+
return Files.createTempFile(tempDir, "dummy", ".jar");
67+
}
68+
69+
private void createEmptyValidJar() throws IOException {
70+
new ZipArchiveOutputStream(createEmptyBadJar().toFile()).close();
71+
}
72+
}

core/pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,14 @@ Copyright (c) 2012 Jeremy Long. All Rights Reserved.
197197
<groupId>org.whitesource</groupId>
198198
<artifactId>pecoff4j</artifactId>
199199
</dependency>
200-
<dependency>
201-
<groupId>org.apache.commons</groupId>
202-
<artifactId>commons-jcs3-core</artifactId>
203-
</dependency>
200+
<!-- JCS 3 logging adapter; intentionally higher on classpath than JCS itself -->
204201
<dependency>
205202
<groupId>io.github.jeremylong</groupId>
206203
<artifactId>jcs3-slf4j</artifactId>
207-
<scope>runtime</scope>
204+
</dependency>
205+
<dependency>
206+
<groupId>org.apache.commons</groupId>
207+
<artifactId>commons-jcs3-core</artifactId>
208208
</dependency>
209209
<dependency>
210210
<groupId>com.github.package-url</groupId>

0 commit comments

Comments
 (0)