From d8a916ea721288d09a07f17bc86d1e049e36d1d9 Mon Sep 17 00:00:00 2001 From: kingthorin Date: Wed, 23 Oct 2024 14:01:12 -0400 Subject: [PATCH] graphql: Add tech detection for GraphQL Engines - CHANGELOG > Added note. - build file > Added dependency. - GraphQlFingerprinter > Updated to add Technology matches when GraphQL engines are fingerprinted. - ExtensionTechDetection > Added to facilitate optional dependence. - ExtensionTechDetectionUnitTest > Tests :) - Messages.properties > Added key/value pairs to support the optional dependency. - alert.html > Added note about the new functionality. Signed-off-by: kingthorin --- addOns/graphql/CHANGELOG.md | 3 + addOns/graphql/graphql.gradle.kts | 14 +++ .../addon/graphql/ExtensionGraphQl.java | 3 +- .../addon/graphql/GraphQlFingerprinter.java | 60 ++++++++-- .../ExtensionTechDetectionGraphQl.java | 103 ++++++++++++++++++ .../resources/help/contents/alerts.html | 3 +- .../graphql/resources/Messages.properties | 3 + .../ExtensionTechDetectionUnitTest.java | 57 ++++++++++ .../wappalyzer/ExtensionWappalyzer.java | 11 ++ 9 files changed, 247 insertions(+), 10 deletions(-) create mode 100644 addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionGraphQl.java create mode 100644 addOns/graphql/src/test/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionUnitTest.java diff --git a/addOns/graphql/CHANGELOG.md b/addOns/graphql/CHANGELOG.md index 8013ca23e99..3915d4a0e6b 100644 --- a/addOns/graphql/CHANGELOG.md +++ b/addOns/graphql/CHANGELOG.md @@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Changed - Maintenance changes. +### Added +- If the Tech Detection (Wappalyzer) add-on is installed and a GraphQL engine is successfully finger printed it is added to the Technology tab/data. + ## [0.25.0] - 2024-09-24 ### Changed - Dependency updates. diff --git a/addOns/graphql/graphql.gradle.kts b/addOns/graphql/graphql.gradle.kts index 2fef61098db..9559566e97e 100644 --- a/addOns/graphql/graphql.gradle.kts +++ b/addOns/graphql/graphql.gradle.kts @@ -39,6 +39,19 @@ zapAddOn { } } } + + register("org.zaproxy.addon.graphql.techdetection.ExtensionTechDetection") { + classnames { + allowed.set(listOf("org.zaproxy.addon.graphql.techdetection")) + } + dependencies { + addOns { + register("wappalyzer") { + version.set(">= 21.43.0") + } + } + } + } } } @@ -61,6 +74,7 @@ dependencies { zapAddOn("automation") zapAddOn("commonlib") zapAddOn("spider") + zapAddOn("wappalyzer") implementation("com.graphql-java:graphql-java:22.3") diff --git a/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/ExtensionGraphQl.java b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/ExtensionGraphQl.java index 5efeead4637..21fefc326c2 100644 --- a/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/ExtensionGraphQl.java +++ b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/ExtensionGraphQl.java @@ -41,6 +41,7 @@ import org.parosproxy.paros.model.Session; import org.parosproxy.paros.network.HttpSender; import org.zaproxy.addon.commonlib.ExtensionCommonlib; +import org.zaproxy.addon.graphql.GraphQlFingerprinter.Engine; import org.zaproxy.zap.extension.alert.ExampleAlertProvider; import org.zaproxy.zap.extension.script.ExtensionScript; import org.zaproxy.zap.model.ValueGenerator; @@ -297,6 +298,6 @@ public boolean handleFile(File file) { public List getExampleAlerts() { return List.of( GraphQlParser.createIntrospectionAlert().build(), - GraphQlFingerprinter.createFingerprintingAlert("example").build()); + GraphQlFingerprinter.createFingerprintingAlert(new Engine("example")).build()); } } diff --git a/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlFingerprinter.java b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlFingerprinter.java index 93536419f75..75fdd7cc388 100644 --- a/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlFingerprinter.java +++ b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlFingerprinter.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import org.apache.commons.httpclient.URI; import org.apache.logging.log4j.LogManager; @@ -44,12 +45,14 @@ public class GraphQlFingerprinter { CommonAlertTag.toMap(CommonAlertTag.WSTG_V42_INFO_02_FINGERPRINT_WEB_SERVER); private static final Logger LOGGER = LogManager.getLogger(GraphQlFingerprinter.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final BiConsumer DEFAULT_APP_CONSUMER = (site, app) -> {}; private final Requestor requestor; private final Map queryCache; private HttpMessage lastQueryMsg; private String matchedString; + private static BiConsumer appConsumer = DEFAULT_APP_CONSUMER; public GraphQlFingerprinter(URI endpointUrl) { requestor = new Requestor(endpointUrl, HttpSender.MANUAL_REQUEST_INITIATOR); @@ -94,7 +97,9 @@ public void fingerprint() { for (var fingerprinter : fingerprinters.entrySet()) { try { if (fingerprinter.getValue().getAsBoolean()) { - raiseFingerprintingAlert(fingerprinter.getKey()); + Engine engine = new Engine(fingerprinter.getKey()); + raiseFingerprintingAlert(engine); + tryConsumingTechDetection(lastQueryMsg.getRequestHeader().getURI(), engine); break; } } catch (Exception e) { @@ -104,6 +109,14 @@ public void fingerprint() { queryCache.clear(); } + private static void tryConsumingTechDetection(URI uri, Engine engine) { + try { + appConsumer.accept(uri, engine); + } catch (Exception ex) { + LOGGER.warn("Unable to add consume: {}", engine.getName()); + } + } + void sendQuery(String query) { lastQueryMsg = queryCache.computeIfAbsent( @@ -146,8 +159,7 @@ boolean errorContains(String substring, String errorField) { return false; } - static Alert.Builder createFingerprintingAlert(String engineId) { - final String enginePrefix = "graphql.engine." + engineId + "."; + static Alert.Builder createFingerprintingAlert(Engine engine) { return Alert.builder() .setPluginId(ExtensionGraphQl.TOOL_ALERT_ID) .setAlertRef(FINGERPRINTING_ALERT_REF) @@ -155,9 +167,9 @@ static Alert.Builder createFingerprintingAlert(String engineId) { .setDescription( Constant.messages.getString( "graphql.fingerprinting.alert.desc", - Constant.messages.getString(enginePrefix + "name"), - Constant.messages.getString(enginePrefix + "technologies"))) - .setReference(Constant.messages.getString(enginePrefix + "docsUrl")) + engine.getName(), + engine.getTechnologies())) + .setReference(engine.getDocsUrl()) .setConfidence(Alert.CONFIDENCE_HIGH) .setRisk(Alert.RISK_INFO) .setCweId(205) @@ -166,14 +178,14 @@ static Alert.Builder createFingerprintingAlert(String engineId) { .setTags(FINGERPRINTING_ALERT_TAGS); } - private void raiseFingerprintingAlert(String engineId) { + private void raiseFingerprintingAlert(Engine engine) { var extAlert = Control.getSingleton().getExtensionLoader().getExtension(ExtensionAlert.class); if (extAlert == null) { return; } Alert alert = - createFingerprintingAlert(engineId) + createFingerprintingAlert(engine) .setEvidence(matchedString) .setMessage(lastQueryMsg) .setUri(requestor.getEndpointUrl().toString()) @@ -576,4 +588,36 @@ private boolean checkWpGraphQlEngine() { } return false; } + + public static void setAppConsumer(BiConsumer consumer) { + appConsumer = consumer == null ? DEFAULT_APP_CONSUMER : consumer; + } + + public static class Engine { + private static final String PREFIX = "graphql.engine."; + private String enginePrefix; + private String name; + private String docsUrl; + private String technologies; + + public Engine(String engineId) { + this.enginePrefix = PREFIX + engineId + "."; + + this.name = Constant.messages.getString(enginePrefix + "name"); + this.docsUrl = Constant.messages.getString(enginePrefix + "docsUrl"); + this.technologies = Constant.messages.getString(enginePrefix + "technologies"); + } + + public String getName() { + return name; + } + + public String getDocsUrl() { + return docsUrl; + } + + public String getTechnologies() { + return technologies; + } + } } diff --git a/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionGraphQl.java b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionGraphQl.java new file mode 100644 index 00000000000..d13cc3a5302 --- /dev/null +++ b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionGraphQl.java @@ -0,0 +1,103 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2024 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.addon.graphql.techdetection; + +import java.util.List; +import org.apache.commons.httpclient.URI; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.extension.Extension; +import org.parosproxy.paros.extension.ExtensionAdaptor; +import org.parosproxy.paros.extension.ExtensionHook; +import org.zaproxy.addon.graphql.GraphQlFingerprinter; +import org.zaproxy.addon.graphql.GraphQlFingerprinter.Engine; +import org.zaproxy.zap.extension.wappalyzer.Application; +import org.zaproxy.zap.extension.wappalyzer.ApplicationMatch; +import org.zaproxy.zap.extension.wappalyzer.ExtensionWappalyzer; + +public class ExtensionTechDetectionGraphQl extends ExtensionAdaptor { + + public static final String NAME = "ExtensionTechDetectionGraphQl"; + + private static final List> DEPENDENCIES = + List.of(ExtensionWappalyzer.class); + + private static ExtensionWappalyzer extTech; + + public ExtensionTechDetectionGraphQl() { + super(NAME); + } + + @Override + public String getUIName() { + return Constant.messages.getString("graphql.techdetection.name"); + } + + @Override + public String getDescription() { + return Constant.messages.getString("graphql.techdetection.desc"); + } + + @Override + public List> getDependencies() { + return DEPENDENCIES; + } + + @Override + public void hook(ExtensionHook extensionHook) { + super.hook(extensionHook); + GraphQlFingerprinter.setAppConsumer( + (site, engine) -> ExtensionTechDetectionGraphQl.addApp(site, engine)); + } + + @Override + public boolean canUnload() { + return true; + } + + @Override + public void unload() { + GraphQlFingerprinter.setAppConsumer(null); + } + + private static ApplicationMatch getAppForEngine(Engine engine) { + Application gqlEgine = new Application(); + gqlEgine.setName(engine.getName()); + gqlEgine.setCategories(List.of("GraphQL Engine")); + gqlEgine.setWebsite(engine.getDocsUrl()); + gqlEgine.setImplies(List.of(engine.getTechnologies())); + + return new ApplicationMatch(gqlEgine); + } + + private static void addApp(URI uri, Engine engine) { + getExtTech().addApplicationsToSite(uri, getAppForEngine(engine)); + } + + private static ExtensionWappalyzer getExtTech() { + if (extTech == null) { + extTech = + Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionWappalyzer.class); + } + return extTech; + } +} diff --git a/addOns/graphql/src/main/javahelp/org/zaproxy/addon/graphql/resources/help/contents/alerts.html b/addOns/graphql/src/main/javahelp/org/zaproxy/addon/graphql/resources/help/contents/alerts.html index 66af478d761..85c660d0101 100644 --- a/addOns/graphql/src/main/javahelp/org/zaproxy/addon/graphql/resources/help/contents/alerts.html +++ b/addOns/graphql/src/main/javahelp/org/zaproxy/addon/graphql/resources/help/contents/alerts.html @@ -22,7 +22,8 @@

GraphQL Alerts

50007-2 GraphQL Server Implementation Identified This alert is raised when the GraphQL implementation used by the server is identified. It utilises - fingerprinting techniques adapted from the tool graphw00f. + fingerprinting techniques adapted from the tool graphw00f.
+ Note: If the Tech Detection (Wappalyzer) add-on is installed the finger printer will also add identified GraphQL Engines to the Technology tab/data. GraphQlFingerprinter.java diff --git a/addOns/graphql/src/main/resources/org/zaproxy/addon/graphql/resources/Messages.properties b/addOns/graphql/src/main/resources/org/zaproxy/addon/graphql/resources/Messages.properties index 8071f47c4c9..8733fd20139 100644 --- a/addOns/graphql/src/main/resources/org/zaproxy/addon/graphql/resources/Messages.properties +++ b/addOns/graphql/src/main/resources/org/zaproxy/addon/graphql/resources/Messages.properties @@ -239,5 +239,8 @@ graphql.options.value.split.rootField = Each Field of an Operation graphql.spider.desc = GraphQL Spider Integration graphql.spider.name = GraphQL Spider +graphql.techdetection.desc = GraphQL Technology Detection Integration +graphql.techdetection.name = GraphQL Tech Detection + graphql.topmenu.import.importgraphql = Import a GraphQL Schema graphql.topmenu.import.importgraphql.tooltip = Specify a GraphQL endpoint and optionally a GraphQL schema file to import. diff --git a/addOns/graphql/src/test/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionUnitTest.java b/addOns/graphql/src/test/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionUnitTest.java new file mode 100644 index 00000000000..741431d7035 --- /dev/null +++ b/addOns/graphql/src/test/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionUnitTest.java @@ -0,0 +1,57 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2024 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.addon.graphql.techdetection; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; +import org.zaproxy.addon.graphql.ExtensionGraphQl; +import org.zaproxy.zap.testutils.TestUtils; + +class ExtensionTechDetectionUnitTest extends TestUtils { + + @Test + void shouldHaveName() { + // Given + ExtensionTechDetectionGraphQl td = new ExtensionTechDetectionGraphQl(); + mockMessages(new ExtensionGraphQl()); + // When / Then + assertThat(td.getName(), is(equalTo("ExtensionTechDetectionGraphQl"))); + } + + @Test + void shouldHaveUiName() { + // Given + ExtensionTechDetectionGraphQl td = new ExtensionTechDetectionGraphQl(); + mockMessages(new ExtensionGraphQl()); + // When / Then + assertThat(td.getUIName(), is(equalTo("GraphQL Tech Detection"))); + } + + @Test + void shouldBeUnloadable() { + // Given + ExtensionTechDetectionGraphQl td = new ExtensionTechDetectionGraphQl(); + // When / Then + assertThat(td.canUnload(), is(equalTo(true))); + } +} diff --git a/addOns/wappalyzer/src/main/java/org/zaproxy/zap/extension/wappalyzer/ExtensionWappalyzer.java b/addOns/wappalyzer/src/main/java/org/zaproxy/zap/extension/wappalyzer/ExtensionWappalyzer.java index 7399e98d217..3a16d19dcfd 100644 --- a/addOns/wappalyzer/src/main/java/org/zaproxy/zap/extension/wappalyzer/ExtensionWappalyzer.java +++ b/addOns/wappalyzer/src/main/java/org/zaproxy/zap/extension/wappalyzer/ExtensionWappalyzer.java @@ -294,6 +294,17 @@ public void addApplicationsToSite(String site, ApplicationMatch applicationMatch } } + /** + * Accept an {@code URI} which will be normalized into a site string usable by the Tech + * Detection add-on and adds the provided {@code ApplicationMatch} + * + * @param uri The URI to be normalized and used + * @param applicationMatch the ApplicationMatch for the tech to be added + */ + public void addApplicationsToSite(URI uri, ApplicationMatch applicationMatch) { + this.addApplicationsToSite(normalizeSite(uri), applicationMatch); + } + public Application getSelectedApp() { if (hasView()) { String appName = this.getTechPanel().getSelectedApplicationName();