diff --git a/vulnz/build.gradle b/vulnz/build.gradle index 8448a8c0..24c6a1f6 100644 --- a/vulnz/build.gradle +++ b/vulnz/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter:2.7.18' } implementation 'com.diogonunes:JColor:5.5.1' + implementation 'org.jline:jline:3.26.2' implementation 'commons-io:commons-io:2.16.1' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' diff --git a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/CveCommand.java b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/CveCommand.java index 3ec14f52..fa2ca36e 100644 --- a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/CveCommand.java +++ b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/CveCommand.java @@ -32,6 +32,9 @@ import io.github.jeremylong.vulnz.cli.cache.CacheException; import io.github.jeremylong.vulnz.cli.cache.CacheProperties; import io.github.jeremylong.vulnz.cli.model.BasicOutput; +import io.github.jeremylong.vulnz.cli.ui.IProgressMonitor; +import io.github.jeremylong.vulnz.cli.ui.JlineShutdownHook; +import io.github.jeremylong.vulnz.cli.ui.ProgressMonitor; import org.apache.commons.io.output.CountingOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -110,6 +113,9 @@ public class CveCommand extends AbstractNvdCommand { @CommandLine.Option(names = {"--cvssV3Severity"}, description = "") private NvdCveClientBuilder.CvssV3Severity cvssV3Severity; + @CommandLine.Option(names = {"--interactive"}, description = "Displays a progress bar") + private boolean interactive; + @Override public Integer timedCall() throws Exception { if (isDebug()) { @@ -265,12 +271,16 @@ private Integer processRequest(NvdCveClientBuilder builder, CacheProperties prop } } ZonedDateTime lastModified = null; + int count = 0; // retrieve from NVD API - try (NvdCveClient api = builder.build()) { + try (NvdCveClient api = builder.build(); IProgressMonitor monitor = new ProgressMonitor(interactive, "NVD")) { + Runtime.getRuntime().addShutdownHook(new JlineShutdownHook()); while (api.hasNext()) { Collection data = api.next(); collectCves(cves, data); lastModified = api.getLastUpdated(); + count += data.size(); + monitor.updateProgress("NVD", count, api.getTotalAvailable()); } } catch (Exception ex) { LOG.debug("\nERROR", ex); @@ -385,9 +395,15 @@ private int processRequest(NvdCveClientBuilder builder) throws IOException { jsonOut.writeFieldName("cves"); jsonOut.writeStartArray(); BasicOutput output = new BasicOutput(); - try (NvdCveClient api = builder.build()) { + int count = 0; + try (NvdCveClient api = builder.build(); IProgressMonitor monitor = new ProgressMonitor(interactive, "NVD")) { + Runtime.getRuntime().addShutdownHook(new JlineShutdownHook()); while (api.hasNext()) { Collection list = api.next(); + if (list != null) { + count += list.size(); + } + monitor.updateProgress("NVD", count, api.getTotalAvailable()); if (list != null) { output.setSuccess(true); output.addCount(list.size()); @@ -415,7 +431,7 @@ private int processRequest(NvdCveClientBuilder builder) throws IOException { } LOG.info(colorize("\nSUCCESS", Attribute.GREEN_TEXT())); status = 0; - } catch (Exception ex) { + } catch (Throwable ex) { LOG.error("\nERROR", ex); } return status; diff --git a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/IProgressMonitor.java b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/IProgressMonitor.java new file mode 100644 index 00000000..455232f1 --- /dev/null +++ b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/IProgressMonitor.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024 Jeremy Long. All Rights Reserved. + */ +package io.github.jeremylong.vulnz.cli.ui; + +public interface IProgressMonitor extends AutoCloseable { + + public void addMonitor(String name); + + public void updateProgress(String name, int current, int max); +} diff --git a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JLineAppender.java b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JLineAppender.java new file mode 100644 index 00000000..ab5d41e7 --- /dev/null +++ b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JLineAppender.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024 Jeremy Long. All Rights Reserved. + */ +package io.github.jeremylong.vulnz.cli.ui; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.layout.TTLLLayout; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.Layout; +import org.jline.terminal.Terminal; + +public class JLineAppender extends AppenderBase { + + private Layout layout; + + public void setLayout(Layout layout) { + this.layout = layout; + } + + @Override + protected void append(ILoggingEvent event) { + Terminal terminal = ProgressMonitor.getTerminal(); + if (terminal != null) { + terminal.writer().println(layout.doLayout(event)); + terminal.flush(); + } else { + if (event.getLevel() == Level.TRACE || event.getLevel() == Level.DEBUG) { + System.err.println(layout.doLayout(event)); + } else { + System.out.println(layout.doLayout(event)); + } + } + } +} diff --git a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JlineShutdownHook.java b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JlineShutdownHook.java new file mode 100644 index 00000000..92fa9eca --- /dev/null +++ b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JlineShutdownHook.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024 Jeremy Long. All Rights Reserved. + */ +package io.github.jeremylong.vulnz.cli.ui; + +import org.jline.terminal.Terminal; + +import java.io.IOException; + +public class JlineShutdownHook extends Thread { + + public void run() { + try { + ProgressMonitor.closeTerminal(); + } catch (IOException e) { + // ignore + } + } +} diff --git a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitor.java b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitor.java new file mode 100644 index 00000000..ad96dc22 --- /dev/null +++ b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitor.java @@ -0,0 +1,126 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2023-2024 Jeremy Long. All Rights Reserved. + */ +package io.github.jeremylong.vulnz.cli.ui; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.AttributedString; +import org.jline.utils.Status; + +public class ProgressMonitor implements IProgressMonitor { + + boolean enabled; + Map rows = new HashMap<>(); + + private static Terminal terminal = null; + private Status status; + + static Terminal getTerminal() { + return terminal; + } + + @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") + public ProgressMonitor(boolean enabled) throws IOException { + + } + + @SuppressFBWarnings({"CT_CONSTRUCTOR_THROW", "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD"}) + public ProgressMonitor(boolean enabled, String name) throws IOException { + this.enabled = enabled; + if (enabled) { + addMonitor(name); + terminal = TerminalBuilder.terminal(); + status = new Status(terminal); + } + } + + @Override + public void addMonitor(String name) { + this.rows.put(name, 0); + } + + private List determineStatusBar() { + int maxNameWidth = rows.keySet().stream().mapToInt(String::length).max().orElse(0); + return rows.entrySet().stream().map(entry -> { + String name = entry.getKey(); + int percent = entry.getValue(); + int remaining = terminal.getWidth(); + remaining = Math.min(remaining, 100); + StringBuilder string = new StringBuilder(remaining); + remaining -= maxNameWidth; + string.append(name); + int spaces = maxNameWidth - name.length(); + if (spaces > 0) { + string.append(String.join("", Collections.nCopies(spaces, " "))); + } + if (percent >= 100) { + string.append(" complete"); + } else { + String spacer = percent < 10 ? " " : ""; + string.append(spacer).append(String.format(" %d%% [", percent)); + remaining -= 10; + int completed = remaining * percent / 100; + int filler = remaining - completed; + System.out.println("completed: " + completed + " filler: " + filler + " remaining: " + remaining); + string.append(String.join("", Collections.nCopies(completed, "="))).append('>') + .append(String.join("", Collections.nCopies(filler, " "))).append(']'); + } + String s = string.toString(); + return new AttributedString(s); + }).sorted().collect(Collectors.toList()); + } + + @Override + public void updateProgress(String name, int current, int max) { + int percent = (int) (current * 100 / max); + rows.put(name, percent); + if (enabled) { + + status.update(new ArrayList()); + status.resize(); + List displayedRows = determineStatusBar(); + status.update(displayedRows, true); + } + } + + @Override + public void close() throws Exception { + if (enabled) { + if (status != null) { + status.close(); + } + closeTerminal(); + enabled = false; + } + } + + static void closeTerminal() throws IOException { + if (terminal != null) { + terminal.close(); + terminal = null; + } + } +} diff --git a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/Screen.java b/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/Screen.java deleted file mode 100644 index 59b0da55..00000000 --- a/vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/Screen.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) 2023-2024 Jeremy Long. All Rights Reserved. - */ -package io.github.jeremylong.vulnz.cli.ui; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -public class Screen { - - boolean quiet; - boolean supported; - Map rows = new HashMap<>(); - - public Screen(boolean quiet) { - this.quiet = quiet; - this.supported = isSupported(); - // tput cols 2> /dev/tty - - clearScreen(); - } - - private void clearScreen() { - System.out.print("\033[H\033[2J"); - } - - private void printProgress(String banner, long total, long current) { - if (!quiet && supported) { - StringBuilder string = new StringBuilder(80); - int percent = (int) (current * 100 / total); - string.append("\033[H").append(banner); - if (current >= total) { - string.append("\n\nComplete"); - } else { - int completed = percent / 2; - int remaining = 50 - completed; - string.append( - String.join("", Collections.nCopies(percent == 0 ? 2 : 2 - (int) (Math.log10(percent)), " "))) - .append(String.format(" %d%% [", percent)) - .append(String.join("", Collections.nCopies(completed, "="))).append('>') - .append(String.join("", Collections.nCopies(remaining, " "))).append(']') - .append(String.join("", - Collections.nCopies((int) (Math.log10(total)) - (int) (Math.log10(current)), " "))) - .append(String.format(" %d/%d", current, total)); - } - System.out.print(string); - } - } - - public void addRow(String name) { - this.rows.put(name, 0); - } - - public void updateProgress(String name, int current, int max) { - rows.put(name, current / max); - } - - private boolean isSupported() { - String os = System.getProperty("os.name").toLowerCase(); - return os.contains("mac") || os.contains("linux"); - } - -} diff --git a/vulnz/src/main/resources/logback-spring.xml b/vulnz/src/main/resources/logback-spring.xml index 655ad80d..f46f1fe4 100644 --- a/vulnz/src/main/resources/logback-spring.xml +++ b/vulnz/src/main/resources/logback-spring.xml @@ -1,6 +1,12 @@ - - System.err + + + + + + + + %msg%n%throwable diff --git a/vulnz/src/test/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitorTest.java b/vulnz/src/test/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitorTest.java new file mode 100644 index 00000000..de116fc5 --- /dev/null +++ b/vulnz/src/test/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitorTest.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2024 Jeremy Long. All Rights Reserved. + */ +package io.github.jeremylong.vulnz.cli.ui; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ProgressMonitorTest { + + @Test + void addRow() { + } +} \ No newline at end of file