From 98ca05d4d13cf751fb7392d2f99387b7f41b4874 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. Signed-off-by: kingthorin --- addOns/graphql/CHANGELOG.md | 3 +- addOns/graphql/graphql.gradle.kts | 14 +++ .../addon/graphql/GraphQlFingerprinter.java | 10 ++ .../techdetection/ExtensionTechDetection.java | 113 ++++++++++++++++++ .../graphql/resources/Messages.properties | 3 + .../ExtensionTechDetectionUnitTest.java | 57 +++++++++ 6 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetection.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 670b12d040d..f8d96381182 100644 --- a/addOns/graphql/CHANGELOG.md +++ b/addOns/graphql/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - +### 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 diff --git a/addOns/graphql/graphql.gradle.kts b/addOns/graphql/graphql.gradle.kts index 2fef61098db..53ec60fdf18 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.42.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/GraphQlFingerprinter.java b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlFingerprinter.java index 93536419f75..782928ea089 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); @@ -95,6 +98,9 @@ public void fingerprint() { try { if (fingerprinter.getValue().getAsBoolean()) { raiseFingerprintingAlert(fingerprinter.getKey()); + + appConsumer.accept( + lastQueryMsg.getRequestHeader().getURI(), fingerprinter.getKey()); break; } } catch (Exception e) { @@ -576,4 +582,8 @@ private boolean checkWpGraphQlEngine() { } return false; } + + public static void setAppConsumer(BiConsumer consumer) { + appConsumer = consumer == null ? DEFAULT_APP_CONSUMER : consumer; + } } diff --git a/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetection.java b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetection.java new file mode 100644 index 00000000000..4b5f64123bb --- /dev/null +++ b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetection.java @@ -0,0 +1,113 @@ +/* + * 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.apache.commons.httpclient.URIException; +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.zap.extension.wappalyzer.Application; +import org.zaproxy.zap.extension.wappalyzer.ApplicationMatch; +import org.zaproxy.zap.extension.wappalyzer.ExtensionWappalyzer; +import org.zaproxy.zap.view.ScanPanel; + +public class ExtensionTechDetection extends ExtensionAdaptor { + + public static final String NAME = "ExtensionTechDetectionGraphQl"; + + private static final List> DEPENDENCIES = + List.of(ExtensionWappalyzer.class); + + private static ExtensionWappalyzer extTech; + + public ExtensionTechDetection() { + 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(this::addApp); + } + + @Override + public boolean canUnload() { + return true; + } + + @Override + public void unload() { + // Nothing to do + } + + private static String normalizeSite(URI uri) { + String lead = uri.getScheme() + "://"; + try { + return lead + uri.getAuthority(); + } catch (URIException e) { + // Shouldn't happen, but sure fallback + return ScanPanel.cleanSiteName(uri.toString(), true); + } + } + + private static ApplicationMatch getAppForEngine(String engineId) { + final String enginePrefix = "graphql.engine." + engineId + "."; + + Application engine = new Application(); + engine.setName(Constant.messages.getString(enginePrefix + "name")); + engine.setCategories(List.of("GraphQL Engine")); + engine.setWebsite(Constant.messages.getString(enginePrefix + "docsUrl")); + engine.setImplies(List.of(Constant.messages.getString(enginePrefix + "technologies"))); + + return new ApplicationMatch(engine); + } + + public void addApp(URI uri, String engineId) { + getExtTech().addApplicationsToSite(normalizeSite(uri), getAppForEngine(engineId)); + } + + private static ExtensionWappalyzer getExtTech() { + if (extTech == null) { + extTech = Control.getSingleton().getExtensionLoader() + .getExtension(ExtensionWappalyzer.class); + } + return extTech; + } +} 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..c6a51a23f66 --- /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 + ExtensionTechDetection td = new ExtensionTechDetection(); + mockMessages(new ExtensionGraphQl()); + // When / Then + assertThat(td.getName(), is(equalTo("ExtensionTechDetectionGraphQl"))); + } + + @Test + void shouldHaveUiName() { + // Given + ExtensionTechDetection td = new ExtensionTechDetection(); + mockMessages(new ExtensionGraphQl()); + // When / Then + assertThat(td.getUIName(), is(equalTo("GraphQL Tech Detection"))); + } + + @Test + void shouldBeUnloadable() { + // Given + ExtensionTechDetection td = new ExtensionTechDetection(); + // When / Then + assertThat(td.canUnload(), is(equalTo(true))); + } +}