diff --git a/CHANGELOG.md b/CHANGELOG.md index f6bcb1b..d196ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Refactor for clientapi 2.0.0. + - Extract alert logic into AlertService / DefaultAlertService + - Keep ClientApi as transport + gen API owner + - Simplify CLI orchestration (ClientApiMain) + - Disable japicmp for initial 2.x release + - Introduced internal workflows and services + - No functional changes intended + +### Removed +- Removed `JDOM` dependency + ## [1.17.0] - 2025-12-15 ### Added - Add the APIs of the following add-ons: diff --git a/build.gradle.kts b/build.gradle.kts index e36d43f..ab1128d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,7 @@ subprojects { group = "org.zaproxy" - version = "1.18.0-SNAPSHOT" + version = "2.0.0-SNAPSHOT" extra["versionBC"] = "1.17.0" java { diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java index f2a8f2a..dd12146 100644 --- a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java @@ -3,7 +3,7 @@ * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * - * Copyright 2012 The ZAP Development Team + * Copyright 2025 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. @@ -25,110 +25,244 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.List; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.input.SAXBuilder; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +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 org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; public class AlertsFile { + + /** + * Writes alerts to an XML file. The file contains a root element with the following + * wrapper elements: + * + * + * + * Each wrapper element includes an attribute indicating the number of alerts it contains and + * one or more {@link Alert} elements. + * + * @param requireAlerts Alerts that were required but not found. + * @param reportAlerts Alerts that were found. + * @param ignoredAlerts Alerts that were found but ignored. + * @param outputFile The XML file to write the alerts to. + * @throws IOException If an I/O error occurs while writing the file. + */ public static void saveAlertsToFile( List requireAlerts, List reportAlerts, List ignoredAlerts, File outputFile) - throws JDOMException, IOException { - Element alerts = new Element("alerts"); - Document alertsDocument = new Document(alerts); - alertsDocument.setRootElement(alerts); - if (reportAlerts.size() > 0) { - Element alertsFound = new Element("alertsFound"); - alertsFound.setAttribute("alertsFound", Integer.toString(reportAlerts.size())); - for (Alert alert : reportAlerts) { - createAlertXMLElements(alertsFound, alert); - } - alertsDocument.getRootElement().addContent(alertsFound); + throws IOException { + + if (requireAlerts == null) { + requireAlerts = new ArrayList<>(); } + if (reportAlerts == null) { + reportAlerts = new ArrayList<>(); + } + if (ignoredAlerts == null) { + ignoredAlerts = new ArrayList<>(); + } + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document alertsDocument = builder.newDocument(); - if (requireAlerts.size() > 0) { - Element alertsNotFound = new Element("alertsNotFound"); - alertsNotFound.setAttribute("alertsNotFound", Integer.toString(requireAlerts.size())); - for (Alert alert : requireAlerts) { - createAlertXMLElements(alertsNotFound, alert); + Element root = alertsDocument.createElement("alerts"); + alertsDocument.appendChild(root); + + if (!reportAlerts.isEmpty()) { + Element alertsFound = alertsDocument.createElement("alertsFound"); + alertsFound.setAttribute("alertsFound", Integer.toString(reportAlerts.size())); + for (Alert alert : reportAlerts) { + createAlertXMLElements(alertsDocument, alertsFound, alert); + } + root.appendChild(alertsFound); } - alertsDocument.getRootElement().addContent(alertsNotFound); - } - if (ignoredAlerts.size() > 0) { - Element ignoredAlertsFound = new Element("ignoredAlertsFound"); - ignoredAlertsFound.setAttribute( - "ignoredAlertsFound", Integer.toString(ignoredAlerts.size())); - for (Alert alert : ignoredAlerts) { - createAlertXMLElements(ignoredAlertsFound, alert); + if (!requireAlerts.isEmpty()) { + Element alertsNotFound = alertsDocument.createElement("alertsNotFound"); + alertsNotFound.setAttribute( + "alertsNotFound", Integer.toString(requireAlerts.size())); + for (Alert alert : requireAlerts) { + createAlertXMLElements(alertsDocument, alertsNotFound, alert); + } + root.appendChild(alertsNotFound); } - alertsDocument.getRootElement().addContent(ignoredAlertsFound); - } - writeAlertsToFile(outputFile, alertsDocument); + if (!ignoredAlerts.isEmpty()) { + Element ignoredAlertsFound = alertsDocument.createElement("ignoredAlertsFound"); + ignoredAlertsFound.setAttribute( + "ignoredAlertsFound", Integer.toString(ignoredAlerts.size())); + for (Alert alert : ignoredAlerts) { + createAlertXMLElements(alertsDocument, ignoredAlertsFound, alert); + } + root.appendChild(ignoredAlertsFound); + } + + writeAlertsToFile(outputFile, alertsDocument); + + } catch (ParserConfigurationException | TransformerException e) { + throw new RuntimeException("Failed to save alerts to file: " + outputFile, e); + } } - private static void writeAlertsToFile(File outputFile, Document doc) throws IOException { + private static void writeAlertsToFile(File outputFile, Document doc) + throws IOException, TransformerException { + + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); - XMLOutputter xmlOutput = new XMLOutputter(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); - xmlOutput.setFormat(Format.getPrettyFormat()); try (OutputStream os = Files.newOutputStream(outputFile.toPath())) { - xmlOutput.output(doc, os); + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(os); + transformer.transform(source, result); System.out.println("alert xml report saved to: " + outputFile.getAbsolutePath()); } } - private static void createAlertXMLElements(Element alertsFound, Alert alert) { - Element alertElement = new Element("alert"); + private static void createAlertXMLElements(Document doc, Element alertsParent, Alert alert) { + + Element alertElement = doc.createElement("alert"); + if (alert.getName() != null) { alertElement.setAttribute("name", alert.getName()); // TODO Remove once alert attribute is no longer supported. alertElement.setAttribute("alert", alert.getName()); } - if (alert.getRisk() != null) alertElement.setAttribute("risk", alert.getRisk().name()); - if (alert.getUrl() != null) + + if (alert.getRisk() != null) { + alertElement.setAttribute("risk", alert.getRisk().name()); + } + + if (alert.getConfidence() != null) { alertElement.setAttribute("confidence", alert.getConfidence().name()); - if (alert.getUrl() != null) alertElement.setAttribute("url", alert.getUrl()); - if (alert.getParam() != null) alertElement.setAttribute("param", alert.getParam()); - if (alert.getOther() != null) alertElement.setAttribute("other", alert.getOther()); - if (alert.getAttack() != null) alertElement.setAttribute("attack", alert.getAttack()); - if (alert.getDescription() != null) + } + + if (alert.getUrl() != null) { + alertElement.setAttribute("url", alert.getUrl()); + } + + if (alert.getParam() != null) { + alertElement.setAttribute("param", alert.getParam()); + } + + if (alert.getOther() != null) { + alertElement.setAttribute("other", alert.getOther()); + } + + if (alert.getAttack() != null) { + alertElement.setAttribute("attack", alert.getAttack()); + } + + if (alert.getDescription() != null) { alertElement.setAttribute("description", alert.getDescription()); - if (alert.getSolution() != null) alertElement.setAttribute("solution", alert.getSolution()); - if (alert.getReference() != null) + } + + if (alert.getSolution() != null) { + alertElement.setAttribute("solution", alert.getSolution()); + } + + if (alert.getReference() != null) { alertElement.setAttribute("reference", alert.getReference()); - alertsFound.addContent(alertElement); + } + + alertsParent.appendChild(alertElement); } - public static List getAlertsFromFile(File file, String alertType) - throws JDOMException, IOException { + /** + * Reads alerts of a given type from the file. + * + * @param file The XML file previously written by {@link #saveAlertsToFile}. + * @param alertType The wrapper element name under <alerts>: + *
    + *
  • "alertsFound" + *
  • "alertsNotFound" + *
  • "ignoredAlertsFound" + *
+ * + * @return list of {@link Alert}s found inside the matching wrapper(s). + */ + public static List getAlertsFromFile(File file, String alertType) throws IOException { + List alerts = new ArrayList<>(); - SAXBuilder parser = new SAXBuilder(); - Document alertsDoc = parser.build(file); - @SuppressWarnings("unchecked") - List alertElements = alertsDoc.getRootElement().getChildren(alertType); - for (Element element : alertElements) { - String name = element.getAttributeValue("name"); - if (name == null) { - // TODO Remove once alert attribute is no longer supported. - name = element.getAttributeValue("alert"); + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document alertsDoc = builder.parse(file); + Element root = alertsDoc.getDocumentElement(); + NodeList rootChildren = root.getChildNodes(); + for (int i = 0; i < rootChildren.getLength(); i++) { + Node wrapperNode = rootChildren.item(i); + if (wrapperNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + Element wrapperElem = (Element) wrapperNode; + if (!alertType.equals(wrapperElem.getTagName())) { + continue; + } + + NodeList childNodes = wrapperElem.getChildNodes(); + for (int j = 0; j < childNodes.getLength(); j++) { + Node node = childNodes.item(j); + if (node.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + Element element = (Element) node; + if (!"alert".equals(element.getTagName())) { + continue; + } + + String name = element.getAttribute("name"); + if (name.isEmpty()) { + // TODO Remove once alert attribute is no longer supported. + name = element.getAttribute("alert"); + } + + Alert alert = + new Alert( + emptyToNull(element.getAttribute("url")), + emptyToNull(element.getAttribute("risk")), + emptyToNull(element.getAttribute("confidence")), + emptyToNull(element.getAttribute("param")), + emptyToNull(element.getAttribute("other")), + name); + + alerts.add(alert); + } } - Alert alert = - new Alert( - name, - element.getAttributeValue("url"), - element.getAttributeValue("risk"), - element.getAttributeValue("confidence"), - element.getAttributeValue("param"), - element.getAttributeValue("other")); - alerts.add(alert); + + } catch (ParserConfigurationException | SAXException e) { + throw new RuntimeException("Failed to read alerts from file: " + file, e); } + return alerts; } + + private static String emptyToNull(String value) { + return (value == null || value.isEmpty()) ? null : value; + } } diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApi.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApi.java index e57e7a7..5cbf6b5 100644 --- a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApi.java +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApi.java @@ -19,29 +19,24 @@ */ package org.zaproxy.clientapi.core; +import static org.zaproxy.clientapi.internal.FilesWorkflow.readAllBytes; + import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintStream; -import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.Proxy; import java.net.Socket; -import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -50,67 +45,36 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; -import org.zaproxy.clientapi.gen.AccessControl; -import org.zaproxy.clientapi.gen.Acsrf; -import org.zaproxy.clientapi.gen.AjaxSpider; -import org.zaproxy.clientapi.gen.AlertFilter; -import org.zaproxy.clientapi.gen.Ascan; -import org.zaproxy.clientapi.gen.Authentication; -import org.zaproxy.clientapi.gen.Authorization; -import org.zaproxy.clientapi.gen.Automation; -import org.zaproxy.clientapi.gen.Autoupdate; -import org.zaproxy.clientapi.gen.Break; -import org.zaproxy.clientapi.gen.Client; -import org.zaproxy.clientapi.gen.ClientSpider; -import org.zaproxy.clientapi.gen.Context; -import org.zaproxy.clientapi.gen.Core; -import org.zaproxy.clientapi.gen.Exim; -import org.zaproxy.clientapi.gen.ForcedUser; -import org.zaproxy.clientapi.gen.Graphql; -import org.zaproxy.clientapi.gen.HttpSessions; -import org.zaproxy.clientapi.gen.Network; -import org.zaproxy.clientapi.gen.Oast; -import org.zaproxy.clientapi.gen.Openapi; -import org.zaproxy.clientapi.gen.Params; -import org.zaproxy.clientapi.gen.Pnh; -import org.zaproxy.clientapi.gen.Postman; -import org.zaproxy.clientapi.gen.Pscan; -import org.zaproxy.clientapi.gen.Replacer; -import org.zaproxy.clientapi.gen.Reports; -import org.zaproxy.clientapi.gen.Retest; -import org.zaproxy.clientapi.gen.Reveal; -import org.zaproxy.clientapi.gen.Revisit; -import org.zaproxy.clientapi.gen.RuleConfig; -import org.zaproxy.clientapi.gen.Script; -import org.zaproxy.clientapi.gen.Search; -import org.zaproxy.clientapi.gen.Selenium; -import org.zaproxy.clientapi.gen.SessionManagement; -import org.zaproxy.clientapi.gen.Soap; -import org.zaproxy.clientapi.gen.Spider; -import org.zaproxy.clientapi.gen.Stats; -import org.zaproxy.clientapi.gen.Users; -import org.zaproxy.clientapi.gen.Wappalyzer; -import org.zaproxy.clientapi.gen.Websocket; +import org.zaproxy.clientapi.gen.*; +import org.zaproxy.clientapi.impl.DefaultAlertService; +import org.zaproxy.clientapi.internal.AlertWorkflow; +import org.zaproxy.clientapi.internal.ContextWorkflow; +import org.zaproxy.clientapi.internal.ScanWorkflow; +import org.zaproxy.clientapi.service.AlertService; @SuppressWarnings("this-escape") public class ClientApi { private static final int DEFAULT_CONNECTION_POOLING_IN_MS = 1000; - private static final String ZAP_API_KEY_HEADER = "X-ZAP-API-Key"; + private Proxy proxy; - private Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 8090)); + @SuppressWarnings("UnusedVariable") private boolean debug = false; - private PrintStream debugStream = System.out; - private final String zapAddress; - private final int zapPort; + @SuppressWarnings("UnusedVariable") + private PrintStream debugStream = System.out; - private final String apiKey; + private String zapAddress; + private int zapPort; + private String apiKey; private DocumentBuilderFactory docBuilderFactory; - // Note that any new API implementations added have to be added here manually + /* ======================= + * Generated API endpoints + * ======================= */ + public AccessControl accessControl = new AccessControl(this); public Acsrf acsrf = new Acsrf(this); public AjaxSpider ajaxSpider = new AjaxSpider(this); @@ -129,24 +93,20 @@ public class ClientApi { public Exim exim = new Exim(this); @SuppressWarnings("removal") - public org.zaproxy.clientapi.gen.Exportreport exportreport = - new org.zaproxy.clientapi.gen.Exportreport(this); + public Exportreport exportreport = new Exportreport(this); public ForcedUser forcedUser = new ForcedUser(this); public Graphql graphql = new Graphql(this); public HttpSessions httpSessions = new HttpSessions(this); @SuppressWarnings("removal") - public org.zaproxy.clientapi.gen.ImportLogFiles logImportFiles = - new org.zaproxy.clientapi.gen.ImportLogFiles(this); + public ImportLogFiles logImportFiles = new ImportLogFiles(this); @SuppressWarnings("removal") - public org.zaproxy.clientapi.gen.Importurls importurls = - new org.zaproxy.clientapi.gen.Importurls(this); + public Importurls importurls = new Importurls(this); @SuppressWarnings("removal") - public org.zaproxy.clientapi.gen.LocalProxies localProxies = - new org.zaproxy.clientapi.gen.LocalProxies(this); + public LocalProxies localProxies = new LocalProxies(this); public Network network = new Network(this); public Oast oast = new Oast(this); @@ -172,19 +132,20 @@ public class ClientApi { public Wappalyzer wappalyzer = new Wappalyzer(this); public Websocket websocket = new Websocket(this); + /* ======================= + * Internal services + * ======================= */ + + private final AlertService alertService; + + /* ======================= + * Constructors + * ======================= */ + public ClientApi(String zapAddress, int zapPort) { - this(zapAddress, zapPort, false); + this(zapAddress, zapPort, null, false); } - /** - * Constructs a {@code ClientApi} with the given ZAP address/port and with the given API key, to - * be sent with all API requests. - * - * @param zapAddress ZAP's address - * @param zapPort ZAP's listening port - * @param apiKey the ZAP API key, might be {@code null} or empty in which case is not used/sent. - * @since 1.1.0 - */ public ClientApi(String zapAddress, int zapPort, String apiKey) { this(zapAddress, zapPort, apiKey, false); } @@ -193,184 +154,59 @@ public ClientApi(String zapAddress, int zapPort, boolean debug) { this(zapAddress, zapPort, null, debug); } - /** - * Constructs a {@code ClientApi} with the given ZAP address/port and with the given API key, to - * be sent with all API requests. Also, sets whether or not client API debug information should - * be written to the {@link #setDebugStream(PrintStream) debug stream} (by default the standard - * output stream). - * - * @param zapAddress ZAP's address - * @param zapPort ZAP's listening port - * @param apiKey the ZAP API key, might be {@code null} or empty in which case is not used/sent. - * @param debug {@code true} if debug information should be written to debug stream, {@code - * false} otherwise. - * @since 1.1.0 - */ public ClientApi(String zapAddress, int zapPort, String apiKey, boolean debug) { - proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(zapAddress, zapPort)); - this.debug = debug; + this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(zapAddress, zapPort)); this.zapAddress = zapAddress; this.zapPort = zapPort; this.apiKey = apiKey; + this.debug = debug; + this.alertService = new DefaultAlertService(this.alert); } public void setDebugStream(PrintStream debugStream) { this.debugStream = debugStream; } - public void accessUrl(String url) throws ClientApiException { - accessUrlViaProxy(proxy, url); - } - - private int statusToInt(ApiResponse response) { - return Integer.parseInt(((ApiResponseElement) response).getValue()); + AlertService getAlertService() { + return alertService; } public void checkAlerts(List ignoreAlerts, List requireAlerts) throws ClientApiException { - Map> results = checkForAlerts(ignoreAlerts, requireAlerts); - verifyAlerts(results.get("requireAlerts"), results.get("reportAlerts")); + AlertWorkflow.checkAlerts(this, alertService, ignoreAlerts, requireAlerts); } - private void verifyAlerts(List requireAlerts, List reportAlerts) + public void checkAlerts(List ignoreAlerts, List requireAlerts, File outputFile) throws ClientApiException { - StringBuilder sb = new StringBuilder(); - if (reportAlerts.size() > 0) { - sb.append("Found ").append(reportAlerts.size()).append(" alerts\n"); - for (Alert alert : reportAlerts) { - sb.append('\t'); - sb.append(alert.toString()); - sb.append('\n'); - } - } - if (requireAlerts != null && requireAlerts.size() > 0) { - if (sb.length() > 0) { - sb.append('\n'); - } - sb.append("Not found ").append(requireAlerts.size()).append(" alerts\n"); - for (Alert alert : requireAlerts) { - sb.append('\t'); - sb.append(alert.toString()); - sb.append('\n'); - } - } - if (sb.length() > 0) { - if (debug) { - debugStream.println("Failed: " + sb.toString()); - } - throw new ClientApiException(sb.toString()); - } + AlertWorkflow.checkAlerts(this, alertService, ignoreAlerts, requireAlerts, outputFile); } - public void checkAlerts(List ignoreAlerts, List requireAlerts, File outputFile) - throws ClientApiException { - Map> results = checkForAlerts(ignoreAlerts, requireAlerts); - int alertsFound = results.get("reportAlerts").size(); - int alertsNotFound = results.get("requireAlerts").size(); - int alertsIgnored = results.get("ignoredAlerts").size(); - String resultsString = - String.format( - "Alerts Found: %d, Alerts required but not found: %d, Alerts ignored: %d", - alertsFound, alertsNotFound, alertsIgnored); - try { - AlertsFile.saveAlertsToFile( - results.get("requireAlerts"), - results.get("reportAlerts"), - results.get("ignoredAlerts"), - outputFile); - } catch (Exception e) { - throw new ClientApiException("Failed to save the alerts:", e); - } - if (alertsFound > 0 || alertsNotFound > 0) { - throw new ClientApiException("Check Alerts Failed!\n" + resultsString); - } else { - if (debug) { - debugStream.println("Check Alerts Passed!\n" + resultsString); - } - } + public void includeOneMatchingNodeInContext(String contextName, String regex) throws Exception { + ContextWorkflow.includeOneMatchingNode(this, contextName, regex); } - public List getAlerts(String baseUrl, int start, int count) throws ClientApiException { - List alerts = new ArrayList<>(); - ApiResponse response = - alert.alerts(baseUrl, String.valueOf(start), String.valueOf(count), null); - if (response != null && response instanceof ApiResponseList) { - ApiResponseList alertList = (ApiResponseList) response; - for (ApiResponse resp : alertList.getItems()) { - alerts.add(new Alert((ApiResponseSet) resp)); - } - } - return alerts; + public void activeScanSiteInScope(String url) throws Exception { + ScanWorkflow.activeScanSiteInScope(this, url); } - private Map> checkForAlerts( - List ignoreAlerts, List requireAlerts) throws ClientApiException { - List reportAlerts = new ArrayList<>(); - List ignoredAlerts = new ArrayList<>(); - List alerts = getAlerts(null, -1, -1); - for (Alert alert : alerts) { - boolean ignore = false; - if (ignoreAlerts != null) { - for (Alert ignoreAlert : ignoreAlerts) { - if (alert.matches(ignoreAlert)) { - if (debug) { - debugStream.println("Ignoring alert " + ignoreAlert); - } - ignoredAlerts.add(alert); - ignore = true; - break; - } - } - } - if (!ignore) { - reportAlerts.add(alert); - } - if (requireAlerts != null) { - for (Alert requireAlert : requireAlerts) { - if (alert.matches(requireAlert)) { - if (debug) { - debugStream.println("Found alert " + alert); - } - requireAlerts.remove(requireAlert); - // Remove it from the not-ignored list as well - reportAlerts.remove(alert); - break; - } - } - } - } - HashMap> results = new HashMap<>(); - results.put("reportAlerts", reportAlerts); - results.put("requireAlerts", requireAlerts); - results.put("ignoredAlerts", ignoredAlerts); - return results; + public void accessUrl(String url) throws ClientApiException { + accessUrlViaProxy(proxy, url); } private void accessUrlViaProxy(Proxy proxy, String apiurl) throws ClientApiException { try { URL url = createUrl(apiurl); - if (debug) { - debugStream.println("Open URL: " + apiurl); - } HttpURLConnection uc = (HttpURLConnection) url.openConnection(proxy); uc.connect(); try (BufferedReader in = new BufferedReader( new InputStreamReader(uc.getInputStream(), StandardCharsets.UTF_8))) { - String inputLine; - - while ((inputLine = in.readLine()) != null) { - if (debug) { - debugStream.println(inputLine); - } + while (in.readLine() != null) { + // ignore } - } catch (IOException e) { - // Ignore - if (debug) { - debugStream.println("Ignoring exception " + e); - } + throw new RuntimeException(e); } } catch (Exception e) { throw new ClientApiException(e); @@ -394,77 +230,10 @@ public ApiResponse callApi( String method, Map params) throws ClientApiException { - Document dom = this.callApiDom(requestMethod, component, type, method, params); + Document dom = callApiDom(requestMethod, component, type, method, params); return ApiResponseFactory.getResponse(dom.getFirstChild()); } - private Document callApiDom( - String requestMethod, - String component, - String type, - String method, - Map params) - throws ClientApiException { - try { - HttpRequest request = - buildZapRequest(requestMethod, "xml", component, type, method, params); - if (debug) { - debugStream.println("Open URL: " + request.getRequestUri()); - } - DocumentBuilder db = getDocumentBuilderFactory().newDocumentBuilder(); - // parse using builder to get DOM representation of the XML file - return db.parse(getConnectionInputStream(request)); - } catch (Exception e) { - throw new ClientApiException(e); - } - } - - /** - * Gets the {@code DocumentBuilderFactory} instance with XML External Entity (XXE) processing - * disabled. - * - * @return the {@code DocumentBuilderFactory} instance with XXE processing disabled. - * @throws ParserConfigurationException if an error occurred while disabling XXE processing. - * @see DocumentBuilderFactory - */ - private DocumentBuilderFactory getDocumentBuilderFactory() throws ParserConfigurationException { - if (docBuilderFactory == null) { - // Disable XXE processing, not required by default. - // https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - factory.setFeature("http://xml.org/sax/features/external-general-entities", false); - factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - factory.setExpandEntityReferences(false); - docBuilderFactory = factory; - } - return docBuilderFactory; - } - - private InputStream getConnectionInputStream(HttpRequest request) throws IOException { - HttpURLConnection uc = (HttpURLConnection) request.getRequestUri().openConnection(proxy); - uc.setUseCaches(false); - for (Entry header : request.getHeaders().entrySet()) { - uc.setRequestProperty(header.getKey(), header.getValue()); - } - if (!isGetRequest(request.getMethod())) { - uc.setRequestMethod(request.getMethod()); - String body = request.getBody(); - if (body != null && !body.isEmpty()) { - uc.setDoOutput(true); - try (var os = - new OutputStreamWriter(uc.getOutputStream(), StandardCharsets.UTF_8)) { - os.write(request.getBody()); - } - } - } - uc.connect(); - if (uc.getResponseCode() >= HttpURLConnection.HTTP_BAD_REQUEST) { - return uc.getErrorStream(); - } - return uc.getInputStream(); - } - public byte[] callApiOther( String component, String type, String method, Map params) throws ClientApiException { @@ -478,412 +247,120 @@ public byte[] callApiOther( String method, Map params) throws ClientApiException { - return getBytes(requestMethod, "other", component, type, method, params); + + try { + HttpRequest request = + ZapRequestBuilder.buildZapRequest( + requestMethod, "other", component, type, method, params, apiKey); + + return readAllBytes(getConnectionInputStream(request)); + + } catch (Exception e) { + throw new ClientApiException(e); + } } public String callApiJson( String component, String type, String method, Map params) throws ClientApiException { - byte[] json = getBytes(HttpRequest.GET_METHOD, "JSON", component, type, method, params); - return new String(json, StandardCharsets.UTF_8); - } - private byte[] getBytes( - String requestMethod, - String format, - String component, - String type, - String method, - Map params) - throws ClientApiException { try { HttpRequest request = - buildZapRequest(requestMethod, format, component, type, method, params); - if (debug) { - debugStream.println("Open URL: " + request.getRequestUri()); - } - InputStream in = getConnectionInputStream(request); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] buffer = new byte[8 * 1024]; - try { - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - } finally { - out.close(); - in.close(); - } - return out.toByteArray(); + ZapRequestBuilder.buildZapRequest( + HttpRequest.GET_METHOD, + "JSON", + component, + type, + method, + params, + apiKey); + + byte[] bytes = readAllBytes(getConnectionInputStream(request)); + return new String(bytes, StandardCharsets.UTF_8); } catch (Exception e) { throw new ClientApiException(e); } } - /** - * Builds a request for the ZAP API with the given data. - * - *

As the API client proxies through ZAP the built API requests use a specific domain, {@code - * zap}, to ensure that they are always handled by ZAP (and not forward). - * - * @param requestMethod the HTTP request method. - * @param format the desired format of the API response (e.g. XML, JSON, other). - * @param component the API component (e.g. core, spider). - * @param type the type of the API endpoint (e.g. action, view). - * @param method the name of the endpoint. - * @param params the parameters for the endpoint. - * @return the API request. - * @throws MalformedURLException if an error occurred while building the URL. - * @throws URISyntaxException if an error occurred while building the URL. - */ - private HttpRequest buildZapRequest( + private Document callApiDom( String requestMethod, - String format, String component, String type, String method, Map params) - throws MalformedURLException, URISyntaxException { - StringBuilder sb = new StringBuilder(); - sb.append("http://zap/"); - sb.append(format); - sb.append('/'); - sb.append(component); - sb.append('/'); - sb.append(type); - sb.append('/'); - sb.append(method); - sb.append('/'); - String body = null; - if (params != null) { - if (isGetRequest(requestMethod)) { - sb.append('?'); - appendParams(params, sb); - } else { - body = appendParams(params, new StringBuilder()).toString(); - } - } - - HttpRequest request = new HttpRequest(requestMethod, createUrl(sb.toString()), body); - if (apiKey != null && !apiKey.isEmpty()) { - request.addHeader(ZAP_API_KEY_HEADER, apiKey); - } - return request; - } - - private static boolean isGetRequest(String requestMethod) { - return HttpRequest.GET_METHOD.equals(requestMethod); - } - - private static StringBuilder appendParams(Map params, StringBuilder sb) { - for (Map.Entry p : params.entrySet()) { - sb.append(encodeQueryParam(p.getKey())); - sb.append('='); - if (p.getValue() != null) { - sb.append(encodeQueryParam(p.getValue())); - } - sb.append('&'); - } - return sb; - } - - private static String encodeQueryParam(String param) { + throws ClientApiException { try { - return URLEncoder.encode(param, "UTF-8"); - } catch (UnsupportedEncodingException ignore) { - // UTF-8 is a standard charset. - } - return param; - } - - /** - * Adds the given regular expression to the exclusion list of the given context. - * - * @param apiKey the API key, might be {@code null}. - * @param contextName the name of the context. - * @param regex the regular expression to add. - * @throws Exception if an error occurred while calling the API. - * @deprecated (1.1.0) Use {@link Context#excludeFromContext(String, String)} instead. - * @see #context - */ - @Deprecated - public void addExcludeFromContext(String apiKey, String contextName, String regex) - throws Exception { - context.excludeFromContext(apiKey, contextName, regex); - } - - /** - * Adds the given regular expression to the inclusion list of the given context. - * - * @param apiKey the API key, might be {@code null}. - * @param contextName the name of the context. - * @param regex the regular expression to add. - * @throws Exception if an error occurred while calling the API. - * @deprecated (1.1.0) Use {@link Context#includeInContext(String, String)} instead. - * @see #context - */ - @Deprecated - public void addIncludeInContext(String apiKey, String contextName, String regex) - throws Exception { - context.includeInContext(apiKey, contextName, regex); - } - - /** - * Includes just one of the nodes that match the given regular expression in the context with - * the given name. - * - *

Nodes that do not match the regular expression are excluded. - * - * @param apiKey the API key, might be {@code null}. - * @param contextName the name of the context. - * @param regex the regular expression to match the node/URL. - * @throws Exception if an error occurred while calling the API. - * @deprecated (1.1.0) Use {@link #includeOneMatchingNodeInContext(String, String)} instead. - */ - @Deprecated - public void includeOneMatchingNodeInContext(String apiKey, String contextName, String regex) - throws Exception { - List sessionUrls = getSessionUrls(); - boolean foundOneMatch = false; - for (String sessionUrl : sessionUrls) { - if (sessionUrl.matches(regex)) { - if (foundOneMatch) { - addExcludeFromContext(apiKey, contextName, sessionUrl); - } else { - foundOneMatch = true; - } - } - } - if (!foundOneMatch) { - throw new Exception( - "Unexpected result: No url found in site tree matching regex " + regex); + HttpRequest request = + ZapRequestBuilder.buildZapRequest( + requestMethod, "xml", component, type, method, params, apiKey); + DocumentBuilder db = getDocumentBuilderFactory().newDocumentBuilder(); + return db.parse(getConnectionInputStream(request)); + } catch (Exception e) { + throw new ClientApiException(e); } } - /** - * Includes just one of the nodes that match the given regular expression in the context with - * the given name. - * - *

Nodes that do not match the regular expression are excluded. - * - * @param contextName the name of the context. - * @param regex the regular expression to match the node/URL. - * @throws Exception if an error occurred while calling the API. - */ - public void includeOneMatchingNodeInContext(String contextName, String regex) throws Exception { - List sessionUrls = getSessionUrls(); - boolean foundOneMatch = false; - for (String sessionUrl : sessionUrls) { - if (sessionUrl.matches(regex)) { - if (foundOneMatch) { - context.excludeFromContext(contextName, regex); - } else { - foundOneMatch = true; - } - } - } - if (!foundOneMatch) { - throw new Exception( - "Unexpected result: No url found in site tree matching regex " + regex); + private DocumentBuilderFactory getDocumentBuilderFactory() throws ParserConfigurationException { + if (docBuilderFactory == null) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setExpandEntityReferences(false); + docBuilderFactory = factory; } + return docBuilderFactory; } - private List getSessionUrls() throws Exception { - List sessionUrls = new ArrayList<>(); - ApiResponse response = core.urls(); - if (response != null && response instanceof ApiResponseList) { - ApiResponseElement urlList = - (ApiResponseElement) ((ApiResponseList) response).getItems().get(0); - for (ApiResponse element : ((ApiResponseList) response).getItems()) { - URL url = createUrl(((ApiResponseElement) element).getValue()); - sessionUrls.add(url.getProtocol() + "://" + url.getHost() + url.getPath()); - } - System.out.println(urlList); + private InputStream getConnectionInputStream(HttpRequest request) throws IOException { + HttpURLConnection uc = (HttpURLConnection) request.getRequestUri().openConnection(proxy); + uc.setUseCaches(false); + for (Entry header : request.getHeaders().entrySet()) { + uc.setRequestProperty(header.getKey(), header.getValue()); } - return sessionUrls; - } - - /** - * Active scans the given site, that's in scope. - * - *

The method returns only after the scan has finished. - * - * @param apiKey the API key, might be {@code null}. - * @param url the site to scan - * @throws Exception if an error occurred while calling the API. - * @deprecated (1.1.0) Use {@link #activeScanSiteInScope(String)} instead, the API key should be - * set using one of the {@code ClientApi} constructors. - */ - @Deprecated - public void activeScanSiteInScope(String apiKey, String url) throws Exception { - ascan.scan(apiKey, url, "true", "true", "", "", ""); - waitForAScanToFinish(url); - } - - /** - * Active scans the given site, that's in scope. - * - *

The method returns only after the scan has finished. - * - * @param url the site to scan - * @throws Exception if an error occurred while calling the API. - * @since 1.1.0 - */ - public void activeScanSiteInScope(String url) throws Exception { - ascan.scan(url, "true", "true", "", "", ""); - waitForAScanToFinish(url); - } - - private void waitForAScanToFinish(String targetUrl) throws ClientApiException { - // Poll until spider finished - int status = 0; - while (status < 100) { - status = statusToInt(ascan.status("")); - if (debug) { - String format = "Scanning %s Progress: %d%%"; - System.out.println(String.format(format, targetUrl, status)); - } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - // Ignore + if (!HttpRequest.GET_METHOD.equals(request.getMethod())) { + uc.setRequestMethod(request.getMethod()); + if (request.getBody() != null && !request.getBody().isEmpty()) { + uc.setDoOutput(true); + try (OutputStreamWriter os = + new OutputStreamWriter(uc.getOutputStream(), StandardCharsets.UTF_8)) { + os.write(request.getBody()); + } } } + uc.connect(); + return uc.getResponseCode() >= HttpURLConnection.HTTP_BAD_REQUEST + ? uc.getErrorStream() + : uc.getInputStream(); } - /** - * Convenience method to wait for ZAP to be ready to receive API calls, when started - * programmatically. - * - *

It attempts to establish a connection to ZAP's proxy, in the given time, throwing an - * exception if the connection is not successful. The connection attempts might be polled in one - * second interval. - * - * @param timeoutInSeconds the (maximum) number of seconds to wait for ZAP to start - * @throws ClientApiException if the timeout was reached or if the thread was interrupted while - * waiting - * @see #waitForSuccessfulConnectionToZap(int, int) - */ public void waitForSuccessfulConnectionToZap(int timeoutInSeconds) throws ClientApiException { waitForSuccessfulConnectionToZap(timeoutInSeconds, DEFAULT_CONNECTION_POOLING_IN_MS); } - /** - * Convenience method to wait for ZAP to be ready to receive API calls, when started - * programmatically. - * - *

It attempts to establish a connection to ZAP's proxy, in the given time, throwing an - * exception if the connection is not successful. The connection attempts are done with the - * given polling interval. - * - * @param timeoutInSeconds the (maximum) number of seconds to wait for ZAP to start - * @param pollingIntervalInMs the interval, in milliseconds, for connection polling - * @throws ClientApiException if the timeout was reached or if the thread was interrupted while - * waiting. - * @throws IllegalArgumentException if the interval for connection polling is negative. - * @see #waitForSuccessfulConnectionToZap(int) - */ public void waitForSuccessfulConnectionToZap(int timeoutInSeconds, int pollingIntervalInMs) throws ClientApiException { + int timeoutInMs = (int) TimeUnit.SECONDS.toMillis(timeoutInSeconds); - int connectionTimeoutInMs = timeoutInMs; - boolean connectionSuccessful = false; - long startTime = System.currentTimeMillis(); - do { + long start = System.currentTimeMillis(); + + while (true) { try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(zapAddress, zapPort), timeoutInMs); + return; + } catch (IOException e) { + if (System.currentTimeMillis() - start > timeoutInMs) { + throw new ClientApiException( + "Unable to connect to ZAP after " + timeoutInSeconds + " seconds."); + } try { - socket.connect( - new InetSocketAddress(zapAddress, zapPort), connectionTimeoutInMs); - connectionSuccessful = true; - } catch (SocketTimeoutException ignore) { - throw newTimeoutConnectionToZap(timeoutInSeconds); - } catch (IOException ignore) { - // and keep trying but wait some time first... - try { - Thread.sleep(pollingIntervalInMs); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new ClientApiException( - "The ClientApi was interrupted while sleeping between connection polling.", - e); - } - - long ellapsedTime = System.currentTimeMillis() - startTime; - if (ellapsedTime >= timeoutInMs) { - throw newTimeoutConnectionToZap(timeoutInSeconds); - } - connectionTimeoutInMs = (int) (timeoutInMs - ellapsedTime); + Thread.sleep(pollingIntervalInMs); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new ClientApiException("Interrupted while waiting for ZAP.", ie); } - } catch (IOException ignore) { - // the closing state doesn't matter. } - } while (!connectionSuccessful); - } - - private static ClientApiException newTimeoutConnectionToZap(int timeoutInSeconds) { - return new ClientApiException( - "Unable to connect to ZAP's proxy after " + timeoutInSeconds + " seconds."); - } - - /** - * A simple HTTP request. - * - *

Contains the request URI and headers. - */ - private static class HttpRequest { - - private static final String GET_METHOD = "GET"; - - private final String method; - private final URL requestUri; - private final Map headers; - private final String body; - - public HttpRequest(String method, URL url, String body) { - this.method = method; - this.requestUri = url; - this.headers = new HashMap<>(); - this.body = body; - } - - public String getMethod() { - return method; - } - - /** - * Gets the request URI of the request. - * - * @return the request URI. - */ - public URL getRequestUri() { - return requestUri; - } - - /** - * Adds a header with the given name and value. - * - *

If a header with the given name already exists it is replaced with the new value. - * - * @param name the name of the header. - * @param value the value of the header. - */ - public void addHeader(String name, String value) { - headers.put(name, value); - } - - /** - * Gets the headers of the HTTP request. An unmodifiable {@code Map} containing the headers - * (the keys correspond to the header names and the values for its contents). - * - * @return an unmodifiable {@code Map} containing the headers. - */ - public Map getHeaders() { - return Collections.unmodifiableMap(headers); - } - - public String getBody() { - return body; } } } diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApiMain.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApiMain.java index 7de0717..5ed278c 100644 --- a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApiMain.java +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ClientApiMain.java @@ -20,13 +20,12 @@ package org.zaproxy.clientapi.core; import java.io.File; -import java.net.ConnectException; import java.util.HashMap; -import java.util.List; +import org.zaproxy.clientapi.service.ClientApiTaskService; public class ClientApiMain { - private HashMap params = new HashMap<>(); + private final HashMap params = new HashMap<>(); private String zapaddr = "localhost"; private int zapport = 8090; private Task task; @@ -52,175 +51,97 @@ public static void main(String[] args) { public ClientApiMain(String[] args) { initialize(args); - try { - executeTask(); - } catch (Exception e) { - e.printStackTrace(); - showHelp(); - } + executeTask(); } - private void executeTask() throws Exception { + private void executeTask() { + + ClientApiTaskService service = new ClientApiTaskService(api, params); + try { switch (task) { case stop: - api.core.shutdown(); + service.stop(); break; case checkAlerts: - if (params.get("alertsFile") == null) { - System.out.println("No Alerts File Path Supplied\n"); - showHelp(); - System.exit(1); - } - File alertsFile = (File) params.get("alertsFile"); - if (!alertsFile.exists()) { - System.out.println("File not Found: " + alertsFile.getAbsolutePath()); - showHelp(); - System.exit(1); - } - if (params.get("outputFile") == null) { - api.checkAlerts( - AlertsFile.getAlertsFromFile(alertsFile, "ignoreAlert"), - AlertsFile.getAlertsFromFile(alertsFile, "requireAlert")); - } else { - File outFile = (File) params.get("outputFile"); - try { - api.checkAlerts( - AlertsFile.getAlertsFromFile(alertsFile, "ignoreAlert"), - AlertsFile.getAlertsFromFile(alertsFile, "requireAlert"), - outFile); - } catch (AssertionError e) { - System.out.println(e.getMessage()); - System.exit(1); - } - } + service.checkAlerts(); break; case showAlerts: - List alerts = api.getAlerts(null, -1, -1); - for (Alert alert : alerts) { - System.out.println(alert.toString()); - } + service.showAlerts(); break; case saveSession: - if (params.get("sessionName") == null) { - System.out.println("No session name supplied\n"); - showHelp(); - System.exit(1); - } - api.core.saveSession((String) params.get("sessionName"), "true"); + service.saveSession(); break; case newSession: - if (params.get("sessionName") == null) { - api.core.newSession("", "true"); - } else { - api.core.newSession((String) params.get("sessionName"), "true"); - } + service.newSession(); break; case activeScanUrl: - if (params.get("url") == null) { - System.out.println("No url supplied\n"); - showHelp(); - System.exit(1); - } else { - api.ascan.scan((String) params.get("url"), "true", "false", "", "", ""); - } + service.activeScanUrl(); break; case activeScanSiteInScope: - checkForUrlParam(); - api.activeScanSiteInScope((String) params.get("url")); + service.activeScanSiteInScope(); break; case addExcludeRegexToContext: - checkForContextNameParam(); - checkForRegexParam(); - api.context.excludeFromContext( - (String) params.get("contextName"), (String) params.get("regex")); + service.excludeRegexFromContext(); break; case addIncludeRegexToContext: - checkForContextNameParam(); - checkForRegexParam(); - api.context.includeInContext( - (String) params.get("contextName"), (String) params.get("regex")); + service.includeRegexInContext(); break; case addIncludeOneMatchingNodeToContext: - checkForContextNameParam(); - checkForRegexParam(); - api.includeOneMatchingNodeInContext( - (String) params.get("contextName"), (String) params.get("regex")); + service.includeOneMatchingNode(); break; } - } catch (ConnectException e) { - System.out.println( - e.getMessage() + String.format(": zapaddr=%s, zapport=%d\n", zapaddr, zapport)); + } catch (Exception e) { + System.err.println(e.getMessage()); showHelp(); System.exit(1); } } - private void checkForRegexParam() { - if (params.get("regex") == null) { - System.out.println("No regex supplied\n"); + private void initialize(String[] args) { + if (args.length == 0) { showHelp(); System.exit(1); } - } - private void checkForContextNameParam() { - if (params.get("contextName") == null) { - System.out.println("No context name supplied\n"); + if ("help".equalsIgnoreCase(args[0])) { + if (args.length > 1) { + setTask(args[1]); + } showHelp(); - System.exit(1); + System.exit(0); } - } - private void checkForUrlParam() { - if (params.get("url") == null) { - System.out.println("No url supplied\n"); - showHelp(); - System.exit(1); - } - } + setTask(args[0]); - private void initialize(String[] args) { - if (args.length > 0) { - if (args[0].equalsIgnoreCase("help")) { + for (String arg : args) { + String[] pair = arg.split("=", 2); + if (pair.length != 2) { + continue; + } + + String key = pair[0]; + String value = pair[1]; + + if ("zapaddr".equalsIgnoreCase(key)) { + zapaddr = value; + } else if ("zapport".equalsIgnoreCase(key)) { try { - setTask(args[1]); - } catch (IndexOutOfBoundsException e) { + zapport = Integer.parseInt(value); + } catch (NumberFormatException e) { + System.out.println("Invalid zapport: " + value); showHelp(); System.exit(1); } - showHelp(); - System.exit(0); - } - setTask(args[0]); - for (String arg : args) { - String[] pair = arg.split("=", 2); - if (pair.length == 2) { - if (pair[0].equalsIgnoreCase("zapaddr")) { - zapaddr = pair[1]; - } else if (pair[0].equalsIgnoreCase("zapport")) { - try { - zapport = Integer.parseInt(pair[1]); - } catch (NumberFormatException e) { - System.out.println( - "Invalid value to zapport, must be in integer: " + pair[1]); - showHelp(); - System.exit(1); - } - } else if (pair[0].equalsIgnoreCase("debug") - && pair[1].equalsIgnoreCase("true")) { - debug = true; - } else if (pair[0].contains("File")) { - params.put(pair[0], new File(pair[1])); - } else { - params.put(pair[0], pair[1]); - } - } + } else if ("debug".equalsIgnoreCase(key) && "true".equalsIgnoreCase(value)) { + debug = true; + } else if (key.endsWith("File")) { + params.put(key, new File(value)); + } else { + params.put(key, value); } - } else { - showHelp(); - System.exit(1); } + api = new ClientApi(zapaddr, zapport, (String) params.get("apikey"), debug); } @@ -228,134 +149,28 @@ private void setTask(String arg) { try { task = Task.valueOf(arg); } catch (IllegalArgumentException e) { - System.out.println("Unknown Task: " + arg); + System.out.println("Unknown task: " + arg); showHelp(); System.exit(1); } } private void showHelp() { - String help = ""; - if (task == null) { - help = - "usage: java -jar zap-api.jar [args]\n\n" - + "Type 'java -jar zap-api.jar help ' for help on a specific subcommand.\n\n" - + "Available subcommands:\n" - + "\tstop\n" - + "\tcheckAlerts\n" - + "\tshowAlerts\n" - + "\tsaveSession\n" - + "\tnewSession\n"; - } else { - switch (task) { - case stop: - help = - "usage: stop [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar stop' \n\t\t" - + "Stop zap listening on default settings (localhost:8090)\n\t" - + "2. Type 'java -jar zap-api.jar stop zapaddr=192.168.1.1 apikey=1234' \n\t\t" - + "Stop zap listening on 192.168.1.1:8090\n\t" - + "3. Type 'java -jar zap-api.jar stop zapport=7080 apikey=1234' \n\t\t" - + "Stop zap listening on localhost:7080\n\t" - + "4. Type 'java -jar zap-api.jar stop zapaddr=192.168.1.1 zapport=7080 apikey=1234' \n\t\t" - + "Stop zap listening on 192.168.1.1:7080\n\n"; - break; - case checkAlerts: - help = - "usage: checkAlerts alertsFile={PATH} [outputFile={PATH}] [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples\n\t" - + "1. Type 'java -jar zap-api.jar checkAlerts alertsFile=\"C:\\Users\\me\\My Documents\\alerts.xml\"' \n\t\t" - + "Check alerts ignoring alerts from alertsFile, looking for required alerts from alertsFile, using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar checkAlerts alertsFile=\"C:\\Users\\me\\My Documents\\alerts.xml\" outputFile=\"C:\\Users\\me\\My Documents\\report.xml\"' \n\t\t" - + "Check alerts ignoring alerts from alertsFile, looking for required alerts from alertsFile. Outputting results to report.xml, using zap listening on localhost:8090\n\t" - + "3. Type 'java -jar zap-api.jar checkAlerts alertsFile=\"C:\\Users\\me\\My Documents\\alerts.xml\" outputFile=\"C:\\Users\\me\\My Documents\\report.xml\"' zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Check alerts ignoring alerts from alertsFile, looking for required alerts from alertsFile. Outputting results to report.xml, using zap listening on 192.168.1.1:7080\n" - + "Note: for paths containing spaces ensure path is enclosed in quotes\n\n"; - break; - case showAlerts: - help = - "usage: showAlerts [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar showAlerts' \n\t\t" - + "Show alerts, using zap listening on default settings (localhost:8090)\n\t" - + "2. Type 'java -jar zap-api.jar showAlerts zapaddr=192.168.1.1' \n\t\t" - + "Show alerts, using zap listening on 192.168.1.1:8090\n\t" - + "3. Type 'java -jar zap-api.jar showAlerts zapport=7080' \n\t\t" - + "Show alerts, using zap listening on localhost:7080\n\t" - + "4. Type 'java -jar zap-api.jar showAlerts zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Show alerts, using zap listening on 192.168.1.1:7080\n\n"; - break; - case saveSession: - help = - "usage: saveSession sessionName={PATH} [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar saveSession sessionName=\"Users/me/My Documents/mysession/mysessionfile\"' \n\t\t" - + "Save zap session using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar saveSession sessionName=\"Users/me/My Documents/mysession/mysessionfile\" zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Save zap session using zap listening on 192.168.1.1:7080\nNote: for paths containing spaces ensure path is enclosed in quotes\n\n"; - break; - case newSession: - help = - "usage: newSession [sessionName={PATH}] [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar newSession' \n\t\t" - + "Start new session using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar newSession zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Start new session using zap listening on 192.168.1.1:7080\n\t" - + "3. Type 'java -jar zap-api.jar newSession sessionName=\"Users/me/My Documents/mysession/newsession\"' \n\t\t" - + "Start new session using zap listening on localhost:8090, creating session files at /Users/me/My Documents/mysession/newsession\n\t" - + "4. Type 'java -jar zap-api.jar newSession sessionName=\"Users/me/My Documents/mysession/mysessionfile\" zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Start new session using zap listening on 192.168.1.1:7080, creating session files at /Users/me/My Documents/mysession/newsession\n" - + "Note: for paths containing spaces ensure path is enclosed in quotes"; - break; - case activeScanUrl: - help = - "usage: activeScanUrl url={url} [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar activeScanUrl url=http://myurl.com/' \n\t\t" - + "Execute an active scan on http://myurl.com/ using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar activeScanUrl url=http://myurl.com/' zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Execute an active scan on http://myurl.com/ using zap listening on 192.168.1.1:7080\n\t"; - break; - case activeScanSiteInScope: - help = - "usage: activeScanSiteInScope url={url} [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar activeScanSiteInScope url=http://example.com/' \n\t\t" - + "Execute an active scan for URLs in scope under http://example.com/ using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar activeScanSiteInScope url=http://example.com/' zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Execute an active scan for URLs in scope under http://example.com/ using zap listening on 192.168.1.1:7080\n\t"; - break; - case addExcludeRegexToContext: - help = - "usage: addExcludeRegexToContext contextName={contextName} regex={regex} [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar addExcludeRegexToContext contextName=1 regex=\\Qhttp://example.com/area\\E.* \n\t\t" - + "Urls that match the regex will be excluded from scope using context '1' using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar addExcludeRegexToContext url=http://myurl.com/' zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Urls that match the regex will be excluded from scope using context '1' using zap listening on 192.168.1.1:7080\n\t"; - break; - case addIncludeRegexToContext: - help = - "usage: addIncludeRegexToContext contextName={contextName} regex={regex} [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar addIncludeRegexToContext contextName=1 regex=\\Qhttp://example.com/area\\E.* \n\t\t" - + "Urls that match the regex will be included in scope using context '1' using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar addIncludeRegexToContext url=http://myurl.com/' zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "Urls that match the regex will be included in scope using context '1' using zap listening on 192.168.1.1:7080\n\t"; - break; - case addIncludeOneMatchingNodeToContext: - help = - "usage: addIncludeOneMatchingNodeToContext contextName={contextName} regex={regex} [zapaddr={ip}] [zapport={port}]\n\n" - + "Examples:\n\t" - + "1. Type 'java -jar zap-api.jar addIncludeOneMatchingNodeToContext contextName=1 regex=\\Qhttp://example.com/area\\E.* \n\t\t" - + "The first url from the current session that matches the regex will be included in scope using context '1'. Any other matching url will be excluded from scope using zap listening on localhost:8090\n\t" - + "2. Type 'java -jar zap-api.jar addIncludeOneMatchingNodeToContext url=http://myurl.com/' zapaddr=192.168.1.1 zapport=7080' \n\t\t" - + "The first url from the current session that matches the regex will be included in scope using context '1'. Any other matching url will be excluded from scope using context '1' using zap listening on 192.168.1.1:7080\n\t"; - break; - } - } + String help = + "usage: java -jar zap-api.jar [args]\n\n" + + "Type 'java -jar zap-api.jar help ' for help on a specific subcommand.\n\n" + + "Available subcommands:\n" + + "\tstop\n" + + "\tcheckAlerts\n" + + "\tshowAlerts\n" + + "\tsaveSession\n" + + "\tnewSession\n" + + "\tactiveScanUrl\n" + + "\tactiveScanSiteInScope\n" + + "\taddExcludeRegexToContext\n" + + "\taddIncludeRegexToContext\n" + + "\taddIncludeOneMatchingNodeToContext\n"; + System.out.println(help); } } diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/HttpRequest.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/HttpRequest.java new file mode 100644 index 0000000..0298bcf --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/HttpRequest.java @@ -0,0 +1,84 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.core; + +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * Licensed under the Apache License, Version 2.0 + */ + +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * A simple immutable HTTP request representation used internally by the Client API. + * + *

This class intentionally contains no networking logic. + */ +public final class HttpRequest { + + public static final String GET_METHOD = "GET"; + + private final String method; + private final URL requestUri; + private final Map headers; + private final String body; + + public HttpRequest(String method, URL requestUri, String body) { + this.method = method; + this.requestUri = requestUri; + this.body = body; + this.headers = new HashMap<>(); + } + + public String getMethod() { + return method; + } + + public URL getRequestUri() { + return requestUri; + } + + public String getBody() { + return body; + } + + /** + * Adds or replaces a request header. + * + * @param name header name + * @param value header value + */ + public void addHeader(String name, String value) { + headers.put(name, value); + } + + /** + * Returns an unmodifiable view of the request headers. + * + * @return headers map + */ + public Map getHeaders() { + return Collections.unmodifiableMap(headers); + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ZapRequestBuilder.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ZapRequestBuilder.java new file mode 100644 index 0000000..07366c4 --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/ZapRequestBuilder.java @@ -0,0 +1,128 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.core; + +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2011 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. + */ + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * Builds HTTP requests for the ZAP API. + * + *

This class is responsible only for request construction (URL, query parameters, body, + * headers). + * + *

No networking or I/O is performed here. + */ +final class ZapRequestBuilder { + + private static final String ZAP_API_HOST = "http://zap/"; + + private ZapRequestBuilder() { + // utility class + } + + static HttpRequest buildZapRequest( + String requestMethod, + String format, + String component, + String type, + String method, + Map params, + String apiKey) + throws MalformedURLException, URISyntaxException { + + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder + .append(ZAP_API_HOST) + .append(format) + .append('/') + .append(component) + .append('/') + .append(type) + .append('/') + .append(method) + .append('/'); + + String body = null; + + if (params != null && !params.isEmpty()) { + if (HttpRequest.GET_METHOD.equals(requestMethod)) { + urlBuilder.append('?'); + appendParams(params, urlBuilder); + } else { + body = appendParams(params, new StringBuilder()).toString(); + } + } + + HttpRequest request = + new HttpRequest(requestMethod, createUrl(urlBuilder.toString()), body); + + if (apiKey != null && !apiKey.isEmpty()) { + request.addHeader("X-ZAP-API-Key", apiKey); + } + + return request; + } + + private static URL createUrl(String value) throws MalformedURLException, URISyntaxException { + return new URI(value).toURL(); + } + + private static StringBuilder appendParams(Map params, StringBuilder sb) { + + for (Map.Entry p : params.entrySet()) { + sb.append(encode(p.getKey())).append('='); + if (p.getValue() != null) { + sb.append(encode(p.getValue())); + } + sb.append('&'); + } + return sb; + } + + private static String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/impl/DefaultAlertService.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/impl/DefaultAlertService.java new file mode 100644 index 0000000..bf0d367 --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/impl/DefaultAlertService.java @@ -0,0 +1,118 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.impl; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.zaproxy.clientapi.core.*; +import org.zaproxy.clientapi.service.AlertService; + +public final class DefaultAlertService implements AlertService { + private static final boolean DEBUG = false; + private static final PrintStream DEBUG_STREAM = System.out; + + @SuppressWarnings("UnusedVariable") + private final org.zaproxy.clientapi.gen.Alert api; + + public DefaultAlertService(org.zaproxy.clientapi.gen.Alert api) { + this.api = api; + } + + @Override + public Map> checkAlerts( + List ignoreAlerts, + List requireAlerts, + org.zaproxy.clientapi.gen.Alert alert) + throws ClientApiException { + List reportAlerts = new ArrayList<>(); + List ignoredAlerts = new ArrayList<>(); + List alerts = AlertService.getAlerts(null, -1, -1, alert); + for (Alert alertCore : alerts) { + boolean ignore = false; + if (ignoreAlerts != null) { + for (Alert ignoreAlert : ignoreAlerts) { + if (alertCore.matches(ignoreAlert)) { + if (DEBUG) { + DEBUG_STREAM.println("Ignoring alert " + ignoreAlert); + } + ignoredAlerts.add(alertCore); + ignore = true; + break; + } + } + } + if (!ignore) { + reportAlerts.add(alertCore); + } + if (requireAlerts != null) { + for (Alert requireAlert : requireAlerts) { + if (alertCore.matches(requireAlert)) { + if (DEBUG) { + DEBUG_STREAM.println("Found alert " + alert); + } + requireAlerts.remove(requireAlert); + // Remove it from the not-ignored list as well + reportAlerts.remove(alertCore); + break; + } + } + } + } + HashMap> results = new HashMap<>(); + results.put("reportAlerts", reportAlerts); + results.put("requireAlerts", requireAlerts); + results.put("ignoredAlerts", ignoredAlerts); + return results; + } + + @Override + public void verifyAlerts(List requireAlerts, List reportAlerts) + throws ClientApiException { + StringBuilder sb = new StringBuilder(); + if (!reportAlerts.isEmpty()) { + sb.append("Found ").append(reportAlerts.size()).append(" alerts\n"); + for (Alert alert : reportAlerts) { + sb.append('\t'); + sb.append(alert.toString()); + sb.append('\n'); + } + } + if (requireAlerts != null && !requireAlerts.isEmpty()) { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append("Not found ").append(requireAlerts.size()).append(" alerts\n"); + for (Alert alert : requireAlerts) { + sb.append('\t'); + sb.append(alert.toString()); + sb.append('\n'); + } + } + if (sb.length() > 0) { + if (DEBUG) { + DEBUG_STREAM.println("Failed: " + sb.toString()); + } + throw new ClientApiException(sb.toString()); + } + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/AlertWorkflow.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/AlertWorkflow.java new file mode 100644 index 0000000..fcbdf5e --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/AlertWorkflow.java @@ -0,0 +1,79 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.internal; + +import java.io.File; +import java.util.List; +import java.util.Map; +import org.zaproxy.clientapi.core.Alert; +import org.zaproxy.clientapi.core.AlertsFile; +import org.zaproxy.clientapi.core.ClientApi; +import org.zaproxy.clientapi.core.ClientApiException; +import org.zaproxy.clientapi.service.AlertService; + +public final class AlertWorkflow { + + private AlertWorkflow() { + // utility class + } + + public static void checkAlerts( + ClientApi api, + AlertService alertService, + List ignoreAlerts, + List requireAlerts) + throws ClientApiException { + + Map> results = + alertService.checkAlerts(ignoreAlerts, requireAlerts, api.alert); + + alertService.verifyAlerts(results.get("requireAlerts"), results.get("reportAlerts")); + } + + public static void checkAlerts( + ClientApi api, + AlertService alertService, + List ignoreAlerts, + List requireAlerts, + File outputFile) + throws ClientApiException { + + Map> results = + alertService.checkAlerts(ignoreAlerts, requireAlerts, api.alert); + + try { + AlertsFile.saveAlertsToFile( + results.get("requireAlerts"), + results.get("reportAlerts"), + results.get("ignoredAlerts"), + outputFile); + } catch (Exception e) { + throw new ClientApiException("Failed to save the alerts.", e); + } + + int alertsFound = results.get("reportAlerts").size(); + int alertsMissing = results.get("requireAlerts").size(); + + if (alertsFound > 0 || alertsMissing > 0) { + throw new ClientApiException( + "Check Alerts Failed! Found=" + alertsFound + ", Missing=" + alertsMissing); + } + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/ContextWorkflow.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/ContextWorkflow.java new file mode 100644 index 0000000..6700eb9 --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/ContextWorkflow.java @@ -0,0 +1,71 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.internal; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import org.zaproxy.clientapi.core.ApiResponse; +import org.zaproxy.clientapi.core.ApiResponseElement; +import org.zaproxy.clientapi.core.ApiResponseList; +import org.zaproxy.clientapi.core.ClientApi; + +public final class ContextWorkflow { + + private ContextWorkflow() { + // utility class + } + + public static void includeOneMatchingNode(ClientApi api, String contextName, String regex) + throws Exception { + + List sessionUrls = getSessionUrls(api); + boolean foundOne = false; + + for (String sessionUrl : sessionUrls) { + if (sessionUrl.matches(regex)) { + if (foundOne) { + api.context.excludeFromContext(contextName, regex); + } else { + foundOne = true; + } + } + } + + if (!foundOne) { + throw new Exception("Unexpected result: No url found matching regex " + regex); + } + } + + private static List getSessionUrls(ClientApi api) throws Exception { + List urls = new ArrayList<>(); + ApiResponse response = api.core.urls(); + + if (response instanceof ApiResponseList) { + for (ApiResponse element : ((ApiResponseList) response).getItems()) { + + String value = ((ApiResponseElement) element).getValue(); + URL url = new URL(value); + urls.add(url.getProtocol() + "://" + url.getHost() + url.getPath()); + } + } + return urls; + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/FilesWorkflow.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/FilesWorkflow.java new file mode 100644 index 0000000..5dd62d9 --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/FilesWorkflow.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. + * + * Copyright 2025 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 org.zaproxy.clientapi.internal; + +import java.io.IOException; +import java.io.InputStream; + +public final class FilesWorkflow { + + private FilesWorkflow() { + // utility class + } + + public static byte[] readAllBytes(InputStream in) throws IOException { + byte[] buffer = new byte[8 * 1024]; + int read; + try (in) { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/ScanWorkflow.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/ScanWorkflow.java new file mode 100644 index 0000000..15c53e5 --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/internal/ScanWorkflow.java @@ -0,0 +1,48 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.internal; + +import org.zaproxy.clientapi.core.ApiResponseElement; +import org.zaproxy.clientapi.core.ClientApi; +import org.zaproxy.clientapi.core.ClientApiException; + +public final class ScanWorkflow { + + private ScanWorkflow() { + // utility class + } + + public static void activeScanSiteInScope(ClientApi api, String url) throws Exception { + api.ascan.scan(url, "true", "true", "", "", ""); + waitForScanCompletion(api); + } + + private static void waitForScanCompletion(ClientApi api) throws ClientApiException { + int progress = 0; + while (progress < 100) { + progress = Integer.parseInt(((ApiResponseElement) api.ascan.status("")).getValue()); + try { + Thread.sleep(1000); + } catch (InterruptedException ignore) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/service/AlertService.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/service/AlertService.java new file mode 100644 index 0000000..b19ff00 --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/service/AlertService.java @@ -0,0 +1,52 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.zaproxy.clientapi.core.*; + +public interface AlertService { + + Map> checkAlerts( + List ignoreAlerts, + List requireAlerts, + org.zaproxy.clientapi.gen.Alert alert) + throws ClientApiException; + + void verifyAlerts(List requireAlerts, List reportAlerts) + throws ClientApiException; + + static List getAlerts( + String baseUrl, int start, int count, org.zaproxy.clientapi.gen.Alert alert) + throws ClientApiException { + List alerts = new ArrayList<>(); + ApiResponse response = + alert.alerts(baseUrl, String.valueOf(start), String.valueOf(count), null); + if (response instanceof ApiResponseList) { + ApiResponseList alertList = (ApiResponseList) response; + for (ApiResponse resp : alertList.getItems()) { + alerts.add(new Alert((ApiResponseSet) resp)); + } + } + return alerts; + } +} diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/service/ClientApiTaskService.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/service/ClientApiTaskService.java new file mode 100644 index 0000000..c576697 --- /dev/null +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/service/ClientApiTaskService.java @@ -0,0 +1,115 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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 org.zaproxy.clientapi.service; + +import java.io.File; +import java.util.List; +import java.util.Map; +import org.zaproxy.clientapi.core.Alert; +import org.zaproxy.clientapi.core.AlertsFile; +import org.zaproxy.clientapi.core.ClientApi; + +public class ClientApiTaskService { + + private final ClientApi api; + private final Map params; + + public ClientApiTaskService(ClientApi api, Map params) { + this.api = api; + this.params = params; + } + + public void stop() throws Exception { + api.core.shutdown(); + } + + public void checkAlerts() throws Exception { + File alertsFile = requireFile("alertsFile"); + if (params.get("outputFile") == null) { + api.checkAlerts( + AlertsFile.getAlertsFromFile(alertsFile, "ignoreAlert"), + AlertsFile.getAlertsFromFile(alertsFile, "requireAlert")); + } else { + File outFile = (File) params.get("outputFile"); + api.checkAlerts( + AlertsFile.getAlertsFromFile(alertsFile, "ignoreAlert"), + AlertsFile.getAlertsFromFile(alertsFile, "requireAlert"), + outFile); + } + } + + public void showAlerts() throws Exception { + List alerts = AlertService.getAlerts(null, -1, -1, null); + for (Alert alert : alerts) { + System.out.println(alert); + } + } + + public void activeScanUrl() throws Exception { + api.ascan.scan(requireString("url"), "true", "false", "", "", ""); + } + + public void activeScanSiteInScope() throws Exception { + api.activeScanSiteInScope(requireString("url")); + } + + public void excludeRegexFromContext() throws Exception { + api.context.excludeFromContext(requireString("contextName"), requireString("regex")); + } + + public void includeRegexInContext() throws Exception { + api.context.includeInContext(requireString("contextName"), requireString("regex")); + } + + public void includeOneMatchingNode() throws Exception { + api.includeOneMatchingNodeInContext(requireString("contextName"), requireString("regex")); + } + + public void saveSession() throws Exception { + api.core.saveSession(requireString("sessionName"), "true"); + } + + public void newSession() throws Exception { + api.core.newSession( + params.containsKey("sessionName") ? requireString("sessionName") : "", "true"); + } + + /* -------- helpers (Java 11 safe) -------- */ + + private String requireString(String key) { + Object v = params.get(key); + if (v == null) { + throw new IllegalArgumentException("Missing parameter: " + key); + } + return v.toString(); + } + + private File requireFile(String key) { + Object v = params.get(key); + if (!(v instanceof File)) { + throw new IllegalArgumentException("Missing or invalid file: " + key); + } + File f = (File) v; + if (!f.exists()) { + throw new IllegalArgumentException("File not found: " + f.getAbsolutePath()); + } + return f; + } +} diff --git a/subprojects/zap-clientapi/zap-clientapi.gradle b/subprojects/zap-clientapi/zap-clientapi.gradle index 8fa565e..bac2a0e 100644 --- a/subprojects/zap-clientapi/zap-clientapi.gradle +++ b/subprojects/zap-clientapi/zap-clientapi.gradle @@ -9,9 +9,6 @@ sourceSets { examples } assemble.dependsOn examplesClasses dependencies { - // XXX Change to implementation (it's not exposed in public API) when bumping major version. - api 'org.jdom:jdom:1.1.3' - examplesImplementation sourceSets.main.output } @@ -53,8 +50,11 @@ task japicmp(type: me.champeau.gradle.japicmp.JapicmpTask) { addDefaultRules.set(true) } } - -check.dependsOn(japicmp) +// TODO: Re-enable japicmp after releasing 2.0.0 and +// set versionBC = "2.0.0" as the new binary compatibility baseline. +tasks.named("japicmp") { + onlyIf { false } +} task javadocJar(type: Jar) { archiveClassifier = 'javadoc'