Skip to content

Commit

Permalink
Use SAXParser for parsing fingerprint data
Browse files Browse the repository at this point in the history
This enables capturing of fingerprint line numbers to provide enhanced
verify reporting.
  • Loading branch information
mkienow-r7 committed Mar 15, 2022
1 parent 1c9cad2 commit a276276
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 160 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
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.RecogPatternMatcher;
import java.io.IOException;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class FingerprintsDefaultHandler extends DefaultHandler {
private static final String FINGERPRINTS = "fingerprints";
private static final String FINGERPRINT = "fingerprint";
private static final String DESCRIPTION = "description";
private static final String EXAMPLE = "example";
private static final String PARAM = "param";
private static final String FILENAME_KEY = "_filename";
private static final Logger LOGGER = LoggerFactory.getLogger(FingerprintsDefaultHandler.class);

private final RecogParser.PatternMatcherFactory patternMatcherFactory;
private final boolean strictMode;
private final String path;
private final String name;

private Locator locator;
private RecogMatchers matchers;
private RecogMatcher fingerprintPattern;
private final StringBuilder elementValue;
private HashMap<String, String> exampleAttributeMap;

public RecogMatchers getMatchers() {
return matchers;
}

/**
* Creates a RecogMatcher with the specified {@link RecogPatternMatcher}.
*
* @param patternMatcherFactory Factory used to create the underlying {@link RecogPatternMatcher}.
* @param strictMode {@code true} if the parser should throw exceptions when any error is
* encountered, {@code false} otherwise.
* @param path Optional XML content file path.
* @param name Value used for {@link RecogMatchers} key if parsed value is null or empty.
*/
public FingerprintsDefaultHandler(RecogParser.PatternMatcherFactory patternMatcherFactory, boolean strictMode, String path, String name) {
super();
this.patternMatcherFactory = patternMatcherFactory;
this.strictMode = strictMode;
this.path = path;
this.name = name;
this.elementValue = new StringBuilder();
}

@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator; //Save the locator, so that it can be used later for line tracking when traversing nodes.
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
elementValue.append(ch, start, length);
}

@SuppressWarnings("checkstyle:ParameterName") // TODO: doesn't work, temp renamed lName/llName & qName/qqName
@Override
public void startElement(String uri, String llName, String qqName, Attributes attr) throws SAXException {
elementValue.setLength(0);
try {
switch (qqName.toLowerCase()) {
case FINGERPRINTS:
String preferenceValue = getAttribute(attr, "preference");
float preference = 0;
try {
preference = preferenceValue == null || preferenceValue.isEmpty() ? 0 : Float.parseFloat(preferenceValue);
} catch (NumberFormatException exception) {
// ignore - use default
}

String recogKey = getAttribute(attr, "matches");

if (recogKey == null || recogKey.isEmpty()) {
LOGGER.debug("Recog Matcher Key is Empty or Null. File Name: " + name);
recogKey = name;
}

matchers = new RecogMatchers(path, recogKey, getAttribute(attr, "protocol"), getAttribute(attr, "database_type"), preference);
break;
case FINGERPRINT:
// the pattern is required
String pattern = getRequiredAttribute(attr, "pattern");

// parse the flags for the regular expression
int regexFlags = parseFlags(getAttribute(attr,"flags"));

// construct a pattern
fingerprintPattern = new RecogMatcher(patternMatcherFactory.create(pattern, regexFlags));
fingerprintPattern.setLine(locator.getLineNumber());
break;
case DESCRIPTION:
// NOP
break;
case EXAMPLE:
// example (optional)
exampleAttributeMap = new HashMap<>();
for (int i = 0; i < attr.getLength(); i++) {
String attrName = attr.getQName(i);
String attrValue = getAttribute(attr, i);
exampleAttributeMap.put(attrName, attrValue);
}
break;
case PARAM:
if (fingerprintPattern == null) {
break;
}

int position = Integer.parseInt(getRequiredAttribute(attr, "pos"));
String paramName = getRequiredAttribute(attr, "name");

// zero position indicates a "constant" value
if (position == 0) {
String paramValue = getRequiredAttribute(attr, "value");
fingerprintPattern.addValue(paramName, paramValue);
} else {
// otherwise the position indicates a group match result
String value = getAttribute(attr, "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);
}
break;
default:
LOGGER.info("Unknown qualified name '{}'", qqName);
}
} catch (ParseException | IllegalArgumentException exception) {
LOGGER.warn("Failed to parse fingerprint.", exception);
if (strictMode) {
throw exception;
}
}
}

@SuppressWarnings("checkstyle:ParameterName") // TODO: doesn't work, temp renamed lName/llName & qName/qqName
@Override
public void endElement(String uri, String localName, String qqName) throws SAXException {
try {
switch (qqName.toLowerCase()) {
case FINGERPRINTS:
case PARAM:
// NOP
break;
case FINGERPRINT:
if (fingerprintPattern == null) {
break;
}
matchers.add(fingerprintPattern);
break;
case DESCRIPTION:
// description (optional)
if (fingerprintPattern != null && elementValue.length() > 0) {
fingerprintPattern.setDescription(elementValue.toString().replaceAll("\\s+", " ").trim());
}
break;
case EXAMPLE:
if (fingerprintPattern == null) {
break;
}

String exampleText;
if (exampleAttributeMap != null && exampleAttributeMap.containsKey(FILENAME_KEY)) {
// process external example file
String filename = exampleAttributeMap.get(FILENAME_KEY);
exampleText = getExternalExampleText(path, name, filename);
} else {
exampleText = elementValue.toString();
}
fingerprintPattern.addExample(new FingerprintExample(exampleText, exampleAttributeMap));
break;
default:
LOGGER.info("Unknown qualified name '{}'", qqName);
}
} catch (ParseException | IllegalArgumentException exception) {
LOGGER.warn("Failed to parse fingerprint.", exception);
if (strictMode) {
throw exception;
}
}
}

/////////////////////////////////////////////////////////////////////////
// Non-public methods
/////////////////////////////////////////////////////////////////////////

private int parseFlags(String flags) {
int cflags = Pattern.UNIX_LINES;
if (flags != null && flags.length() != 0) {
StringTokenizer tok = new StringTokenizer(flags, "|,; \t");
while (tok.hasMoreTokens()) {
switch (tok.nextToken()) {
case "REG_ICASE":
case "IGNORECASE":
cflags |= Pattern.CASE_INSENSITIVE;
break;
case "REG_DOT_NEWLINE":
case "REG_MULTILINE":
cflags |= Pattern.DOTALL;
cflags |= Pattern.MULTILINE;
break;
default:
// ignore any other flags
break;
}
}
}

return cflags;
}

public String getAttribute(Attributes attr, String name) {
String value = attr.getValue(name);
return (value == null) ? "" : value;
}

public String getAttribute(Attributes attr, int index) {
String value = attr.getValue(index);
return (value == null) ? "" : value;
}

private String getRequiredAttribute(Attributes attr, String name) throws ParseException {
//String value = attr.getValue(name);
String value = getAttribute(attr, name);
if (value.length() == 0)
throw new ParseException(String.format("Attribute \"%s\" does not exist.", name));

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);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.rapid7.recog.parser;

import org.xml.sax.SAXException;

/**
* Thrown when an error parsing recog input occurs.
*/
public class ParseException extends Exception {
public class ParseException extends SAXException {

public ParseException(String message) {
super(message);
}

public ParseException(String message, Throwable cause) {
public ParseException(String message, Exception cause) {
super(message, cause);
}
}

0 comments on commit a276276

Please sign in to comment.