diff --git a/.gitignore b/.gitignore index 128ab20..988e850 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ gradle-app.setting *.classpath *.project *.settings +*.idea /bin + diff --git a/README.md b/README.md index 3093bbb..da4c85f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ -# ZAP Template Java Add-on +This project contains the Software Risk Manager add-on for the [Zed Attack Proxy](https://github.com/zaproxy/zaproxy) (ZAP). -A template repo for a 3rd party [ZAP](https://www.zaproxy.org) Java add-on. +If you are using the latest version of ZAP, you can browse and download "Software Risk Manager Extension" directly from within ZAP by clicking the Marketplace button in the toolbar. -If you'd like to create your own ZAP add-on which could be published to the [ZAP Marketplace](https://www.zaproxy.org/addons/) then copy this repo and make the following changes: +![Image](https://github.com/zaproxy/zap-extensions/wiki/images/zap-screenshot-browse-addons.png) -1. Rename the `youruser` directories to be your github name (or choose another package hierarchy which works better for you). +## Build and Import Steps +The add-ons are built with [Gradle], -1. Change the add-on ID `addonjava` to be a new ID which is not already on the [ZAP Marketplace](https://www.zaproxy.org/addons/). +1\. Run `./gradlew assemble` to build the add-on. +2\. Locate the output file at `/build/zapAddOn/bin/srm-alpha-*.zap`. +3\. In ZAP, go to the menu option `File / Load Add-on File...`. +4\. Select the generated `.zap` file to import the add-on. -1. Change the code and help files to do whatever you want :smiley: - -1. Deploy and test your app locally, e.g. using `./gradlew copyZapAddon` - -1. Optional: Raise an [Issue](https://github.com/zaproxy/zaproxy/issues/new?assignees=&labels=marketplace&template=third-party-addon.yml) to get your add-on published on the [ZAP Marketplace](https://www.zaproxy.org/addons/). - -For more info on developing ZAP add-ons see https://www.zaproxy.org/docs/developer/ \ No newline at end of file +[Gradle]: https://gradle.org/ diff --git a/build.gradle.kts b/build.gradle.kts index 14d1598..f92a25d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,11 +8,14 @@ plugins { id("org.zaproxy.common") } -description = "A template for a 3rd party ZAP Java add-on." +description = ( + "Includes request and response data in XML reports and provides the ability " + + "to upload reports directly to a Software Risk Manager server" +) zapAddOn { - addOnId.set("addonjava") - addOnName.set("A Template Java Add-on") + addOnId.set("srm") + addOnName.set("Software Risk Manager Extension") zapVersion.set("2.16.0") addOnStatus.set(AddOnStatus.ALPHA) @@ -20,9 +23,9 @@ zapAddOn { unreleasedLink.set("https://github.com/youruser/javaexample/compare/v@CURRENT_VERSION@...HEAD") manifest { - author.set("ZAP Dev Team") - url.set("https://www.zaproxy.org/docs/desktop/addons/addonjava/") - repo.set("https://github.com/zaproxy/addon-java") + author.set("Black Duck, Inc.") + url.set("https://www.zaproxy.org/docs/desktop/addons/software-risk-manager/") + repo.set("https://github.com/codedx/srm-zap-extension/") changesFile.set(tasks.named("generateManifestChanges").flatMap { it.html }) dependencies { @@ -35,6 +38,15 @@ zapAddOn { } } +dependencies { + compileOnly("org.zaproxy.addon:commonlib:1.36.0") + implementation("org.apache.httpcomponents:httpmime:4.5.13") + implementation("com.googlecode.json-simple:json-simple:1.1.1") { + // Not needed. + exclude(group = "junit") + } +} + java { val javaVersion = JavaVersion.VERSION_17 sourceCompatibility = javaVersion @@ -42,11 +54,8 @@ java { } spotless { - kotlinGradle { - ktlint() + java { + // Don't check license nor format/style, 3rd-party add-on. + clearSteps() } } - -dependencies { - compileOnly("org.zaproxy.addon:commonlib:1.36.0") -} diff --git a/gradle.properties b/gradle.properties index 0ea36b1..ed2f91e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=0.1.0 +version=2025.9.0 release=false diff --git a/settings.gradle.kts b/settings.gradle.kts index 74c0109..be781eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,4 +3,4 @@ plugins { id("com.diffplug.spotless") version "6.25.0" apply false } -rootProject.name = "addon-java" +rootProject.name = "zap-srm-addon" diff --git a/src/main/java/com/blackduck/zap/srm/ExtensionAlertHttp.java b/src/main/java/com/blackduck/zap/srm/ExtensionAlertHttp.java new file mode 100644 index 0000000..559c8a9 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/ExtensionAlertHttp.java @@ -0,0 +1,98 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.model.SiteNode; +import org.parosproxy.paros.network.HttpMessage; + +import java.util.List; + +public class ExtensionAlertHttp { + + private static final Logger LOGGER = LogManager.getLogger(ExtensionAlertHttp.class); + + public ExtensionAlertHttp() { + } + + public String getXml(SiteNode site) { + StringBuilder xml = new StringBuilder(); + xml.append(""); + List alerts = site.getAlerts(); + for (Alert alert : alerts) { + if (alert.getConfidence() != Alert.CONFIDENCE_FALSE_POSITIVE) { + String urlParamXML = getUrlParamXML(alert); + xml.append(alert.toPluginXML(urlParamXML)); + } + } + xml.append(""); + return xml.toString(); + } + + private String getHTML(Alert alert) { + // gets HttpMessage request and response data from each alert and removes illegal and + // special characters + StringBuilder httpMessage = new StringBuilder(); + + HttpMessage message = alert.getMessage(); + + if (message == null) { + LOGGER.warn(Constant.messages.getString("srm.error.httpMessage", alert.getAlertId())); + return httpMessage.toString(); + } + + String requestHeader = message.getRequestHeader().toString(); + String requestBody = message.getRequestBody().toString(); + String responseHeader = message.getResponseHeader().toString(); + String responseBody = message.getResponseBody().toString(); + + httpMessage.append(""); + httpMessage.append(ReportGenerator.entityEncode(requestHeader)); + httpMessage.append(ReportGenerator.entityEncode(requestBody)); + httpMessage.append("\n\n"); + httpMessage.append(""); + httpMessage.append(ReportGenerator.entityEncode(responseHeader)); + httpMessage.append(ReportGenerator.entityEncode(responseBody)); + httpMessage.append("\n\n"); + + return httpMessage.toString(); + } + + public String getUrlParamXML(Alert alert) { + + String uri = alert.getUri(); + String param = alert.getParam(); + String attack = alert.getAttack(); + String otherInfo = alert.getOtherInfo(); + String evidence = alert.getEvidence(); + + StringBuilder sb = new StringBuilder(200); // ZAP: Changed the type to StringBuilder. + sb.append(getHTML(alert)); + sb.append(" ").append(ReportGenerator.entityEncode(uri)).append("\r\n"); + sb.append(" ").append(ReportGenerator.entityEncode(param)).append("\r\n"); + sb.append(" ").append(ReportGenerator.entityEncode(attack)).append("\r\n"); + if (evidence != null && evidence.length() > 0) { + sb.append(" ").append(ReportGenerator.entityEncode(evidence)).append("\r\n"); + } + sb.append(" ").append(ReportGenerator.entityEncode(otherInfo)).append("\r\n"); + return sb.toString(); + } +} diff --git a/src/main/java/com/blackduck/zap/srm/ReportGenerator.java b/src/main/java/com/blackduck/zap/srm/ReportGenerator.java new file mode 100644 index 0000000..a06cf79 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/ReportGenerator.java @@ -0,0 +1,331 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import net.sf.json.xml.XMLSerializer; +import org.apache.commons.text.StringEscapeUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.view.View; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.zaproxy.zap.utils.XMLStringUtil; +import org.zaproxy.zap.utils.XmlUtils; + +import javax.swing.*; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ReportGenerator { + + private static final Logger LOGGER = LogManager.getLogger(ReportGenerator.class); + + // private static Pattern patternWindows = Pattern.compile("window", Pattern.CASE_INSENSITIVE); + // private static Pattern patternLinux = Pattern.compile("linux", Pattern.CASE_INSENSITIVE); + + private static final SimpleDateFormat staticDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss"); + + public static File XMLToHtml(Document xmlDocument, String infilexsl, File outFile) { + File stylesheet = null; + + outFile = new File(outFile.getAbsolutePath()); + try { + stylesheet = new File(infilexsl); + + DOMSource source = new DOMSource(xmlDocument); + + // Use a Transformer for output + TransformerFactory tFactory = TransformerFactory.newInstance(); + StreamSource stylesource = new StreamSource(stylesheet); + Transformer transformer = tFactory.newTransformer(stylesource); + + // Make the transformation and write to the output file + StreamResult result = new StreamResult(outFile.getPath()); + transformer.transform(source, result); + + } catch (TransformerException e) { + LOGGER.error(e.getMessage(), e); + } + + return outFile; + } + + public static File stringToHtml(String inxml, String infilexsl, String outfilename) { + return stringToHtml(inxml, infilexsl != null ? new StreamSource(new File(infilexsl)) : null, outfilename); + } + + public static File stringToHtml(String inxml, StreamSource stylesource, String outfilename) { + if (stylesource != null) { + Document doc = null; + + // factory.setNamespaceAware(true); + // factory.setValidating(true); + File outfile = null; + StringReader inReader = new StringReader(inxml); + String tempOutfilename = outfilename + ".temp"; + + try { + outfile = new File(tempOutfilename); + + DocumentBuilder builder = XmlUtils.newXxeDisabledDocumentBuilderFactory().newDocumentBuilder(); + doc = builder.parse(new InputSource(inReader)); + + // Use a Transformer for output + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(stylesource); + transformer.setParameter("datetime", getCurrentDateTimeString()); + + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(outfile.getPath()); + transformer.transform(source, result); + + } catch (TransformerException | SAXException | ParserConfigurationException | IOException e) { + LOGGER.error(e.getMessage(), e); + // Save the xml for diagnosing the problem + BufferedWriter bw = null; + showDialogForGUI(); + try { + bw = Files.newBufferedWriter(new File(outfilename + "-orig.xml").toPath(), StandardCharsets.UTF_8); + bw.write(inxml); + } catch (IOException e2) { + LOGGER.error("Failed to write debug XML file", e); + return new File(outfilename); + } finally { + try { + if (bw != null) { + bw.close(); + } + } catch (IOException ex) { + } + } + return new File(outfilename); + } + // Replace the escaped tags used to make the report look slightly better. + // This is a temp fix to ensure reports always get generated + // we should really adopt something other than XSLT ;) + String line; + + try ( + BufferedReader br = Files.newBufferedReader(new File(tempOutfilename).toPath(), StandardCharsets.UTF_8); + BufferedWriter bw = Files.newBufferedWriter(new File(outfilename).toPath(), StandardCharsets.UTF_8) + ) { + while ((line = br.readLine()) != null) { + bw.write(line.replace("<p>", "

").replace("</p>", "

")); + bw.newLine(); + } + } catch (IOException e) { + showDialogForGUI(); + LOGGER.error(e.getMessage(), e); + } + // Remove the temporary file + outfile.delete(); + } else { + // No XSLT file specified, just output the XML straight to the file + BufferedWriter bw = null; + + try { + bw = Files.newBufferedWriter(new File(outfilename).toPath(), StandardCharsets.UTF_8); + bw.write(inxml); + } catch (IOException e2) { + showDialogForGUI(); + LOGGER.error(e2.getMessage(), e2); + } finally { + try { + if (bw != null) { + bw.close(); + } + } catch (IOException ex) { + } + } + } + + return new File(outfilename); + } + + public static File stringToJson(String inxml, String outfilename) { + BufferedWriter bw = null; + try { + bw = Files.newBufferedWriter(new File(outfilename).toPath(), StandardCharsets.UTF_8); + bw.write(stringToJson(inxml)); + } catch (IOException e2) { + showDialogForGUI(); + LOGGER.error(e2.getMessage(), e2); + } finally { + try { + if (bw != null) { + bw.close(); + } + } catch (IOException ex) { + } + } + + return new File(outfilename); + } + + public static String stringToHtml(String inxml, String infilexsl) { + return stringToHtml(inxml, new StreamSource(new File(infilexsl))); + } + + public static String stringToHtml(String inxml, StreamSource stylesource) { + Document doc = null; + + StringReader inReader = new StringReader(inxml); + StringWriter writer = new StringWriter(); + + try { + + DocumentBuilder builder = XmlUtils.newXxeDisabledDocumentBuilderFactory().newDocumentBuilder(); + doc = builder.parse(new InputSource(inReader)); + + // Use a Transformer for output + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(stylesource); + transformer.setParameter("datetime", getCurrentDateTimeString()); + + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(writer); + transformer.transform(source, result); + + } catch (TransformerException | SAXException | ParserConfigurationException | IOException e) { + showDialogForGUI(); + LOGGER.error(e.getMessage(), e); + } + + // Replace the escaped tags used to make the report look slightly better. + // This is a temp fix to ensure reports always get generated + // we should really adopt something other than XSLT ;) + return writer.toString().replace("<p>", "

").replace("</p>", "

"); + } + + public static File fileToHtml(String infilexml, String infilexsl, String outfilename) { + Document doc = null; + + // factory.setNamespaceAware(true); + // factory.setValidating(true); + File stylesheet = null; + File datafile = null; + File outfile = null; + + try { + stylesheet = new File(infilexsl); + datafile = new File(infilexml); + outfile = new File(outfilename); + + DocumentBuilder builder = XmlUtils.newXxeDisabledDocumentBuilderFactory().newDocumentBuilder(); + doc = builder.parse(datafile); + + // Use a Transformer for output + TransformerFactory tFactory = TransformerFactory.newInstance(); + StreamSource stylesource = new StreamSource(stylesheet); + Transformer transformer = tFactory.newTransformer(stylesource); + transformer.setParameter("datetime", getCurrentDateTimeString()); + + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(outfile.getPath()); + transformer.transform(source, result); + + } catch (TransformerException | SAXException | ParserConfigurationException | IOException e) { + showDialogForGUI(); + LOGGER.error(e.getMessage(), e); + } + + return outfile; + } + + public static String stringToJson(String inxml) { + JSONObject report = (JSONObject) new XMLSerializer().read(inxml); + Object site = report.get("site"); + if (!(site instanceof JSONArray)) { + JSONArray siteArray = new JSONArray(); + if (site != null) { + siteArray.add(site); + } + report.put("site", siteArray); + } + return report.toString(); + } + + /** + * Encode entity for HTML or XML output. + */ + public static String entityEncode(String text) { + String result = text; + + if (result == null) { + return result; + } + + // The escapeXml function doesn't cope with some 'special' chrs + + return StringEscapeUtils.escapeXml10(XMLStringUtil.escapeControlChrs(result)); + } + + /** + * Get today's date string. + */ + public static String getCurrentDateTimeString() { + Date dateTime = new Date(System.currentTimeMillis()); + return getDateTimeString(dateTime); + } + + public static String getDateTimeString(Date dateTime) { + // ZAP: fix unsafe call to DateFormats + synchronized (staticDateFormat) { + return staticDateFormat.format(dateTime); + } + } + + public static void addChildTextNode(Document doc, Element parent, String nodeName, String text) { + Element child = doc.createElement(nodeName); + child.appendChild(doc.createTextNode(text)); + parent.appendChild(child); + } + + public static String getDebugXMLString(Document doc) throws TransformerException { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + StringWriter writer = new StringWriter(); + transformer.transform(new DOMSource(doc), new StreamResult(writer)); + return writer.getBuffer().toString().replaceAll("\n|\r", ""); + } + + private static void showDialogForGUI() { + if (View.isInitialised()) { + JOptionPane.showMessageDialog(null, Constant.messages.getString("report.write.dialog.message")); + } + } +} diff --git a/src/main/java/com/blackduck/zap/srm/ReportLastScan.java b/src/main/java/com/blackduck/zap/srm/ReportLastScan.java new file mode 100644 index 0000000..622db2c --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/ReportLastScan.java @@ -0,0 +1,304 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.extension.Extension; +import org.parosproxy.paros.extension.ExtensionLoader; +import org.parosproxy.paros.extension.ViewDelegate; +import org.parosproxy.paros.model.Model; +import org.parosproxy.paros.model.SiteMap; +import org.parosproxy.paros.model.SiteNode; +import org.parosproxy.paros.view.View; +import org.zaproxy.zap.extension.XmlReporterExtension; +import org.zaproxy.zap.utils.DesktopUtils; +import org.zaproxy.zap.utils.XMLStringUtil; +import org.zaproxy.zap.view.ScanPanel; +import org.zaproxy.zap.view.widgets.WritableFileChooser; + +import javax.swing.*; +import javax.swing.filechooser.FileFilter; +import javax.xml.transform.stream.StreamSource; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; + +public class ReportLastScan { + + private static final Logger LOGGER = LogManager.getLogger(ReportLastScan.class); + + private static final String HTM_FILE_EXTENSION = ".htm"; + private static final String HTML_FILE_EXTENSION = ".html"; + private static final String XML_FILE_EXTENSION = ".xml"; + private static final String MD_FILE_EXTENSION = ".md"; + private static final String JSON_FILE_EXTENSION = ".json"; + + public enum ReportType { + HTML, XML, MD, JSON + } + + public ReportLastScan() { + } + + /** + * @deprecated generate has been deprecated in favor of using {@link #generate(String fileName, + * ReportType reportType)} + */ + @Deprecated + public File generate(String fileName, Model model, String xslFile) throws Exception { + StringBuilder sb = new StringBuilder(500); + this.generate(sb); + return ReportGenerator.stringToHtml(sb.toString(), xslFile, fileName); + } + + /** + * @deprecated generate has been deprecated in favor of using {@link #generate(String filename, + * ReportType reportType)} + */ + @Deprecated + public File generate(String fileName, Model model, ReportType reportType) throws Exception { + return generate(fileName, reportType); + } + + public File generate(String fileName, ReportType reportType) throws Exception { + StringBuilder sb = new StringBuilder(500); + this.generate(sb); + if (reportType == ReportType.JSON) { + return ReportGenerator.stringToJson(sb.toString(), fileName); + } + + if (reportType == ReportType.XML) { + return ReportGenerator.stringToHtml(sb.toString(), (String) null, fileName); + } + + String xslFileName = reportType == ReportType.MD ? "report.md.xsl" : "report.html.xsl"; + return generateReportWithXsl(sb.toString(), fileName, xslFileName); + } + + private static File generateReportWithXsl(String report, String reportFile, String xslFileName) throws IOException { + Path xslFile = Paths.get(Constant.getZapInstall(), "xml", xslFileName); + if (Files.exists(xslFile)) { + return ReportGenerator.stringToHtml(report, xslFile.toString(), reportFile); + } + + String path = "/org/zaproxy/zap/resources/xml/" + xslFileName; + try (InputStream is = ReportLastScan.class.getResourceAsStream(path)) { + if (is == null) { + LOGGER.error("Bundled file not found: {}", path); + return new File(reportFile); + } + return ReportGenerator.stringToHtml(report, new StreamSource(is), reportFile); + } + } + + /** + * @deprecated generate has been deprecated in favor of using {@link #generate(StringBuilder + * report)} + */ + @Deprecated + public void generate(StringBuilder report, Model model) throws Exception { + generate(report); + } + + public void generate(StringBuilder report) throws Exception { + report.append(""); + report.append("\r\n"); + siteXML(report); + report.append(""); + } + + private void siteXML(StringBuilder report) { + SiteMap siteMap = Model.getSingleton().getSession().getSiteTree(); + SiteNode root = siteMap.getRoot(); + int siteNumber = root.getChildCount(); + for (int i = 0; i < siteNumber; i++) { + SiteNode site = (SiteNode) root.getChildAt(i); + String siteName = ScanPanel.cleanSiteName(site, true); + String[] hostAndPort = siteName.split(":"); + boolean isSSL = (site.getNodeName().startsWith("https")); + String siteStart = ""; + StringBuilder extensionsXML = getExtensionsXML(site); + String siteEnd = ""; + report.append(siteStart); + report.append(extensionsXML); + report.append(siteEnd); + } + } + + public StringBuilder getExtensionsXML(SiteNode site) { + StringBuilder extensionXml = new StringBuilder(); + ExtensionLoader loader = Control.getSingleton().getExtensionLoader(); + int extensionCount = loader.getExtensionCount(); + for (int i = 0; i < extensionCount; i++) { + Extension extension = loader.getExtension(i); + if (extension instanceof XmlReporterExtension) { + extensionXml.append(((XmlReporterExtension) extension).getXml(site)); + } + } + return extensionXml; + } + + /** + * @deprecated generate has been deprecated in favor of using {@link + * #generateReport(ViewDelegate view, ReportType reportType)} + */ + @Deprecated + public void generateReport(ViewDelegate view, Model model, ReportType reportType) { + generateReport(view, reportType); + } + + /** + * Generates a report. Defaults to HTML report if reportType is null. + * + * @param view + * @param reportType + */ + public void generateReport(ViewDelegate view, ReportType reportType) { + // ZAP: Allow scan report file name to be specified + + final ReportType localReportType; + + if (reportType == null) { + localReportType = ReportType.HTML; + } else { + localReportType = reportType; + } + + try { + JFileChooser chooser = new WritableFileChooser(Model.getSingleton().getOptionsParam().getUserDirectory()); + + chooser.setFileFilter(new FileFilter() { + + @Override + public boolean accept(File file) { + if (file.isDirectory()) { + return true; + } else if (file.isFile()) { + String lcFileName = file.getName().toLowerCase(Locale.ROOT); + switch (localReportType) { + case XML: + return lcFileName.endsWith(XML_FILE_EXTENSION); + case MD: + return lcFileName.endsWith(MD_FILE_EXTENSION); + case JSON: + return lcFileName.endsWith(JSON_FILE_EXTENSION); + case HTML: + default: + return (lcFileName.endsWith(HTM_FILE_EXTENSION) || lcFileName.endsWith(HTML_FILE_EXTENSION)); + } + } + return false; + } + + @Override + public String getDescription() { + switch (localReportType) { + case XML: + return Constant.messages.getString("file.format.xml"); + case MD: + return Constant.messages.getString("file.format.md"); + case JSON: + return Constant.messages.getString("file.format.json"); + case HTML: + default: + return Constant.messages.getString("file.format.html"); + } + } + }); + + String fileExtension = ""; + switch (localReportType) { + case XML: + fileExtension = XML_FILE_EXTENSION; + break; + case JSON: + fileExtension = JSON_FILE_EXTENSION; + break; + case MD: + fileExtension = MD_FILE_EXTENSION; + break; + case HTML: + default: + fileExtension = HTML_FILE_EXTENSION; + break; + } + chooser.setSelectedFile(new File(fileExtension)); // Default the filename to a reasonable extension; + + int rc = chooser.showSaveDialog(View.getSingleton().getMainFrame()); + File file = null; + if (rc == JFileChooser.APPROVE_OPTION) { + file = chooser.getSelectedFile(); + + File report = generate(file.getAbsolutePath(), localReportType); + if (report == null) { + view.showMessageDialog(Constant.messages.getString("report.unknown.error", file.getAbsolutePath())); + return; + } + + if (Files.notExists(report.toPath())) { + LOGGER.info("Not opening report, does not exist: {}", report); + return; + } + + try { + DesktopUtils.openUrlInBrowser(report.toURI()); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + view.showMessageDialog(Constant.messages.getString("report.complete.warning", report.getAbsolutePath())); + } + } + + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + view.showWarningDialog(Constant.messages.getString("report.unexpected.error")); + } + } + + /** + * @deprecated generateHtml has been deprecated in favor of using {@link + * #generateReport(ViewDelegate, Model, ReportType)} + */ + @Deprecated + public void generateHtml(ViewDelegate view, Model model) { + generateReport(view, model, ReportType.HTML); + } + + /** + * @deprecated generateXml has been deprecated in favor of using {@link + * #generateReport(ViewDelegate, Model, ReportType)} + */ + @Deprecated + public void generateXml(ViewDelegate view, Model model) { + generateReport(view, model, ReportType.XML); + } +} diff --git a/src/main/java/com/blackduck/zap/srm/ReportLastScanHttp.java b/src/main/java/com/blackduck/zap/srm/ReportLastScanHttp.java new file mode 100644 index 0000000..2eec8f0 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/ReportLastScanHttp.java @@ -0,0 +1,31 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import org.parosproxy.paros.model.SiteNode; + +public class ReportLastScanHttp extends ReportLastScan { + + ReportLastScanHttp() { + } + + @Override + public StringBuilder getExtensionsXML(SiteNode site) { + return new StringBuilder(new ExtensionAlertHttp().getXml(site)); + } +} diff --git a/src/main/java/com/blackduck/zap/srm/SrmAPI.java b/src/main/java/com/blackduck/zap/srm/SrmAPI.java new file mode 100644 index 0000000..6ad08ee --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/SrmAPI.java @@ -0,0 +1,156 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import net.sf.json.JSONObject; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.zaproxy.zap.extension.api.*; +import org.zaproxy.zap.extension.api.ApiException.Type; + +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; + +public class SrmAPI extends ApiImplementor { + + private static final Logger LOGGER = LogManager.getLogger(SrmExtension.class); + private static final String PREFIX = "srm"; + + private static final String VIEW_GENERATE = "generateReport"; + private static final String ACTION_UPLOAD = "uploadReport"; + private static final String ACTION_GEN_UPLOAD = "generateAndUpload"; + + private static final String ACTION_PARAM_FILE_PATH = "filePath"; + private static final String ACTION_PARAM_SERVER_URL = "serverUrl"; + private static final String ACTION_PARAM_API_KEY = "codeDxApiKey"; + private static final String ACTION_PARAM_PROJECT = "projectId"; + + // Optional + private static final String ACTION_PARAM_FINGERPRINT = "fingerprint"; + private static final String ACTION_PARAM_ACCEPT_PERM = "acceptPermanently"; + + private final SrmExtension extension; + + @SuppressWarnings("this-escape") + public SrmAPI(SrmExtension extension) { + this.extension = extension; + this.addApiView(new ApiView(VIEW_GENERATE)); + + String[] optionalParams = new String[]{ACTION_PARAM_FINGERPRINT, ACTION_PARAM_ACCEPT_PERM}; + + this.addApiAction( + new ApiAction( + ACTION_UPLOAD, + new String[]{ + ACTION_PARAM_FILE_PATH, + ACTION_PARAM_SERVER_URL, + ACTION_PARAM_API_KEY, + ACTION_PARAM_PROJECT + }, + optionalParams + )); + this.addApiAction( + new ApiAction( + ACTION_GEN_UPLOAD, + new String[]{ + ACTION_PARAM_SERVER_URL, + ACTION_PARAM_API_KEY, + ACTION_PARAM_PROJECT + }, + optionalParams + )); + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public ApiResponse handleApiAction(String name, JSONObject params) throws ApiException { + if (ACTION_UPLOAD.equals(name)) { + File reportFile = new File(params.getString(ACTION_PARAM_FILE_PATH)); + String serverUrl = params.getString(ACTION_PARAM_SERVER_URL); + String apiKey = params.getString(ACTION_PARAM_API_KEY); + String projectId = params.getString(ACTION_PARAM_PROJECT); + + String fingerprint = this.getParam(params, ACTION_PARAM_FINGERPRINT, ""); + boolean acceptPermanently = this.getParam(params, ACTION_PARAM_ACCEPT_PERM, false); + + uploadFile(reportFile, serverUrl, apiKey, projectId, fingerprint, acceptPermanently); + return ApiResponseElement.OK; + } else if (ACTION_GEN_UPLOAD.equals(name)) { + File reportFile; + String serverUrl = params.getString(ACTION_PARAM_SERVER_URL); + String apiKey = params.getString(ACTION_PARAM_API_KEY); + String projectId = params.getString(ACTION_PARAM_PROJECT); + + String fingerprint = this.getParam(params, ACTION_PARAM_FINGERPRINT, ""); + boolean acceptPermanently = this.getParam(params, ACTION_PARAM_ACCEPT_PERM, false); + + boolean isEmpty = false; + try { + reportFile = UploadActionListener.generateReportFile(extension); + isEmpty = UploadActionListener.reportIsEmpty(reportFile); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + throw new ApiException(Type.INTERNAL_ERROR, e.getMessage()); + } + try { + if (!isEmpty) uploadFile(reportFile, serverUrl, apiKey, projectId, fingerprint, acceptPermanently); + else return new ApiResponseElement("Result", "empty"); + } finally { + reportFile.delete(); + } + return ApiResponseElement.OK; + } + throw new ApiException(Type.BAD_ACTION); + } + + @Override + public ApiResponse handleApiView(String name, JSONObject params) throws ApiException { + if (VIEW_GENERATE.equals(name)) { + try { + StringBuilder report = new StringBuilder(); + UploadActionListener.generateReportString(extension, report); + return new ApiResponseElement(name, report.toString()); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + throw new ApiException(Type.INTERNAL_ERROR, e.getMessage()); + } + } + throw new ApiException(Type.BAD_VIEW); + } + + private void uploadFile(File reportFile, String serverUrl, String apiKey, String project, String fingerprint, boolean acceptPermanently) throws ApiException { + if (serverUrl.endsWith("/")) serverUrl = serverUrl.substring(0, serverUrl.length() - 1); + try { + CloseableHttpClient client = extension.getHttpClient(serverUrl, fingerprint, acceptPermanently); + String err = UploadActionListener.uploadFile(client, reportFile, serverUrl, apiKey, project); + if (err != null) { + LOGGER.error(err); + throw new ApiException(Type.ILLEGAL_PARAMETER, err); + } + } catch (GeneralSecurityException | IOException e) { + LOGGER.error(e.getMessage(), e); + throw new ApiException(Type.ILLEGAL_PARAMETER, e.getMessage()); + } + } +} diff --git a/src/main/java/com/blackduck/zap/srm/SrmExtension.java b/src/main/java/com/blackduck/zap/srm/SrmExtension.java new file mode 100644 index 0000000..cd60917 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/SrmExtension.java @@ -0,0 +1,176 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import com.blackduck.zap.srm.ReportLastScan.ReportType; +import com.blackduck.zap.srm.security.SSLConnectionSocketFactoryFactory; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.extension.ExtensionAdaptor; +import org.parosproxy.paros.extension.ExtensionHook; +import org.parosproxy.paros.model.Model; +import org.parosproxy.paros.network.ConnectionParam; +import org.parosproxy.paros.view.View; +import org.zaproxy.zap.extension.api.API; +import org.zaproxy.zap.view.ZapMenuItem; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.security.GeneralSecurityException; + +/* + * The Software Risk Manager ZAP extension used to include request and response data in alert reports. + * + */ +public class SrmExtension extends ExtensionAdaptor { + + private static final Logger LOGGER = LogManager.getLogger(SrmExtension.class); + + private SrmAPI cdxAPIImpl; + protected static final String PREFIX = "srm"; + + // The name is public so that other extensions can access it + public static final String NAME = "SrmExtension"; + + private ZapMenuItem menuUpload = null; + private ZapMenuItem menuExport = null; + + public SrmExtension() { + super(NAME); + } + + @Override + public boolean canUnload() { + return true; + } + + @Override + public void hook(ExtensionHook extensionHook) { + super.hook(extensionHook); + cdxAPIImpl = new SrmAPI(this); + API.getInstance().registerApiImplementor(cdxAPIImpl); + if (hasView()) { + extensionHook.getHookMenu().addReportMenuItem(getUploadMenu()); + extensionHook.getHookMenu().addReportMenuItem(getExportMenu()); + } + } + + @Override + public void unload() { + API.getInstance().removeApiImplementor(cdxAPIImpl); + } + + public ZapMenuItem getUploadMenu() { + if (menuUpload == null) { + menuUpload = new ZapMenuItem("srm.topmenu.upload.title"); + menuUpload.addActionListener(new UploadActionListener(this)); + } + return menuUpload; + } + + public ZapMenuItem getExportMenu() { + if (menuExport == null) { + menuExport = new ZapMenuItem("srm.topmenu.report.title"); + + menuExport.addActionListener(e -> { + ReportLastScanHttp saver = new ReportLastScanHttp(); + saver.generateReport(getView(), ReportType.XML); + }); + } + return menuExport; + } + + public CloseableHttpClient getHttpClient() { + try { + return getHttpClient(SrmProperties.getInstance().getServerUrl()); + } catch (MalformedURLException e) { + View.getSingleton().showWarningDialog(Constant.messages.getString("srm.error.client.invalid")); + } catch (IOException | GeneralSecurityException e) { + View.getSingleton().showWarningDialog(Constant.messages.getString("srm.error.client.failed")); + } + return null; + } + + public CloseableHttpClient getHttpClient(String url) throws IOException, GeneralSecurityException { + return getHttpClient(url, null, false); + } + + @SuppressWarnings("deprecation") + public CloseableHttpClient getHttpClient(String url, String fingerprint, boolean acceptPermanently) throws IOException, GeneralSecurityException { + RequestConfig.Builder configBuilder = RequestConfig.custom() + .setConnectTimeout(getTimeout()) + .setSocketTimeout(getTimeout()) + .setConnectionRequestTimeout(getTimeout()); + + HttpClientBuilder builder = HttpClientBuilder.create(); + if (fingerprint != null) { + builder.setSSLSocketFactory(SSLConnectionSocketFactoryFactory.getFactory(URI.create(url).getHost(), this, fingerprint, acceptPermanently)); + } else { + builder.setSSLSocketFactory(SSLConnectionSocketFactoryFactory.getFactory(URI.create(url).getHost(), this)); + } + + ConnectionParam connParam = Model.getSingleton().getOptionsParam().getConnectionParam(); + if (connParam.isUseProxyChain()) { + String proxyHost = connParam.getProxyChainName(); + int proxyPort = connParam.getProxyChainPort(); + HttpHost proxy = new HttpHost(proxyHost, proxyPort); + configBuilder.setProxy(proxy); + + if (connParam.isUseProxyChainAuth()) { + BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope(proxyHost, proxyPort), + new UsernamePasswordCredentials(connParam.getProxyChainUserName(), connParam.getProxyChainPassword()) + ); + builder.setDefaultCredentialsProvider(credsProvider); + } + } + builder.setDefaultRequestConfig(configBuilder.build()); + return builder.build(); + } + + @Override + public String getDescription() { + return Constant.messages.getString("srm.desc"); + } + + @Override + public URL getURL() { + return null; + } + + private int getTimeout() { + try { + return Integer.parseInt(SrmProperties.getInstance().getTimeout()) * 1000; + } catch (NumberFormatException e) { + // If for some reason the saved timeout value can't be parsed as an int, we will return + // the default value of 120 seconds + return SrmProperties.DEFAULT_TIMEOUT_INT; + } + } +} diff --git a/src/main/java/com/blackduck/zap/srm/SrmProperties.java b/src/main/java/com/blackduck/zap/srm/SrmProperties.java new file mode 100644 index 0000000..0788419 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/SrmProperties.java @@ -0,0 +1,160 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import org.apache.commons.configuration.Configuration; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.model.Model; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Properties; + +public class SrmProperties { + private static class Holder { + static final SrmProperties INSTANCE = new SrmProperties(); + } + + public static SrmProperties getInstance() { + return Holder.INSTANCE; + } + + private SrmProperties() { + loadProperties(); + } + + private static final Logger LOGGER = LogManager.getLogger(SrmProperties.class); + + private static final String PROP_FILE = "srm.properties"; + private static final String FALLBACK_PROP_FILE = "codedx.properties"; + private static final String KEY_SERVER = "serverUrl"; + private static final String KEY_API = "apiKey"; + private static final String KEY_SELECTED = "selectedId"; + private static final String KEY_TIMEOUT = "timeout"; + + // ZAP config keys with prefix + private static final String ZAP_CONFIG_PREFIX = "srm."; + private static final String ZAP_KEY_SERVER = ZAP_CONFIG_PREFIX + KEY_SERVER; + private static final String ZAP_KEY_API = ZAP_CONFIG_PREFIX + KEY_API; + private static final String ZAP_KEY_SELECTED = ZAP_CONFIG_PREFIX + KEY_SELECTED; + private static final String ZAP_KEY_TIMEOUT = ZAP_CONFIG_PREFIX + KEY_TIMEOUT; + + private Properties prop; + private File configFile; + + public static final String DEFAULT_TIMEOUT_STRING = "120"; + public static final int DEFAULT_TIMEOUT_INT = 120000; + + public String getServerUrl() { + String text = getProperty(KEY_SERVER); + if (text != null && text.endsWith("/")) { + return text.substring(0, text.length() - 1); + } + return text != null ? text : ""; + } + + public String getApiKey() { + return getProperty(KEY_API); + } + + public String getSelectedId() { + return getProperty(KEY_SELECTED); + } + + public String getTimeout() { + String timeout = getProperty(KEY_TIMEOUT); + if (timeout == null || timeout.isEmpty()) { + timeout = DEFAULT_TIMEOUT_STRING; + } + return timeout; + } + + private String getProperty(String key) { + if (configFile == null) { + Configuration config = Model.getSingleton().getOptionsParam().getConfig(); + String zapKey = ZAP_CONFIG_PREFIX + key; + return config.getString(zapKey, ""); + } else { + String value = prop.getProperty(key); + return value == null ? "" : value; + } + } + + + public void setProperties(String server, String api, String selectedId, String timeout) { + if (configFile == null) { + Configuration config = Model.getSingleton().getOptionsParam().getConfig(); + config.setProperty(ZAP_KEY_SERVER, server); + config.setProperty(ZAP_KEY_API, api); + config.setProperty(ZAP_KEY_SELECTED, selectedId); + config.setProperty(ZAP_KEY_TIMEOUT, timeout); + } else { + prop.setProperty(KEY_SERVER, server); + prop.setProperty(KEY_API, api); + prop.setProperty(KEY_SELECTED, selectedId); + prop.setProperty(KEY_TIMEOUT, timeout); + saveProperties(); + } + } + + private void loadProperties() { + if (prop == null) prop = new Properties(); + + File srmFile = Paths.get(Constant.getZapHome(), PROP_FILE).toFile(); + if (srmFile.exists()) { + configFile = srmFile; + loadFromFile(configFile); + return; + } + + File fallbackFile = Paths.get(Constant.getZapHome(), FALLBACK_PROP_FILE).toFile(); + if (fallbackFile.exists()) { + configFile = fallbackFile; + loadFromFile(configFile); + return; + } + + // Neither file exists, use ZAP config + configFile = null; + LOGGER.info("Using ZAP configuration for SRM properties"); + } + + private void loadFromFile(File file) { + try (FileInputStream inp = new FileInputStream(file)) { + prop.load(inp); + LOGGER.info("Loaded SRM properties from: " + file.getName()); + } catch (IOException e) { + LOGGER.error("Error loading properties file: " + file.getName(), e); + } + } + + private void saveProperties() { + if (configFile != null) { + try (FileOutputStream out = new FileOutputStream(configFile)) { + prop.store(out, null); + } catch (IOException e) { + LOGGER.error("Error saving properties file: " + configFile.getName(), e); + } + } + } +} diff --git a/src/main/java/com/blackduck/zap/srm/UploadActionListener.java b/src/main/java/com/blackduck/zap/srm/UploadActionListener.java new file mode 100644 index 0000000..7e5867a --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/UploadActionListener.java @@ -0,0 +1,188 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import com.blackduck.zap.srm.ReportLastScan.ReportType; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.FileBody; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.view.View; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class UploadActionListener implements ActionListener { + + private static final Logger LOGGER = LogManager.getLogger(UploadActionListener.class); + + private static final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); + + static { + xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + } + + private final SrmExtension extension; + private final UploadPropertiesDialog prop; + + public UploadActionListener(SrmExtension extension) { + this.extension = extension; + this.prop = new UploadPropertiesDialog(extension); + } + + @Override + public void actionPerformed(ActionEvent e) { + prop.openProperties(this); + } + + public void generateAndUploadReport() { + String error = null; + try { + final File reportFile = generateReportFile(extension); + if (!reportIsEmpty(reportFile)) { + Thread uploadThread = new Thread() { + @Override + public void run() { + String err; + try { + err = uploadFile( + extension.getHttpClient(), + reportFile, + SrmProperties.getInstance().getServerUrl(), + SrmProperties.getInstance().getApiKey(), + prop.getProject().getValue() + ); + } catch (IOException ex1) { + err = Constant.messages.getString("srm.error.unexpected"); + LOGGER.error("Unexpected error while uploading report: ", ex1); + } + if (err != null) View.getSingleton().showMessageDialog(err); + else View.getSingleton().showMessageDialog(Constant.messages.getString("srm.message.success")); + reportFile.delete(); + } + }; + uploadThread.start(); + } else { + error = Constant.messages.getString("srm.error.empty"); + } + } catch (Exception ex2) { + error = Constant.messages.getString("srm.error.failed"); + LOGGER.error("Unexpected error while generating report: ", ex2); + } + if (error != null) View.getSingleton().showWarningDialog(error); + } + + public static String uploadFile(CloseableHttpClient client, File reportFile, String serverUrl, String apiKey, String project) throws IOException { + String err = null; + HttpResponse response = sendData(client, reportFile, serverUrl, apiKey, project); + StatusLine responseLine = null; + int responseCode = -1; + if (response != null) { + responseLine = response.getStatusLine(); + responseCode = responseLine.getStatusCode(); + } + if (responseCode == 400) { + err = Constant.messages.getString("srm.error.unexpected") + "\n" + Constant.messages.getString("srm.error.http.400"); + } else if (responseCode == 403) { + err = Constant.messages.getString("srm.error.unsent") + " " + Constant.messages.getString("srm.error.http.403"); + } else if (responseCode == 404) { + err = Constant.messages.getString("srm.error.unsent") + " " + Constant.messages.getString("srm.error.http.404"); + } else if (responseCode == 415) { + err = Constant.messages.getString("srm.error.unexpected") + "\n" + Constant.messages.getString("srm.error.http.415"); + } else if (responseCode != 200 && responseCode != 202) { + err = Constant.messages.getString("srm.error.unexpected"); + if (response != null) err += Constant.messages.getString("srm.error.http.other") + " " + responseLine; + } + return err; + } + + private static HttpResponse sendData(CloseableHttpClient client, File reportFile, String serverUrl, String apiKey, String project) throws IOException { + if (client == null) return null; + try { + HttpPost post = new HttpPost(serverUrl + "/api/projects/" + project + "/analysis"); + post.setHeader("API-Key", apiKey); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + builder.addPart("file", new FileBody(reportFile)); + + HttpEntity entity = builder.build(); + post.setEntity(entity); + + HttpResponse response = client.execute(post); + HttpEntity resEntity = response.getEntity(); + + if (resEntity != null) { + EntityUtils.consume(resEntity); + } + + return response; + } finally { + client.close(); + } + } + + public static void generateReportString(SrmExtension extension, StringBuilder report) throws Exception { + ReportLastScanHttp saver = new ReportLastScanHttp(); + saver.generate(report); + } + + public static File generateReportFile(SrmExtension extension) throws Exception { + File reportFile = File.createTempFile("srm-zap-report", ".xml"); + reportFile.deleteOnExit(); + + ReportLastScanHttp saver = new ReportLastScanHttp(); + saver.generate(reportFile.getCanonicalPath(), ReportType.XML); + + return reportFile; + } + + public static Boolean reportIsEmpty(File reportFile) throws IOException, XMLStreamException { + BufferedReader br = Files.newBufferedReader(reportFile.toPath()); + try { + XMLEventReader reader = xmlInputFactory.createXMLEventReader(br); + + while (reader.hasNext()) { + XMLEvent event = reader.nextEvent(); + if (event.isStartElement() && !event.asStartElement().getName().getLocalPart().equals("OWASPZAPReport")) { + return false; + } + } + } finally { + br.close(); + } + return true; + } +} diff --git a/src/main/java/com/blackduck/zap/srm/UploadPropertiesDialog.java b/src/main/java/com/blackduck/zap/srm/UploadPropertiesDialog.java new file mode 100644 index 0000000..2bb822f --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/UploadPropertiesDialog.java @@ -0,0 +1,309 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm; + +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.parosproxy.paros.Constant; + +import javax.swing.*; +import java.awt.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +public class UploadPropertiesDialog { + + private static final Logger LOGGER = LogManager.getLogger(UploadPropertiesDialog.class); + + private static final String[] DIALOG_BUTTONS = { + Constant.messages.getString("srm.settings.upload"), + Constant.messages.getString("srm.settings.cancel") + }; + + public static final ImageIcon REFRESH_ICON = new ImageIcon(UploadPropertiesDialog.class.getResource("/com/blackduck/zap/srm/resources/refresh.png")); + + private JTextField serverUrl; + private JTextField apiKey; + private JComboBox projectBox; + private JTextField timeout; + private JDialog dialog; + + private ModifiedNameValuePair[] projectArr = new ModifiedNameValuePair[0]; + + private final SrmExtension extension; + + public UploadPropertiesDialog(SrmExtension extension) { + this.extension = extension; + } + + public void openProperties(final UploadActionListener uploader) { + JPanel message = new JPanel(new GridBagLayout()); + + serverUrl = labelTextField(Constant.messages.getString("srm.settings.serverurl") + " ", message, SrmProperties.getInstance().getServerUrl(), 30); + apiKey = labelTextField(Constant.messages.getString("srm.settings.apikey") + " ", message, SrmProperties.getInstance().getApiKey(), 30); + projectBox = createProjectComboBox(message); + timeout = labelTextField(Constant.messages.getString("srm.setting.timeout") + " ", message, SrmProperties.getInstance().getTimeout(), 5); + + final JOptionPane pane = new JOptionPane(message, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION, null, DIALOG_BUTTONS, null); + dialog = pane.createDialog(Constant.messages.getString("srm.settings.title")); + + Thread popupThread = new Thread() { + @Override + public void run() { + dialog.setVisible(true); + if (DIALOG_BUTTONS[0].equals(pane.getValue())) { + String timeoutValue = timeout.getText(); + if (!isStringNumber(timeoutValue)) { + timeoutValue = SrmProperties.DEFAULT_TIMEOUT_STRING; + error(Constant.messages.getString("srm.error.timeout")); + } + SrmProperties.getInstance().setProperties(serverUrl.getText(), apiKey.getText(), getProject().getValue(), timeoutValue); + uploader.generateAndUploadReport(); + } + } + }; + Thread updateThread = new Thread() { + @Override + public void run() { + if (!"".equals(serverUrl.getText()) && !"".equals(apiKey.getText())) { + updateProjects(true); + String previousId = SrmProperties.getInstance().getSelectedId(); + for (NameValuePair p : projectArr) { + if (previousId.equals(p.getValue())) projectBox.setSelectedItem(p); + } + } + } + }; + popupThread.start(); + updateThread.start(); + } + + private boolean isStringNumber(String value) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (!Character.isDigit(c)) { + return false; + } + } + return true; + } + + private JTextField labelTextField(String label, Container cont, String base, int columns) { + createSettingsLabel(label, cont); + + JTextField textField = new JTextField(base, columns); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.anchor = GridBagConstraints.WEST; + cont.add(textField, gbc); + + return textField; + } + + private JComboBox createProjectComboBox(Container cont) { + createSettingsLabel("Project: ", cont); + + JComboBox box = new JComboBox<>(); + box.setPreferredSize(new Dimension(300, 27)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + cont.add(box, gbc); + + JButton refresh = new JButton(REFRESH_ICON); + refresh.setPreferredSize(new Dimension(REFRESH_ICON.getIconHeight() + 6, REFRESH_ICON.getIconHeight() + 6)); + refresh.addActionListener(e -> { + if ("".equals(serverUrl.getText()) || "".equals(apiKey.getText())) { + error(Constant.messages.getString("srm.error.required")); + return; + } + dialog.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + updateProjects(); + dialog.setCursor(Cursor.getDefaultCursor()); + }); + gbc = new GridBagConstraints(); + gbc.gridx = 2; + gbc.gridy = 2; + gbc.anchor = GridBagConstraints.WEST; + cont.add(refresh, gbc); + + return box; + } + + private void createSettingsLabel(String label, Container cont) { + JLabel labelField = new JLabel(label); + labelField.setHorizontalAlignment(SwingConstants.LEFT); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = 1; + gbc.gridx = 0; + gbc.insets = new Insets(0, 10, 0, 0); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + cont.add(labelField, gbc); + } + + public void updateProjects() { + updateProjects(false); + } + + public void updateProjects(boolean initialRefresh) { + dialog.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + CloseableHttpClient client = null; + BufferedReader rd = null; + projectArr = new ModifiedNameValuePair[0]; + try { + client = extension.getHttpClient(getServerUrl()); + if (client != null) { + HttpGet get = new HttpGet(getServerUrl() + "/api/projects"); + get.setHeader("API-Key", getApiKey()); + HttpResponse response = client.execute(get); + rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)); + + StringBuilder result = new StringBuilder(); + String line = ""; + while ((line = rd.readLine()) != null) { + result.append(line); + } + + int code = response.getStatusLine().getStatusCode(); + if (code == 200) { + projectArr = parseProjectJson(result.toString(), initialRefresh); + } else if (!initialRefresh) { + String msg = Constant.messages.getString("srm.refresh.non200") + ' ' + response.getStatusLine() + '.'; + if (code == 403) msg += Constant.messages.getString("srm.refresh.403"); + else if (code == 404) msg += Constant.messages.getString("srm.refresh.404"); + else if (code == 400) msg += Constant.messages.getString("srm.refresh.400"); + error(msg); + } + } + } catch (GeneralSecurityException | ParseException | IOException e) { + if (!initialRefresh) { + if (e instanceof MalformedURLException) error(Constant.messages.getString("srm.error.client.invalid")); + else error(Constant.messages.getString("srm.refresh.failed")); + } + LOGGER.error("Error refreshing project list: ", e); + } finally { + if (client != null) + try { client.close(); } catch (IOException e) {} + if (rd != null) + try { rd.close(); } catch (IOException e) {} + } + updateProjectComboBox(); + dialog.setCursor(Cursor.getDefaultCursor()); + } + + private ModifiedNameValuePair[] parseProjectJson(String json, boolean initialRefresh) throws ParseException { + JSONParser parser = new JSONParser(); + JSONObject obj = (JSONObject) parser.parse(json); + JSONArray projects = (JSONArray) obj.get("projects"); + + ModifiedNameValuePair[] projectArr = new ModifiedNameValuePair[projects.size()]; + for (int i = 0; i < projectArr.length; i++) { + JSONObject project = (JSONObject) projects.get(i); + int id = ((Long) project.get("id")).intValue(); + String name = (String) project.get("name"); + projectArr[i] = new ModifiedNameValuePair(name, Integer.toString(id)); + } + if (projectArr.length > 0) { + Arrays.sort(projectArr); + // set the project ids to visible if the names are the same + for (int i = 0; i < projectArr.length - 1; i++) { + if (projectArr[i].getName() != null && projectArr[i].getName().equals(projectArr[i + 1].getName())) { + projectArr[i].setUseId(true); + projectArr[i + 1].setUseId(true); + } + } + } else if (!initialRefresh) warn(Constant.messages.getString("srm.refresh.noproject")); + return projectArr; + } + + public void updateProjectComboBox() { + if (projectBox != null) { + NameValuePair selected = getProject(); + projectBox.removeAllItems(); + for (NameValuePair p : projectArr) { + projectBox.addItem(p); + if (selected != null && selected.equals(p)) { + projectBox.setSelectedItem(p); + } + } + } + } + + public NameValuePair getProject() { + return (NameValuePair) projectBox.getSelectedItem(); + } + + private void warn(String message) { + JOptionPane.showMessageDialog(dialog, message, Constant.messages.getString("srm.warning"), JOptionPane.WARNING_MESSAGE); + } + + private void error(String message) { + JOptionPane.showMessageDialog(dialog, message, Constant.messages.getString("srm.error"), JOptionPane.ERROR_MESSAGE); + } + + private String getServerUrl() { + String text = serverUrl.getText(); + if (text.endsWith("/")) return text.substring(0, text.length() - 1); + return text; + } + + private String getApiKey() { + return apiKey.getText(); + } + + private static class ModifiedNameValuePair extends BasicNameValuePair implements Comparable { + private static final long serialVersionUID = -6671681121783779976L; + private boolean useId = false; + + public ModifiedNameValuePair(String name, String value) { + super(name, value); + } + + public void setUseId(boolean useId) { + this.useId = useId; + } + + @Override + public String toString() { + if (useId) return getName() + " (id: " + getValue() + ")"; + return getName(); + } + + @Override + public int compareTo(ModifiedNameValuePair o) { + int val = this.getName().compareTo(o.getName()); + if (val == 0) return this.getValue().compareTo(o.getValue()); + return val; + } + } +} diff --git a/src/main/java/com/blackduck/zap/srm/security/CertificateAcceptance.java b/src/main/java/com/blackduck/zap/srm/security/CertificateAcceptance.java new file mode 100644 index 0000000..9c2302a --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/CertificateAcceptance.java @@ -0,0 +1,44 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +/** + * Enumeration to describe the possible outcomes of an {@link InvalidCertificateStrategy} when + * presented with an invalid certificate. + */ +public enum CertificateAcceptance { + + /** + * The invalid certificate should be rejected. + */ + REJECT, + + /** + * The invalid certificate should be accepted on a short-term basis, e.g. for the duration of + * the session, or until the current JVM stops. The actual interpretation is up to the + * corresponding {@link ExtraCertManager}. + */ + ACCEPT_TEMPORARILY, + + /** + * The invalid certificate should be accepted on a long-term basis, e.g. by adding the + * certificate to a custom KeyStore and persisting it to disk. The actual interpretation is up + * to the corresponding {@link ExtraCertManager}. + */ + ACCEPT_PERMANENTLY +} diff --git a/src/main/java/com/blackduck/zap/srm/security/CompositeX509TrustManager.java b/src/main/java/com/blackduck/zap/srm/security/CompositeX509TrustManager.java new file mode 100644 index 0000000..2f97725 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/CompositeX509TrustManager.java @@ -0,0 +1,94 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Represents an ordered list of {@link X509TrustManager}s with additive trust. If any one of the + * composed managers trusts a certificate chain, then it is trusted by the composite manager. + * + *

This is necessary because of the fine-print on {@link SSLContext#init}: Only the first + * instance of a particular key and/or trust manager implementation type in the array is used. (For + * example, only the first javax.net.ssl.X509KeyManager in the array will be used.) + * + *

see + * StackOverflow and the related blog post + * + * @author codyaray + * @see + * @since 4/22/2013 + */ +public class CompositeX509TrustManager implements X509TrustManager { + + private final List trustManagers = new LinkedList<>(); + + /** + * Initializes the composite trust manager, copying all of the non-null entries in the given + * trustManagers list into its own internal list. + * + * @param trustManagers A list of (potentially null) trust managers. + */ + public CompositeX509TrustManager(List trustManagers) { + for (X509TrustManager tm : trustManagers) { + if (tm != null) this.trustManagers.add(tm); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + for (X509TrustManager trustManager : trustManagers) { + try { + trustManager.checkClientTrusted(chain, authType); + return; // someone trusts them. success! + } catch (CertificateException e) { + // maybe someone else will trust them + } + } + throw new CertificateException("None of the TrustManagers trust this certificate chain"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + for (X509TrustManager trustManager : trustManagers) { + try { + trustManager.checkServerTrusted(chain, authType); + return; // someone trusts them. success! + } catch (CertificateException e) { + // maybe someone else will trust them + } + } + throw new CertificateException("None of the TrustManagers trust this certificate chain"); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + List certificates = new LinkedList<>(); + for (X509TrustManager trustManager : trustManagers) { + Collections.addAll(certificates, trustManager.getAcceptedIssuers()); + } + return certificates.toArray(new X509Certificate[certificates.size()]); + } +} diff --git a/src/main/java/com/blackduck/zap/srm/security/ExtraCertManager.java b/src/main/java/com/blackduck/zap/srm/security/ExtraCertManager.java new file mode 100644 index 0000000..3c3d5b2 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/ExtraCertManager.java @@ -0,0 +1,68 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; + +public interface ExtraCertManager { + + /** + * Add a certificate that will be accepted until some event (as determined by the implementation + * of this interface) occurs, causing it to be "forgotten". + * + * @param cert + */ + void addTemporaryCert(Certificate cert) throws IOException, GeneralSecurityException; + + /** + * Add a certificate that will be accepted until this manager is "purged". Certificates added in + * this way will generally be written to disk, and will be available upon restarting the + * program. + * + * @param cert + * @throws IOException + * @throws GeneralSecurityException + */ + void addPermanentCert(Certificate cert) throws IOException, GeneralSecurityException; + + /** + * Remove all certificates that have been added via {@link #addTemporaryCert(Certificate)}. + */ + void purgeTemporaryCerts() throws IOException, GeneralSecurityException; + + /** + * Remove all certificates that have been added via {@link #addPermanentCert(Certificate)}. + */ + void purgePermanentCerts() throws IOException, GeneralSecurityException; + + /** + * Remove all certificates that have been added either by {@link #addTemporaryCert(Certificate)} + * or {@link #addPermanentCert(Certificate)}. + */ + void purgeAllCerts() throws IOException, GeneralSecurityException; + + /** + * Return a representation of this manager as a KeyStore instance. + * + * @return A new KeyStore that represents the contents of this certificate manager + */ + KeyStore asKeyStore() throws IOException, GeneralSecurityException; +} diff --git a/src/main/java/com/blackduck/zap/srm/security/HostnameVerifierWithExceptions.java b/src/main/java/com/blackduck/zap/srm/security/HostnameVerifierWithExceptions.java new file mode 100644 index 0000000..1634295 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/HostnameVerifierWithExceptions.java @@ -0,0 +1,42 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import java.util.Set; + +/** + * HostnameVerifier implementation that delegates to another one, but will allow a particular set of + * hosts through even if the delegate verifier fails. + */ +public class HostnameVerifierWithExceptions implements HostnameVerifier { + + private final HostnameVerifier delegate; + private final Set allowedExceptions; + + public HostnameVerifierWithExceptions(HostnameVerifier delegate, Set allowedExceptions) { + this.delegate = delegate; + this.allowedExceptions = allowedExceptions; + } + + @Override + public boolean verify(String host, SSLSession session) { + return delegate.verify(host, session) || allowedExceptions.contains(host); + } +} diff --git a/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateDialogStrategy.java b/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateDialogStrategy.java new file mode 100644 index 0000000..4cb7d95 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateDialogStrategy.java @@ -0,0 +1,178 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import com.blackduck.zap.srm.SrmExtension; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.parosproxy.paros.Constant; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLException; +import javax.swing.*; +import java.awt.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Formatter; +import java.util.StringTokenizer; + +/** + * An InvalidCertificateStrategy implementation that opens a dialog, prompting the user for their + * choice of action. + */ +public class InvalidCertificateDialogStrategy implements InvalidCertificateStrategy { + + private final HostnameVerifier defaultHostVerifier; + private final String host; + private final SrmExtension extension; + + private static final String dialogTitle = Constant.messages.getString("srm.ssl.title"); + private static final String[] dialogButtons = { + Constant.messages.getString("srm.ssl.reject"), + Constant.messages.getString("srm.ssl.accepttemp"), + Constant.messages.getString("srm.ssl.acceptperm") + }; + + public InvalidCertificateDialogStrategy(HostnameVerifier defaultHostVerifier, String host, SrmExtension extension) { + this.defaultHostVerifier = defaultHostVerifier; + this.host = host; + this.extension = extension; + } + + @Override + public CertificateAcceptance checkAcceptance(Certificate genericCert, CertificateException certError) { + if (genericCert instanceof X509Certificate cert && defaultHostVerifier instanceof DefaultHostnameVerifier verifier) { + + JPanel message = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = 2; + gbc.insets = new Insets(0, 0, 10, 0); + gbc.anchor = GridBagConstraints.WEST; + message.add(new JLabel(Constant.messages.getString("srm.ssl.description")), gbc); + + gbc = new GridBagConstraints(); + gbc.gridy = 2; + gbc.insets = new Insets(2, 0, 2, 0); + gbc.anchor = GridBagConstraints.WEST; + + JLabel issuer = new JLabel(Constant.messages.getString("srm.ssl.issuer") + " "); + Font defaultFont = issuer.getFont(); + Font bold = new Font(defaultFont.getName(), Font.BOLD, defaultFont.getSize()); + issuer.setFont(bold); + + message.add(issuer, gbc); + gbc.gridx = 1; + message.add(new JLabel(cert.getIssuerX500Principal().getName()), gbc); + + try { + JLabel fingerprint = new JLabel(Constant.messages.getString("srm.ssl.fingerprint") + " "); + fingerprint.setFont(bold); + gbc.gridx = 0; + gbc.gridy += 1; + message.add(fingerprint, gbc); + + gbc.gridx = 1; + message.add(new JLabel(toHexString(getSHA1(cert.getEncoded()), " ")), gbc); + } catch (CertificateEncodingException e) { + // this shouldn't actually ever happen + } + + try { + verifier.verify(host, cert); + } catch (SSLException e) { + String cn = getCN(cert); + + JLabel mismatch = new JLabel(Constant.messages.getString("srm.ssl.mismatch") + " "); + mismatch.setFont(bold); + gbc.gridx = 0; + gbc.gridy += 1; + message.add(mismatch, gbc); + + String msg; + if (cn != null) { + msg = String.format(Constant.messages.getString("srm.ssl.mismatchmsg"), host, cn); + } else { + msg = e.getMessage(); + } + + gbc.gridx = 1; + message.add(new JLabel(msg), gbc); + } + + // Open the dialog, and return its result + int choice = JOptionPane.showOptionDialog( + extension.getView().getMainFrame(), + message, + dialogTitle, + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + dialogButtons, + null + ); + switch (choice) { + case (0): + return CertificateAcceptance.REJECT; + case (1): + return CertificateAcceptance.ACCEPT_TEMPORARILY; + case (2): + return CertificateAcceptance.ACCEPT_PERMANENTLY; + } + } + return CertificateAcceptance.REJECT; + } + + public static byte[] getSHA1(byte[] input) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.reset(); + return md.digest(input); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static String toHexString(byte[] bytes, String sep) { + Formatter f = new Formatter(); + for (int i = 0; i < bytes.length; i++) { + f.format("%02x", bytes[i]); + if (i < bytes.length - 1) { + f.format(sep); + } + } + String result = f.toString(); + f.close(); + return result; + } + + private static String getCN(X509Certificate cert) { + String principal = cert.getSubjectX500Principal().toString(); + StringTokenizer tokenizer = new StringTokenizer(principal, ","); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + int i = token.indexOf("CN="); + if (i >= 0) { + return token.substring(i + 3); + } + } + return null; + } +} diff --git a/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateFingerprintStrategy.java b/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateFingerprintStrategy.java new file mode 100644 index 0000000..5c0020d --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateFingerprintStrategy.java @@ -0,0 +1,49 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; + +public class InvalidCertificateFingerprintStrategy implements InvalidCertificateStrategy { + + private final String fingerprint; + private final boolean acceptPermanently; + + public InvalidCertificateFingerprintStrategy(String fingerprint, boolean acceptPermanently) { + this.fingerprint = fingerprint.replaceAll("\\s", ""); + this.acceptPermanently = acceptPermanently; + } + + @Override + public CertificateAcceptance checkAcceptance(Certificate cert, CertificateException certError) { + try { + byte[] encoded = InvalidCertificateDialogStrategy.getSHA1(cert.getEncoded()); + String obsPrint = InvalidCertificateDialogStrategy.toHexString(encoded, ""); + if (obsPrint.equalsIgnoreCase(fingerprint)) { + if (acceptPermanently) return CertificateAcceptance.ACCEPT_PERMANENTLY; + else return CertificateAcceptance.ACCEPT_TEMPORARILY; + } else { + return CertificateAcceptance.REJECT; + } + } catch (CertificateEncodingException e) { + return CertificateAcceptance.REJECT; + } + } +} diff --git a/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateStrategy.java b/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateStrategy.java new file mode 100644 index 0000000..2284cbb --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/InvalidCertificateStrategy.java @@ -0,0 +1,33 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import java.security.cert.Certificate; +import java.security.cert.CertificateException; + +public interface InvalidCertificateStrategy { + /** + * Determine what to do with a certificate (reject, accept temporarily, or accept permanently) + * + * @param cert A (probably invalid) certificate + * @param certError An exception (or null) that caused the certificate to be considered invalid + * @return A CertificateAcceptance value that determines whether (and for how long) the + * certificate should be considered valid. + */ + CertificateAcceptance checkAcceptance(Certificate cert, CertificateException certError); +} diff --git a/src/main/java/com/blackduck/zap/srm/security/ReloadableX509TrustManager.java b/src/main/java/com/blackduck/zap/srm/security/ReloadableX509TrustManager.java new file mode 100644 index 0000000..a865ac8 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/ReloadableX509TrustManager.java @@ -0,0 +1,135 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * This X509TrustManager implementation allows invalid certificates to possibly be accepted by the + * decision of an {@link InvalidCertificateStrategy} that is passed as a constructor argument. + * Certificates added in this way will be added via a {@link ExtraCertManager}, causing the + * underlying trust manager to be reloaded. + * + *

Adapted from the implementation at "Managing a + * Dynamic Java Trust Store" (blog post) + */ +public class ReloadableX509TrustManager implements X509TrustManager { + + /* package-private */ final ExtraCertManager certManager; + private final InvalidCertificateStrategy invalidCertStrat; + private X509TrustManager tmDelegate; + + public ReloadableX509TrustManager(ExtraCertManager certManager, InvalidCertificateStrategy invalidCertStrat) throws IOException, GeneralSecurityException { + this.certManager = certManager; + this.invalidCertStrat = invalidCertStrat; + reloadTrustManager(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + tmDelegate.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + tmDelegate.checkServerTrusted(chain, authType); + } catch (CertificateException cx) { + + /* + * At this point, we have come across an apparently-invalid + * certificate. We use the `InvalidCertificateStrategy` to decide + * what to do about it; either reject it (rethrow the exception), or + * accept it. If accepting, the certificate can be added + * "temporarily" or "permanently", which is done via the + * `ExtraCertManager`. + */ + Certificate cert = chain[0]; + CertificateAcceptance certAcceptance = invalidCertStrat.checkAcceptance(cert, cx); + + switch (certAcceptance) { + case REJECT: + throw cx; + + case ACCEPT_TEMPORARILY: + try { + certManager.addTemporaryCert(cert); + reloadTrustManager(); + } catch (IOException | GeneralSecurityException e) { + // wrap errors from the cert manipulation + throw new CertificateException("Error handling temporary acceptance of the certificate", e); + } + // now retry the trust check + tmDelegate.checkServerTrusted(chain, authType); + break; + + case ACCEPT_PERMANENTLY: + try { + certManager.addPermanentCert(cert); + reloadTrustManager(); + } catch (IOException | GeneralSecurityException e) { + // wrap errors from the cert manipulation + throw new CertificateException("Error handling permanent acceptance of the certificate", e); + } + // now retry the trust check + tmDelegate.checkServerTrusted(chain, authType); + break; + + default: + throw cx; + } + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return tmDelegate.getAcceptedIssuers(); + } + + /* package-private */ + final void reloadTrustManager() throws IOException, GeneralSecurityException { + KeyStore ks = certManager.asKeyStore(); + + // initialize a new TMF with the KeyStore we just created + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + + // acquire an X509 trust manager from the TMF + // and update the `tmDelegate` to that value + TrustManager[] tms = tmf.getTrustManagers(); + for (TrustManager tm : tms) { + if (tm instanceof X509TrustManager) { + tmDelegate = (X509TrustManager) tm; + return; + } + } + + // should have returned in the `for` loop above + throw new NoSuchAlgorithmException("No X509TrustManager in TrustManagerFactory"); + } +} diff --git a/src/main/java/com/blackduck/zap/srm/security/SSLConnectionSocketFactoryFactory.java b/src/main/java/com/blackduck/zap/srm/security/SSLConnectionSocketFactoryFactory.java new file mode 100644 index 0000000..597587b --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/SSLConnectionSocketFactoryFactory.java @@ -0,0 +1,196 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import com.blackduck.zap.srm.SrmExtension; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; + +import javax.net.ssl.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +public class SSLConnectionSocketFactoryFactory { + + private static final Map dialogFactoriesByHost = new HashMap<>(); + private static final Map fingerprintFactoriesByHost = new HashMap<>(); + + /** + * Returns a SSLConnectionSocketFactory for the given host. When a SSL connection is created + * with the returned socket factory, if the server's certificate appears to be invalid, the user + * will be prompted to either reject temporarily accept, or permanently accept the certificate. + * Permanently accepted certificates will be stored in a .truststore file so the + * user won't need to prompted again for the same host. + * + * @param host The host (URL component) that the socket factory will be used to connect to + * @param extension + * @return A socket factory for the given host + * @throws IOException + * @throws GeneralSecurityException + */ + public static SSLConnectionSocketFactory getFactory(String host, SrmExtension extension) throws IOException, GeneralSecurityException { + + SSLConnectionSocketFactory instance = dialogFactoriesByHost.get(host); + if (instance == null) { + initializeFactory(host, extension, null, false); + return dialogFactoriesByHost.get(host); + } else { + return instance; + } + } + + /** + * Returns a SSLConnectionSocketFactory for the given host. When a SSL connection is created + * with the returned socket factory, if the server's certificate appears to be invalid, the user + * will be prompted to either reject temporarily accept, or permanently accept the certificate. + * Permanently accepted certificates will be stored in a .truststore file so the + * user won't need to prompted again for the same host. + * + * @param host The host (URL component) that the socket factory will be used to connect to + * @param extension + * @param fingerprint Expected SHA1 fingerprint of an invalid certificate + * @param acceptPermanently + * @return A socket factory for the given host + * @throws IOException + * @throws GeneralSecurityException + */ + public static SSLConnectionSocketFactory getFactory(String host, SrmExtension extension, String fingerprint, boolean acceptPermanently) throws IOException, GeneralSecurityException { + SSLConnectionSocketFactory instance = fingerprintFactoriesByHost.get(host); + if (instance == null) { + initializeFactory(host, extension, fingerprint, acceptPermanently); + return fingerprintFactoriesByHost.get(host); + } else { + return instance; + } + } + + /** + * Determines the location for the truststore file for the given host. Each {@link + * SSLConnectionSocketFactory} returned by {@link #initializeFactory(String, SrmExtension, + * String, boolean)} needs to have a file to store user-accepted invalid certificates; these + * files will be stored in the user's OS-appropriate "appdata" directory. + * + * @param host A URL hostname, e.g. "www.google.com" + * @return The file where the trust store for the given host should be stored + */ + private static File getTrustStoreForHost(String host) { + String OS = System.getProperty("os.name").toUpperCase(); + Path env; + if (OS.contains("WIN")) { + env = Paths.get(System.getenv("APPDATA"), "Software Risk Manager", "ZAP"); + } else if (OS.contains("MAC")) { + env = Paths.get(System.getProperty("user.home"), "Library", "Application Support", "Software Risk Manager", "ZAP"); + } else if (OS.contains("NUX")) { + env = Paths.get(System.getProperty("user.home"), ".srm", "zap"); + } else { + env = Paths.get(System.getProperty("user.dir"), "srm", "zap"); + } + + File keystoreDir = new File(env.toFile(), ".usertrust"); + keystoreDir.mkdirs(); + + // Host may only contain alphanumerics, dash, and dot. + // Replace anything else with an underscore. + String safeHost = host.replaceAll("[^a-zA-Z0-9\\-\\.]", "_"); + + // /.usertrust/.truststore + return new File(keystoreDir, safeHost + ".truststore"); + } + + /** + * Creates a new SSLConnectionSocketFactory with the behavior described in + * {@link #getFactory(String, SrmExtension)}. Instead of returning, this + * method registers the factory instance to the factoriesByHost + * map, as well as registering its ExtraCertManager to the + * certManagersByHost map. The cert manager registration is + * important in order to detect and purge trusted certificates on a per-host + * basis. + * + * @param host + * @param extension + * @throws IOException + * @throws GeneralSecurityException + */ + private static void initializeFactory(String host, SrmExtension extension, String fingerprint, boolean acceptPermanently) throws IOException, GeneralSecurityException { + // set up the certificate management + File managedKeyStoreFile = getTrustStoreForHost(host); + ExtraCertManager certManager = new SingleExtraCertManager(managedKeyStoreFile, "u9lwIfUpaN"); + + // get the default hostname verifier that gets used by the modified one + // and the invalid cert dialog + HostnameVerifier defaultHostnameVerifier = new DefaultHostnameVerifier(); + + InvalidCertificateStrategy invalidCertStrat; + if (fingerprint == null) { + invalidCertStrat = new InvalidCertificateDialogStrategy(defaultHostnameVerifier, host, extension); + } else { + invalidCertStrat = new InvalidCertificateFingerprintStrategy(fingerprint, acceptPermanently); + } + + /* + * Set up a composite trust manager that uses the default trust manager + * before delegating to the "reloadable" trust manager that allows users + * to accept invalid certificates. + */ + List trustManagersForComposite = new LinkedList<>(); + X509TrustManager systemTrustManager = getDefaultTrustManager(); + ReloadableX509TrustManager customTrustManager = new ReloadableX509TrustManager(certManager, invalidCertStrat); + trustManagersForComposite.add(systemTrustManager); + trustManagersForComposite.add(customTrustManager); + X509TrustManager trustManager = new CompositeX509TrustManager(trustManagersForComposite); + + // setup the SSLContext using the custom trust manager + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[]{trustManager}, null); + // the actual hostname verifier that will be used with the socket + // factory + Set allowedHosts = new HashSet<>(); + allowedHosts.add(host); + HostnameVerifier modifiedHostnameVerifier = new HostnameVerifierWithExceptions(defaultHostnameVerifier, allowedHosts); + + SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslContext, modifiedHostnameVerifier); + // Register the `factory` and the `customTrustManager` under the given + // `host` + if (fingerprint == null) { + dialogFactoriesByHost.put(host, factory); + } else { + fingerprintFactoriesByHost.put(host, factory); + } + } + + private static X509TrustManager getDefaultTrustManager() throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory defaultFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + defaultFactory.init((KeyStore) null); + + TrustManager[] managers = defaultFactory.getTrustManagers(); + for (TrustManager mgr : managers) { + if (mgr instanceof X509TrustManager) { + return (X509TrustManager) mgr; + } + } + + return null; + } +} diff --git a/src/main/java/com/blackduck/zap/srm/security/SingleExtraCertManager.java b/src/main/java/com/blackduck/zap/srm/security/SingleExtraCertManager.java new file mode 100644 index 0000000..3624383 --- /dev/null +++ b/src/main/java/com/blackduck/zap/srm/security/SingleExtraCertManager.java @@ -0,0 +1,126 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blackduck.zap.srm.security; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; + +/** + * ExtraCertManager implementation that only allows a single accepted certificate at once. Any time + * a certificate is added (be it temporarily or permanently), any previous certificates will be + * forgotten. At any given time, the {@link #asKeyStore()} method should return a KeyStore with 0 or + * 1 certificates registered. + */ +public class SingleExtraCertManager implements ExtraCertManager { + + private final File keystoreFile; + private final char[] password; + private boolean isUsingFile; + private Certificate tempCert = null; + + public SingleExtraCertManager(File keystoreFile, String password) { + this.keystoreFile = keystoreFile; + this.password = password.toCharArray(); + isUsingFile = true; + } + + @Override + public void addTemporaryCert(Certificate cert) { + if (isUsingFile) { + keystoreFile.delete(); + } + isUsingFile = false; + tempCert = cert; + } + + @Override + public void addPermanentCert(Certificate cert) throws IOException, GeneralSecurityException { + tempCert = null; + isUsingFile = true; + + // create a keystore and put the cert in it + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null, password); + ks.setCertificateEntry("default", cert); + + // store the new keystore to the keystoreFile + FileOutputStream out = new FileOutputStream(keystoreFile); + try { + ks.store(out, password); + } finally { + out.close(); + } + } + + @Override + public void purgeTemporaryCerts() { + isUsingFile = true; + tempCert = null; + } + + @Override + public void purgePermanentCerts() { + if (isUsingFile) { + // keep the flag, but delete the file + keystoreFile.delete(); + } + } + + @Override + public void purgeAllCerts() { + isUsingFile = true; + tempCert = null; + keystoreFile.delete(); + } + + @Override + public KeyStore asKeyStore() throws IOException, GeneralSecurityException { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + + if (isUsingFile) { + // load from the file, as long as it exists + if (keystoreFile.canRead()) { + FileInputStream in = new FileInputStream(keystoreFile); + try { + ks.load(in, password); + } finally { + in.close(); + } + } else { + ks.load(null, password); + } + } else { + ks.load(null, password); + // insert the tempCert to the keystore + if (tempCert != null) { + ks.setCertificateEntry("default", tempCert); + } + } + + if (ks.aliases().hasMoreElements()) { + return ks; + } else { + return null; + } + } +} diff --git a/src/main/java/com/github/youruser/zap/javaexample/ExampleFileActiveScanRule.java b/src/main/java/com/github/youruser/zap/javaexample/ExampleFileActiveScanRule.java deleted file mode 100644 index 9c28b14..0000000 --- a/src/main/java/com/github/youruser/zap/javaexample/ExampleFileActiveScanRule.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Zed Attack Proxy (ZAP) and its related class files. - * - * ZAP is an HTTP/HTTPS proxy for assessing web application security. - * - * Copyright 2014 The ZAP Development Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.youruser.zap.javaexample; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.parosproxy.paros.Constant; -import org.parosproxy.paros.core.scanner.AbstractAppParamPlugin; -import org.parosproxy.paros.core.scanner.Alert; -import org.parosproxy.paros.core.scanner.Category; -import org.parosproxy.paros.core.scanner.Plugin; -import org.parosproxy.paros.network.HttpBody; -import org.parosproxy.paros.network.HttpMessage; -import org.zaproxy.zap.model.Tech; -import org.zaproxy.zap.model.TechSet; - -/** - * An example active scan rule, for more details see - * https://www.zaproxy.org/blog/2014-04-30-hacking-zap-4-active-scan-rules/ - * - * @author psiinon - */ -public class ExampleFileActiveScanRule extends AbstractAppParamPlugin { - - /** Prefix for internationalized messages used by this rule */ - private static final String MESSAGE_PREFIX = "javaexample.active.examplefile."; - - private static final String exampleAscanFile = "txt/example-ascan-file.txt"; - private List strings = null; - private static final Logger LOGGER = LogManager.getLogger(ExampleFileActiveScanRule.class); - - @Override - public int getId() { - /* - * This should be unique across all active and passive rules. - * The master list is https://github.com/zaproxy/zaproxy/blob/main/docs/scanners.md - */ - return 60101; - } - - @Override - public String getName() { - return Constant.messages.getString(MESSAGE_PREFIX + "name"); - } - - @Override - public boolean targets( - TechSet technologies) { // This method allows the programmer or user to restrict when a - // scanner is run based on the technologies selected. For example, to restrict the scanner - // to run just when - // C language is selected - return technologies.includes(Tech.C); - } - - @Override - public String getDescription() { - return Constant.messages.getString(MESSAGE_PREFIX + "desc"); - } - - private static String getOtherInfo() { - return Constant.messages.getString(MESSAGE_PREFIX + "other"); - } - - @Override - public String getSolution() { - return Constant.messages.getString(MESSAGE_PREFIX + "soln"); - } - - @Override - public String getReference() { - return Constant.messages.getString(MESSAGE_PREFIX + "refs"); - } - - @Override - public int getCategory() { - return Category.MISC; - } - - /* - * This method is called by the active scanner for each GET and POST parameter for every page - * @see org.parosproxy.paros.core.scanner.AbstractAppParamPlugin#scan(org.parosproxy.paros.network.HttpMessage, java.lang.String, java.lang.String) - */ - @Override - public void scan(HttpMessage msg, String param, String value) { - try { - if (!Constant.isDevBuild()) { - // Only run this example scan rule in dev mode - // Uncomment locally if you want to see these alerts in non dev mode ;) - return; - } - - if (this.strings == null) { - this.strings = loadFile(exampleAscanFile); - } - // This is where you change the 'good' request to attack the application - // You can make multiple requests if needed - int numAttacks = 0; - - switch (this.getAttackStrength()) { - case LOW: - numAttacks = 6; - break; - case MEDIUM: - numAttacks = 12; - break; - case HIGH: - numAttacks = 24; - break; - case INSANE: - numAttacks = 96; - break; - default: - break; - } - - for (int i = 0; i < numAttacks; i++) { - if (this.isStop()) { - // User has stopped the scan - break; - } - if (i >= this.strings.size()) { - // run out of attack strings - break; - } - String attack = this.strings.get(i); - // Always use getNewMsg() for each new request - HttpMessage testMsg = getNewMsg(); - setParameter(testMsg, param, attack); - sendAndReceive(testMsg); - - // This is where you detect potential vulnerabilities in the response - String evidence; - if ((evidence = doesResponseContainString(msg.getResponseBody(), attack)) != null) { - // Raise an alert - createAlert(param, attack, evidence).setMessage(testMsg).raise(); - return; - } - } - - } catch (IOException e) { - LOGGER.error(e.getMessage(), e); - } - } - - private String doesResponseContainString(HttpBody body, String str) { - String sBody; - if (Plugin.AlertThreshold.HIGH.equals(this.getAlertThreshold())) { - // For a high threshold perform a case exact check - sBody = body.toString(); - } else { - // For all other thresholds perform a case ignore check - sBody = body.toString().toLowerCase(); - } - - if (!Plugin.AlertThreshold.HIGH.equals(this.getAlertThreshold())) { - // Use case ignore unless a high threshold has been specified - str = str.toLowerCase(); - } - int start = sBody.indexOf(str); - if (start >= 0) { - // Return the original (case exact) string so we can match it in the response - return body.toString().substring(start, start + str.length()); - } - return null; - } - - private AlertBuilder createAlert(String param, String attack, String evidence) { - return newAlert() - .setConfidence(Alert.CONFIDENCE_MEDIUM) - .setParam(param) - .setAttack(attack) - .setOtherInfo(getOtherInfo()) - .setEvidence(evidence); - } - - private static List loadFile(String file) { - /* - * ZAP will have already extracted the file from the add-on and put it underneath the 'ZAP home' directory - */ - List strings = new ArrayList<>(); - BufferedReader reader = null; - File f = new File(Constant.getZapHome() + File.separator + file); - if (!f.exists()) { - LOGGER.error("No such file: {}", f.getAbsolutePath()); - return strings; - } - try { - String line; - reader = new BufferedReader(new FileReader(f)); - while ((line = reader.readLine()) != null) { - if (!line.startsWith("#") && line.length() > 0) { - strings.add(line); - } - } - } catch (IOException e) { - LOGGER.error( - "Error on opening/reading example error file. Error: {}", e.getMessage(), e); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - LOGGER.debug("Error on closing the file reader. Error: {}", e.getMessage(), e); - } - } - } - return strings; - } - - @Override - public int getRisk() { - return Alert.RISK_HIGH; - } - - @Override - public int getCweId() { - // The CWE id - return 0; - } - - @Override - public int getWascId() { - // The WASC ID - return 0; - } - - @Override - public List getExampleAlerts() { - return List.of(createAlert("foo", " -'';!--"=&{()} - - - - - - - -SRC= - - - - - - - - -'"--> - - +ADw-SCRIPT+AD4-alert('XSS');+ADw-/SCRIPT+AD4- - - - - -PT SRC="http://ha.ckers.org/xss.js">