diff --git a/README.md b/README.md index 393fa0f..96e6359 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Recog Java -[![Travis (.org)](https://img.shields.io/travis/rapid7/recog-java.svg)](https://travis-ci.org/rapid7/recog-java) [![Maven Central](https://img.shields.io/maven-central/v/com.rapid7.recog/recog-java.svg)](https://search.maven.org/artifact/com.rapid7.recog/recog-java) [![Javadocs](https://www.javadoc.io/badge/com.rapid7.recog/recog-java.svg)](https://www.javadoc.io/doc/com.rapid7.recog/recog-java) +[![CI workflow](https://github.com/rapid7/recog-java/actions/workflows/ci.yml/badge.svg)](https://github.com/rapid7/recog-java/actions/workflows/ci.yml) [![Maven Central](https://img.shields.io/maven-central/v/com.rapid7.recog/recog-java.svg)](https://search.maven.org/artifact/com.rapid7.recog/recog-java) [![Javadocs](https://www.javadoc.io/badge/com.rapid7.recog/recog-java.svg)](https://www.javadoc.io/doc/com.rapid7.recog/recog-java) Java implementation of [Recog](https://github.com/rapid7/recog) that supports parsing and matching. @@ -82,7 +82,7 @@ Missing features: - Matching against multi-line input strings - Matching against base64 encoded strings -- Command line tools like `recog_match` and `recog_verify` +- Command line tools like `recog_match` ## Development diff --git a/pom.xml b/pom.xml index 5395d91..9ee63b5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,12 +3,9 @@ 4.0.0 com.rapid7.recog - recog-java - 0.7.2-SNAPSHOT - jar - recog-java - https://www.rapid7.com - Java implementation of Recog that supports parsing and matching. + recog-parent + 0.8.0-SNAPSHOT + pom scm:git:git@github.com:rapid7/recog-java.git @@ -45,11 +42,12 @@ - 2.2.1 + 2.3.21 1.8 1.8 UTF-8 + 1.4 1.7.25 2.6 @@ -59,69 +57,92 @@ 2.18.0 - - - commons-io - commons-io - ${commons.io.version} - test - + + + + com.rapid7.recog + recog-java + ${project.version} + - - org.apache.commons - commons-lang3 - ${commons.lang3.version} - test - + + com.rapid7.recog + recog-java + ${project.version} + test-jar + test + - - org.hamcrest - hamcrest-core - ${hamcrest.version} - test - + + commons-cli + commons-cli + ${commons.cli.version} + compile + - - org.hamcrest - hamcrest-library - ${hamcrest.version} - test - + + commons-io + commons-io + ${commons.io.version} + test + - - org.junit.jupiter - junit-jupiter-api - ${junit.jupiter.version} - test - + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + test + - - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} - test - + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + - - org.mockito - mockito-core - ${mockito.version} - test - + + org.hamcrest + hamcrest-library + ${hamcrest.version} + test + - - org.slf4j - slf4j-api - ${slf4j.version} - + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + - - org.slf4j - slf4j-simple - ${slf4j.version} - test - - + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + + org.mockito + mockito-core + ${mockito.version} + test + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + @@ -153,7 +174,7 @@ maven-checkstyle-plugin 3.0.0 - ${basedir}/google_checks.xml + ${maven.multiModuleProjectDirectory}/google_checks.xml warning @@ -164,7 +185,7 @@ check - ${basedir}/google_checks.xml + ${maven.multiModuleProjectDirectory}/google_checks.xml @@ -177,53 +198,6 @@ - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.22.0 - - - - integration-test - verify - - - - **/*Integration.java - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.7 - - - recog-download - pre-integration-test - - - - - - - - - - - - - - - run - - - - @@ -296,4 +270,10 @@ + + + recog + recog-verify + + diff --git a/recog-verify/pom.xml b/recog-verify/pom.xml new file mode 100644 index 0000000..d62913b --- /dev/null +++ b/recog-verify/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.rapid7.recog + recog-verify + jar + recog-verify + https://www.rapid7.com + Java implementation of Recog fingerprint verification tool. + + + com.rapid7.recog + recog-parent + 0.8.0-SNAPSHOT + + + + + com.rapid7.recog + recog-java + + + + com.rapid7.recog + recog-java + test-jar + test + + + + commons-cli + commons-cli + + + + commons-io + commons-io + + + + org.apache.commons + commons-lang3 + + + + org.hamcrest + hamcrest-core + + + + org.hamcrest + hamcrest-library + + + + org.junit.jupiter + junit-jupiter-api + + + + org.junit.jupiter + junit-jupiter-engine + + + + org.mockito + mockito-core + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-simple + + + + diff --git a/recog-verify/src/main/java/com/rapid7/recog/verify/Color.java b/recog-verify/src/main/java/com/rapid7/recog/verify/Color.java new file mode 100644 index 0000000..a2fd459 --- /dev/null +++ b/recog-verify/src/main/java/com/rapid7/recog/verify/Color.java @@ -0,0 +1,20 @@ +package com.rapid7.recog.verify; + +// ANSI color escape codes +enum Color { + Reset(0), + Red(31), + Yellow(33), + Green(32), + White(15); + + private final int code; + + Color(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/recog-verify/src/main/java/com/rapid7/recog/verify/Formatter.java b/recog-verify/src/main/java/com/rapid7/recog/verify/Formatter.java new file mode 100644 index 0000000..151285e --- /dev/null +++ b/recog-verify/src/main/java/com/rapid7/recog/verify/Formatter.java @@ -0,0 +1,40 @@ +package com.rapid7.recog.verify; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +public class Formatter { + + private final VerifierOptions options; + private final PrintWriter writer; + + public Formatter(VerifierOptions options, java.io.OutputStream output) { + this.options = options; + this.writer = new PrintWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8), true); + } + + public void statusMessage(String text) { + writer.println(color(text, Color.White)); + } + + public void successMessage(String text) { + writer.println(color(text, Color.Green)); + } + + public void warningMessage(String text) { + writer.println(color(text, Color.Yellow)); + } + + public void failureMessage(String text) { + writer.println(color(text, Color.Red)); + } + + private String color(String text, Color color) { + return options.isColor() ? colorize(text, color) : text; + } + + private String colorize(String text, Color color) { + return String.format("\u001B[%dm%s\u001B[%dm", color.getCode(), text, Color.Reset.getCode()); + } +} diff --git a/recog-verify/src/main/java/com/rapid7/recog/verify/RecogVerifier.java b/recog-verify/src/main/java/com/rapid7/recog/verify/RecogVerifier.java new file mode 100644 index 0000000..47d376a --- /dev/null +++ b/recog-verify/src/main/java/com/rapid7/recog/verify/RecogVerifier.java @@ -0,0 +1,182 @@ +package com.rapid7.recog.verify; + +import com.rapid7.recog.RecogMatcher; +import com.rapid7.recog.RecogMatchers; +import com.rapid7.recog.parser.ParseException; +import com.rapid7.recog.parser.RecogParser; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +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.Options; +import static java.util.Objects.requireNonNull; + +public class RecogVerifier { + + private final RecogMatchers fingerprints; + private final VerifyReporter reporter; + + public static RecogVerifier create(VerifierOptions verifierOpts, RecogMatchers matchers) { + return create(verifierOpts, matchers, System.out); + } + + public static RecogVerifier create(VerifierOptions verifierOpts, RecogMatchers matchers, java.io.OutputStream output) { + requireNonNull(verifierOpts); + + Formatter formatter = new Formatter(verifierOpts, requireNonNull(output)); + VerifyReporter reporter = new VerifyReporter(verifierOpts, formatter); + return new RecogVerifier(requireNonNull(matchers), reporter); + } + + public RecogVerifier(RecogMatchers fingerprints, VerifyReporter reporter) { + this.fingerprints = fingerprints; + this.reporter = reporter; + } + + public RecogMatchers getFingerprints() { + return fingerprints; + } + + public VerifyReporter getReporter() { + return reporter; + } + + public void verify() { + for (RecogMatcher matcher : fingerprints) { + reporter.printName(matcher); + + // NOTE: RecogParser.parse ensures all parameters are valid + matcher.verifyExamples((status, message) -> { + switch (status) { + case Warn: + reporter.warning(String.format("WARN: %s", message)); + break; + case Fail: + reporter.failure(String.format("FAIL: %s", message)); + break; + case Success: + reporter.success(message); + break; + default: + break; + } + }); + } + reporter.report(fingerprints.size()); + } + + public static void main(String[] args) { + CommandLineParser parser = new DefaultParser(); + Options options = new Options(); + options.addOption(Option.builder("f") + .longOpt("format") + .hasArg() + .argName("FORMATTER") + .type(Character.class) + .desc("Choose a formatter.\n [s]ummary (default - failure/warning msgs and summary\n [q]uiet (configured failure/warning msgs only)\n [d]etail (fingerprint name with tests and expanded summary)") + .build()); + options.addOption(new Option("c", "color", false, "Enable color in the output.")); + options.addOption(new Option(null, "warnings", false, "Do not track warnings")); + options.addOption(new Option(null, "no-warnings", false, "Track warnings")); + options.addOption(new Option("h", "help", false, "Command help")); + + + VerifierOptions verifierOpts = null; + String[] filePaths = {}; + try { + // parse the command line arguments + CommandLine line = parser.parse(options, args); + + if (line.hasOption("help")) { + usage(options); + System.exit(1); + } else if (line.getArgs().length == 0) { + System.err.println("Missing XML fingerprint files"); + usage(options); + System.exit(1); + } + + verifierOpts = getVerifierOptions(line); + filePaths = line.getArgs(); + } catch (org.apache.commons.cli.ParseException exception) { + System.err.println("error: command line parsing failed: " + exception.getMessage()); + System.exit(-1); + } + + int failures = 0; + int warnings = 0; + + for (String filePath : filePaths) { + List globPaths = null; + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + filePath); + try (Stream stream = Files.walk(FileSystems.getDefault().getPath(""))) { + globPaths = stream + .filter(Files::isRegularFile) + .filter(pathMatcher::matches) + .collect(Collectors.toList()); + } catch (IOException exception) { + System.err.printf("error: processing path '%s': %s%n", filePath, exception.getMessage()); + System.exit(-1); + } + + for (Path p : globPaths) { + try { + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(p.toFile()); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers); + verifier.verify(); + failures += verifier.getReporter().getFailureCount(); + warnings += verifier.getReporter().getWarningCount(); + } catch (ParseException exception) { + System.err.printf("error: parsing fingerprints file '%s': %s%n", p.toFile(), exception.getMessage()); + System.exit(-1); + } + } + } + + System.exit(failures + warnings); + } + + private static void usage(Options options) { + // generate the help statement + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("recog_verify [options] XML_FINGERPRINT_FILE1 ...", + "Verifies that each fingerprint passes its internal tests.", options, null); + } + + private static VerifierOptions getVerifierOptions(CommandLine line) { + VerifierOptions verifierOpts = new VerifierOptions(); + + if (line.hasOption("format")) { + verifierOpts.setDetail(true); + if (line.getOptionValue("format").startsWith("d")) { + verifierOpts.setDetail(true); + } else if (line.getOptionValue("format").startsWith("q")) { + verifierOpts.setQuiet(true); + } + } + + if (line.hasOption("color")) { + verifierOpts.setColor(true); + } + + if (line.hasOption("warnings")) { + verifierOpts.setWarnings(true); + } + + if (line.hasOption("no-warnings")) { + verifierOpts.setWarnings(false); + } + + return verifierOpts; + } +} diff --git a/recog-verify/src/main/java/com/rapid7/recog/verify/VerifierOptions.java b/recog-verify/src/main/java/com/rapid7/recog/verify/VerifierOptions.java new file mode 100644 index 0000000..ad32d05 --- /dev/null +++ b/recog-verify/src/main/java/com/rapid7/recog/verify/VerifierOptions.java @@ -0,0 +1,47 @@ +package com.rapid7.recog.verify; + +public class VerifierOptions { + private boolean color; + private boolean detail; + private boolean quiet; + private boolean warnings; + + public VerifierOptions() { + color = false; + detail = false; + quiet = false; + warnings = true; + } + + public boolean isColor() { + return color; + } + + public void setColor(boolean color) { + this.color = color; + } + + public boolean isDetail() { + return detail; + } + + public void setDetail(boolean detail) { + this.detail = detail; + } + + public boolean isQuiet() { + return quiet; + } + + public void setQuiet(boolean quiet) { + this.quiet = quiet; + } + + public boolean isWarnings() { + return warnings; + } + + public void setWarnings(boolean warnings) { + this.warnings = warnings; + } +} diff --git a/recog-verify/src/main/java/com/rapid7/recog/verify/VerifyReporter.java b/recog-verify/src/main/java/com/rapid7/recog/verify/VerifyReporter.java new file mode 100644 index 0000000..096d394 --- /dev/null +++ b/recog-verify/src/main/java/com/rapid7/recog/verify/VerifyReporter.java @@ -0,0 +1,112 @@ +package com.rapid7.recog.verify; + +import com.rapid7.recog.RecogMatcher; + +public class VerifyReporter { + + private final VerifierOptions options; + private final Formatter formatter; + private int successCount; + private int warningCount; + private int failureCount; + + public VerifyReporter(VerifierOptions options, Formatter formatter) { + this.options = options; + this.formatter = formatter; + resetCounts(); + } + + public Formatter getFormatter() { + return formatter; + } + + public int getSuccessCount() { + return successCount; + } + + public int getWarningCount() { + return warningCount; + } + + public int getFailureCount() { + return failureCount; + } + + public void report(int fingerprintCount) { + if (!options.isQuiet()) { + summarize(fingerprintCount); + } + } + + public void success(String text) { + successCount++; + if (options.isDetail()) { + formatter.successMessage(String.format("%s%s", padding(), text)); + } + } + + public void warning(String text) { + if (!options.isWarnings()) { + return; + } + + warningCount++; + formatter.warningMessage(String.format("%s%s", padding(), text)); + } + + public void failure(String text) { + failureCount++; + formatter.failureMessage(String.format("%s%s", padding(), text)); + } + + public void printName(RecogMatcher fingerprint) { + if (options.isDetail() && !fingerprint.getExamples().isEmpty()) { + String name = fingerprint.getDescription().isEmpty() ? "[unnamed]" : fingerprint.getDescription(); + formatter.statusMessage(String.format("\n%s", name)); + } + } + + public void summarize(int fingerprintCount) { + if (options.isDetail()) { + printFingerprintCount(fingerprintCount); + } + printSummary(); + } + + public void printFingerprintCount(int count) { + formatter.statusMessage(String.format("\nVerified %d fingerprints:", count)); + } + + public void printSummary() { + colorizeSummary(summaryLine()); + } + + private void resetCounts() { + successCount = 0; + failureCount = 0; + warningCount = 0; + } + + private String padding() { + if (options.isDetail()) { + return " "; + } + return ""; + } + + private String summaryLine() { + return String.format("SUMMARY: Test completed with %d successful, %d warnings" + + ", and %d failures", successCount, warningCount, failureCount); + } + + private void colorizeSummary(String summary) { + if (failureCount > 0) { + formatter.failureMessage(summary); + } else if (warningCount > 0) { + formatter.warningMessage(summary); + } else { + formatter.successMessage(summary); + } + } + +} \ No newline at end of file diff --git a/recog-verify/src/test/java/com/rapid7/recog/verify/RecogVerifierTest.java b/recog-verify/src/test/java/com/rapid7/recog/verify/RecogVerifierTest.java new file mode 100644 index 0000000..51d6b0c --- /dev/null +++ b/recog-verify/src/test/java/com/rapid7/recog/verify/RecogVerifierTest.java @@ -0,0 +1,206 @@ +package com.rapid7.recog.verify; + +import com.rapid7.recog.RecogMatchers; +import com.rapid7.recog.parser.ParseException; +import com.rapid7.recog.parser.RecogParser; +import java.io.StringReader; +import org.apache.commons.io.output.NullOutputStream; +import org.junit.jupiter.api.Test; +import static com.rapid7.recog.TestGenerators.anyString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecogVerifierTest { + @Test + public void verifyNoExampleNoParamsWarnCount() throws ParseException { + // given + String xml = "\n" + + "\n" + + " \n" + + " Service Server - no examples or params\n" + + " \n" + + ""; + + // when + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(new StringReader(xml), anyString()); + VerifierOptions verifierOpts = new VerifierOptions(); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers, NullOutputStream.NULL_OUTPUT_STREAM); + verifier.verify(); + + // then + assertEquals(0, verifier.getReporter().getSuccessCount()); + assertEquals(0, verifier.getReporter().getFailureCount()); + assertEquals(1, verifier.getReporter().getWarningCount()); + } + + @Test + public void verifyNoExampleZeroPositionParamsWarnCount() throws ParseException { + // given + String xml = "\n" + + "\n" + + " \n" + + " Service Server - no examples or params\n" + + " \n" + + " \n" + + " \n" + + ""; + + // when + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(new StringReader(xml), anyString()); + VerifierOptions verifierOpts = new VerifierOptions(); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers, NullOutputStream.NULL_OUTPUT_STREAM); + verifier.verify(); + + // then + assertEquals(0, verifier.getReporter().getSuccessCount()); + assertEquals(0, verifier.getReporter().getFailureCount()); + assertEquals(1, verifier.getReporter().getWarningCount()); + } + + @Test + public void verifyNoExampleNonZeroPositionParamsWarnCount() throws ParseException { + // given + String xml = "\n" + + "\n" + + " \n" + + " Service Server - no examples or params\n" + + " \n" + + " \n" + + " \n" + + " " + + " " + + " \n" + + ""; + + // when + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(new StringReader(xml), anyString()); + VerifierOptions verifierOpts = new VerifierOptions(); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers, NullOutputStream.NULL_OUTPUT_STREAM); + verifier.verify(); + + // then + assertEquals(0, verifier.getReporter().getSuccessCount()); + assertEquals(0, verifier.getReporter().getFailureCount()); + assertEquals(4, verifier.getReporter().getWarningCount()); + } + + @Test + public void verifySuccessfulExampleNonZeroPositionParamsWarnCount() throws ParseException { + // given + String xml = "\n" + + "\n" + + " \n" + + " Service Server - no examples or params\n" + + " Media Server 7.9.3 - 1631723269\n" + + " \n" + + " \n" + + " \n" + + " " + + " " + + " \n" + + ""; + + // when + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(new StringReader(xml), anyString()); + VerifierOptions verifierOpts = new VerifierOptions(); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers, NullOutputStream.NULL_OUTPUT_STREAM); + verifier.verify(); + + // then + assertEquals(1, verifier.getReporter().getSuccessCount()); + assertEquals(0, verifier.getReporter().getFailureCount()); + assertEquals(3, verifier.getReporter().getWarningCount()); + } + + @Test + public void verifySuccessfulExample() throws ParseException { + // given + String xml = "\n" + + "\n" + + " \n" + + " Service Server - no examples or params\n" + + " Media Server 7.9.3 - 1631723269\n" + + " \n" + + " \n" + + " \n" + + " " + + " " + + " \n" + + ""; + + // when + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(new StringReader(xml), anyString()); + VerifierOptions verifierOpts = new VerifierOptions(); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers, NullOutputStream.NULL_OUTPUT_STREAM); + verifier.verify(); + + // then + assertEquals(1, verifier.getReporter().getSuccessCount()); + assertEquals(0, verifier.getReporter().getFailureCount()); + assertEquals(0, verifier.getReporter().getWarningCount()); + } + + @Test + public void verify1FailureAnd1SuccessfulExamples() throws ParseException { + // given + String xml = "\n" + + "\n" + + " \n" + + " Service Server - no examples or params\n" + + " Media Server 1.2.3.4\n" + + " Media Server 7.9.3 - 1631723269\n" + + " \n" + + " \n" + + " \n" + + " " + + " " + + " \n" + + ""; + + // when + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(new StringReader(xml), anyString()); + VerifierOptions verifierOpts = new VerifierOptions(); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers, NullOutputStream.NULL_OUTPUT_STREAM); + verifier.verify(); + + // then + assertEquals(1, verifier.getReporter().getSuccessCount()); + assertEquals(1, verifier.getReporter().getFailureCount()); + assertEquals(0, verifier.getReporter().getWarningCount()); + } + + @Test + public void verify2FailureExamples() throws ParseException { + // given + String xml = "\n" + + "\n" + + " \n" + + " Service Server - no examples or params\n" + + " Media Server 1.2.3.4\n" + + " Media Server 7.9.3 - 1631723269\n" + + " \n" + + " \n" + + " \n" + + " " + + " " + + " \n" + + ""; + + // when + RecogParser recogParser = new RecogParser(true); + RecogMatchers matchers = recogParser.parse(new StringReader(xml), anyString()); + VerifierOptions verifierOpts = new VerifierOptions(); + RecogVerifier verifier = RecogVerifier.create(verifierOpts, matchers, NullOutputStream.NULL_OUTPUT_STREAM); + verifier.verify(); + + // then + assertEquals(0, verifier.getReporter().getSuccessCount()); + assertEquals(2, verifier.getReporter().getFailureCount()); + assertEquals(0, verifier.getReporter().getWarningCount()); + } +} diff --git a/recog/pom.xml b/recog/pom.xml new file mode 100644 index 0000000..fa84377 --- /dev/null +++ b/recog/pom.xml @@ -0,0 +1,129 @@ + + + 4.0.0 + + com.rapid7.recog + recog-java + jar + recog-java + https://www.rapid7.com + Java implementation of Recog that supports parsing and matching. + + + com.rapid7.recog + recog-parent + 0.8.0-SNAPSHOT + + + + + commons-io + commons-io + + + + org.apache.commons + commons-lang3 + + + + org.hamcrest + hamcrest-core + + + + org.hamcrest + hamcrest-library + + + + org.junit.jupiter + junit-jupiter-api + + + + org.junit.jupiter + junit-jupiter-engine + + + + org.mockito + mockito-core + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-simple + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + test-jar + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.0 + + + + integration-test + verify + + + + **/*Integration.java + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + recog-download + pre-integration-test + + + + + + + + + + + + + + + run + + + + + + + + diff --git a/recog/src/main/java/com/rapid7/recog/FingerprintExample.java b/recog/src/main/java/com/rapid7/recog/FingerprintExample.java new file mode 100644 index 0000000..6f56e09 --- /dev/null +++ b/recog/src/main/java/com/rapid7/recog/FingerprintExample.java @@ -0,0 +1,22 @@ +package com.rapid7.recog; + +import java.util.Map; + +// Represents a fingerprint example and associated data. +public class FingerprintExample { + private final String text; + private final Map attributeMap; + + public FingerprintExample(String text, Map attributeMap) { + this.text = text; + this.attributeMap = attributeMap; + } + + public String getText() { + return text; + } + + public Map getAttributeMap() { + return attributeMap; + } +} diff --git a/src/main/java/com/rapid7/recog/Recog.java b/recog/src/main/java/com/rapid7/recog/Recog.java similarity index 100% rename from src/main/java/com/rapid7/recog/Recog.java rename to recog/src/main/java/com/rapid7/recog/Recog.java diff --git a/src/main/java/com/rapid7/recog/RecogMatch.java b/recog/src/main/java/com/rapid7/recog/RecogMatch.java similarity index 100% rename from src/main/java/com/rapid7/recog/RecogMatch.java rename to recog/src/main/java/com/rapid7/recog/RecogMatch.java diff --git a/src/main/java/com/rapid7/recog/RecogMatchResult.java b/recog/src/main/java/com/rapid7/recog/RecogMatchResult.java similarity index 100% rename from src/main/java/com/rapid7/recog/RecogMatchResult.java rename to recog/src/main/java/com/rapid7/recog/RecogMatchResult.java diff --git a/src/main/java/com/rapid7/recog/RecogMatcher.java b/recog/src/main/java/com/rapid7/recog/RecogMatcher.java similarity index 65% rename from src/main/java/com/rapid7/recog/RecogMatcher.java rename to recog/src/main/java/com/rapid7/recog/RecogMatcher.java index c78e6e1..a7cf372 100644 --- a/src/main/java/com/rapid7/recog/RecogMatcher.java +++ b/recog/src/main/java/com/rapid7/recog/RecogMatcher.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.Set; import java.util.StringJoiner; +import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.util.Collections.emptySet; @@ -24,6 +25,54 @@ */ public class RecogMatcher implements Serializable { + /** + * Interpolate the string using the "recog interpolation syntax" + * This syntax will take a string like "adsf {service.version} {service.family}" + * and attempt to resolve "{service.version}" and "{service.family}" in the string + * against the parameter map. So, given these parameters: + * - service.cpe23: "adsf {service.version} {service.family}" + * - service.version: "1.1" + * - service.family: "foo" + * + *

The map will resolve to: + * - service.cpe23: "asdf 1.1 foo" + * - service.version: "1.1" + * - service.family: "foo" + * + * @param keyEndsWith A string used to filter which keys will have their + * values interpolated (any key ending with this value). If {@code null}, + * all keys are considered. + * @param match The map containing values that can be interpolated. Must not + * be {@code null}. + * @return A map containing the interpolated key/values. + */ + public static Map interpolate(String keyEndsWith, Map match) { + requireNonNull(match); + + for (Entry entry : match.entrySet()) { + // For all keys that end with a certain extension (for optimization)... + if (keyEndsWith == null || entry.getKey().endsWith(keyEndsWith)) { + String value = entry.getValue(); + if (value != null) { + // The operation below is a "fold left" -- basically iterate over + // all the items in the map, and attempt to replace the items in + // this string with those map items. + String result = + match.entrySet().stream() + .reduce( + entry.getValue(), + (part, item) -> { + return part.replace("{" + item.getKey() + "}", item.getValue() == null ? "-" : item.getValue()); + }, + (part, item) -> part); + match.put(entry.getKey(), result); + } + } + } + + return match; + } + private final RecogPatternMatcher matcher; /** "Constant" values always matched as parameters. Key is the name, value is the value. */ @@ -45,7 +94,7 @@ public class RecogMatcher implements Serializable { private String description; /** Optional examples that illustrate the matcher (or that can be used to test the matcher). */ - private Set examples; + private Set examples; /** * Creates a new RecogMatcher using a {@link JavaRegexRecogPatternMatcher} to @@ -98,9 +147,10 @@ public String getDescription() { * {@code null}. * @return A reference to this matcher to allow for method chaining. */ - public RecogMatcher addExample(String example) { - if (example != null) + public RecogMatcher addExample(FingerprintExample example) { + if (example != null) { examples.add(example); + } return this; } @@ -111,7 +161,7 @@ public RecogMatcher addExample(String example) { * * @return A non-null, immutable {@link Set} of examples. May be empty. */ - public Set getExamples() { + public Set getExamples() { return examples == null ? emptySet() : unmodifiableSet(examples); } @@ -164,11 +214,81 @@ public Map match(String input) { } } - return values; + return interpolate(null, values); } else return null; } + public void verifyExamples(BiConsumer consumer) { + // look for the presence of test cases + if (examples.size() == 0) { + consumer.accept(VerifyStatus.Warn, String.format("'%s' has no test cases", description)); + } + + // make sure each test case passes + for (FingerprintExample example : examples) { + Map result = match(example.getText()); + if (result == null) { + consumer.accept(VerifyStatus.Fail, String.format("'%s' failed to match \"%s\" with '%s'", + description, example.getText(), matcher.getPattern())); + continue; + } + + VerifyStatus verifyStatus = VerifyStatus.Success; + String message = example.getText(); + // Ensure that all the attributes as provided by the example were parsed + // out correctly and match the capture group values we expect. + for (Map.Entry entry : example.getAttributeMap().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key.equals("_encoding")) { + continue; + } + + if (!result.containsKey(key) || !result.get(key).equals(value)) { + verifyStatus = VerifyStatus.Fail; + message = String.format("'%s' failed to find expected capture group %s '%s'. Result was %s", + description, key, value, result.get(key)); + break; + } + } + consumer.accept(verifyStatus, message); + } + + verifyExamplesHaveCaptureGroups(consumer); + } + + private void verifyExamplesHaveCaptureGroups(BiConsumer consumer) { + Map captureGroupUsed = new HashMap<>(); + // get a list of parameters that are defined by capture groups + for (Entry parameter : positionalParameters.entrySet()) { + if (parameter.getValue() > 0 && !parameter.getKey().isEmpty()) { + captureGroupUsed.put(parameter.getKey(), false); + } + } + + // match up the fingerprint parameters with test attributes + for (FingerprintExample example : examples) { + Map result = match(example.getText()); + for (Entry entry : example.getAttributeMap().entrySet()) { + String key = entry.getKey(); + if (captureGroupUsed.containsKey(key)) { + captureGroupUsed.replace(key, true); + } + } + } + + // alert on untested parameters + for (Entry entry : captureGroupUsed.entrySet()) { + String paramName = entry.getKey(); + Boolean paramUsed = entry.getValue(); + if (!paramUsed) { + String message = String.format("'%s' is missing an example that checks for parameter '%s' which is derived from a capture group", description, paramName); + consumer.accept(VerifyStatus.Warn, message); + } + } + } + /** * Adds a constant parameter value with the given name. If the matcher matches, this key-value * pair is guaranteed to be returned in the result of {@link #match(String)}. diff --git a/src/main/java/com/rapid7/recog/RecogMatchers.java b/recog/src/main/java/com/rapid7/recog/RecogMatchers.java similarity index 50% rename from src/main/java/com/rapid7/recog/RecogMatchers.java rename to recog/src/main/java/com/rapid7/recog/RecogMatchers.java index baf62b5..188633d 100644 --- a/src/main/java/com/rapid7/recog/RecogMatchers.java +++ b/recog/src/main/java/com/rapid7/recog/RecogMatchers.java @@ -15,7 +15,6 @@ */ public class RecogMatchers extends ArrayList { - private static final String CPE_SUFFIX = ".cpe23"; private String key; private String protocol; private String type; @@ -48,54 +47,6 @@ public float getPreference() { return preference; } - /** - * Interpolate the string using the "recog interpolation syntax" - * This syntax will take a string like "adsf {service.version} {service.family}" - * and attempt to resolve "{service.version}" and "{service.family}" in the string - * against the parameter map. So, given these parameters: - * - service.cpe23: "adsf {service.version} {service.family}" - * - service.version: "1.1" - * - service.family: "foo" - * - *

The map will resolve to: - * - service.cpe23: "asdf 1.1 foo" - * - service.version: "1.1" - * - service.family: "foo" - * - * @param keyEndsWith A string used to filter which keys will have their - * values interpolated (any key ending with this value). If {@code null}, - * all keys are considered. - * @param match The map containing values that can be interpolated. Must not - * be {@code null}. - * @return A map containing the interpolated key/values. - */ - public Map interpolate(String keyEndsWith, Map match) { - requireNonNull(match); - - for (Map.Entry entry : match.entrySet()) { - // For all keys that end with a certain extension (for optimization)... - if (keyEndsWith == null || entry.getKey().endsWith(keyEndsWith)) { - String value = entry.getValue(); - if (value != null) { - // The operation below is a "fold left" -- basically iterate over - // all the items in the map, and attempt to replace the items in - // this string with those map items. - String result = - match.entrySet().stream() - .reduce( - entry.getValue(), - (part, item) -> { - return part.replace("{" + item.getKey() + "}", item.getValue() == null ? "-" : item.getValue()); - }, - (part, item) -> part); - match.put(entry.getKey(), result.replaceAll(":$", "")); - } - } - } - - return match; - } - /** * Finds matches for a string input against all matchers. * @@ -108,7 +59,7 @@ public List getMatches(String input) { else return stream().map(matcher -> { Map match = matcher.match(input); - return match != null ? new RecogMatch(matcher, interpolate(CPE_SUFFIX, match)) : null; + return match != null ? new RecogMatch(matcher, match) : null; }).filter(Objects::nonNull).collect(toList()); } @@ -125,7 +76,7 @@ public RecogMatch getFirstMatch(String input) { for (RecogMatcher matcher : this) { Map match = matcher.match(input); if (match != null) - return new RecogMatch(matcher, interpolate(CPE_SUFFIX, match)); + return new RecogMatch(matcher, match); } return null; diff --git a/src/main/java/com/rapid7/recog/RecogType.java b/recog/src/main/java/com/rapid7/recog/RecogType.java similarity index 100% rename from src/main/java/com/rapid7/recog/RecogType.java rename to recog/src/main/java/com/rapid7/recog/RecogType.java diff --git a/src/main/java/com/rapid7/recog/RecogVersion.java b/recog/src/main/java/com/rapid7/recog/RecogVersion.java similarity index 100% rename from src/main/java/com/rapid7/recog/RecogVersion.java rename to recog/src/main/java/com/rapid7/recog/RecogVersion.java diff --git a/recog/src/main/java/com/rapid7/recog/VerifyStatus.java b/recog/src/main/java/com/rapid7/recog/VerifyStatus.java new file mode 100644 index 0000000..9640315 --- /dev/null +++ b/recog/src/main/java/com/rapid7/recog/VerifyStatus.java @@ -0,0 +1,8 @@ +package com.rapid7.recog; + +// Verifier status +public enum VerifyStatus { + Warn, + Fail, + Success +} diff --git a/src/main/java/com/rapid7/recog/parser/ParseException.java b/recog/src/main/java/com/rapid7/recog/parser/ParseException.java similarity index 100% rename from src/main/java/com/rapid7/recog/parser/ParseException.java rename to recog/src/main/java/com/rapid7/recog/parser/ParseException.java diff --git a/src/main/java/com/rapid7/recog/parser/RecogParser.java b/recog/src/main/java/com/rapid7/recog/parser/RecogParser.java similarity index 90% rename from src/main/java/com/rapid7/recog/parser/RecogParser.java rename to recog/src/main/java/com/rapid7/recog/parser/RecogParser.java index c29c6c2..5d4f877 100644 --- a/src/main/java/com/rapid7/recog/parser/RecogParser.java +++ b/recog/src/main/java/com/rapid7/recog/parser/RecogParser.java @@ -1,5 +1,6 @@ package com.rapid7.recog.parser; +import com.rapid7.recog.FingerprintExample; import com.rapid7.recog.RecogMatcher; import com.rapid7.recog.RecogMatchers; import com.rapid7.recog.pattern.JavaRegexRecogPatternMatcher; @@ -8,6 +9,7 @@ import java.io.FileReader; import java.io.IOException; import java.io.Reader; +import java.util.HashMap; import java.util.StringTokenizer; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; @@ -16,6 +18,8 @@ import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -162,8 +166,19 @@ public RecogMatchers parse(Reader reader, String name) if ("base64".equals(example.getAttribute("_encoding"))) { // TODO: these are currently ignored as the Base64 decoding isn't working properly - } else - fingerprintPattern.addExample(exampleContent); + } else { + HashMap attributeMap = new HashMap<>(); + NamedNodeMap exAttributes = example.getAttributes(); + + for (int i = 0; i < exAttributes.getLength(); i++) { + Node attr = exAttributes.item(i); + String attrName = attr.getNodeName(); + String attrValue = attr.getNodeValue(); + attributeMap.put(attrName, attrValue); + } + + fingerprintPattern.addExample(new FingerprintExample(exampleContent, attributeMap)); + } } // parse and add parameter specifications @@ -178,9 +193,12 @@ public RecogMatchers parse(Reader reader, String name) if (position == 0) { String paramValue = getRequiredAttribute(parameter, "value"); fingerprintPattern.addValue(paramName, paramValue); - } - // otherwise the position indicates a group match result - else { + } else { + // otherwise the position indicates a group match result + String value = parameter.getAttribute("value"); + if (!value.isEmpty()) { + throw new ParseException(String.format("Attribute \"%s\" has a non-zero position but specifies a value of \"%s\"", paramName, value)); + } fingerprintPattern.addParam(position, paramName); } } diff --git a/src/main/java/com/rapid7/recog/pattern/JavaRegexRecogPatternMatcher.java b/recog/src/main/java/com/rapid7/recog/pattern/JavaRegexRecogPatternMatcher.java similarity index 100% rename from src/main/java/com/rapid7/recog/pattern/JavaRegexRecogPatternMatcher.java rename to recog/src/main/java/com/rapid7/recog/pattern/JavaRegexRecogPatternMatcher.java diff --git a/src/main/java/com/rapid7/recog/pattern/RecogPatternMatchResult.java b/recog/src/main/java/com/rapid7/recog/pattern/RecogPatternMatchResult.java similarity index 100% rename from src/main/java/com/rapid7/recog/pattern/RecogPatternMatchResult.java rename to recog/src/main/java/com/rapid7/recog/pattern/RecogPatternMatchResult.java diff --git a/src/main/java/com/rapid7/recog/pattern/RecogPatternMatcher.java b/recog/src/main/java/com/rapid7/recog/pattern/RecogPatternMatcher.java similarity index 100% rename from src/main/java/com/rapid7/recog/pattern/RecogPatternMatcher.java rename to recog/src/main/java/com/rapid7/recog/pattern/RecogPatternMatcher.java diff --git a/src/main/java/com/rapid7/recog/provider/CompositeRecogMatchersProvider.java b/recog/src/main/java/com/rapid7/recog/provider/CompositeRecogMatchersProvider.java similarity index 100% rename from src/main/java/com/rapid7/recog/provider/CompositeRecogMatchersProvider.java rename to recog/src/main/java/com/rapid7/recog/provider/CompositeRecogMatchersProvider.java diff --git a/src/main/java/com/rapid7/recog/provider/IRecogMatchersProvider.java b/recog/src/main/java/com/rapid7/recog/provider/IRecogMatchersProvider.java similarity index 100% rename from src/main/java/com/rapid7/recog/provider/IRecogMatchersProvider.java rename to recog/src/main/java/com/rapid7/recog/provider/IRecogMatchersProvider.java diff --git a/src/main/java/com/rapid7/recog/provider/RecogMatchersProvider.java b/recog/src/main/java/com/rapid7/recog/provider/RecogMatchersProvider.java similarity index 100% rename from src/main/java/com/rapid7/recog/provider/RecogMatchersProvider.java rename to recog/src/main/java/com/rapid7/recog/provider/RecogMatchersProvider.java diff --git a/src/test/java/com/rapid7/recog/CustomPatternMatcherTest.java b/recog/src/test/java/com/rapid7/recog/CustomPatternMatcherTest.java similarity index 100% rename from src/test/java/com/rapid7/recog/CustomPatternMatcherTest.java rename to recog/src/test/java/com/rapid7/recog/CustomPatternMatcherTest.java diff --git a/src/test/java/com/rapid7/recog/FingerprintMatcherTest.java b/recog/src/test/java/com/rapid7/recog/FingerprintMatcherTest.java similarity index 100% rename from src/test/java/com/rapid7/recog/FingerprintMatcherTest.java rename to recog/src/test/java/com/rapid7/recog/FingerprintMatcherTest.java diff --git a/src/test/java/com/rapid7/recog/FingerprintMatchersTest.java b/recog/src/test/java/com/rapid7/recog/FingerprintMatchersTest.java similarity index 97% rename from src/test/java/com/rapid7/recog/FingerprintMatchersTest.java rename to recog/src/test/java/com/rapid7/recog/FingerprintMatchersTest.java index d6f31d7..27b6ea1 100644 --- a/src/test/java/com/rapid7/recog/FingerprintMatchersTest.java +++ b/recog/src/test/java/com/rapid7/recog/FingerprintMatchersTest.java @@ -217,13 +217,12 @@ public void multipleOfTheSameInterpolationProperty() { @Test public void interpolateWithNullSuffix() { // given - RecogMatchers matchers = new RecogMatchers(); HashMap map = new HashMap<>(); map.put("foo", "test"); map.put("bar", "{foo}"); // when - matchers.interpolate(null, map); + RecogMatcher.interpolate(null, map); // then assertThat(map.get("bar"), is("test")); @@ -233,13 +232,12 @@ public void interpolateWithNullSuffix() { @Test public void interpolateWithNonNullSuffix() { // given - RecogMatchers matchers = new RecogMatchers(); HashMap map = new HashMap<>(); map.put("foo", "test"); map.put("bar", "{foo}"); // when - matchers.interpolate("bar", map); + RecogMatcher.interpolate("bar", map); // then assertThat(map.get("bar"), is("test")); diff --git a/src/test/java/com/rapid7/recog/RecogIntegration.java b/recog/src/test/java/com/rapid7/recog/RecogIntegration.java similarity index 87% rename from src/test/java/com/rapid7/recog/RecogIntegration.java rename to recog/src/test/java/com/rapid7/recog/RecogIntegration.java index 4f2637d..7190b5b 100644 --- a/src/test/java/com/rapid7/recog/RecogIntegration.java +++ b/recog/src/test/java/com/rapid7/recog/RecogIntegration.java @@ -30,9 +30,9 @@ public void allRecogContentParses() throws FileNotFoundException, ParseException RecogMatchers matchers = parser.parse(file); for (RecogMatcher matcher : matchers) { // when - the matcher has examples - for (String example : matcher.getExamples()) + for (FingerprintExample example : matcher.getExamples()) // then - the example matches - assertThat("Matcher in " + file + " with pattern '" + matcher.getPattern() + "' does not match example '" + example + "'.", matcher.matches(example), is(true)); + assertThat("Matcher in " + file + " with pattern '" + matcher.getPattern() + "' does not match example '" + example.getText() + "'.", matcher.matches(example.getText()), is(true)); } } } diff --git a/src/test/java/com/rapid7/recog/TestGenerators.java b/recog/src/test/java/com/rapid7/recog/TestGenerators.java similarity index 100% rename from src/test/java/com/rapid7/recog/TestGenerators.java rename to recog/src/test/java/com/rapid7/recog/TestGenerators.java diff --git a/recog/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java b/recog/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java new file mode 100644 index 0000000..faee403 --- /dev/null +++ b/recog/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java @@ -0,0 +1,282 @@ +package com.rapid7.recog.parser; + +import com.rapid7.recog.RecogMatcher; +import com.rapid7.recog.RecogMatchers; +import java.io.StringReader; +import org.junit.jupiter.api.Test; +import static com.rapid7.recog.RecogMatcher.pattern; +import static com.rapid7.recog.TestGenerators.anyString; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static java.util.regex.Pattern.DOTALL; +import static java.util.regex.Pattern.MULTILINE; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class FingerprintMatcherParserTest { + + @Test + public void noFingerprintsAreReadWhenInputIsEmpty() throws ParseException { + // given + String xml = ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(0)); + } + + @Test + public void invalidXmlInputCausesExceptionToBeThrown() throws ParseException { + // given + String xml = "foo"; + + // when + assertThrows(ParseException.class, () -> new RecogParser().parse(new StringReader(xml), anyString())); + + // then - throws exception + } + + @Test + public void validFingerprint() throws ParseException { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache 1\n" + + " Apache 2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(1)); + assertThat(patterns, hasItems(new RecogMatcher(pattern("^Apache (\\d)$", DOTALL, MULTILINE)).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache").addParam(1, "service.version"))); + } + + @Test + public void twoValidFingerprints() throws ParseException { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache/1\n" + + " Apache/2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Apache returning no version information\n" + + " Apache\n" + + " apache\n" + + " \n" + + " \n" + + " \n" + + " " + + ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(2)); + assertThat(patterns, hasItems( + new RecogMatcher(pattern("^Apache/\\d$", CASE_INSENSITIVE)).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache"), + new RecogMatcher(pattern("^Apache$", MULTILINE)).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache"))); + } + + @Test + public void invalidFingerprintsIgnored() throws ParseException { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache/1\n" + + " Apache/2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Apache returning only its major version number\n" + + " Apache/1\n" + + " \n" + + " \n" + + " \n" + + " Apache returning no version information\n" + + " Apache\n" + + " apache\n" + + " \n" + + " \n" + + " \n" + + " " + + ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(2)); + } + + @Test + public void patternIsRequired() throws ParseException { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache 1\n" + + " Apache 2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(0)); + } + + @Test + public void patternMustBeValid() throws ParseException { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache 1\n" + + " Apache 2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(0)); + } + + @Test + public void emptyFlagsIgnored() throws ParseException { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache 1\n" + + " Apache 2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(1)); + assertThat(patterns, hasItems(new RecogMatcher(pattern("^Apache (\\d)$")).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache").addParam(1, "service.version"))); + } + + @Test + public void unknownFlagIgnored() throws ParseException { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache 1\n" + + " Apache 2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + // when + RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); + + // then + assertThat(patterns.size(), is(1)); + assertThat(patterns, hasItems(new RecogMatcher(pattern("^Apache (\\d)$")).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache").addParam(1, "service.version"))); + } + + @Test + public void paramZeroPositionWithNoValueFailsWhenStrict() { + // given + String xml = "\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache 1\n" + + " Apache 2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + String expectedMessage = "Attribute \"value\" does not exist."; + + // when + Exception exception = assertThrows(ParseException.class, () -> { + new RecogParser(true).parse(new StringReader(xml), anyString()); + }, expectedMessage); + + // then + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + public void paramNonZeroPositionWithValueFailsWhenStrict() { + // given + String paramName = "service.version"; + String paramValue = "1"; + String xml = String.format("\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " Apache 1\n" + + " Apache 2\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "", paramName, paramValue); + String expectedMessage = String.format("Attribute \"%s\" has a non-zero position but specifies a value of \"%s\"", paramName, paramValue); + + // when + Exception exception = assertThrows(ParseException.class, () -> { + new RecogParser(true).parse(new StringReader(xml), anyString()); + }, expectedMessage); + + // then + assertEquals(expectedMessage, exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/com/rapid7/recog/provider/CompositeFingerprintMatchersProviderTest.java b/recog/src/test/java/com/rapid7/recog/provider/CompositeFingerprintMatchersProviderTest.java similarity index 100% rename from src/test/java/com/rapid7/recog/provider/CompositeFingerprintMatchersProviderTest.java rename to recog/src/test/java/com/rapid7/recog/provider/CompositeFingerprintMatchersProviderTest.java diff --git a/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java b/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java deleted file mode 100644 index 9693d1c..0000000 --- a/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.rapid7.recog.parser; - -import com.rapid7.recog.RecogMatcher; -import com.rapid7.recog.RecogMatchers; -import java.io.StringReader; -import org.junit.jupiter.api.Test; -import static com.rapid7.recog.RecogMatcher.pattern; -import static com.rapid7.recog.TestGenerators.anyString; -import static java.util.regex.Pattern.CASE_INSENSITIVE; -import static java.util.regex.Pattern.DOTALL; -import static java.util.regex.Pattern.MULTILINE; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class FingerprintMatcherParserTest { - - @Test - public void noFingerprintsAreReadWhenInputIsEmpty() throws ParseException { - // given - String xml = ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(0)); - } - - @Test - public void invalidXmlInputCausesExceptionToBeThrown() throws ParseException { - // given - String xml = "foo"; - - // when - assertThrows(ParseException.class, () -> new RecogParser().parse(new StringReader(xml), anyString())); - - // then - throws exception - } - - @Test - public void validFingerprint() throws ParseException { - // given - String xml = "\n" - + "" - + " \n" - + " Apache returning only its major version number\n" - + " Apache 1\n" - + " Apache 2\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(1)); - assertThat(patterns, hasItems(new RecogMatcher(pattern("^Apache (\\d)$", DOTALL, MULTILINE)).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache").addParam(1, "service.version"))); - } - - @Test - public void twoValidFingerprints() throws ParseException { - // given - String xml = "\n" - + "" - + " \n" - + " Apache returning only its major version number\n" - + " Apache/1\n" - + " Apache/2\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Apache returning no version information\n" - + " Apache\n" - + " apache\n" - + " \n" - + " \n" - + " \n" - + " " - + ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(2)); - assertThat(patterns, hasItems( - new RecogMatcher(pattern("^Apache/\\d$", CASE_INSENSITIVE)).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache"), - new RecogMatcher(pattern("^Apache$", MULTILINE)).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache"))); - } - - @Test - public void invalidFingerprintsIgnored() throws ParseException { - // given - String xml = "\n" - + "" - + " \n" - + " Apache returning only its major version number\n" - + " Apache/1\n" - + " Apache/2\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Apache returning only its major version number\n" - + " Apache/1\n" - + " \n" - + " \n" - + " \n" - + " Apache returning no version information\n" - + " Apache\n" - + " apache\n" - + " \n" - + " \n" - + " \n" - + " " - + ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(2)); - } - - @Test - public void patternIsRequired() throws ParseException { - // given - String xml = "\n" - + "" - + " \n" - + " Apache returning only its major version number\n" - + " Apache 1\n" - + " Apache 2\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(0)); - } - - @Test - public void patternMustBeValid() throws ParseException { - // given - String xml = "\n" - + "" - + " \n" - + " Apache returning only its major version number\n" - + " Apache 1\n" - + " Apache 2\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(0)); - } - - @Test - public void emptyFlagsIgnored() throws ParseException { - // given - String xml = "\n" - + "" - + " \n" - + " Apache returning only its major version number\n" - + " Apache 1\n" - + " Apache 2\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(1)); - assertThat(patterns, hasItems(new RecogMatcher(pattern("^Apache (\\d)$")).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache").addParam(1, "service.version"))); - } - - @Test - public void unknownFlagIgnored() throws ParseException { - // given - String xml = "\n" - + "" - + " \n" - + " Apache returning only its major version number\n" - + " Apache 1\n" - + " Apache 2\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; - - // when - RecogMatchers patterns = new RecogParser().parse(new StringReader(xml), anyString()); - - // then - assertThat(patterns.size(), is(1)); - assertThat(patterns, hasItems(new RecogMatcher(pattern("^Apache (\\d)$")).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache").addParam(1, "service.version"))); - } -}