Skip to content

Commit ba9d8ba

Browse files
committed
fix: Launch CLI from executable jar so we can ensure deterministic classpath ordering
- without using wildcards, avoids getting into Windows command length issues as in #2062 - tidies the appassembler Windows launch script generation to use a template, similar to *nix systems - adds a Java Agent plugin loading mechanism to be able to add additional "plugin" jars onto the classpath as before Signed-off-by: Chad Wilson <[email protected]>
1 parent 9eca5b4 commit ba9d8ba

File tree

5 files changed

+214
-38
lines changed

5 files changed

+214
-38
lines changed

cli/pom.xml

Lines changed: 16 additions & 23 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>
@@ -198,5 +186,10 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved.
198186
</exclusion>
199187
</exclusions>
200188
</dependency>
189+
<dependency>
190+
<groupId>org.mockito</groupId>
191+
<artifactId>mockito-core</artifactId>
192+
<scope>test</scope>
193+
</dependency>
201194
</dependencies>
202195
</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%
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+
}
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+
}

0 commit comments

Comments
 (0)