diff --git a/cli/src/main/java/org/owasp/dependencycheck/App.java b/cli/src/main/java/org/owasp/dependencycheck/App.java index 5bccba7218c..672e74dbb24 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/App.java +++ b/cli/src/main/java/org/owasp/dependencycheck/App.java @@ -24,6 +24,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.FileAppender; import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.LogLevel; import org.owasp.dependencycheck.data.nvdcve.DatabaseException; @@ -46,6 +47,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; @@ -95,6 +97,14 @@ public static void main(String[] args) { */ public App() { settings = new Settings(); + settings.setString(Settings.KEYS.APPLICATION_NAME, determineName()); + } + + private static String determineName() { + return Optional.ofNullable(System.getenv("ODC_NAME")) + .filter(StringUtils::isNotBlank) + .map(n -> n.replace('/', '-').replace(' ', '_')) + .orElse("dependency-check-cli"); } /** @@ -120,11 +130,11 @@ public int run(String[] args) { cli.parse(args); } catch (FileNotFoundException ex) { System.err.println(ex.getMessage()); - cli.printHelp(); + cli.printHelp(System.out); return 1; } catch (ParseException ex) { System.err.println(ex.getMessage()); - cli.printHelp(); + cli.printHelp(System.out); return 2; } final String verboseLog = cli.getStringArgument(CliParser.ARGUMENT.VERBOSE_LOG); @@ -224,7 +234,7 @@ public int run(String[] args) { settings.cleanup(); } } else { - cli.printHelp(); + cli.printHelp(System.out); } return exitCode; } @@ -456,12 +466,6 @@ private void runUpdateOnly() throws UpdateException, DatabaseException { * file is unable to be loaded. */ protected void populateSettings(CliParser cli) throws InvalidSettingException { - String name = System.getenv("ODC_NAME") != null ? System.getenv("ODC_NAME") : "dependency-check-cli"; - if (name.isBlank()) { - name = "dependency-check-cli"; - } - name = name.replace("/", "-").replace(" ", "_"); - settings.setString(Settings.KEYS.APPLICATION_NAME, name); final File propertiesFile = cli.getFileArgument(CliParser.ARGUMENT.PROP); if (propertiesFile != null) { try { diff --git a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java index 2db9e05aa88..4a5a17f5d6d 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java +++ b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java @@ -21,11 +21,12 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.commons.cli.help.HelpFormatter; +import org.apache.commons.cli.help.TextHelpAppendable; import org.owasp.dependencycheck.reporting.ReportGenerator.Format; import org.owasp.dependencycheck.utils.InvalidSettingException; import org.owasp.dependencycheck.utils.Settings; @@ -34,6 +35,9 @@ import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Comparator; /** * A utility to parse command line arguments for the DependencyCheck. @@ -65,6 +69,12 @@ public final class CliParser { */ private static final String SUPPORTED_FORMATS = "HTML, XML, CSV, JSON, JUNIT, SARIF, JENKINS, GITLAB or ALL"; + private static final String HELP_MSG = String.format( + "Dependency-Check can be used to identify if there are any known CVE vulnerabilities in libraries " + + "utilized by an application. Dependency-Check will automatically update required data from the " + + "Internet, such as the CVE and CPE data files from nvd.nist.gov.%n" + ); + /** * Constructs a new CLI Parser object with the configured settings. * @@ -82,7 +92,7 @@ public CliParser(Settings settings) { * point to a file that exists. * @throws ParseException is thrown when a Parse Exception occurs. */ - public void parse(String[] args) throws FileNotFoundException, ParseException { + public void parse(String... args) throws FileNotFoundException, ParseException { line = parseArgs(args); if (line != null) { @@ -97,7 +107,7 @@ public void parse(String[] args) throws FileNotFoundException, ParseException { * @return the results of parsing the command line arguments * @throws ParseException if the arguments are invalid */ - private CommandLine parseArgs(String[] args) throws ParseException { + private CommandLine parseArgs(String... args) throws ParseException { final CommandLineParser parser = new DefaultParser(); final Options options = createCommandLineOptions(); return parser.parse(options, args); @@ -213,7 +223,7 @@ private boolean isValidFormat(String format) { * @param argumentName the argument being validated (e.g. scan, out, etc.) * @return true, if path exists; false otherwise */ - private boolean isValidFilePath(String path, String argumentName) { + private boolean isValidFilePath(String path, @SuppressWarnings("SameParameterValue") String argumentName) { try { validatePathExists(path, argumentName); return true; @@ -232,7 +242,7 @@ private boolean isValidFilePath(String path, String argumentName) { * @throws FileNotFoundException is thrown if one of the paths being * validated does not exist. */ - private void validatePathExists(String[] paths, String optType) throws FileNotFoundException { + private void validatePathExists(String[] paths, @SuppressWarnings("SameParameterValue") String optType) throws FileNotFoundException { for (String path : paths) { validatePathExists(path, optType); } @@ -301,7 +311,6 @@ private void validatePathExists(String path, String argumentName) throws FileNot * * @return the command line options used for parsing the command line */ - @SuppressWarnings("static-access") private Options createCommandLineOptions() { final Options options = new Options(); addStandardOptions(options); @@ -315,10 +324,8 @@ private Options createCommandLineOptions() { * * @param options a collection of command line arguments */ - @SuppressWarnings("static-access") private void addStandardOptions(final Options options) { - //This is an option group because it can be specified more then once. - + //This is an option group because it can be specified more than once. options.addOptionGroup(newOptionGroup(newOptionWithArg(ARGUMENT.SCAN_SHORT, ARGUMENT.SCAN, "path", "The path to scan - this option can be specified multiple times. Ant style paths are supported (e.g. 'path/**/*.jar'); " + "if using Ant style paths it is highly recommended to quote the argument value."))) @@ -357,7 +364,6 @@ private void addStandardOptions(final Options options) { * * @param options a collection of command line arguments */ - @SuppressWarnings("static-access") private void addAdvancedOptions(final Options options) { options .addOption(newOption(ARGUMENT.UPDATE_ONLY, @@ -445,11 +451,11 @@ private void addAdvancedOptions(final Options options) { .addOption(newOptionWithArg(ARGUMENT.RETIREJS_URL, "url", "The Retire JS Repository URL")) .addOption(newOptionWithArg(ARGUMENT.RETIREJS_URL_USER, "username", - "The password to authenticate to Retire JS Repository URL")) + "Credentials for basic auth towards the Retire JS Repository URL")) .addOption(newOptionWithArg(ARGUMENT.RETIREJS_URL_PASSWORD, "password", - "The password to authenticate to Retire JS Repository URL")) + "Credentials for basic auth towards the Retire JS Repository URL")) .addOption(newOptionWithArg(ARGUMENT.RETIREJS_URL_BEARER_TOKEN, "token", - "The password to authenticate to Retire JS Repository URL")) + "Token for bearer auth towards the Retire JS Repository URL")) .addOption(newOption(ARGUMENT.RETIRE_JS_FILTER_NON_VULNERABLE, "Specifies that the Retire JS " + "Analyzer should filter out non-vulnerable JS files from the report.")) .addOption(newOptionWithArg(ARGUMENT.ARTIFACTORY_PARALLEL_ANALYSIS, "true/false", @@ -571,7 +577,6 @@ private void addAdvancedOptions(final Options options) { * * @param options a collection of command line arguments */ - @SuppressWarnings({"static-access", "deprecation"}) private void addDeprecatedOptions(final Options options) { //not a real option - but enables java debugging via the shell script options.addOption(newOption("debug", @@ -784,26 +789,28 @@ public File getFileArgument(String option) { } /** - * Displays the command line help message to the standard output. + * Appends the command line help message to the passed appendable */ - public void printHelp() { - final HelpFormatter formatter = new HelpFormatter(); + void printHelp(Appendable appendable) { + TextHelpAppendable helpAppendable = new TextHelpAppendable(appendable); + helpAppendable.setMaxWidth(100); + HelpFormatter formatter = HelpFormatter.builder() + .setShowSince(false) + .setComparator(Comparator.comparing(Option::getKey, String::compareToIgnoreCase)) + .setHelpAppendable(helpAppendable) + .get(); + final Options options = new Options(); addStandardOptions(options); if (line != null && line.hasOption(ARGUMENT.ADVANCED_HELP)) { addAdvancedOptions(options); } - final String helpMsg = String.format("%n%s" - + " can be used to identify if there are any known CVE vulnerabilities in libraries utilized by an application. " - + "%s will automatically update required data from the Internet, such as the CVE and CPE data files from nvd.nist.gov.%n%n", - settings.getString(Settings.KEYS.APPLICATION_NAME, "DependencyCheck"), - settings.getString(Settings.KEYS.APPLICATION_NAME, "DependencyCheck")); - formatter.printHelp(settings.getString(Settings.KEYS.APPLICATION_NAME, "DependencyCheck"), - helpMsg, - options, - "", - true); + try { + formatter.printHelp("dependency-check", HELP_MSG, formatter.sort(options), "", true); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } /** @@ -1005,7 +1012,7 @@ public float getFloatArgument(String option, float defaultValue) { * @return a new option */ private Option newOption(String name, String description) { - return Option.builder().longOpt(name).desc(description).build(); + return Option.builder().longOpt(name).desc(description).get(); } /** @@ -1017,7 +1024,7 @@ private Option newOption(String name, String description) { * @return a new option */ private Option newOption(String shortName, String name, String description) { - return Option.builder(shortName).longOpt(name).desc(description).build(); + return Option.builder(shortName).longOpt(name).desc(description).get(); } /** @@ -1029,7 +1036,7 @@ private Option newOption(String shortName, String name, String description) { * @return a new option */ private Option newOptionWithArg(String name, String arg, String description) { - return Option.builder().longOpt(name).argName(arg).hasArg().desc(description).build(); + return Option.builder().longOpt(name).argName(arg).hasArg().desc(description).get(); } /** @@ -1042,7 +1049,7 @@ private Option newOptionWithArg(String name, String arg, String description) { * @return a new option */ private Option newOptionWithArg(String shortName, String name, String arg, String description) { - return Option.builder(shortName).longOpt(name).argName(arg).hasArg().desc(description).build(); + return Option.builder(shortName).longOpt(name).argName(arg).hasArg().desc(description).get(); } /** diff --git a/cli/src/test/java/org/owasp/dependencycheck/CliParserTest.java b/cli/src/test/java/org/owasp/dependencycheck/CliParserTest.java index aba771547d2..d63337316dc 100644 --- a/cli/src/test/java/org/owasp/dependencycheck/CliParserTest.java +++ b/cli/src/test/java/org/owasp/dependencycheck/CliParserTest.java @@ -24,14 +24,21 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.PrintStream; +import java.io.StringWriter; +import java.util.List; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInRelativeOrder; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.matchesPattern; +import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; /** * @@ -47,13 +54,8 @@ class CliParserTest extends BaseTest { @Test void testParse() throws Exception { - String[] args = {}; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - System.setOut(new PrintStream(baos, true, UTF_8)); - CliParser instance = new CliParser(getSettings()); - instance.parse(args); + instance.parse(); assertFalse(instance.isGetVersion()); assertFalse(instance.isGetHelp()); @@ -160,16 +162,9 @@ void testParse_failOnCVSSValidArgument() throws Exception { @Test void testParse_unknown() { - String[] args = {"-unknown"}; - - ByteArrayOutputStream baos_out = new ByteArrayOutputStream(); - ByteArrayOutputStream baos_err = new ByteArrayOutputStream(); - System.setOut(new PrintStream(baos_out, true, UTF_8)); - System.setErr(new PrintStream(baos_err, true, UTF_8)); - CliParser instance = new CliParser(getSettings()); - ParseException ex = assertThrows(ParseException.class, () -> instance.parse(args) , + ParseException ex = assertThrows(ParseException.class, () -> instance.parse("-unknown"), "Unrecognized option should have caused an exception"); assertTrue(ex.getMessage().contains("Unrecognized option")); @@ -243,7 +238,6 @@ void testParse_scan_withFileExists() throws Exception { * */ @Test - @SuppressWarnings("StringSplitter") void testParse_printVersionInfo() { PrintStream out = System.out; @@ -253,11 +247,7 @@ void testParse_printVersionInfo() { CliParser instance = new CliParser(getSettings()); instance.printVersionInfo(); try { - String text = baos.toString(UTF_8).toLowerCase(); - String[] lines = text.split(System.lineSeparator()); - assertTrue(lines.length >= 1); - assertTrue(text.contains("version")); - assertFalse(text.contains("unknown")); + assertThat(baos.toString(UTF_8), matchesPattern("(?mi)dependency-check.*version [0-9]+.*\n")); } finally { System.setOut(out); } @@ -269,28 +259,32 @@ void testParse_printVersionInfo() { * @throws Exception thrown when an exception occurs. */ @Test - @SuppressWarnings("StringSplitter") void testParse_printHelp() throws Exception { + CliParser instance = new CliParser(getSettings()); + instance.parse("-h"); + StringWriter text = new StringWriter(); + instance.printHelp(text); - PrintStream out = System.out; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - System.setOut(new PrintStream(baos, true, UTF_8)); + List lines = text.toString().lines().collect(toList()); + assertThat(lines, containsInRelativeOrder(startsWith(" usage: dependency-check"), startsWith(" -v, --version"))); + assertThat(lines.size(), greaterThan(10)); + } + /** + * Test of printHelp, of class CliParser. + * + * @throws Exception thrown when an exception occurs. + */ + @Test + void testParse_printHelpAdvanced() throws Exception { CliParser instance = new CliParser(getSettings()); - String[] args = {"-h"}; - instance.parse(args); - instance.printHelp(); - args[0] = "--advancedHelp"; - instance.parse(args); - instance.printHelp(); - try { - String text = (baos.toString(UTF_8)); - String[] lines = text.split(System.lineSeparator()); - assertTrue(lines[0].startsWith("usage: ")); - assertTrue((lines.length > 2)); - } finally { - System.setOut(out); - } + instance.parse("--advancedHelp"); + StringWriter text = new StringWriter(); + instance.printHelp(text); + + List lines = text.toString().lines().collect(toList()); + assertThat(lines, containsInRelativeOrder(startsWith(" usage: dependency-check"), startsWith(" -c, --connectiontimeout"), startsWith(" -v, --version"))); + assertThat(lines.size(), greaterThan(60)); } /**