package com.rackspace.threadfix.service.defects;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.StringReader;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import javax.net.ssl.SSLHandshakeException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.versionone.om.V1Instance;
import com.versionone.om.V1InstanceGetter;
import com.versionone.om.AssetID;
import com.versionone.om.Project;
import com.versionone.om.filters.ProjectFilter;
import com.denimgroup.threadfix.data.entities.Defect;
import com.denimgroup.threadfix.data.entities.Vulnerability;
import com.denimgroup.threadfix.data.entities.ChannelType;
import com.denimgroup.threadfix.data.entities.Finding;
import com.denimgroup.threadfix.data.entities.SurfaceLocation;
import com.denimgroup.threadfix.service.defects.AbstractDefectTracker;
import com.denimgroup.threadfix.service.defects.DefectMetadata;
import com.denimgroup.threadfix.service.defects.ProjectMetadata;
import com.denimgroup.threadfix.service.SanitizedLogger;
public class VersionOneDefectTracker extends AbstractDefectTracker {
private static String V1_REST_ENDPOINT = "rest-1.v1";
private V1Instance v1;
// targetProject is the selected child project (in component) or the parent project if parent do not have children
private Project targetProject;
private Project childProject;
private String url = null;
private String username;
private String password;
private static String jdbcPropertiesFile = "jdbc.properties";
private static Properties jdbcProperties = new Properties();
private static String JDBC_DRIVER;
private static String DB_URL;
private static String USER;
private static String PASS;
private static Connection conn;
private static PreparedStatement preparedStatementLongDesc;
private static PreparedStatement preparedStatementProjNames;
private static PreparedStatement preparedStatementSeverity;
private static String longDescSQL = "select F.LONGDESCRIPTION, F.ID from FINDING F, SURFACELOCATION S where S.id = F.surfacelocationid and S.parameter = ? and S.path = ?";
private static String projectNamesSQL = "select name from application order by name";
private static String severitySQL = "select gs.name from finding f, vulnerability v, genericseverity gs where f.vulnerabilityid = v.id and v.genericseverityid = gs.id and f.id = ?";
private static String projs;
static org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory.getLog(VersionOneDefectTracker.class);
static {
if (jdbcProperties.isEmpty()) {
logger.info("-- loading jdbc properties");
try {
InputStream inputStream = VersionOneDefectTracker.class.getClassLoader().getResourceAsStream(jdbcPropertiesFile);
if (inputStream == null) {
logger.error("file '" + jdbcPropertiesFile + "' not found in the classpath");
}
jdbcProperties.load(inputStream);
}
catch(Exception e) {
logger.error("Exception loading jdbc properties " + e);
}
}
logger.info("loaded " + jdbcProperties.size() + " properties from " + jdbcPropertiesFile);
connectToDatabase();
prepareStatementLongDesc();
prepareStatementProjNames();
prepareStatementSeverity();
populateProjectNamesFromDB();
}
/*
* You can implement only parts of this class and have functional integration.
*
* If you assume valid input, return true from all three hasValid... methods.
*
* If you don't care about error messages when submission fails, return a string literal from getTrackerError()
*
* Static options can be used in getProjectMetadata, just expand on the code that's already there.
* Not all of the fields need to be implemented.
*
* You could also get away with returning string literals from getProductNames() and getProjectIdByName() if they
* don't change often or you are tracking only certain applications.
*
* getBugUrl is purely for convenience when viewing the defect page.
*
* createDefect() is obviously the most important. If you only need to export, just implement that.
*
* getMultipleDefectStatus is only necessary if you want changes to appear in ThreadFix.
*
*/
@Override
public String createDefect(List<Vulnerability> vulnerabilities,
DefectMetadata metadata) {
log.info(">>> Entering createDefect");
AssetID aid = null;
Project p = getProjectByName();
for (Vulnerability vulnerability : vulnerabilities) {
String vulnerabilityName = "";
String vulnerabilityId = "";
if (vulnerability.getGenericVulnerability() != null) {
if (vulnerability.getGenericVulnerability().getName() != null) {
vulnerabilityName = vulnerability.getGenericVulnerability().getName();
log.info("vulnerability.getGenericVulnerability().getName()=" + vulnerabilityName);
}
if (vulnerability.getGenericVulnerability().getId() != null) {
vulnerabilityId = "" + vulnerability.getGenericVulnerability().getId();
log.info("vulnerability.getGenericVulnerability().getId()=CWE-ID " + vulnerabilityId);
}
}
String path = "";
String param = "";
String url = "";
if (vulnerability.getSurfaceLocation() != null) {
if (vulnerability.getSurfaceLocation().getPath() != null) {
path = vulnerability.getSurfaceLocation().getPath();
log.info("vulnerability.getSurfaceLocation().getPath()=" + path);
}
if (vulnerability.getSurfaceLocation().getParameter() != null) {
param = vulnerability.getSurfaceLocation().getParameter();
log.info("vulnerability.getSurfaceLocation().getParameter()=" + param);
}
if (vulnerability.getSurfaceLocation().getUrl() != null) {
url = vulnerability.getSurfaceLocation().getUrl().toString();
log.info("vulnerability.getSurfaceLocation().getUrl()=" + url);
}
}
String component = "";
if (metadata.getComponent() != null) {
component = metadata.getComponent();
log.info(" -- selected component=" + component);
String childProjectName = component;
v1 = getV1Instance();
V1InstanceGetter v1getter = v1.get();
targetProject = v1getter.projectByName(childProjectName);
}
DescAndFindingID df = getLongDescription(param, path);
String longDescription = df.longDescription;
int findingId = df.findingId;
if (longDescription != null && !longDescription.equals("")) {
String severityPS = translateSeverity(getSeverity(findingId));
StringBuilder ldesc = new StringBuilder(longDescription);
editDescription(ldesc, "\n", "<br>");
com.versionone.om.Defect d = targetProject.createDefect("Product_Security - " +
severityPS + " - " +
getProjectName() + " - " +
path + " - " +
vulnerabilityName);
d.setDescription(ldesc.toString());
d.setFoundBy("Product Security");
aid = d.getID();
d.save();
log.info(">>> Exiting createDefect - success");
return aid.getToken();
}
}
log.info(">>> Exiting createDefect - fail");
return "";
}
@Override
public String getBugURL(String endpointURL, String bugID) {
// Here you want to return the URL that points to the bug page. Something like
// return endpointURL + "/xmlrpc.cgi?bugId=" + bugID;
log.info(">>> Entering getBugURL");
if (endpointURL != null)
log.info(" --endpointURL=" + endpointURL);
if (endpointURL != null)
log.info(" --bugID=" + bugID);
log.info(">>> Exiting getBugURL");
return null;
}
@Override
public Map<Defect, Boolean> getMultipleDefectStatus(List<Defect> defectList) {
log.info(">>> Entering getMultipleDefectStatus");
Map<Defect, Boolean> returnMap = new HashMap<Defect, Boolean>();
// Find the open or closed status for each defect.
if (defectList != null && defectList.size() != 0) {
log.info("Updating VersionOne defect status for " + defectList.size() + " defects.");
v1 = getV1Instance();
V1InstanceGetter v1getter = v1.get();
String status = null;
for (Defect defect : defectList) {
if (defect != null) {
// do whatever to get the status
status = getStatus(defect, v1getter);
log.info(" defect status = " + status);
if ( !status.equals("Done") ) {
log.info(" defect is open");
returnMap.put(defect, true);
//defect.setStatus("Actual Status"); // ASSIGNED / RESOLVED / CLOSED / etc.
// This will be displayed as the status for the defect
}
else {
log.info(" defect is closed");
returnMap.put(defect, false);
}
}
}
} else {
log.info("Tried to update defects but no defects were found.");
}
log.info(">>> Exiting getMultipleDefectStatus");
return returnMap;
}
private String getStatus(Defect defect, V1InstanceGetter v1getter) {
log.info(">>> Entering getStatus");
if (defect == null) {
log.info(" defect is null");
return "";
}
String v1defectId = defect.getNativeId();
if (v1defectId == null) {
log.info(" v1defectId is null");
return "";
}
com.versionone.om.Defect v1defect = v1getter.defectByID(v1defectId);
if (v1defect == null) {
log.info(" v1defect is null");
return "";
}
com.versionone.om.IListValueProperty prop = v1defect.getStatus();
if (prop == null) {
log.info(" prop is null");
return "";
}
if (prop != null) {
log.info(" prop.getCurrentValue=" + prop.getCurrentValue());
return prop.getCurrentValue();
}
log.info(">>> Exiting getStatus");
return "";
}
@SuppressWarnings("unused")
@Override
public String getProductNames() {
log.info(">>> Entering getProductNames");
// In this method you will have a username and password that the user configured as well as
// the endpoint URL that was previously configured. These can be accessed with
String usernameToSubmit = getUsername();
String passwordToSubmit = getPassword();
String urlToUse = getUrl();
// Do whatever to get the product names, then concatenate them together with commas separating them
// If you use the setLastError method and return null,
// the user will be presented with the passed string as an error message.
//setLastError("The request for product names failed because the credentials are incorrect.");
//return "Comma,Separated,Product Names";
if (projs == null) {
log.error("-- something is wrong. product list is empty");
}
log.info(">>> Exiting getProductNames");
return projs;
}
@Override
public String getProjectIdByName() {
// In this method, in addition to getUsername(), getPassword(), and getUrl(),
// getProjectName() should return a valid product name.
// Your job is to use this information to find the corresponding ID.
// If you do not have separate names and IDs, just use the name.
// maybe do web requests
log.info(">>> Entering getProjectIdByName");
log.info("Finding id for project " + getProjectName());
//Project p = getProjectByName();
//if (p != null) {
// AssetID aid = p.getID();
// log.info(" project id=" + aid.getToken());
// return aid.getToken();
//}
//return "";
log.info(">>> Exiting getProjectIdByName");
return getProjectId();
}
@Override
public ProjectMetadata getProjectMetadata() {
log.info(">>> Entering getProjectMetadata");
// This method is a little trickier than the previous ones.
// You will have username, password, url, projectname, and projectid.
// either make a request or populate lists in code and return a ProjectMetadata object.
// The values you present here are options that the user will select that will then come back
// in the defectmetadata object for createDefect().
List<String> blankList = Arrays.asList(new String[] {"-"});
List<String> statuses = new ArrayList<String>();
List<String> components = new ArrayList<String>();
List<String> severities = new ArrayList<String>();
List<String> versions = new ArrayList<String>();
List<String> priorities = new ArrayList<String>();
String projectName = getProjectName();
if (projectName != null) {
v1 = getV1Instance();
V1InstanceGetter v1getter = v1.get();
Project parentProj = v1getter.projectByName(projectName);
if (parentProj != null) {
ProjectFilter filter = new ProjectFilter();
Collection<Project> childrenProjs = parentProj.getChildProjects(filter);
for (Project p : childrenProjs) {
components.add(p.getName());
}
}
if (components.size() == 0) {
log.info(" parent project do not have children! setting component as the parent!");
components.add(parentProj.getName());
}
}
log.info("-- After populating components");
statuses.add("-");
severities.add("-");
versions.add("-");
priorities.add("-");
log.info(">>> Exiting getProjectMetadata");
return new ProjectMetadata(components, blankList, blankList, statuses, priorities);
}
@Override
public String getTrackerError() {
// When a defect submission fails, this method is used to update the JobStatus with the correct
// error message. Not absolutely necessary.
log.info(">>> Entering getTrackerError");
log.info("Returning the error from the tracker.");
log.info(">>> Exiting getTrackerError");
return "The tracker failed to export a defect.";
}
@Override
public boolean hasValidCredentials() {
// getUrl(), getPassword(), and getUsername will work here.
// Given those, check the credentials for validity.
log.info(">>> Entering hasValidCredentials");
try {
v1 = new V1Instance(getUrl(), getUsername(), getPassword());
}
catch(Exception e) {
log.info(">>> Exiting hasValidCredentials");
return false;
}
log.info(">>> Exiting hasValidCredentials");
return true;
}
@Override
public boolean hasValidProjectName() {
// getProjectName() as well as credentials and the URL are available here.
// this is used to check server-side that the user picked a valid option from the drop-down.
log.info(">>> Entering hasValidProjectName");
log.info("Checking Project Name.");
Project p = getProjectByName();
if (p != null) {
log.info(">>> Exiting hasValidProjectName : true");
return true;
}
else {
log.info(">>> Exiting hasValidProjectName : false");
return false;
}
}
@Override
public boolean hasValidUrl() {
// Given only getUrl(), make sure that there is a valid endpoint that you can use.
log.info(">>> Entering hasValidUrl");
if (getUrlWithRest() == null) {
log.info("URL was invalid.");
log.info(">>> Exiting hasValidUrl : false");
return false;
}
log.info(">>> Exiting hasValidUrl : true");
return true;
}
private String getUrlWithRest() {
log.info(">>> Entering getUrlWithRest");
if (getUrl() == null || getUrl().trim().equals("")) {
return null;
}
try {
new URL(getUrl());
} catch (MalformedURLException e) {
setLastError("The URL format was bad.");
return null;
}
if (getUrl().endsWith(V1_REST_ENDPOINT + "/")) {
return getUrl();
}
if (getUrl().endsWith(V1_REST_ENDPOINT)) {
return getUrl() + "/";
}
String tempUrl = getUrl().trim();
if (tempUrl.endsWith("/")) {
tempUrl = tempUrl.concat(V1_REST_ENDPOINT + "/");
} else {
tempUrl = tempUrl.concat("/" + V1_REST_ENDPOINT + "/");
}
log.info(">>> Exiting getUrlWithRest");
return tempUrl;
}
private com.versionone.om.Project getProjectByName() {
// In this method, in addition to getUsername(), getPassword(), and getUrl(),
// getProjectName() should return a valid product name.
// Your job is to use this information to find the corresponding ID.
// If you do not have separate names and IDs, just use the name.
log.info(">>> Entering getProjectByName");
log.info("Finding project " + getProjectName());
v1 = getV1Instance();
V1InstanceGetter v1getter = v1.get();
Project p = v1getter.projectByName(getProjectName());
if (p != null)
return p;
/* Project p = null;
Iterator<Project> it = rootProjects.iterator();
for (; it.hasNext();) {
p = it.next();
log.info("found proj=" + p.getName());
if (p.getName().equals(getProjectName())) {
log.info("returning proj=" + p.getName());
return p;
}
} */
log.info("-- getProjectByName is returning null!!!");
log.info(">>> Exiting getProjectByName");
return null;
}
private V1Instance getV1Instance() {
log.info(">>> Entering getV1Instance");
log.info("Getting v1 instance ");
if (v1 == null) {
try {
v1 = new V1Instance(getUrl(), getUsername(), getPassword());
log.info("-- Successfully connected to v1 in getV1Instance");
}
catch(Exception e) {
setLastError("Unable to connect to V1 server.");
log.error("-- Exception getting connection obj v1 in getV1Instance");
throw new RuntimeException("Exception getting connection obj v1 in getV1Instance");
}
}
log.info(">>> Exiting getV1Instance");
return v1;
}
private static void connectToDatabase() {
logger.info(">>> Entering connectToDatabase");
if (conn == null) {
try {
JDBC_DRIVER = jdbcProperties.getProperty("jdbc.driverClassName");
Class.forName(JDBC_DRIVER);
USER = jdbcProperties.getProperty("jdbc.username");
PASS = jdbcProperties.getProperty("jdbc.password");
DB_URL = jdbcProperties.getProperty("jdbc.url");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
logger.info("Connected database successfully...");
}catch(SQLException se){
logger.error("-- got exception getting prepared stmt db");
se.printStackTrace();
}catch(Exception e){
logger.error("-- got exception connecting to db");
e.printStackTrace();
}
}
logger.info(">>> Exiting connectToDatabase");
}
private static void prepareStatementLongDesc() {
logger.info(">>> Entering prepareStatementLongDesc");
if (preparedStatementLongDesc == null) {
try {
connectToDatabase();
preparedStatementLongDesc = conn.prepareStatement(longDescSQL);
}catch(SQLException se){
logger.error("-- got exception getting prepared stmt db for long desc");
se.printStackTrace();
}catch(Exception e){
logger.error("-- got exception connecting to db for long desc");
e.printStackTrace();
}
}
logger.info(">>> Exiting prepareStatementLongDesc");
}
private static void prepareStatementProjNames() {
logger.info(">>> Entering prepareStatementProjNames");
if (preparedStatementProjNames == null) {
try {
connectToDatabase();
preparedStatementProjNames = conn.prepareStatement(projectNamesSQL);
}catch(SQLException se){
logger.error("-- got exception getting prepared stmt db for proj names");
se.printStackTrace();
}catch(Exception e){
logger.error("-- got exception connecting to db for proj names");
e.printStackTrace();
}
}
logger.info(">>> Exiting prepareStatementProjNames");
}
private static void prepareStatementSeverity() {
logger.info(">>> Entering prepareStatementSeverity");
if (preparedStatementSeverity == null) {
try {
connectToDatabase();
preparedStatementSeverity = conn.prepareStatement(severitySQL);
}catch(SQLException se){
logger.error("-- got exception getting prepared stmt db for severity");
se.printStackTrace();
}catch(Exception e){
logger.error("-- got exception connecting to db for severity");
e.printStackTrace();
}
}
logger.info(">>> Exiting prepareStatementSeverity");
}
private String getSeverity(int findingId) {
logger.info(">>> Entering getSeverity");
logger.info(" findingId=" + findingId);
String genericSeverity = "";
try {
prepareStatementSeverity();
preparedStatementSeverity.setInt(1, findingId);
ResultSet rs = preparedStatementSeverity.executeQuery();
while (rs.next()) {
genericSeverity = rs.getString(1);
logger.info(" genericSeverity=" + genericSeverity);
return genericSeverity;
}
}catch(Exception e){
logger.error("-- got exception reading generic severity from db " + e);
e.printStackTrace();
}
logger.info(">>> Exiting getSeverity");
return "";
}
private String translateSeverity(String genericSeverity) {
logger.info(">>> Entering translateSeverity with genericSeverity=" + genericSeverity);
String severityPS = "";
if (genericSeverity.equals("Critical"))
severityPS = "S0";
else if (genericSeverity.equals("High"))
severityPS = "S1";
else if (genericSeverity.equals("Medium"))
severityPS = "S2";
else if (genericSeverity.equals("Low"))
severityPS = "S3";
else
severityPS = "Unknown";
logger.info(">>> Exiting translateSeverity");
return severityPS;
}
private DescAndFindingID getLongDescription(String param, String path) {
logger.info(">>> Entering getLongDescription with param=" + param + " path=" + path);
String longDescription = "";
int findingId = -1;
try {
prepareStatementLongDesc();
preparedStatementLongDesc.setString(1, param);
preparedStatementLongDesc.setString(2, path);
ResultSet rs = preparedStatementLongDesc.executeQuery();
while (rs.next()) {
longDescription = rs.getString("LONGDESCRIPTION");
findingId = rs.getInt("ID");
log.info(" findingId=" + findingId);
return new DescAndFindingID(longDescription, findingId);
}
}catch(Exception e){
log.error("-- got exception reading desc from db " + e);
e.printStackTrace();
}
logger.info(">>> Exiting getLongDescription");
return null;
}
private void editDescription(StringBuilder input, String from, String to) {
logger.info(">>> Entering editDescription");
int index = input.indexOf(from);
while (index != -1)
{
input.replace(index, index + from.length(), to);
index += to.length();
index = input.indexOf(from, index);
}
logger.info(">>> Exiting editDescription");
}
private synchronized static void populateProjectNamesFromDB() {
logger.info(">>> Entering populateProjectNamesFromDB");
StringBuilder sb = new StringBuilder("System (All Projects),");
sb.append("testprojectbtv,");
try {
String projName = "";
ResultSet rs = preparedStatementProjNames.executeQuery(projectNamesSQL);
while (rs.next()) {
projName = rs.getString("NAME");
sb.append(projName + ",");
}
}catch(Exception e){
logger.error("-- got exception reading proj name from db " + e);
e.printStackTrace();
}
projs = sb.toString();
projs = projs.substring(0, projs.length()-1);
logger.info(">>> Exiting populateProjectNamesFromDB");
return;
}
class DescAndFindingID {
DescAndFindingID(String longDescription, int findingId) {
this.longDescription = longDescription;
this.findingId = findingId;
}
String longDescription;
int findingId;
}
}