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));
+ }
+}