package com.denimgroup.threadfix.service.channel; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import org.apache.commons.io.IOUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import com.denimgroup.threadfix.data.dao.ChannelSeverityDao; import com.denimgroup.threadfix.data.dao.ChannelTypeDao; import com.denimgroup.threadfix.data.dao.ChannelVulnerabilityDao; import com.denimgroup.threadfix.data.entities.ChannelType; import com.denimgroup.threadfix.data.entities.DataFlowElement; import com.denimgroup.threadfix.data.entities.Finding; import com.denimgroup.threadfix.data.entities.Scan; import com.denimgroup.threadfix.webapp.controller.ScanCheckResultBean; /** * This class currently handles JSON output from either the flat JSONArray version * or the JSONObject version with the date and other information included. * * @author mcollins */ public class BrakemanChannelImporter extends AbstractChannelImporter { boolean hasFindings = false, correctFormat = false, hasDate = false; // This is a hybrid confidence / vuln type mix. We may not end up keeping this. private static final Map<String, Integer> SEVERITIES_MAP = new HashMap<String, Integer>(); static { SEVERITIES_MAP.put("Cross Site Scripting", 3); SEVERITIES_MAP.put("Response Splitting", 2); SEVERITIES_MAP.put("Nested Attributes", 1); SEVERITIES_MAP.put("Mass Assignment", 2); SEVERITIES_MAP.put("Format Validation", 1); SEVERITIES_MAP.put("Redirect", 3); SEVERITIES_MAP.put("Command Injection", 3); SEVERITIES_MAP.put("Dynamic Render Path", 2); SEVERITIES_MAP.put("Mail Link", 2); SEVERITIES_MAP.put("SQL Injection", 3); SEVERITIES_MAP.put("Session Setting", 3); SEVERITIES_MAP.put("Dangerous Send", 3); SEVERITIES_MAP.put("File Access", 3); SEVERITIES_MAP.put("Basic Auth", 1); SEVERITIES_MAP.put("Attribute Restriction", 1); SEVERITIES_MAP.put("Dangerous Eval", 2); SEVERITIES_MAP.put("Default Routes", 1); SEVERITIES_MAP.put("Cross-Site Request Forgery", 2); SEVERITIES_MAP.put("Remote Code Execution", 3); SEVERITIES_MAP.put("Denial of Service", 2); SEVERITIES_MAP.put("Authentication", 1); } // This is a hybrid confidence / vuln type mix. We may not end up keeping this. private static final Map<String, Integer> CONFIDENCE_MAP = new HashMap<String, Integer>(); static { CONFIDENCE_MAP.put("High", 2); CONFIDENCE_MAP.put("Medium", 1); CONFIDENCE_MAP.put("Weak", 0); } @Autowired public BrakemanChannelImporter(ChannelTypeDao channelTypeDao, ChannelVulnerabilityDao channelVulnerabilityDao, ChannelSeverityDao channelSeverityDao) { this.channelTypeDao = channelTypeDao; this.channelVulnerabilityDao = channelVulnerabilityDao; this.channelSeverityDao = channelSeverityDao; this.channelType = channelTypeDao.retrieveByName(ChannelType.BRAKEMAN); } public Calendar getDate(String jsonString) { try { JSONObject jsonObject = new JSONObject(jsonString); JSONObject scanInfo = jsonObject.getJSONObject("scan_info"); String dateString = scanInfo.getString("timestamp"); return getCalendarFromString("EEE MMM dd hh:mm:ss Z yyyy",dateString); } catch (JSONException e) { log.warn("JSON input was probably version 1.", e); return null; } } @Override public Scan parseInput() { if (inputStream == null) { return null; } Scan scan = new Scan(); scan.setFindings(new ArrayList<Finding>()); scan.setApplicationChannel(applicationChannel); boolean isVersion2 = false; String inputString = null; try { inputString = IOUtils.toString(inputStream); } catch (IOException e) { log.warn("Something went wrong with the input stream. Weird.", e); } finally { closeInputStream(inputStream); } if (inputString == null) { return null; } JSONObject resultingObject = null; if (inputString.trim().startsWith("{")) { try { resultingObject = new JSONObject(inputString); if (resultingObject != null) { isVersion2 = true; } } catch (JSONException e) { log.info("JSONException raised when trying to create a JSON Object. Probably version 1.", e); } } try { JSONArray jsonArray = null; if (isVersion2) { jsonArray = resultingObject.getJSONArray("warnings"); scan.setImportTime(getDate(inputString)); } else { jsonArray = new JSONArray(inputString); } if (jsonArray == null) { return null; } for (int index = 0; index < jsonArray.length(); index++) { log.debug("Checking item[" + index + "] in the jsonArray"); Object item = jsonArray.get(index); if (item instanceof JSONObject) { JSONObject jsonItem = (JSONObject) item; String jsConfidence = jsonItem.getString("confidence"); log.debug("JSON confidence value is " + jsConfidence); String jsWarningType = jsonItem.getString("warning_type"); log.debug("JSON warning_type is " + jsWarningType); Integer confidence = CONFIDENCE_MAP.get(jsConfidence); log.debug("Mapped confidence is " + confidence); Integer severity = SEVERITIES_MAP.get(jsWarningType); log.debug("Mapped severity for warning_type " + jsWarningType + " is " + severity); // Make sure we got valid values back. As the Brakeman JSON file format advances over // time they might add vulnerability types or confidence values that we have not // anticipated and we want to be able to at least partially fight through. if(confidence == null) { log.warn("Got a null ThreadFix confidence for JSON confidence of " + jsConfidence); continue; } else if(severity == null) { log.warn("Got a null ThreadFix severity for JSON warning_type of " + jsWarningType); continue; } String severityCode = String.valueOf(confidence + severity); String parameter = null; if (isVersion2) { parameter = jsonItem.getString("user_input"); } Finding finding = constructFinding(jsonItem.getString("file"), parameter, jsonItem.getString("warning_type"), severityCode); if (finding != null) { finding.setIsStatic(true); finding.setNativeId(hashFindingInfo(jsonItem.toString(),null,null)); if (jsonItem.getString("code") != null) { DataFlowElement element = new DataFlowElement(); element.setLineText(jsonItem.getString("code")); element.setSourceFileName(jsonItem.getString("file")); if (isVersion2) { String lineString = jsonItem.getString("line"); if (!lineString.equals("null")) { try { element.setLineNumber(Integer.valueOf(lineString)); } catch (NumberFormatException e) { log.error("Non-numeric value found in Brakeman JSON file.", e); } } } finding.setDataFlowElements(Arrays.asList(new DataFlowElement[] {element})); } scan.getFindings().add(finding); } } else { log.debug("Got a non-JSONObject object: " + item); } } } catch (JSONException e) { log.warn("Encountered JSONException.", e); } return scan; } private ScanImportStatus getTestStatus() { if (!correctFormat) testStatus = ScanImportStatus.WRONG_FORMAT_ERROR; else if (hasDate) testStatus = checkTestDate(); if (ScanImportStatus.SUCCESSFUL_SCAN.equals(testStatus) && !hasFindings) testStatus = ScanImportStatus.EMPTY_SCAN_ERROR; else if (testStatus == null) testStatus = ScanImportStatus.SUCCESSFUL_SCAN; return testStatus; } @Override public ScanCheckResultBean checkFile() { boolean done = false; byte[] byteArray = null; try { byteArray = IOUtils.toByteArray(inputStream); closeInputStream(inputStream); inputStream = new ByteArrayInputStream(byteArray); } catch (IOException e) { log.error("Problems manipulating input stream and byte array.", e); } if (byteArray == null) { return new ScanCheckResultBean(ScanImportStatus.WRONG_FORMAT_ERROR); } String jsonString = new String(byteArray); // Check the first character to avoid a possible exception if (jsonString.trim().startsWith("[")) { try { JSONArray array = new JSONArray(jsonString); if (array != null) { done = true; log.info("Scan is using the old JSON output format."); if (array.length() > 0) { hasFindings = true; JSONObject oneFinding = array.getJSONObject(0); if (oneFinding != null) { correctFormat = oneFinding.get("location") != null && oneFinding.get("file") != null && oneFinding.get("message") != null && oneFinding.get("confidence") != null && oneFinding.get("code") != null && oneFinding.get("warning_type") != null; } } } } catch (JSONException e) { log.warn("Encountered JSONException.", e); } } // Output Version 2 // Check the first character to avoid a possible exception if (!done && jsonString.trim().startsWith("{")) { try { JSONObject object = new JSONObject(jsonString); if (object != null) { log.info("Scan is using the new JSON output format."); testDate = getDate(jsonString); hasDate = testDate != null; JSONArray array = object.getJSONArray("warnings"); if (array.length() > 0) { hasFindings = true; JSONObject oneFinding = array.getJSONObject(0); if (oneFinding != null) { correctFormat = oneFinding.get("location") != null && oneFinding.get("file") != null && oneFinding.get("message") != null && oneFinding.get("confidence") != null && oneFinding.get("code") != null && oneFinding.get("user_input") != null && oneFinding.get("line") != null && oneFinding.get("warning_type") != null; } } } } catch (JSONException e) { log.warn("Encountered JSONException.", e); } } return new ScanCheckResultBean(getTestStatus(), testDate); } }