diff --git a/pom.xml b/pom.xml index 6415dc1..4e0612d 100644 --- a/pom.xml +++ b/pom.xml @@ -124,7 +124,7 @@ org.mockito - mockito-core + mockito-inline ${mockito.version} test diff --git a/recog-verify/pom.xml b/recog-verify/pom.xml index d62913b..3956c24 100644 --- a/recog-verify/pom.xml +++ b/recog-verify/pom.xml @@ -66,7 +66,7 @@ org.mockito - mockito-core + mockito-inline 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 index 479cc0f..c6d1eee 100644 --- a/recog-verify/src/main/java/com/rapid7/recog/verify/RecogVerifier.java +++ b/recog-verify/src/main/java/com/rapid7/recog/verify/RecogVerifier.java @@ -138,7 +138,8 @@ public static void main(String[] args) { 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()); + String message = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage(); + System.err.printf("error: parsing fingerprints file '%s': %s%n", p.toFile(), message); System.exit(-1); } } diff --git a/recog/pom.xml b/recog/pom.xml index fa84377..ab26cc9 100644 --- a/recog/pom.xml +++ b/recog/pom.xml @@ -48,7 +48,7 @@ org.mockito - mockito-core + mockito-inline diff --git a/recog/src/main/java/com/rapid7/recog/RecogMatcher.java b/recog/src/main/java/com/rapid7/recog/RecogMatcher.java index c41eda5..c760102 100644 --- a/recog/src/main/java/com/rapid7/recog/RecogMatcher.java +++ b/recog/src/main/java/com/rapid7/recog/RecogMatcher.java @@ -243,7 +243,7 @@ public void verifyExamples(BiConsumer consumer) { for (Map.Entry entry : example.getAttributeMap().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - if (key.equals("_encoding")) { + if (key.equals("_encoding") || key.equals("_filename")) { continue; } diff --git a/recog/src/main/java/com/rapid7/recog/parser/RecogParser.java b/recog/src/main/java/com/rapid7/recog/parser/RecogParser.java index 128010f..d1dcc92 100644 --- a/recog/src/main/java/com/rapid7/recog/parser/RecogParser.java +++ b/recog/src/main/java/com/rapid7/recog/parser/RecogParser.java @@ -9,6 +9,10 @@ import java.io.FileReader; import java.io.IOException; import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.StringTokenizer; import java.util.regex.Pattern; @@ -32,6 +36,7 @@ * halt processing. */ public class RecogParser { + private static final String FILENAME_KEY = "_filename"; /** * Factory used to create the underlying {@link RecogPatternMatcher} used @@ -188,7 +193,15 @@ public RecogMatchers parse(Reader reader, String path, String name) attributeMap.put(attrName, attrValue); } - fingerprintPattern.addExample(new FingerprintExample(example.getTextContent(), attributeMap)); + String exampleText; + if (attributeMap.containsKey(FILENAME_KEY)) { + // process external example file + String filename = attributeMap.get(FILENAME_KEY); + exampleText = getExternalExampleText(path, name, filename); + } else { + exampleText = example.getTextContent(); + } + fingerprintPattern.addExample(new FingerprintExample(exampleText, attributeMap)); } // parse and add parameter specifications @@ -260,4 +273,21 @@ private String getRequiredAttribute(Element element, String name) throws ParseEx return value; } + + private String getExternalExampleText(String path, String name, String filename) throws ParseException { + Path examplePath; + if (path != null) { + examplePath = Paths.get(Paths.get(path).getParent().toString(), name, filename); + } else { + examplePath = Paths.get(name, filename); + } + + byte[] exampleBytes; + try { + exampleBytes = Files.readAllBytes(examplePath); + } catch (IOException exception) { + throw new ParseException(String.format("Unable to process fingerprint example file '%s'", examplePath), exception); + } + return new String(exampleBytes, StandardCharsets.US_ASCII); + } } diff --git a/recog/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java b/recog/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java index 0852167..6cebbf7 100644 --- a/recog/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java +++ b/recog/src/test/java/com/rapid7/recog/parser/FingerprintMatcherParserTest.java @@ -3,7 +3,13 @@ import com.rapid7.recog.RecogMatcher; import com.rapid7.recog.RecogMatchers; import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import static com.rapid7.recog.RecogMatcher.pattern; import static com.rapid7.recog.TestGenerators.anyString; import static java.util.regex.Pattern.CASE_INSENSITIVE; @@ -12,8 +18,11 @@ import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; public class FingerprintMatcherParserTest { @@ -97,6 +106,40 @@ public void twoValidFingerprints() throws ParseException { new RecogMatcher(pattern("^Apache$", DOTALL, MULTILINE)).addValue("service.vendor", "Apache").addValue("service.product", "HTTPD").addValue("service.family", "Apache"))); } + @Test + public void validFingerprintExternalExampleFile() throws ParseException { + // given + int exServiceVersion = 1; + String exText = String.format("Apache %d", exServiceVersion); + String exFilename = anyString(); + String xml = String.format("\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "", exFilename, exServiceVersion); + + try (MockedStatic mockFiles = Mockito.mockStatic(Files.class)) { + mockFiles.when(() -> Files.readAllBytes(any(Path.class))) + .thenReturn(exText.getBytes(StandardCharsets.US_ASCII)); + + // 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"))); + assertThat(patterns.get(0).getExamples().size(), is(1)); + mockFiles.verify(() -> Files.readAllBytes(any(Path.class))); + assertThat(patterns.get(0).getExamples(), contains(hasProperty("text", is(exText)))); + } + } + @Test public void invalidFingerprintsIgnored() throws ParseException { // given @@ -279,4 +322,31 @@ public void paramNonZeroPositionWithValueFailsWhenStrict() { // then assertEquals(expectedMessage, exception.getMessage()); } -} \ No newline at end of file + + @Test + public void missingExternalExampleFileFailsWhenStrict() { + // given + String exFilename = anyString(); + String xml = String.format("\n" + + "" + + " \n" + + " Apache returning only its major version number\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "", exFilename); + String name = anyString(); + String expectedMessage = String.format("Unable to process fingerprint example file '%s'", Paths.get(name, exFilename)); + + // when + Exception exception = assertThrows(ParseException.class, () -> { + new RecogParser(true).parse(new StringReader(xml), name); + }, expectedMessage); + + // then + assertEquals(expectedMessage, exception.getMessage()); + } +}