diff --git a/addOns/graphql/CHANGELOG.md b/addOns/graphql/CHANGELOG.md index 61c64ec061e..60504523e5e 100644 --- a/addOns/graphql/CHANGELOG.md +++ b/addOns/graphql/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - tailcall - Hot Chocolate - Support for importing an introspection query response from a file (Issue 8569). +- 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 466a10fb47c..85870264334 100644 --- a/addOns/graphql/graphql.gradle.kts +++ b/addOns/graphql/graphql.gradle.kts @@ -39,6 +39,19 @@ zapAddOn { } } } + + register("org.zaproxy.addon.graphql.techdetection.ExtensionTechDetectionGraphQl") { + 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/DiscoveredGraphQlEngineHandler.java b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/DiscoveredGraphQlEngineHandler.java new file mode 100644 index 00000000000..83347f9f692 --- /dev/null +++ b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/DiscoveredGraphQlEngineHandler.java @@ -0,0 +1,27 @@ +/* + * 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; + +import org.zaproxy.addon.graphql.GraphQlFingerprinter.DiscoveredGraphQlEngine; + +public interface DiscoveredGraphQlEngineHandler { + + void process(DiscoveredGraphQlEngine engine); +} 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 1ef19de26a2..eb871d137ad 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 @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -42,6 +43,7 @@ import org.parosproxy.paros.network.HttpSender; import org.zaproxy.addon.commonlib.ExtensionCommonlib; import org.zaproxy.addon.commonlib.ValueProvider; +import org.zaproxy.addon.graphql.GraphQlFingerprinter.DiscoveredGraphQlEngine; import org.zaproxy.zap.extension.alert.ExampleAlertProvider; import org.zaproxy.zap.extension.script.ExtensionScript; import org.zaproxy.zap.view.ZapMenuItem; @@ -295,8 +297,16 @@ public boolean handleFile(File file) { @Override public List getExampleAlerts() { + URI uri = null; + try { + uri = new URI("https://example.com", false); + } catch (URIException | NullPointerException e) { + // Ignore + } return List.of( GraphQlParser.createIntrospectionAlert().build(), - GraphQlFingerprinter.createFingerprintingAlert("example").build()); + GraphQlFingerprinter.createFingerprintingAlert( + new DiscoveredGraphQlEngine("example", uri)) + .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 2648ce7e4f4..6e73c730e22 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 @@ -22,8 +22,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.function.BooleanSupplier; import org.apache.commons.httpclient.URI; @@ -45,6 +47,11 @@ public class GraphQlFingerprinter { private static final Logger LOGGER = LogManager.getLogger(GraphQlFingerprinter.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final DiscoveredGraphQlEngineHandler defaultEngineHandler = + (engine -> raiseFingerprintingAlert(engine)); + + private static List handlers; + private final Requestor requestor; private final Map queryCache; @@ -54,6 +61,7 @@ public class GraphQlFingerprinter { public GraphQlFingerprinter(URI endpointUrl) { requestor = new Requestor(endpointUrl, HttpSender.MANUAL_REQUEST_INITIATOR); queryCache = new HashMap<>(); + addEngineHandler(this::raiseFingerprintingAlert); } public void fingerprint() { @@ -97,7 +105,11 @@ public void fingerprint() { for (var fingerprinter : fingerprinters.entrySet()) { try { if (fingerprinter.getValue().getAsBoolean()) { - raiseFingerprintingAlert(fingerprinter.getKey()); + DiscoveredGraphQlEngine discoveredGraphQlEngine = + new DiscoveredGraphQlEngine( + fingerprinter.getKey(), + lastQueryMsg.getRequestHeader().getURI()); + handleDetectedEngine(discoveredGraphQlEngine); break; } } catch (Exception e) { @@ -107,6 +119,16 @@ public void fingerprint() { queryCache.clear(); } + private static void handleDetectedEngine(DiscoveredGraphQlEngine discoveredGraphQlEngine) { + for (DiscoveredGraphQlEngineHandler handler : handlers) { + try { + handler.process(discoveredGraphQlEngine); + } catch (Exception ex) { + LOGGER.warn("Unable to handle: {}", discoveredGraphQlEngine.getName()); + } + } + } + void sendQuery(String query) { lastQueryMsg = queryCache.computeIfAbsent( @@ -149,8 +171,8 @@ boolean errorContains(String substring, String errorField) { return false; } - static Alert.Builder createFingerprintingAlert(String engineId) { - final String enginePrefix = "graphql.engine." + engineId + "."; + static Alert.Builder createFingerprintingAlert( + DiscoveredGraphQlEngine discoveredGraphQlEngine) { return Alert.builder() .setPluginId(ExtensionGraphQl.TOOL_ALERT_ID) .setAlertRef(FINGERPRINTING_ALERT_REF) @@ -158,9 +180,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")) + discoveredGraphQlEngine.getName(), + discoveredGraphQlEngine.getTechnologies())) + .setReference(discoveredGraphQlEngine.getDocsUrl()) .setConfidence(Alert.CONFIDENCE_HIGH) .setRisk(Alert.RISK_INFO) .setCweId(205) @@ -169,14 +191,15 @@ static Alert.Builder createFingerprintingAlert(String engineId) { .setTags(FINGERPRINTING_ALERT_TAGS); } - private void raiseFingerprintingAlert(String engineId) { + void raiseFingerprintingAlert(DiscoveredGraphQlEngine discoveredGraphQlEngine) { var extAlert = Control.getSingleton().getExtensionLoader().getExtension(ExtensionAlert.class); if (extAlert == null) { return; } + Alert alert = - createFingerprintingAlert(engineId) + createFingerprintingAlert(discoveredGraphQlEngine) .setEvidence(matchedString) .setMessage(lastQueryMsg) .setUri(requestor.getEndpointUrl().toString()) @@ -600,4 +623,49 @@ private boolean checkWpGraphQlEngine() { } return false; } + + public static void addEngineHandler(DiscoveredGraphQlEngineHandler handler) { + if (handlers == null) { + handlers = new ArrayList<>(); + } + handlers.add(handler); + } + + public static void resetHandlers() { + handlers = null; + } + + public static class DiscoveredGraphQlEngine { + private static final String PREFIX = "graphql.engine."; + private String enginePrefix; + private String name; + private String docsUrl; + private String technologies; + private URI uri; + + public DiscoveredGraphQlEngine(String engineId, URI uri) { + 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"); + this.uri = uri; + } + + public String getName() { + return name; + } + + public String getDocsUrl() { + return docsUrl; + } + + public String getTechnologies() { + return technologies; + } + + public URI getUri() { + return uri; + } + } } 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..1a7edb722be --- /dev/null +++ b/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/techdetection/ExtensionTechDetectionGraphQl.java @@ -0,0 +1,101 @@ +/* + * 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.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.DiscoveredGraphQlEngine; +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.addEngineHandler(ExtensionTechDetectionGraphQl::addApp); + } + + @Override + public boolean canUnload() { + return true; + } + + @Override + public void unload() { + GraphQlFingerprinter.resetHandlers(); + } + + private static ApplicationMatch getAppForEngine(DiscoveredGraphQlEngine engine) { + Application gqlEngine = new Application(); + gqlEngine.setName(engine.getName()); + gqlEngine.setCategories(List.of("GraphQL Engine")); + gqlEngine.setWebsite(engine.getDocsUrl()); + gqlEngine.setImplies(List.of(engine.getTechnologies())); + + return new ApplicationMatch(gqlEngine); + } + + private static void addApp(DiscoveredGraphQlEngine engine) { + getExtTech().addApplicationsToSite(engine.getUri(), 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..f3eed1c2a71 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 fingerprinter 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 bd75fb568fb..c04a5454c7c 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 @@ -254,5 +254,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/GraphQlFingerprinterUnitTest.java b/addOns/graphql/src/test/java/org/zaproxy/addon/graphql/GraphQlFingerprinterUnitTest.java index 822e7ab3d3c..b9ecdeab87e 100644 --- a/addOns/graphql/src/test/java/org/zaproxy/addon/graphql/GraphQlFingerprinterUnitTest.java +++ b/addOns/graphql/src/test/java/org/zaproxy/addon/graphql/GraphQlFingerprinterUnitTest.java @@ -27,16 +27,16 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.withSettings; import fi.iki.elonen.NanoHTTPD; import fi.iki.elonen.NanoHTTPD.IHTTPSession; import fi.iki.elonen.NanoHTTPD.Response; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -44,13 +44,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; import org.mockito.quality.Strictness; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; import org.parosproxy.paros.core.scanner.Alert; import org.parosproxy.paros.extension.ExtensionLoader; import org.parosproxy.paros.model.Model; +import org.zaproxy.addon.graphql.GraphQlFingerprinter.DiscoveredGraphQlEngine; import org.zaproxy.zap.extension.alert.ExtensionAlert; import org.zaproxy.zap.testutils.NanoServerHandler; import org.zaproxy.zap.testutils.StaticContentServerHandler; @@ -77,6 +77,7 @@ void teardown() { stopServer(); Constant.messages = null; + GraphQlFingerprinter.resetHandlers(); } @Test @@ -185,7 +186,7 @@ static Stream fingerprintData() { arguments( "Apollo", errorResponse("Directive \\\"@deprecated\\\" may not be used on QUERY.")), - arguments("AWS", errorResponse("MisplacedDirective")), + arguments("AWS AppSync", errorResponse("MisplacedDirective")), arguments("Hasura", "{ \"data\": { \"__typename\":\"query_root\" } }"), arguments( "Hasura", @@ -232,33 +233,34 @@ static Stream fingerprintData() { errorResponse( "Validation error of type UnknownDirective: Unknown directive deprecated @ '__typename'")), arguments( - "ruby", + "graphql-ruby", errorResponse( "'@skip' can't be applied to queries (allowed: fields, fragment spreads, inline fragments)")), arguments( - "ruby", + "graphql-ruby", errorResponse("Directive 'skip' is missing required arguments: if")), - arguments("ruby", errorResponse("'@deprecated' can't be applied to queries")), - arguments("ruby", errorResponse("Parse error on \\\"}\\\" (RCURLY)")), arguments( - "ruby", + "graphql-ruby", errorResponse("'@deprecated' can't be applied to queries")), + arguments("graphql-ruby", errorResponse("Parse error on \\\"}\\\" (RCURLY)")), + arguments( + "graphql-ruby", errorResponse("Directive 'skip' is missing required arguments: if")), arguments( - "PHP", + "graphql-php", errorResponse( "Directive \\\"deprecated\\\" may not be used on \\\"QUERY\\\".")), arguments("gqlgen", errorResponse("expected at least one definition")), arguments("gqlgen", errorResponse("Expected Name, found ")), - arguments("Go", errorResponse("Unexpected empty IN")), - arguments("Go", errorResponse("Must provide an operation.")), - arguments("Go", "{ \"data\": { \"__typename\":\"RootQuery\" } }"), + arguments("graphql-go", errorResponse("Unexpected empty IN")), + arguments("graphql-go", errorResponse("Must provide an operation.")), + arguments("graphql-go", "{ \"data\": { \"__typename\":\"RootQuery\" } }"), arguments("Juniper", errorResponse("Unexpected \\\"queryy\\\"")), arguments("Juniper", errorResponse("Unexpected end of input")), arguments( "Sangria", "{ \"syntaxError\" : \"Syntax error while parsing GraphQL query. Invalid input \\\"queryy\\\", expected ExecutableDefinition or TypeSystemDefinition\" }"), arguments( - "Flutter", + "graphql-flutter", errorResponse("Directive \\\"deprecated\\\" may not be used on FIELD.")), arguments( "Diana.jl", @@ -309,21 +311,25 @@ private static String errorResponse(String error, String field, boolean data) { + " }"; } + @SuppressWarnings("null") @ParameterizedTest @MethodSource("fingerprintData") void shouldFingerprintValidData(String graphqlImpl, String response) throws Exception { // Given - ExtensionAlert extensionAlert = mockExtensionAlert(); nano.addHandler(new GraphQlResponseHandler(response)); var fp = new GraphQlFingerprinter(UrlBuilder.build(endpointUrl)); + List discoveredEngine = new ArrayList<>(1); + GraphQlFingerprinter.addEngineHandler(discoveredEngine::add); // When fp.fingerprint(); // Then - ArgumentCaptor alertArgCaptor = ArgumentCaptor.forClass(Alert.class); - verify(extensionAlert).alertFound(alertArgCaptor.capture(), any()); - Alert alert = alertArgCaptor.getValue(); + Alert alert = + GraphQlFingerprinter.createFingerprintingAlert(discoveredEngine.get(0)).build(); assertThat(alert, is(notNullValue())); assertThat(alert.getDescription(), containsString(graphqlImpl)); + // Check "handled" values + assertThat(discoveredEngine.get(0).getUri().toString(), is(equalTo(endpointUrl))); + assertThat(discoveredEngine.get(0).getName(), is(equalTo(graphqlImpl))); } private static ExtensionAlert mockExtensionAlert() { 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 9b48f95346a..00f2b688285 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();