jscanner

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 2c5fbe36bfcafccaf4c163baa0a507d2b7b41ac0
parent 25bd1759bb87ef7456ad7e4a997ac8425eda27bb
Author: Matthias Jaros <jaros@mailbox.org>
Date:   Mon,  6 Jul 2026 13:02:54 +0200

Completetly reworked progress output. Now instead of individual hosts
give a statusline that prints current stats to err

Diffstat:
Msrc/main/java/com/it_jaros/jscanner/App.java | 55++++++-------------------------------------------------
Dsrc/main/java/com/it_jaros/jscanner/Progress.java | 5-----
Msrc/main/java/com/it_jaros/jscanner/ProgressBar.java | 179+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/main/java/com/it_jaros/jscanner/Scan.java | 54++++++++++++++++++++++++++++--------------------------
Msrc/main/java/com/it_jaros/jscanner/Scanner.java | 51+++++++++++++++++++++++++++++----------------------
5 files changed, 169 insertions(+), 175 deletions(-)

diff --git a/src/main/java/com/it_jaros/jscanner/App.java b/src/main/java/com/it_jaros/jscanner/App.java @@ -1,9 +1,6 @@ package com.it_jaros.jscanner; -import java.util.BitSet; -import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.stream.Collectors; public class App { @@ -14,7 +11,7 @@ public class App { try { run(args); } catch (Exception e) { - System.out.printf("ERROR: %s", e.getMessage()); + System.out.printf("%nFATAL ERROR: %s", e.getMessage()); } } @@ -22,9 +19,12 @@ public class App { ScanOptions options = parseOptions(args); if (options == null) return; try (Scanner scanner = new Scanner(options)) { - Scan scan = Scan.create(options); addShutdownHook(scanner); - scanner.runScan(scan, result -> printPortsResults(result, options.showFilteredPorts())); + Scan scan = Scan.create(options); + ProgressBar progressBar = new ProgressBar(scan, options.quiet()); + progressBar.start(); + scanner.runScan(scan, result -> progressBar.printResult(result, options.showFilteredPorts())); + progressBar.stop(); } finally { shutdownLatch.countDown(); removeShutdownHook(); @@ -44,49 +44,6 @@ public class App { return null; } - private static void printScanResults(List<ScanResult> result, int max, boolean showFilteredPorts) { - printStats(max); - result.forEach((ScanResult r) -> printPortsResults(r, showFilteredPorts)); - } - - private static void printStats(int max) { - System.out.println("--------------------"); - System.out.println("Stats:"); - System.out.println("Peak concurrent connects: " + max); - System.out.println("--------------------"); - } - - private static void printPortsResults(ScanResult result, boolean showFilteredPorts) { - if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) { - return; - } - - System.out.println(result.host()); - if (!result.openPorts().isEmpty()) { - System.out.printf("\t (%d) open:\t%s%n", result.openPorts().cardinality(), map(result.openPorts(), result.bannerRecognition())); - } - if (showFilteredPorts && !result.filteredPorts().isEmpty()) { - System.out.printf("\t (%d) filtered:\t%s%n", result.filteredPorts().cardinality(), map(result.filteredPorts())); - } - System.out.println(); - } - - private static String map(BitSet ports) { - return ports.stream().mapToObj(String::valueOf).collect(Collectors.joining(",")); - } - - private static String map(BitSet ports, java.util.HashMap<Integer, ServiceType> bannerRecognition) { - return ports.stream().mapToObj(port -> { - ServiceType serviceType = bannerRecognition.getOrDefault(port, ServiceType.UNKNOWN); - - if (serviceType == ServiceType.UNKNOWN) { - return String.valueOf(port); - } - - return port + "/" + serviceType.name().toLowerCase(); - }).collect(Collectors.joining(",")); - } - /** * Notice user that the signal was received * Start shutdown of scanner diff --git a/src/main/java/com/it_jaros/jscanner/Progress.java b/src/main/java/com/it_jaros/jscanner/Progress.java @@ -1,5 +0,0 @@ -package com.it_jaros.jscanner; - -import java.util.concurrent.atomic.AtomicInteger; - -public record Progress(String host, int total, AtomicInteger done, AtomicInteger open, AtomicInteger filtered) {} diff --git a/src/main/java/com/it_jaros/jscanner/ProgressBar.java b/src/main/java/com/it_jaros/jscanner/ProgressBar.java @@ -2,29 +2,75 @@ package com.it_jaros.jscanner; import java.io.BufferedReader; import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; +import java.util.BitSet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public class ProgressBar { - private final List<Progress> hosts = new ArrayList<>(); - private final ReentrantLock lock = new ReentrantLock(); private final ScheduledExecutorService ui = Executors.newSingleThreadScheduledExecutor(); + private final Scan scan; private final boolean quiet; private final int columnWidth = ProgressBar.getColumnWidth(); + private final Object outputLock = new Object(); - public ProgressBar(boolean quiet) { + public ProgressBar(Scan scan, boolean quiet) { + this.scan = scan; this.quiet = quiet; } + private void clearStatusLine() { + System.err.print("\r\033[2K"); + System.err.flush(); + } + + public void printResult(ScanResult result, boolean showFilteredPorts) { + synchronized (outputLock) { + if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) { + return; + } + + clearStatusLine(); + printPortsResults(result, showFilteredPorts); + } + } + + private void printPortsResults(ScanResult result, boolean showFilteredPorts) { + if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) { + return; + } + + System.out.println(result.host()); + if (!result.openPorts().isEmpty()) { + System.out.printf("\t (%d) open:\t%s%n", result.openPorts().cardinality(), map(result.openPorts(), result.bannerRecognition())); + } + if (showFilteredPorts && !result.filteredPorts().isEmpty()) { + System.out.printf("\t (%d) filtered:\t%s%n", result.filteredPorts().cardinality(), map(result.filteredPorts())); + } + System.out.println(); + System.out.flush(); + } + + private static String map(BitSet ports) { + return ports.stream().mapToObj(String::valueOf).collect(Collectors.joining(",")); + } + + private static String map(BitSet ports, java.util.HashMap<Integer, ServiceType> bannerRecognition) { + return ports.stream().mapToObj(port -> { + ServiceType serviceType = bannerRecognition.getOrDefault(port, ServiceType.UNKNOWN); + + if (serviceType == ServiceType.UNKNOWN) { + return String.valueOf(port); + } + + return port + "/" + serviceType.name().toLowerCase(); + }).collect(Collectors.joining(",")); + } + private static int getColumnWidth() { try { Process process = new ProcessBuilder("sh", "-c", "stty size < /dev/tty").start(); @@ -47,86 +93,73 @@ public class ProgressBar { } public void start() { - if (!quiet) { - System.out.print("Scanning targets...\n\n"); + if (quiet) { + return; } // although we don't print anything, we still need to // gc all the done hosts + System.err.print("Scanning targets...\n\n"); ui.scheduleAtFixedRate(() -> { - lock.lock(); - drawProgress(false); - lock.unlock(); - }, 1, 500, TimeUnit.MILLISECONDS); - } - - private void drawProgress(boolean last) { - // sort -> the more progressed the host the more up it is - hosts.sort(Comparator.comparingInt((Progress p) -> p.done().get()).reversed()); - - // find and print finished hosts one last time - for (Iterator<Progress> it = hosts.iterator(); it.hasNext(); ) { - Progress p = it.next(); - if (p.done().get() == p.total()) { - it.remove(); - drawBar(p); - } else { - break; + synchronized (outputLock) { + printStatusLine(scan); } - } - - for (Progress progress : hosts) { - drawBar(progress); - } + }, 1, 1000, TimeUnit.MILLISECONDS); + } - if (!last) { - // move cursor up again - if (!quiet) { - System.out.print("\u001b[" + hosts.size() + "A"); - } + public void stop() { + ui.shutdownNow(); + try { + ui.awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); } + printStats(); } - public void add(Progress progress) { - lock.lock(); - hosts.add(progress); - lock.unlock(); + private void printStats() { + System.out.println("\n--------------------"); + System.out.println("Stats:"); + System.out.println("Total hosts scanned: " + scan.getHostTotalCounter().max()); + System.out.println("Peak concurrent connects: " + scan.getSocketCounter().max()); + System.out.println("Peak concurrent threads: " + scan.getThreadCounter().max()); + System.out.println("--------------------\n"); } - private void drawBar(Progress progress) { - if (quiet) return; - System.out.print("\u001b[2K\r"); // clear whole line - System.out.println(createBar( - progress.host(), - progress.open().get(), - progress.filtered().get(), - progress.done().get(), - progress.total())); + private void printStatusLine(final Scan scan) { + this.printStatusLine(scan.getHostCounter(), scan.getHostTotalCounter(), scan.getSocketCounter(), scan.getThreadCounter()); } - public String createBar(String label, int open, int filtered, int done, int total) { - int terminalWidth = columnWidth; - int bracketWidth = terminalWidth / 3; - double pct = total == 0 ? 1.0 : (done / (double) total); - int percent = (int) (pct * 100); - - String statsLeft = String.format("open:%d filtered:%d (%d/%d)", open, filtered, done, total); - String statsRight = String.format("%3d%%", percent); - int filled = (int) (pct * bracketWidth); - String brackets = "[" + "#".repeat(filled) + "-".repeat(bracketWidth - filled) + "]"; - - int spacesBetween = terminalWidth - bracketWidth - (label + statsLeft + statsRight).length() - 5; - return String.format("%s%s%s %s %s", label, " ".repeat(spacesBetween), statsLeft, brackets, statsRight); + private void printStatusLine( + Counter hostCounter, Counter hostTotalCounter, Counter + socketCounter, Counter + threadCounter) { + int doneHosts = hostTotalCounter.current(); + int runningHosts = hostCounter.current(); + int total = doneHosts + runningHosts; + long percent = total == 0 ? 100 : doneHosts * 100L / total; + + String line = String.format( + "hosts: %d running, %d done | sockets: %d active, %d peak | workers: %d active, %d peak", + runningHosts, + doneHosts, + socketCounter.current(), + socketCounter.max(), + threadCounter.current(), + threadCounter.max() + ); + + String barFormat = " | [%s] %3d%%"; + int barWidth = Math.max(5, this.columnWidth - line.length() - barFormat.length()); + String progressBar = progressBar(percent, barWidth); + String status = line + String.format(barFormat, progressBar, percent); + System.err.print("\r" + status); + System.err.flush(); } - public void stop() { - ui.shutdown(); - try { - ui.awaitTermination(1, TimeUnit.SECONDS); - // we need to draw one last time for the 100% to appear - drawProgress(true); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } + private static String progressBar(long percent, int width) { + int filled = (int) (percent * width / 100); + + return "#".repeat(filled) + "-".repeat(width - filled); } } diff --git a/src/main/java/com/it_jaros/jscanner/Scan.java b/src/main/java/com/it_jaros/jscanner/Scan.java @@ -12,27 +12,27 @@ import java.util.stream.Stream; * Object holder for stateful volatile data during scan */ public class Scan { + private final Counter hostCounter; + private final Counter hostTotalCounter; + private final Counter socketCounter; + private final Counter threadCounter; private final Stream<String> hosts; private final String ports; - private final Counter counter; - private final ProgressBar progressBar; - private Scan(Stream<String> hosts, String ports, boolean quiet) { + private Scan(Stream<String> hosts, String ports) { this.hosts = hosts; this.ports = ports; - this.counter = new Counter(); - this.progressBar = new ProgressBar(quiet); - } - - public int getPeakConcurrentConnects() { - return counter.max(); + this.hostCounter = new Counter(); + this.socketCounter = new Counter(); + this.threadCounter = new Counter(); + this.hostTotalCounter = new Counter(); } public static Scan create(ScanOptions options) throws IOException { if (options.hostsFile() != null) { - return create(options.hostsFile(), options.ports(), options.quiet()); + return create(options.hostsFile(), options.ports()); } - return create(options.hostsArgv(), options.ports(), options.quiet()); + return create(options.hostsArgv(), options.ports()); } /** @@ -40,11 +40,10 @@ public class Scan { * * @param sourceFile * @param ports - * @param quiet * @return * @throws IOException */ - public static Scan create(String sourceFile, String ports, boolean quiet) throws IOException { + public static Scan create(String sourceFile, String ports) throws IOException { Stream<String> hosts; if ("-".equals(sourceFile)) { BufferedReader stdinReader = new BufferedReader(new InputStreamReader(System.in)); @@ -60,15 +59,15 @@ public class Scan { }); } - return create(hosts, ports, quiet); + return create(hosts, ports); } - private static Scan create(Stream<String> lines, String ports, boolean quiet) { - return new Scan(lines, ports, quiet); + private static Scan create(Stream<String> lines, String ports) { + return new Scan(lines, ports); } - public static Scan create(List<String> hostsArgv, String ports, boolean quiet) { - return new Scan(hostsArgv.stream(), ports, quiet); + public static Scan create(List<String> hostsArgv, String ports) { + return new Scan(hostsArgv.stream(), ports); } public Stream<String> getHosts() { @@ -80,23 +79,26 @@ public class Scan { } public void start() { - progressBar.start(); + // start } public void stop() { - progressBar.stop(); hosts.close(); // close stream } - public void addHostProgress(Progress progress) { - progressBar.add(progress); + public Counter getThreadCounter() { + return threadCounter; + } + + public Counter getHostCounter() { + return hostCounter; } - public void incSocketCounter() { - counter.inc(); + public Counter getSocketCounter() { + return socketCounter; } - public void decSocketCounter() { - counter.dec(); + public Counter getHostTotalCounter() { + return hostTotalCounter; } } diff --git a/src/main/java/com/it_jaros/jscanner/Scanner.java b/src/main/java/com/it_jaros/jscanner/Scanner.java @@ -67,16 +67,23 @@ public class Scanner implements AutoCloseable { final ProducerState<ScanResult> state = startProducer( scan.getHosts().iterator(), maxHostsLimit, - host -> () -> scanHostPorts(host, scan) + host -> () -> { + try { + scan.getHostCounter().inc(); + return scanHostPorts(host, scan); + } finally { + scan.getHostCounter().dec(); + } + }, + scan.getThreadCounter() ); // the main thread is the consumer - // Let the consumer run as long as - // as the producer runs + // Let the consumer run as long as the producer runs // or if still tasks are pending in pipeline // we do not listen to canceled here because we want - // because we want all results (also partial) ones collected here to print - // whatever is already there + // all results (also partial) collected for the consumer + // with whatever is there already while (state.running().get() || state.inPipeline().get() > 0) { try { // let's check for results and give add them to our scan data holder object @@ -92,6 +99,8 @@ public class Scanner implements AutoCloseable { consumer.accept(result); } } finally { + scan.getHostTotalCounter().inc(); + scan.getThreadCounter().dec(); state.activeWorkers().release(); state.inPipeline().decrementAndGet(); } @@ -118,13 +127,8 @@ public class Scanner implements AutoCloseable { BitSet filteredPorts = new BitSet(PortRange.MAX_PORT); HashMap<Integer, ServiceType> serviceTypes = new HashMap<>(); - // Give progressbar the current progress object which is then updated in the sub virtual threads - Progress progress = new Progress(host, portRange.getTotal(), new AtomicInteger(), new AtomicInteger(), new AtomicInteger()); - scan.addHostProgress(progress); - if (!disableOnlineCheck && !checkHostOnline(host)) { - // Visually show that this host is basically done - progress.done().set(progress.total()); + // Unreachable host return new ScanResult(host, openPorts, filteredPorts, serviceTypes); } @@ -138,7 +142,8 @@ public class Scanner implements AutoCloseable { waitForSlot(portSlotFactory); if (cancelled) return null; return checkPort(host, port, scan); - } + }, + scan.getThreadCounter() ); // consumer is the main thread @@ -159,21 +164,19 @@ public class Scanner implements AutoCloseable { // bitset is not thread-safe, so it is set // outside the other virtual threads that update progress openPorts.set(portResult.getPort()); - progress.open().incrementAndGet(); serviceTypes.put(portResult.getPort(), ServiceDetector.detect(portResult.getBanner())); } case FILTERED -> { filteredPorts.set(portResult.getPort()); - progress.filtered().incrementAndGet(); } default -> { // sonarcube glücklich machen } } } finally { + scan.getThreadCounter().dec(); state.activeWorkers().release(); state.inPipeline().decrementAndGet(); - progress.done().incrementAndGet(); } } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); @@ -201,13 +204,14 @@ public class Scanner implements AutoCloseable { * @param <OUTPUT> * @return */ - private <INPUT, OUTPUT> ProducerState<OUTPUT> startProducer(Iterator<INPUT> queue, int maxWorkers, Function<INPUT, Callable<OUTPUT>> taskFactory) { + private <INPUT, OUTPUT> ProducerState<OUTPUT> startProducer(Iterator<INPUT> queue, int maxWorkers, Function<INPUT, Callable<OUTPUT>> taskFactory, Counter threadCounter) { final AtomicInteger inPipeline = new AtomicInteger(0); final AtomicBoolean running = new AtomicBoolean(true); final Semaphore activeWorkers = new Semaphore(maxWorkers); CompletionService<OUTPUT> completionService = new ExecutorCompletionService<>(executor); executor.submit(() -> { try { + threadCounter.inc(); while (queue.hasNext() && !cancelled) { activeWorkers.acquire(); @@ -226,6 +230,7 @@ public class Scanner implements AutoCloseable { final INPUT item = queue.next(); Callable<OUTPUT> task = taskFactory.apply(item); inPipeline.incrementAndGet(); + threadCounter.inc(); try { // here we are filling the completion service // host <-> virtual thread @@ -233,6 +238,7 @@ public class Scanner implements AutoCloseable { submitted = true; } catch (Throwable e) { inPipeline.decrementAndGet(); + threadCounter.dec(); throw e; } } finally { @@ -244,6 +250,7 @@ public class Scanner implements AutoCloseable { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { + threadCounter.dec(); running.set(false); } }); @@ -277,7 +284,7 @@ public class Scanner implements AutoCloseable { // don't allow more sockets then specified socketLimit.acquire(); // count how many ports are concurrently checked - scan.incSocketCounter(); + scan.getSocketCounter().inc(); try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(host, port), timeoutInMillis); result.setState(PortState.OPEN); @@ -292,7 +299,7 @@ public class Scanner implements AutoCloseable { // NoRouteToHostException: this can be safely ignored because the port is closed if a host is unreachable // Will happen a lot when scanning for open ports, so not needed } finally { - scan.decSocketCounter(); + scan.getSocketCounter().dec(); socketLimit.release(); } @@ -314,6 +321,10 @@ public class Scanner implements AutoCloseable { return null; } + public boolean awaitTermination(Duration duration) throws InterruptedException { + return executor.awaitTermination(duration.toMillis(), TimeUnit.MILLISECONDS); + } + public void cancel() { if (!cancelled) { cancelled = true; @@ -332,8 +343,4 @@ public class Scanner implements AutoCloseable { public void close() throws Exception { cancel(); } - - public boolean awaitTermination(Duration duration) throws InterruptedException { - return executor.awaitTermination(duration.toMillis(), TimeUnit.MILLISECONDS); - } }