Repository: kestra-io/kestra
Issue: Add SECRET-type input
Labels: backend

### Feature description

Currently, when we want to have a flow input that holds sensitive information (password, secrets, ...) it will be displayed in the flow execution form when entering the input and in the execution overview tab in the inputs section.

It would be great for security reason to hide the sensitive information from the UI.

## Implementation idea:
Provide a `SECRET` input type, this type of input will not be shown in the UI and will have a password input in the execution form when entering the input value.
We can also mandate base64 of the input but it may makes thinks complicated.

An alternative would be to add a `sensitive=true` property to all inputs making it available for all type of inputs but there is little advantage as a date for ex is not sensitive and a string can still holds any type of value.

Issue comments:
This issue is only related to `inputs`: if the user has sensitive input information, adding a `SECRET`-type input might be a great solution, as proposed here.

When it comes to obfuscating Secret values from:
- the logs
- inputs passed to subflows/tasks 
- outputs

check this issue: https://github.com/kestra-io/kestra/issues/2046 




Pull request review comments:

Patch that resolved the issue:
diff --git a/core/src/main/java/io/kestra/core/models/flows/Input.java b/core/src/main/java/io/kestra/core/models/flows/Input.java
index 8110a3d056..6028a17a5e 100644
--- a/core/src/main/java/io/kestra/core/models/flows/Input.java
+++ b/core/src/main/java/io/kestra/core/models/flows/Input.java
@@ -32,6 +32,7 @@
     @JsonSubTypes.Type(value = FloatInput.class, name = "FLOAT"),
     @JsonSubTypes.Type(value = IntInput.class, name = "INT"),
     @JsonSubTypes.Type(value = JsonInput.class, name = "JSON"),
+    @JsonSubTypes.Type(value = SecretInput.class, name = "SECRET"),
     @JsonSubTypes.Type(value = StringInput.class, name = "STRING"),
     @JsonSubTypes.Type(value = TimeInput.class, name = "TIME"),
     @JsonSubTypes.Type(value = URIInput.class, name = "URI")
@@ -76,7 +77,8 @@ public enum Type {
         DURATION(DurationInput.class.getName()),
         FILE(FileInput.class.getName()),
         JSON(JsonInput.class.getName()),
-        URI(URIInput.class.getName());
+        URI(URIInput.class.getName()),
+        SECRET(SecretInput.class.getName());
 
         private final String clsName;
 
diff --git a/core/src/main/java/io/kestra/core/models/flows/input/SecretInput.java b/core/src/main/java/io/kestra/core/models/flows/input/SecretInput.java
new file mode 100644
index 0000000000..4ee85aee1e
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/models/flows/input/SecretInput.java
@@ -0,0 +1,38 @@
+package io.kestra.core.models.flows.input;
+
+import io.kestra.core.models.flows.Input;
+import io.kestra.core.models.validations.ManualConstraintViolation;
+import io.kestra.core.validations.Regex;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.ConstraintViolationException;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.util.Set;
+import java.util.regex.Pattern;
+
+@SuperBuilder
+@Getter
+@NoArgsConstructor
+public class SecretInput extends Input<String> {
+    @Schema(
+        title = "Regular expression validating the value."
+    )
+    @Regex
+    String validator;
+
+    @Override
+    public void validate(String input) throws ConstraintViolationException {
+        if (validator != null && ! Pattern.matches(validator, input)) {
+            throw new ConstraintViolationException("Invalid input '" + input + "', it must match the pattern '" + validator + "'",
+                Set.of(ManualConstraintViolation.of(
+                    "Invalid input",
+                    this,
+                    SecretInput.class,
+                    getId(),
+                    input
+                )));
+        }
+    }
+}
diff --git a/core/src/main/java/io/kestra/core/runners/RunContext.java b/core/src/main/java/io/kestra/core/runners/RunContext.java
index 71a664afc7..676d3e8e51 100644
--- a/core/src/main/java/io/kestra/core/runners/RunContext.java
+++ b/core/src/main/java/io/kestra/core/runners/RunContext.java
@@ -1,11 +1,11 @@
 package io.kestra.core.runners;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.CaseFormat;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import io.kestra.core.crypto.CryptoService;
 import io.kestra.core.exceptions.IllegalVariableEvaluationException;
 import io.kestra.core.metrics.MetricRegistry;
 import io.kestra.core.models.executions.AbstractMetricEntry;
@@ -13,6 +13,8 @@
 import io.kestra.core.models.executions.LogEntry;
 import io.kestra.core.models.executions.TaskRun;
 import io.kestra.core.models.flows.Flow;
+import io.kestra.core.models.flows.Input;
+import io.kestra.core.models.flows.input.SecretInput;
 import io.kestra.core.models.tasks.Task;
 import io.kestra.core.models.triggers.AbstractTrigger;
 import io.kestra.core.models.triggers.TriggerContext;
@@ -36,13 +38,8 @@
 import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.AbstractMap;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.security.GeneralSecurityException;
+import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -64,6 +61,7 @@ public class RunContext {
     protected transient Path temporaryDirectory;
     private String triggerExecutionId;
     private Storage storage;
+    private CryptoService cryptoService;
 
     /**
      * Only used by {@link io.kestra.core.models.triggers.types.Flow}
@@ -137,6 +135,7 @@ protected void initBean(ApplicationContext applicationContext) {
         this.storageInterface = applicationContext.findBean(StorageInterface.class).orElse(null);
         this.meterRegistry = applicationContext.findBean(MetricRegistry.class).orElseThrow();
         this.runContextCache = applicationContext.findBean(RunContextCache.class).orElseThrow();
+        this.cryptoService = applicationContext.findBean(CryptoService.class).orElseThrow();
         this.tempBasedPath = Path.of(applicationContext
             .getProperty("kestra.tasks.tmp-dir.path", String.class)
             .orElse(System.getProperty("java.io.tmpdir"))
@@ -286,7 +285,21 @@ protected Map<String, Object> variables(Flow flow, Task task, Execution executio
             }
 
             if (execution.getInputs() != null) {
-                builder.put("inputs", execution.getInputs());
+                Map<String, Object> inputs = new HashMap<>(execution.getInputs());
+                if (flow != null && flow.getInputs() != null) {
+                    // if some inputs are of type secret, we decode them
+                    for (Input<?> input : flow.getInputs()) {
+                        if (input instanceof SecretInput && inputs.containsKey(input.getId())) {
+                            try {
+                                String decoded = cryptoService.decrypt(((String) inputs.get(input.getId())));
+                                inputs.put(input.getId(), decoded);
+                            } catch (GeneralSecurityException e) {
+                                throw new RuntimeException(e);
+                            }
+                        }
+                    }
+                }
+                builder.put("inputs", inputs);
             }
 
             if (execution.getTrigger() != null && execution.getTrigger().getVariables() != null) {
diff --git a/core/src/main/java/io/kestra/core/runners/RunnerUtils.java b/core/src/main/java/io/kestra/core/runners/RunnerUtils.java
index 358a72d6e8..45e041ed21 100644
--- a/core/src/main/java/io/kestra/core/runners/RunnerUtils.java
+++ b/core/src/main/java/io/kestra/core/runners/RunnerUtils.java
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.collect.ImmutableMap;
+import io.kestra.core.crypto.CryptoService;
 import io.kestra.core.exceptions.MissingRequiredInput;
 import io.kestra.core.models.Label;
 import io.kestra.core.models.executions.Execution;
@@ -28,6 +29,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
+import java.security.GeneralSecurityException;
 import java.time.Duration;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -61,6 +63,9 @@ public class RunnerUtils {
     @Inject
     private ConditionService conditionService;
 
+    @Inject
+    private CryptoService cryptoService;
+
     public Map<String, Object> typedInputs(Flow flow, Execution execution, Map<String, Object> in, Publisher<StreamingFileUpload> files) throws IOException {
         if (files == null) {
             return this.typedInputs(flow, execution, in);
@@ -138,31 +143,41 @@ public Map<String, Object> typedInputs(Flow flow, Execution execution, Map<Strin
 
     private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow, Execution execution, Input<?> input, Object current) {
         switch (input.getType()) {
-            case STRING:
+            case STRING -> {
                 return Optional.of(new AbstractMap.SimpleEntry<>(
                     input.getId(),
                     current
                 ));
-
-            case INT:
+            }
+            case SECRET -> {
+                try {
+                    return Optional.of(new AbstractMap.SimpleEntry<>(
+                        input.getId(),
+                        cryptoService.encrypt((String) current)
+                    ));
+                } catch (GeneralSecurityException e) {
+                    throw new MissingRequiredInput("Invalid SECRET format for '" + input.getId() + "' for '" + current + "' with error " + e.getMessage(), e);
+                }
+            }
+            case INT -> {
                 return Optional.of(new AbstractMap.SimpleEntry<>(
                     input.getId(),
                     current instanceof Integer ? current : Integer.valueOf((String) current)
                 ));
-
-            case FLOAT:
+            }
+            case FLOAT -> {
                 return Optional.of(new AbstractMap.SimpleEntry<>(
                     input.getId(),
                     current instanceof Float ? current : Float.valueOf((String) current)
                 ));
-
-            case BOOLEAN:
+            }
+            case BOOLEAN -> {
                 return Optional.of(new AbstractMap.SimpleEntry<>(
                     input.getId(),
                     current instanceof Boolean ? current : Boolean.valueOf((String) current)
                 ));
-
-            case DATETIME:
+            }
+            case DATETIME -> {
                 try {
                     return Optional.of(new AbstractMap.SimpleEntry<>(
                         input.getId(),
@@ -171,8 +186,8 @@ private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow,
                 } catch (DateTimeParseException e) {
                     throw new MissingRequiredInput("Invalid DATETIME format for '" + input.getId() + "' for '" + current + "' with error " + e.getMessage(), e);
                 }
-
-            case DATE:
+            }
+            case DATE -> {
                 try {
                     return Optional.of(new AbstractMap.SimpleEntry<>(
                         input.getId(),
@@ -181,8 +196,8 @@ private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow,
                 } catch (DateTimeParseException e) {
                     throw new MissingRequiredInput("Invalid DATE format for '" + input.getId() + "' for '" + current + "' with error " + e.getMessage(), e);
                 }
-
-            case TIME:
+            }
+            case TIME -> {
                 try {
                     return Optional.of(new AbstractMap.SimpleEntry<>(
                         input.getId(),
@@ -191,8 +206,8 @@ private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow,
                 } catch (DateTimeParseException e) {
                     throw new MissingRequiredInput("Invalid TIME format for '" + input.getId() + "' for '" + current + "' with error " + e.getMessage(), e);
                 }
-
-            case DURATION:
+            }
+            case DURATION -> {
                 try {
                     return Optional.of(new AbstractMap.SimpleEntry<>(
                         input.getId(),
@@ -201,8 +216,8 @@ private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow,
                 } catch (DateTimeParseException e) {
                     throw new MissingRequiredInput("Invalid DURATION format for '" + input.getId() + "' for '" + current + "' with error " + e.getMessage(), e);
                 }
-
-            case FILE:
+            }
+            case FILE -> {
                 try {
                     URI uri = URI.create(((String) current).replace(File.separator, "/"));
 
@@ -220,8 +235,8 @@ private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow,
                 } catch (Exception e) {
                     throw new MissingRequiredInput("Invalid input arguments for file on input '" + input.getId() + "'", e);
                 }
-
-            case JSON:
+            }
+            case JSON -> {
                 try {
                     return Optional.of(new AbstractMap.SimpleEntry<>(
                         input.getId(),
@@ -230,9 +245,9 @@ private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow,
                 } catch (JsonProcessingException e) {
                     throw new MissingRequiredInput("Invalid JSON format for '" + input.getId() + "' for '" + current + "' with error " + e.getMessage(), e);
                 }
-
-            case URI:
-                Matcher matcher = URI_PATTERN.matcher(((String) current));
+            }
+            case URI -> {
+                Matcher matcher = URI_PATTERN.matcher((String) current);
                 if (matcher.matches()) {
                     return Optional.of(new AbstractMap.SimpleEntry<>(
                         input.getId(),
@@ -241,8 +256,8 @@ private Optional<AbstractMap.SimpleEntry<String, Object>> parseInput(Flow flow,
                 } else {
                     throw new MissingRequiredInput("Invalid URI format for '" + input.getId() + "' for '" + current + "'");
                 }
-
-            default:
+            }
+            default ->
                 throw new MissingRequiredInput("Invalid input type '" + input.getType() + "' for '" + input.getId() + "'");
         }
     }
diff --git a/ui/src/components/executions/Overview.vue b/ui/src/components/executions/Overview.vue
index 1ac557ca39..4fd20024ad 100644
--- a/ui/src/components/executions/Overview.vue
+++ b/ui/src/components/executions/Overview.vue
@@ -118,6 +118,7 @@
             }
         },
         computed: {
+            ...mapState("flow", ["flow"]),
             ...mapState("execution", ["execution"]),
             items() {
                 if (!this.execution) {
@@ -168,7 +169,19 @@
                 return ret;
             },
             inputs() {
-                return toRaw(this.execution.inputs);
+              if (!this.flow) {
+                return []
+              }
+
+              let inputs = toRaw(this.execution.inputs);
+              Object.keys(inputs).forEach(key => {
+                this.flow.inputs.forEach(input => {
+                  if(key === input.name && input.type === 'SECRET') {
+                    inputs[key] = '******';
+                  }
+                })
+              })
+              return inputs;
             }
         },
     };
diff --git a/ui/src/components/flows/FlowRun.vue b/ui/src/components/flows/FlowRun.vue
index cf62548d3d..b093e839f4 100644
--- a/ui/src/components/flows/FlowRun.vue
+++ b/ui/src/components/flows/FlowRun.vue
@@ -20,6 +20,12 @@
                     v-if="input.type === 'STRING' || input.type === 'URI'"
                     v-model="inputs[input.id]"
                 />
+                <el-input
+                    type="password"
+                    v-if="input.type === 'SECRET'"
+                    v-model="inputs[input.id]"
+                    show-password
+                />
                 <el-input-number
                     v-if="input.type === 'INT'"
                     v-model="inputs[input.id]"


Test patch:
diff --git a/core/src/test/java/io/kestra/core/runners/InputsTest.java b/core/src/test/java/io/kestra/core/runners/InputsTest.java
index 58c5cb8d7d..003fc93dfe 100644
--- a/core/src/test/java/io/kestra/core/runners/InputsTest.java
+++ b/core/src/test/java/io/kestra/core/runners/InputsTest.java
@@ -8,6 +8,7 @@
 import io.kestra.core.models.flows.State;
 import io.kestra.core.repositories.FlowRepositoryInterface;
 import io.kestra.core.storages.StorageInterface;
+import io.micronaut.context.annotation.Property;
 import jakarta.inject.Inject;
 import org.junit.jupiter.api.Test;
 
@@ -31,6 +32,7 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 @SuppressWarnings("OptionalGetWithoutIsPresent")
+@Property(name = "kestra.crypto.secret-key", value = "I6EGNzRESu3X3pKZidrqCGOHQFUFC0yK")
 public class InputsTest extends AbstractMemoryRunnerTest {
     public static Map<String, Object> inputs = ImmutableMap.<String, Object>builder()
         .put("string", "myString")
@@ -54,6 +56,7 @@ public class InputsTest extends AbstractMemoryRunnerTest {
         .put("validatedDuration", "PT15S")
         .put("validatedFloat", "0.42")
         .put("validatedTime", "11:27:49")
+        .put("secret", "secret")
         .build();
 
     @Inject
@@ -126,6 +129,7 @@ void allValidInputs() throws URISyntaxException, IOException {
         assertThat(typeds.get("validatedDuration"), is(Duration.parse("PT15S")));
         assertThat(typeds.get("validatedFloat"), is(0.42F));
         assertThat(typeds.get("validatedTime"), is(LocalTime.parse("11:27:49")));
+        assertThat(typeds.get("secret"), not("secret")); // secret inputs are encrypted
     }
 
     @Test
@@ -151,12 +155,17 @@ void inputFlow() throws TimeoutException {
             (flow, execution1) -> runnerUtils.typedInputs(flow, execution1, inputs)
         );
 
-        assertThat(execution.getTaskRunList(), hasSize(5));
+        assertThat(execution.getTaskRunList(), hasSize(6));
         assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
         assertThat(
             (String) execution.findTaskRunsByTaskId("file").get(0).getOutputs().get("value"),
             matchesRegex("kestra:///io/kestra/tests/inputs/executions/.*/inputs/file/application-test.yml")
         );
+        // secret inputs are decrypted to be used as task properties
+        assertThat(
+            (String) execution.findTaskRunsByTaskId("secret").get(0).getOutputs().get("value"),
+            is("secret")
+        );
     }
 
     @Test
diff --git a/core/src/test/java/io/kestra/core/serializers/YamlFlowParserTest.java b/core/src/test/java/io/kestra/core/serializers/YamlFlowParserTest.java
index 1230b1f989..fb141f3218 100644
--- a/core/src/test/java/io/kestra/core/serializers/YamlFlowParserTest.java
+++ b/core/src/test/java/io/kestra/core/serializers/YamlFlowParserTest.java
@@ -125,8 +125,8 @@ void inputsFailed() {
     void inputs() {
         Flow flow = this.parse("flows/valids/inputs.yaml");
 
-        assertThat(flow.getInputs().size(), is(24));
-        assertThat(flow.getInputs().stream().filter(Input::getRequired).count(), is(6L));
+        assertThat(flow.getInputs().size(), is(25));
+        assertThat(flow.getInputs().stream().filter(Input::getRequired).count(), is(7L));
         assertThat(flow.getInputs().stream().filter(r -> !r.getRequired()).count(), is(18L));
         assertThat(flow.getInputs().stream().filter(r -> r.getDefaults() != null).count(), is(1L));
         assertThat(flow.getInputs().stream().filter(r -> r instanceof StringInput && ((StringInput)r).getValidator() != null).count(), is(1L));
diff --git a/core/src/test/resources/flows/valids/inputs.yaml b/core/src/test/resources/flows/valids/inputs.yaml
index e69afad8b1..4afa204471 100644
--- a/core/src/test/resources/flows/valids/inputs.yaml
+++ b/core/src/test/resources/flows/valids/inputs.yaml
@@ -85,6 +85,8 @@ inputs:
   after: "01:00:00"
   before: "11:59:59"
   required: false
+- name: secret
+  type: SECRET
 
 tasks:
 - id: string
@@ -102,3 +104,6 @@ tasks:
 - id: file
   type: io.kestra.core.tasks.debugs.Return
   format: "{{inputs.file}}"
+- id: secret
+  type: io.kestra.core.tasks.debugs.Return
+  format: "{{inputs.secret}}"
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
index d08295d044..284c9a0396 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
@@ -18,6 +18,7 @@
 import io.kestra.core.utils.IdUtils;
 import io.kestra.webserver.controllers.h2.JdbcH2ControllerTest;
 import io.kestra.webserver.responses.PagedResults;
+import io.micronaut.context.annotation.Property;
 import io.micronaut.core.type.Argument;
 import io.micronaut.data.model.Pageable;
 import io.micronaut.http.HttpRequest;
@@ -52,6 +53,7 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+@Property(name = "kestra.crypto.secret-key", value = "I6EGNzRESu3X3pKZidrqCGOHQFUFC0yK")
 class ExecutionControllerTest extends JdbcH2ControllerTest {
     public static final String URL_LABEL_VALUE = "https://some-url.com";
     public static final String ENCODED_URL_LABEL_VALUE = URL_LABEL_VALUE.replace("/", URLEncoder.encode("/", StandardCharsets.UTF_8));
@@ -89,6 +91,7 @@ class ExecutionControllerTest extends JdbcH2ControllerTest {
         .put("float", "42.42")
         .put("instant", "2019-10-06T18:27:49Z")
         .put("file", Objects.requireNonNull(InputsTest.class.getClassLoader().getResource("data/hello.txt")).getPath())
+        .put("secret", "secret")
         .build();
 
     @Test
@@ -126,6 +129,7 @@ private MultipartBody createInputsFlowBody() {
             .addPart("instant", "2019-10-06T18:27:49Z")
             .addPart("files", "file", MediaType.TEXT_PLAIN_TYPE, applicationFile)
             .addPart("files", "optionalFile", MediaType.TEXT_XML_TYPE, logbackFile)
+            .addPart("secret", "secret")
             .build();
     }
 
@@ -159,7 +163,7 @@ void triggerAndWait() {
         Execution result = triggerInputsFlowExecution(true);
 
         assertThat(result.getState().getCurrent(), is(State.Type.SUCCESS));
-        assertThat(result.getTaskRunList().size(), is(5));
+        assertThat(result.getTaskRunList().size(), is(6));
     }
 
     @Test
@@ -432,7 +436,7 @@ void restartFromLastFailed() throws TimeoutException {
     @Test
     void downloadFile() throws TimeoutException {
         Execution execution = runnerUtils.runOne(null, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> runnerUtils.typedInputs(flow, execution1, inputs));
-        assertThat(execution.getTaskRunList(), hasSize(5));
+        assertThat(execution.getTaskRunList(), hasSize(6));
 
         String path = (String) execution.getInputs().get("file");
 
@@ -467,7 +471,7 @@ void downloadFile() throws TimeoutException {
     @Test
     void filePreview() throws TimeoutException {
         Execution defaultExecution = runnerUtils.runOne(null, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> runnerUtils.typedInputs(flow, execution1, inputs));
-        assertThat(defaultExecution.getTaskRunList(), hasSize(5));
+        assertThat(defaultExecution.getTaskRunList(), hasSize(6));
 
         String defaultPath = (String) defaultExecution.getInputs().get("file");
 
@@ -485,10 +489,11 @@ void filePreview() throws TimeoutException {
             .put("float", "42.42")
             .put("instant", "2019-10-06T18:27:49Z")
             .put("file", Objects.requireNonNull(ExecutionControllerTest.class.getClassLoader().getResource("data/iso88591.txt")).getPath())
+            .put("secret", "secret")
             .build();
 
         Execution latin1Execution = runnerUtils.runOne(null, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> runnerUtils.typedInputs(flow, execution1, latin1FileInputs));
-        assertThat(latin1Execution.getTaskRunList(), hasSize(5));
+        assertThat(latin1Execution.getTaskRunList(), hasSize(6));
 
         String latin1Path = (String) latin1Execution.getInputs().get("file");
 
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/PluginControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/PluginControllerTest.java
index 92ca323424..96cb31a68f 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/PluginControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/PluginControllerTest.java
@@ -205,7 +205,7 @@ void inputs() throws URISyntaxException {
                 Argument.listOf(InputType.class)
             );
 
-            assertThat(doc.size(), is(11));
+            assertThat(doc.size(), is(12));
         });
     }