diff --git a/addOns/exim/src/main/java/org/zaproxy/addon/exim/ExtensionExim.java b/addOns/exim/src/main/java/org/zaproxy/addon/exim/ExtensionExim.java index cd3019b6ea6..df262da5204 100644 --- a/addOns/exim/src/main/java/org/zaproxy/addon/exim/ExtensionExim.java +++ b/addOns/exim/src/main/java/org/zaproxy/addon/exim/ExtensionExim.java @@ -48,6 +48,7 @@ public class ExtensionExim extends ExtensionAdaptor { List.of(ExtensionCommonlib.class); private Exporter exporter; + private Importer importer; private JMenu menuExport; @@ -62,6 +63,13 @@ public ExtensionExim() { super(NAME); } + @Override + public void init() { + super.init(); + + importer = new Importer(); + } + @Override public void initModel(Model model) { super.initModel(model); @@ -143,6 +151,16 @@ public Exporter getExporter() { return exporter; } + /** + * Gets the importer. + * + * @return the importer, never {@code null}. + * @since 0.13.0 + */ + public Importer getImporter() { + return importer; + } + public static void updateOutput(String messageKey, String filePath) { if (View.isInitialised()) { StringBuilder sb = new StringBuilder(128); diff --git a/addOns/exim/src/main/java/org/zaproxy/addon/exim/Importer.java b/addOns/exim/src/main/java/org/zaproxy/addon/exim/Importer.java new file mode 100644 index 00000000000..e27dd287dec --- /dev/null +++ b/addOns/exim/src/main/java/org/zaproxy/addon/exim/Importer.java @@ -0,0 +1,171 @@ +/* + * 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.exim; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.parosproxy.paros.Constant; +import org.zaproxy.addon.exim.ImporterOptions.MessageHandler; +import org.zaproxy.addon.exim.har.HarImporterType; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.utils.Stats; + +/** + * Importer of data (e.g. HAR, URLs). + * + * @since 0.13.0 + * @see ExtensionExim#getImporter() + */ +public class Importer { + + private static final Exception STOP_IMPORT_EXCEPTION = + new Exception() { + private static final long serialVersionUID = 1L; + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + }; + + Importer() {} + + /** + * Imports the data with the given options. + * + * @param options the importer options. + * @return the result of the import. + */ + public ImporterResult apply(ImporterOptions options) { + ImporterResult result = importImpl(options); + Stats.incCounter( + ExtensionExim.STATS_PREFIX + "importer." + options.getType().getId() + ".count", + result.getCount()); + return result; + } + + private static ImporterResult importImpl(ImporterOptions options) { + ImporterResult result = new ImporterResult(); + Path file = options.getInputFile(); + if (!isValid(file, result)) { + return result; + } + + try (var reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + + ImporterType type = createImporterType(options); + MessageHandler messageHandler = options.getMessageHandler(); + type.begin(reader); + Context context = options.getContext(); + + type.read( + reader, + msg -> { + if (context != null + && !context.isInContext( + msg.getRequestHeader().getURI().toString())) { + return; + } + + try { + messageHandler.handle(msg); + result.incrementCount(); + } catch (Exception e) { + result.addError( + Constant.messages.getString( + "exim.importer.error.handler", e.getLocalizedMessage()), + e); + throw STOP_IMPORT_EXCEPTION; + } + }); + type.end(reader); + } catch (Exception e) { + if (e != STOP_IMPORT_EXCEPTION) { + result.addError( + Constant.messages.getString( + "exim.importer.error.io", e.getLocalizedMessage()), + e); + } + } + + return result; + } + + private static boolean isValid(Path file, ImporterResult result) { + if (Files.notExists(file)) { + result.addError( + Constant.messages.getString("exim.importer.error.file.notexists", file)); + return false; + } + + if (!Files.isRegularFile(file)) { + result.addError(Constant.messages.getString("exim.importer.error.file.notfile", file)); + return false; + } + + if (!Files.isReadable(file)) { + result.addError( + Constant.messages.getString("exim.importer.error.file.notreadable", file)); + return false; + } + + return true; + } + + private static ImporterType createImporterType(ImporterOptions options) { + switch (options.getType()) { + case HAR: + default: + return new HarImporterType(); + } + } + + /** An importer type, knows how to import an {@code HttpMessage} from specific data. */ + public interface ImporterType { + + /** + * Called when the import begins. + * + * @param reader from where to import the data. + * @throws IOException if an error occurs while beginning the import. + */ + void begin(Reader reader) throws IOException; + + /** + * Called while importing. + * + * @param reader from where to import the data + * @param handler the message handler. + * @throws Exception if an error occurs while importing the {@code HttpMessage}. + */ + void read(Reader reader, MessageHandler handler) throws Exception; + + /** + * Called when the import ends. + * + * @param reader from where to import the data. + * @throws IOException if an error occurs while ending the import. + */ + void end(Reader reader) throws IOException; + } +} diff --git a/addOns/exim/src/main/java/org/zaproxy/addon/exim/ImporterOptions.java b/addOns/exim/src/main/java/org/zaproxy/addon/exim/ImporterOptions.java new file mode 100644 index 00000000000..d60ac664b61 --- /dev/null +++ b/addOns/exim/src/main/java/org/zaproxy/addon/exim/ImporterOptions.java @@ -0,0 +1,214 @@ +/* + * 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.exim; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.nio.file.Path; +import java.util.Locale; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.zap.model.Context; + +/** + * The options for the importer. + * + * @since 0.13.0 + * @see Importer + */ +public class ImporterOptions { + + private final Context context; + private final Type type; + private final Path inputFile; + private final MessageHandler messageHandler; + + private ImporterOptions( + Context context, Type type, Path inputFile, MessageHandler messageHandler) { + this.context = context; + this.type = type; + this.inputFile = inputFile; + this.messageHandler = messageHandler; + } + + public Context getContext() { + return context; + } + + public Type getType() { + return type; + } + + public Path getInputFile() { + return inputFile; + } + + public MessageHandler getMessageHandler() { + return messageHandler; + } + + /** + * Returns a new builder. + * + * @return the options builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder of options. + * + * @see #build() + */ + public static class Builder { + + private Context context; + private Type type; + private Path inputFile; + private MessageHandler messageHandler; + + private Builder() { + type = Type.HAR; + } + + /** + * Sets the context. + * + *

Default value: {@code null}. + * + * @param context the context. + * @return the builder for chaining. + */ + public Builder setContext(Context context) { + this.context = context; + return this; + } + + /** + * Sets the type. + * + *

Default value: {@link Type#HAR}. + * + * @param type the type. + * @return the builder for chaining. + * @throws IllegalArgumentException if the type is {@code null}. + */ + public Builder setType(Type type) { + if (type == null) { + throw new IllegalArgumentException("The type must not be null."); + } + this.type = type; + return this; + } + + /** + * Sets the input file. + * + *

Default value: {@code null}. + * + * @param inputFile the input file. + * @return the builder for chaining. + */ + public Builder setInputFile(Path inputFile) { + this.inputFile = inputFile; + return this; + } + + /** + * Sets the message handler. + * + *

Default value: {@code null}. + * + * @param messageHandler the message handler. + * @return the builder for chaining. + */ + public Builder setMessageHandler(MessageHandler messageHandler) { + this.messageHandler = messageHandler; + return this; + } + + /** + * Builds the options from the specified data. + * + * @return the options with specified data. + * @throws IllegalStateException if built without {@link #setInputFile(Path) setting the + * input file} or {@link #setMessageHandler(MessageHandler) the message handler}. + */ + public final ImporterOptions build() { + if (inputFile == null) { + throw new IllegalStateException("The inputFile must be set."); + } + if (messageHandler == null) { + throw new IllegalStateException("The messageHandler must be set."); + } + return new ImporterOptions(context, type, inputFile, messageHandler); + } + } + + /** The type of import. */ + public enum Type { + /** The messages are imported from HAR. */ + HAR; + + private String id; + private String name; + + private Type() { + id = name().toLowerCase(Locale.ROOT); + name = Constant.messages.getString("exim.importer.type." + id); + } + + @JsonValue + public String getId() { + return id; + } + + @Override + public String toString() { + return name; + } + + @JsonCreator + public static Type fromString(String value) { + if (value == null || value.isBlank()) { + return HAR; + } + + if (HAR.id.equalsIgnoreCase(value)) { + return HAR; + } + return HAR; + } + } + + /** Handles the messages being imported. */ + public interface MessageHandler { + + /** + * Handles the given imported message. + * + * @param message the message imported. + * @throws Exception if an error occurred while handling the message and the import should + * be stopped. + */ + void handle(HttpMessage message) throws Exception; + } +} diff --git a/addOns/exim/src/main/java/org/zaproxy/addon/exim/ImporterResult.java b/addOns/exim/src/main/java/org/zaproxy/addon/exim/ImporterResult.java new file mode 100644 index 00000000000..be70f9da606 --- /dev/null +++ b/addOns/exim/src/main/java/org/zaproxy/addon/exim/ImporterResult.java @@ -0,0 +1,88 @@ +/* + * 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.exim; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The result of the import. + * + * @see Importer#apply(ImporterOptions) + * @since 0.13.0 + */ +public class ImporterResult { + + private List errors; + private Throwable cause; + private int count; + + /** + * Gets the count of imported messages. + * + * @return the count. + */ + public int getCount() { + return count; + } + + void incrementCount() { + this.count++; + } + + /** + * Gets the errors that happened while importing, if any. + * + * @return the errors, never {@code null}. + */ + public List getErrors() { + if (errors == null) { + return List.of(); + } + return Collections.unmodifiableList(errors); + } + + /** + * Gets the cause of the error, if any. + * + * @return the cause of the error, might be {@code null}. + */ + public Throwable getCause() { + return cause; + } + + void addError(String error) { + createErrors(); + errors.add(error); + } + + private void createErrors() { + if (errors == null) { + errors = new ArrayList<>(); + } + } + + void addError(String error, Throwable cause) { + createErrors(); + errors.add(error); + this.cause = cause; + } +} diff --git a/addOns/exim/src/main/java/org/zaproxy/addon/exim/har/HarImporterType.java b/addOns/exim/src/main/java/org/zaproxy/addon/exim/har/HarImporterType.java new file mode 100644 index 00000000000..e887a2ccaa6 --- /dev/null +++ b/addOns/exim/src/main/java/org/zaproxy/addon/exim/har/HarImporterType.java @@ -0,0 +1,93 @@ +/* + * 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.exim.har; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import de.sstoehr.harreader.model.HarEntry; +import java.io.IOException; +import java.io.Reader; +import java.util.Objects; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.addon.exim.Importer.ImporterType; +import org.zaproxy.addon.exim.ImporterOptions.MessageHandler; + +public class HarImporterType implements ImporterType { + + private static final String LOG_FIELD = "log"; + private static final String ENTRIES_FIELD = "entries"; + + private JsonParser parser; + + @Override + public void begin(Reader reader) throws IOException { + parser = HarUtils.JSON_MAPPER.createParser(reader); + + validateNextToken(JsonToken.START_OBJECT, null); + validateNextToken(JsonToken.FIELD_NAME, LOG_FIELD); + validateNextToken(JsonToken.START_OBJECT, LOG_FIELD); + + while (!isNextToken(JsonToken.FIELD_NAME, ENTRIES_FIELD)) { + parser.skipChildren(); + } + + validateNextToken(JsonToken.START_ARRAY, ENTRIES_FIELD); + parser.nextToken(); + } + + private boolean isNextToken(JsonToken wantedToken, String wantedName) throws IOException { + JsonToken token = parser.nextToken(); + if (token == null) { + throw new IOException("Failed to find entries property in HAR log."); + } + if (token != wantedToken) { + return false; + } + + return wantedName.equals(parser.currentName()); + } + + private void validateNextToken(JsonToken expectedToken, String expectedName) + throws IOException { + JsonToken token = parser.nextToken(); + if (token != expectedToken) { + throw new IOException("Unexpected token " + token + ", expected: " + expectedToken); + } + + String name = parser.currentName(); + if (!Objects.equals(name, expectedName)) { + throw new IOException("Unexpected name " + name + ", expected: " + expectedName); + } + } + + @Override + public void read(Reader reader, MessageHandler handler) throws Exception { + HarEntry entry; + while ((entry = parser.readValueAs(HarEntry.class)) != null) { + HttpMessage message = HarUtils.createHttpMessage(entry); + handler.handle(message); + } + } + + @Override + public void end(Reader reader) throws IOException { + // Nothing else to do once the "entries" is consumed. + } +} diff --git a/addOns/exim/src/main/resources/org/zaproxy/addon/exim/resources/Messages.properties b/addOns/exim/src/main/resources/org/zaproxy/addon/exim/resources/Messages.properties index 1c08a5a2825..5774db5811b 100644 --- a/addOns/exim/src/main/resources/org/zaproxy/addon/exim/resources/Messages.properties +++ b/addOns/exim/src/main/resources/org/zaproxy/addon/exim/resources/Messages.properties @@ -68,6 +68,13 @@ exim.importLogFiles.import.menu.label = Import URLs from Logs or raw Files... exim.importLogFiles.log.type.modsec2 = ModSecurity2 Logs exim.importLogFiles.log.type.zap = ZAP Messages +exim.importer.error.file.notexists = Cannot read from nonexistent file: {0} +exim.importer.error.file.notfile = Cannot read from non-file: {0} +exim.importer.error.file.notreadable = Cannot read from file: {0} +exim.importer.error.handler = Failed while importing an HTTP message: {0} +exim.importer.error.io = Failed to read from file: {0} +exim.importer.type.har = HAR + exim.importurls.topmenu.import = Import a File Containing URLs exim.importurls.topmenu.import.tooltip = The file must be plain text with one URL per line.\nBlank lines and lines starting with a # are ignored. exim.importurls.warn.scheme = "{0}" does not have a scheme. diff --git a/addOns/exim/src/test/java/org/zaproxy/addon/exim/ExtensionEximUnitTest.java b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ExtensionEximUnitTest.java index 09671f08b73..6b7cd5a3b3f 100644 --- a/addOns/exim/src/test/java/org/zaproxy/addon/exim/ExtensionEximUnitTest.java +++ b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ExtensionEximUnitTest.java @@ -57,4 +57,20 @@ void shouldInitModelAndExporter() { assertThat(extension.getModel(), is(notNullValue())); assertThat(extension.getExporter(), is(notNullValue())); } + + @Test + void shouldNotHaveImporterBeforeInit() { + // Given / When + Importer importer = extension.getImporter(); + // Then + assertThat(importer, is(nullValue())); + } + + @Test + void shouldInitImporter() { + // Given / When + extension.init(); + // Then + assertThat(extension.getImporter(), is(notNullValue())); + } } diff --git a/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterOptionsUnitTest.java b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterOptionsUnitTest.java new file mode 100644 index 00000000000..b3f641280a6 --- /dev/null +++ b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterOptionsUnitTest.java @@ -0,0 +1,158 @@ +/* + * 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.exim; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.zaproxy.addon.exim.ImporterOptions.Builder; +import org.zaproxy.addon.exim.ImporterOptions.MessageHandler; +import org.zaproxy.addon.exim.ImporterOptions.Type; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.testutils.TestUtils; + +/** Unit test for {@link ImporterOptions}. */ +class ImporterOptionsUnitTest extends TestUtils { + + private Path inputFile; + private MessageHandler messageHandler; + + @BeforeAll + static void setupMessages() { + mockMessages(new ExtensionExim()); + } + + @BeforeEach + void setup() { + inputFile = Paths.get("/dir/file"); + messageHandler = msg -> {}; + } + + @Test + void shouldFailToBuildWithoutInputFile() { + // Given + Builder builder = ImporterOptions.builder(); + // When / Then + Exception ex = assertThrows(IllegalStateException.class, () -> builder.build()); + assertThat(ex.getMessage(), containsString("inputFile")); + } + + @Test + void shouldFailToBuildWithoutMessageHandler() { + // Given + Builder builder = ImporterOptions.builder().setInputFile(inputFile); + // When / Then + Exception ex = assertThrows(IllegalStateException.class, () -> builder.build()); + assertThat(ex.getMessage(), containsString("messageHandler")); + } + + @Test + void shouldBuildWithJustInputFileAndMessageHandler() { + // Given + Builder builder = ImporterOptions.builder(); + // When + ImporterOptions options = + builder.setInputFile(inputFile).setMessageHandler(messageHandler).build(); + // Then + assertThat(options.getContext(), is(nullValue())); + assertThat(options.getType(), is(equalTo(Type.HAR))); + assertThat(options.getInputFile(), is(equalTo(inputFile))); + assertThat(options.getMessageHandler(), is(equalTo(messageHandler))); + } + + @Test + void shouldSetContext() { + // Given + ImporterOptions.Builder builder = builderWithInputFileAndMessageHandler(); + Context context = mock(Context.class); + // When + ImporterOptions options = builder.setContext(context).build(); + // Then + assertThat(options.getContext(), is(equalTo(context))); + } + + @ParameterizedTest + @EnumSource(value = Type.class) + void shouldSetType(Type type) { + // Given + ImporterOptions.Builder builder = builderWithInputFileAndMessageHandler(); + // When + ImporterOptions options = builder.setType(type).build(); + // Then + assertThat(options.getType(), is(equalTo(type))); + } + + @Test + void shouldThrowSettingNullType() { + // Given + Builder builder = ImporterOptions.builder(); + // When / Then + Exception ex = assertThrows(IllegalArgumentException.class, () -> builder.setType(null)); + assertThat(ex.getMessage(), containsString("type")); + } + + private Builder builderWithInputFileAndMessageHandler() { + return ImporterOptions.builder().setInputFile(inputFile).setMessageHandler(messageHandler); + } + + /** Unit test for {@link ImporterOptions.Type}. */ + @Nested + class TypeUnitTest { + + @ParameterizedTest + @EnumSource(value = Type.class) + void shouldHaveToStringRepresentation(Type type) { + assertThat(type.toString(), is(notNullValue())); + } + + @ParameterizedTest + @CsvSource({"HAR, har"}) + void shouldReturnId(Type type, String expectedId) { + assertThat(type.getId(), is(equalTo(expectedId))); + } + + @ParameterizedTest + @CsvSource({ + ", HAR", + "'', HAR", + "Something, HAR", + "har, HAR", + "haR, HAR", + }) + void shouldConvertFromString(String value, Type expectedType) { + assertThat(Type.fromString(value), is(equalTo(expectedType))); + } + } +} diff --git a/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterResultUnitTest.java b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterResultUnitTest.java new file mode 100644 index 00000000000..94402876391 --- /dev/null +++ b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterResultUnitTest.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.exim; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.Test; + +/** Unit test for {@link ImporterResult}. */ +class ImporterResultUnitTest { + + @Test + void shouldHaveZeroCountByDefault() { + // Given + ImporterResult result = new ImporterResult(); + // When + int count = result.getCount(); + // Then + assertThat(count, is(equalTo(0))); + } + + @Test + void shouldIncrementCount() { + // Given + ImporterResult result = new ImporterResult(); + // When + result.incrementCount(); + result.incrementCount(); + // Then + assertThat(result.getCount(), is(equalTo(2))); + } + + @Test + void shouldNotHaveErrorsByDefault() { + // Given + ImporterResult result = new ImporterResult(); + // When / Then + assertThat(result.getErrors(), is(empty())); + assertThat(result.getCause(), is(nullValue())); + } + + @Test + void shouldHaveAddedErrors() { + // Given + ImporterResult result = new ImporterResult(); + // When + result.addError("Error 1"); + result.addError("Error 2"); + // Then + assertThat(result.getErrors(), contains("Error 1", "Error 2")); + assertThat(result.getCause(), is(nullValue())); + } + + @Test + void shouldHaveErrorAndCause() { + // Given + ImporterResult result = new ImporterResult(); + Exception exception = new Exception(); + // When + result.addError("Error A", exception); + // Then + assertThat(result.getErrors(), contains("Error A")); + assertThat(result.getCause(), is(equalTo(exception))); + } + + @Test + void shouldOverrideCauses() { + // Given + ImporterResult result = new ImporterResult(); + Exception exceptionA = new Exception(); + Exception exceptionB = new Exception(); + // When + result.addError("Error A", exceptionA); + result.addError("Error B", exceptionB); + // Then + assertThat(result.getErrors(), contains("Error A", "Error B")); + assertThat(result.getCause(), is(equalTo(exceptionB))); + } +} diff --git a/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterUnitTest.java b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterUnitTest.java new file mode 100644 index 00000000000..965c0a7fb44 --- /dev/null +++ b/addOns/exim/src/test/java/org/zaproxy/addon/exim/ImporterUnitTest.java @@ -0,0 +1,251 @@ +/* + * 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.exim; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.quality.Strictness; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.addon.exim.ImporterOptions.Type; +import org.zaproxy.zap.extension.stats.InMemoryStats; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.testutils.TestUtils; +import org.zaproxy.zap.utils.Stats; + +/** Unit test for {@link Importer}. */ +class ImporterUnitTest extends TestUtils { + + private Path inputDir; + private Path inputFile; + private ImporterOptions options; + private InMemoryStats stats; + private List importedMessages; + + private Importer importer; + + @BeforeAll + static void setupMessages() { + mockMessages(new ExtensionExim()); + } + + @BeforeEach + void setup(@TempDir Path dir) { + inputDir = dir; + inputFile = dir.resolve("inputfile"); + + options = mock(ImporterOptions.class, withSettings().strictness(Strictness.LENIENT)); + optionsWithType(Type.HAR); + given(options.getInputFile()).willReturn(inputFile); + importedMessages = new ArrayList<>(); + given(options.getMessageHandler()).willReturn(this::importedMessage); + + stats = new InMemoryStats(); + Stats.addListener(stats); + + importer = new Importer(); + } + + private void importedMessage(HttpMessage message) { + importedMessages.add(message); + } + + @AfterEach + void cleanup() { + Stats.removeListener(stats); + } + + private void optionsWithType(Type type) { + given(options.getType()).willReturn(type); + } + + @Test + void shouldNotImportAnythingIfEmptyHar() throws Exception { + // Given + optionsWithType(Type.HAR); + inputFileWith( + "{\n" + + " \"log\" : {\n" + + " \"version\" : \"1.2\",\n" + + " \"creator\" : {\n" + + " \"name\" : \"ZAP\",\n" + + " \"version\" : \"Dev Build\"\n" + + " },\n" + + " \"entries\" : [ ]\n" + + " }\n" + + "}"); + // When + ImporterResult result = importer.apply(options); + // Then + assertCount(result, 0); + assertThat(result.getErrors(), is(empty())); + assertThat(result.getCause(), is(nullValue())); + } + + private void inputFileWith(String content) throws IOException { + Files.writeString(inputFile, content); + } + + @Test + void shouldImportFromHar() throws Exception { + // Given + optionsWithType(Type.HAR); + inputFileWith( + "{\n" + + " \"log\" : {\n" + + " \"entries\" : [ {\n" + + " \"request\" : {\n" + + " \"method\" : \"GET\",\n" + + " \"url\" : \"http://example.com\",\n" + + " \"httpVersion\" : \"HTTP/1.1\"\n" + + " },\n" + + " \"response\" : {\n" + + " \"status\" : 0\n" + + " }\n" + + " } ]\n" + + " }\n" + + "}"); + // When + ImporterResult result = importer.apply(options); + // Then + assertCount(result, 1); + assertThat(result.getErrors(), is(empty())); + assertThat(result.getCause(), is(nullValue())); + assertThat(importedMessages, hasSize(1)); + HttpMessage importedMessage = importedMessages.get(0); + assertThat( + importedMessage.getRequestHeader().getURI().toString(), + is(equalTo("http://example.com"))); + } + + private void assertCount(ImporterResult result, int count) { + assertThat(result.getCount(), is(equalTo(count))); + assertThat( + stats.getStat("stats.exim.importer." + options.getType().getId() + ".count"), + is(equalTo((long) count))); + } + + @Test + void shouldErrorIfInputFileNotValid() { + // Given + given(options.getInputFile()).willReturn(inputDir); + // When + ImporterResult result = importer.apply(options); + // Then + assertCount(result, 0); + assertThat(result.getErrors(), contains(startsWith("Cannot read from non-file: "))); + assertThat(result.getCause(), is(nullValue())); + } + + @Test + void shouldErrorIfInputFileDoesNotExist() { + // Given + given(options.getInputFile()).willReturn(Paths.get("/not-exists")); + // When + ImporterResult result = importer.apply(options); + // Then + assertCount(result, 0); + assertThat(result.getErrors(), contains(startsWith("Cannot read from nonexistent file: "))); + assertThat(result.getCause(), is(nullValue())); + } + + @Test + void shouldIncludeMessageInContext() throws Exception { + // Given + optionsWithType(Type.HAR); + inputFileWith( + "{\n" + + " \"log\" : {\n" + + " \"entries\" : [ {\n" + + " \"request\" : {\n" + + " \"method\" : \"GET\",\n" + + " \"url\" : \"http://example.com/1\",\n" + + " \"httpVersion\" : \"HTTP/1.1\"\n" + + " }\n" + + " } ]\n" + + " }\n" + + "}"); + Context context = mock(Context.class); + given(options.getContext()).willReturn(context); + given(context.isInContext(anyString())).willReturn(true); + // When + ImporterResult result = importer.apply(options); + // Then + assertCount(result, 1); + assertThat(result.getErrors(), is(empty())); + assertThat(result.getCause(), is(nullValue())); + assertThat(importedMessages, hasSize(1)); + assertThat( + importedMessages.get(0).getRequestHeader().getURI().toString(), + is(equalTo("http://example.com/1"))); + verify(context).isInContext(anyString()); + } + + @Test + void shouldNotIncludeMessageNotInContext() throws Exception { + // Given + optionsWithType(Type.HAR); + inputFileWith( + "{\n" + + " \"log\" : {\n" + + " \"entries\" : [ {\n" + + " \"request\" : {\n" + + " \"method\" : \"GET\",\n" + + " \"url\" : \"http://example.com/1\",\n" + + " \"httpVersion\" : \"HTTP/1.1\"\n" + + " }\n" + + " } ]\n" + + " }\n" + + "}"); + Context context = mock(Context.class); + given(options.getContext()).willReturn(context); + given(context.isInContext(anyString())).willReturn(false); + // When + ImporterResult result = importer.apply(options); + // Then + assertCount(result, 0); + assertThat(result.getErrors(), is(empty())); + assertThat(result.getCause(), is(nullValue())); + assertThat(importedMessages, hasSize(0)); + verify(context).isInContext(anyString()); + } +} diff --git a/addOns/exim/src/test/java/org/zaproxy/addon/exim/har/HarImporterTypeUnitTest.java b/addOns/exim/src/test/java/org/zaproxy/addon/exim/har/HarImporterTypeUnitTest.java new file mode 100644 index 00000000000..be68a45b139 --- /dev/null +++ b/addOns/exim/src/test/java/org/zaproxy/addon/exim/har/HarImporterTypeUnitTest.java @@ -0,0 +1,122 @@ +/* + * 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.exim.har; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit test for {@link HarImporterType}. */ +class HarImporterTypeUnitTest { + + private HarImporterType importer; + + @BeforeEach + void setup() { + importer = new HarImporterType(); + } + + private static Reader reader(String value) { + return new StringReader(value); + } + + @Test + void shouldThrowIfMissingObjectStart() { + // Given + Reader reader = reader(""); + // When / Then + IOException e = assertThrows(IOException.class, () -> importer.begin(reader)); + assertThat(e.getMessage(), is(equalTo("Unexpected token null, expected: START_OBJECT"))); + } + + @Test + void shouldThrowIfMissingLogField() { + // Given + Reader reader = reader("{}"); + // When / Then + IOException e = assertThrows(IOException.class, () -> importer.begin(reader)); + assertThat( + e.getMessage(), is(equalTo("Unexpected token END_OBJECT, expected: FIELD_NAME"))); + } + + @Test + void shouldThrowIfSomethingOtherThanLogField() { + // Given + Reader reader = reader("{\"not_log\":{}}"); + // When / Then + IOException e = assertThrows(IOException.class, () -> importer.begin(reader)); + assertThat(e.getMessage(), is(equalTo("Unexpected name not_log, expected: log"))); + } + + @Test + void shouldThrowIfMissingLogObjectStart() { + // Given + Reader reader = reader("{\"log\":[]}"); + // When / Then + IOException e = assertThrows(IOException.class, () -> importer.begin(reader)); + assertThat( + e.getMessage(), + is(equalTo("Unexpected token START_ARRAY, expected: START_OBJECT"))); + } + + @Test + void shouldThrowIfMissingEntriesField() { + // Given + Reader reader = reader("{\"log\":{}}"); + // When / Then + IOException e = assertThrows(IOException.class, () -> importer.begin(reader)); + assertThat(e.getMessage(), is(equalTo("Failed to find entries property in HAR log."))); + } + + @Test + void shouldThrowIfEntriesNotArray() { + // Given + Reader reader = reader("{\"log\":{\"entries\":{}}}"); + // When / Then + IOException e = assertThrows(IOException.class, () -> importer.begin(reader)); + assertThat( + e.getMessage(), + is(equalTo("Unexpected token START_OBJECT, expected: START_ARRAY"))); + } + + @Test + void shouldNotThrowIfEntriesAnArray() { + // Given + Reader reader = reader("{\"log\":{\"entries\":[]}}"); + // When / Then + assertDoesNotThrow(() -> importer.begin(reader)); + } + + @Test + void shouldNotThrowWhenReadingEnd() { + // Given + Reader reader = reader("…"); + // When / Then + assertDoesNotThrow(() -> importer.end(reader)); + } +}