[MNG-8285] Implement mvnenc CLI tool (#1793)

Implements the `mvnenc` tool that is on par with Maven3 master password encryption functionality wise, but is _really secure_ unlike Maven3 conterpart. On the other hand, _is backward compatible if legacy config is setup_.

Implemented goals: `init`, `encrypt`, `decrypt`, `diag`. Also provides one extra "master source" based on Maven infra Prompter: console master password prompt.

---

https://issues.apache.org/jira/browse/MNG-8285
This commit is contained in:
Tamas Cservenak 2024-10-14 21:57:22 +02:00 committed by GitHub
parent 7df5b16f78
commit f5e54ca6fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1079 additions and 116 deletions

View File

@ -85,6 +85,7 @@ under the License.
<include>mvn</include>
<include>mvnenc</include>
<include>mvnDebug</include>
<include>mvnencDebug</include>
<!-- This is so that CI systems can periodically run the profiler -->
<include>mvnyjp</include>
</includes>

View File

@ -0,0 +1,35 @@
#!/bin/sh
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# -----------------------------------------------------------------------------
# Apache Maven Debug Script
#
# Environment Variable Prerequisites
#
# JAVA_HOME (Optional) Points to a Java installation.
# MAVEN_OPTS (Optional) Java runtime options used when Maven is executed.
# MAVEN_SKIP_RC (Optional) Flag to disable loading of mavenrc files.
# MAVEN_DEBUG_ADDRESS (Optional) Set the debug address. Default value is localhost:8000
# -----------------------------------------------------------------------------
MAVEN_DEBUG_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=${MAVEN_DEBUG_ADDRESS:-localhost:8000}"
echo Preparing to execute Maven in debug mode
env MAVEN_OPTS="$MAVEN_OPTS" MAVEN_DEBUG_OPTS="$MAVEN_DEBUG_OPTS" "`dirname "$0"`/mvnenc" "$@"

View File

@ -0,0 +1,44 @@
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM -----------------------------------------------------------------------------
@REM Apache Maven Debug Script
@REM
@REM Environment Variable Prerequisites
@REM
@REM JAVA_HOME (Optional) Points to a Java installation.
@REM MAVEN_BATCH_ECHO (Optional) Set to 'on' to enable the echoing of the batch commands.
@REM MAVEN_BATCH_PAUSE (Optional) set to 'on' to wait for a key stroke before ending.
@REM MAVEN_OPTS (Optional) Java runtime options used when Maven is executed.
@REM MAVEN_SKIP_RC (Optional) Flag to disable loading of mavenrc files.
@REM MAVEN_DEBUG_ADDRESS (Optional) Set the debug address. Default value is localhost:8000
@REM -----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%"=="on" echo %MAVEN_BATCH_ECHO%
@setlocal
if "%MAVEN_DEBUG_ADDRESS%"=="" @set MAVEN_DEBUG_ADDRESS=localhost:8000
@set MAVEN_DEBUG_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%MAVEN_DEBUG_ADDRESS%
@call "%~dp0"mvnenc.cmd %*

View File

@ -36,28 +36,18 @@ import org.apache.maven.api.cli.Options;
@Experimental
public interface EncryptOptions extends Options {
/**
* Returns the cipher that the user wants to use for non-dispatched encryption.
* Should the operation be forced (ie overwrite existing config, if any).
*
* @return an {@link Optional} containing the cipher string, or empty if not specified
* @return an {@link Optional} containing the boolean value {@code true} if specified, or empty
*/
@Nonnull
Optional<String> cipher();
Optional<Boolean> force();
/**
* Returns the master source that the user wants to use for non-dispatched encryption.
* Should imply "yes" to all questions.
*
* @return an {@link Optional} containing the master source string, or empty if not specified
* @return an {@link Optional} containing the boolean value {@code true} if specified, or empty
*/
@Nonnull
Optional<String> masterSource();
/**
* Returns the dispatcher to use for dispatched encryption.
*
* @return an {@link Optional} containing the dispatcher string, or empty if not specified
*/
@Nonnull
Optional<String> dispatcher();
Optional<Boolean> yes();
/**
* Returns the list of encryption goals to be executed.

View File

@ -92,6 +92,10 @@ under the License.
<build>
<plugins>
<plugin>
<groupId>org.eclipse.sisu</groupId>
<artifactId>sisu-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>

View File

@ -25,7 +25,6 @@ import org.apache.maven.api.cli.InvokerException;
import org.apache.maven.api.cli.InvokerRequest;
import org.apache.maven.api.cli.Options;
import org.apache.maven.api.cli.ParserException;
import org.apache.maven.jline.MessageUtils;
import org.codehaus.plexus.classworlds.ClassWorld;
import static java.util.Objects.requireNonNull;
@ -65,8 +64,6 @@ public abstract class ClingSupport<O extends Options, R extends InvokerRequest<O
* The main entry point.
*/
public int run(String[] args) throws IOException {
MessageUtils.systemInstall();
MessageUtils.registerShutdownHook();
try (Invoker<R> invoker = createInvoker()) {
return invoker.invoke(parseArguments(args));
} catch (ParserException e) {
@ -75,12 +72,8 @@ public abstract class ClingSupport<O extends Options, R extends InvokerRequest<O
} catch (InvokerException e) {
return 1;
} finally {
try {
if (classWorldManaged) {
classWorld.close();
}
} finally {
MessageUtils.systemUninstall();
if (classWorldManaged) {
classWorld.close();
}
}
}

View File

@ -29,6 +29,7 @@ import org.apache.maven.cling.invoker.ProtoLookup;
import org.apache.maven.cling.invoker.mvn.DefaultMavenParser;
import org.apache.maven.cling.invoker.mvn.local.DefaultLocalMavenInvoker;
import org.apache.maven.jline.JLineMessageBuilderFactory;
import org.apache.maven.jline.MessageUtils;
import org.codehaus.plexus.classworlds.ClassWorld;
/**
@ -59,6 +60,17 @@ public class MavenCling extends ClingSupport<MavenOptions, MavenInvokerRequest<M
super(classWorld);
}
@Override
public int run(String[] args) throws IOException {
MessageUtils.systemInstall();
MessageUtils.registerShutdownHook();
try {
return super.run(args);
} finally {
MessageUtils.systemUninstall();
}
}
@Override
protected Invoker<MavenInvokerRequest<MavenOptions>> createInvoker() {
return new DefaultLocalMavenInvoker(

View File

@ -30,7 +30,10 @@ import org.apache.maven.cling.invoker.ProtoLookup;
import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptParser;
import org.apache.maven.jline.JLineMessageBuilderFactory;
import org.apache.maven.jline.MessageUtils;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
/**
* Maven encrypt CLI "new-gen".
@ -52,6 +55,8 @@ public class MavenEncCling extends ClingSupport<EncryptOptions, EncryptInvokerRe
return new MavenEncCling(world).run(args);
}
private Terminal terminal;
public MavenEncCling() {
super();
}
@ -60,10 +65,24 @@ public class MavenEncCling extends ClingSupport<EncryptOptions, EncryptInvokerRe
super(classWorld);
}
@Override
public int run(String[] args) throws IOException {
terminal = TerminalBuilder.builder().build();
MessageUtils.systemInstall(terminal);
MessageUtils.registerShutdownHook();
try {
return super.run(args);
} finally {
MessageUtils.systemUninstall();
}
}
@Override
protected Invoker<EncryptInvokerRequest> createInvoker() {
return new DefaultEncryptInvoker(
ProtoLookup.builder().addMapping(ClassWorld.class, classWorld).build());
return new DefaultEncryptInvoker(ProtoLookup.builder()
.addMapping(ClassWorld.class, classWorld)
.addMapping(Terminal.class, terminal)
.build());
}
@Override

View File

@ -72,25 +72,17 @@ public class CommonsCliEncryptOptions extends CommonsCliOptions implements Encry
}
@Override
public Optional<String> cipher() {
if (commandLine.hasOption(CLIManager.CIPHER)) {
return Optional.of(commandLine.getOptionValue(CLIManager.CIPHER));
public Optional<Boolean> force() {
if (commandLine.hasOption(CLIManager.FORCE)) {
return Optional.of(Boolean.TRUE);
}
return Optional.empty();
}
@Override
public Optional<String> masterSource() {
if (commandLine.hasOption(CLIManager.MASTER_SOURCE)) {
return Optional.of(commandLine.getOptionValue(CLIManager.MASTER_SOURCE));
}
return Optional.empty();
}
@Override
public Optional<String> dispatcher() {
if (commandLine.hasOption(CLIManager.DISPATCHER)) {
return Optional.of(commandLine.getOptionValue(CLIManager.DISPATCHER));
public Optional<Boolean> yes() {
if (commandLine.hasOption(CLIManager.YES)) {
return Optional.of(Boolean.TRUE);
}
return Optional.empty();
}
@ -109,24 +101,19 @@ public class CommonsCliEncryptOptions extends CommonsCliOptions implements Encry
}
protected static class CLIManager extends CommonsCliOptions.CLIManager {
public static final String CIPHER = "c";
public static final String MASTER_SOURCE = "m";
public static final String DISPATCHER = "d";
public static final String FORCE = "f";
public static final String YES = "y";
@Override
protected void prepareOptions(org.apache.commons.cli.Options options) {
super.prepareOptions(options);
options.addOption(Option.builder(CIPHER)
.longOpt("cipher")
.desc("The cipher that user wants to use for non-dispatched encryption")
options.addOption(Option.builder(FORCE)
.longOpt("force")
.desc("Should overwrite without asking any configuration?")
.build());
options.addOption(Option.builder(MASTER_SOURCE)
.longOpt("master-source")
.desc("The master source that user wants to use for non-dispatched encryption")
.build());
options.addOption(Option.builder(DISPATCHER)
.longOpt("dispatcher")
.desc("The dispatcher to use for dispatched encryption")
options.addOption(Option.builder(YES)
.longOpt("yes")
.desc("Should imply user answered \"yes\" to all incoming questions?")
.build());
}
}

View File

@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.maven.api.services.Prompter;
import org.apache.maven.api.services.PrompterException;
import org.codehaus.plexus.components.secdispatcher.MasterSource;
import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
/**
* Trivial master password source using Maven {@link Prompter} service.
*/
@Singleton
@Named(ConsolePasswordPrompt.NAME)
public class ConsolePasswordPrompt implements MasterSource, MasterSourceMeta {
public static final String NAME = "console-prompt";
private final Prompter prompter;
@Inject
public ConsolePasswordPrompt(Prompter prompter) {
this.prompter = prompter;
}
@Override
public String description() {
return "Secure console password prompt";
}
@Override
public Optional<String> configTemplate() {
return Optional.empty();
}
@Override
public String handle(String config) throws SecDispatcherException {
if (NAME.equals(config)) {
try {
return prompter.promptForPassword("Enter the master password: ");
} catch (PrompterException e) {
throw new SecDispatcherException("Could not collect the password", e);
}
}
return null;
}
@Override
public SecDispatcher.ValidationResponse validateConfiguration(String config) {
if (NAME.equals(config)) {
return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), true, Map.of(), List.of());
}
return null;
}
}

View File

@ -18,12 +18,27 @@
*/
package org.apache.maven.cling.invoker.mvnenc;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.maven.api.cli.mvnenc.EncryptInvoker;
import org.apache.maven.api.cli.mvnenc.EncryptInvokerRequest;
import org.apache.maven.api.cli.mvnenc.EncryptOptions;
import org.apache.maven.cli.CLIReportingUtils;
import org.apache.maven.cling.invoker.LookupInvoker;
import org.apache.maven.cling.invoker.ProtoLookup;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import org.jline.consoleui.prompt.ConsolePrompt;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.Colors;
import org.jline.utils.OSUtils;
/**
* Encrypt invoker implementation, when Encrypt CLI is being run. System uses ClassWorld launcher, and class world
@ -33,17 +48,36 @@ public class DefaultEncryptInvoker
extends LookupInvoker<EncryptOptions, EncryptInvokerRequest, DefaultEncryptInvoker.LocalContext>
implements EncryptInvoker {
@SuppressWarnings("VisibilityModifier")
public static class LocalContext
extends LookupInvokerContext<EncryptOptions, EncryptInvokerRequest, DefaultEncryptInvoker.LocalContext> {
protected LocalContext(DefaultEncryptInvoker invoker, EncryptInvokerRequest invokerRequest) {
super(invoker, invokerRequest);
}
protected SecDispatcher secDispatcher;
public Map<String, Goal> goals;
public List<AttributedString> header;
public AttributedStyle style;
public LineReader reader;
public ConsolePrompt prompt;
public void addInHeader(String text) {
addInHeader(AttributedStyle.DEFAULT, text);
}
public void addInHeader(AttributedStyle style, String text) {
AttributedStringBuilder asb = new AttributedStringBuilder();
asb.style(style).append(text);
header.add(asb.toAttributedString());
}
}
private final Terminal terminal;
public DefaultEncryptInvoker(ProtoLookup protoLookup) {
super(protoLookup);
this.terminal = protoLookup.lookup(Terminal.class);
}
@Override
@ -58,14 +92,80 @@ public class DefaultEncryptInvoker
@Override
protected void lookup(LocalContext context) {
context.secDispatcher = context.lookup.lookup(SecDispatcher.class);
context.goals = context.lookup.lookupMap(Goal.class);
}
protected int doExecute(LocalContext localContext) throws Exception {
localContext.logger.info("Hello, this is SecDispatcher.");
localContext.logger.info("Available Ciphers: " + localContext.secDispatcher.availableCiphers());
localContext.logger.info("Available Dispatchers: " + localContext.secDispatcher.availableDispatchers());
// TODO: implement mvnenc
return 0;
public static final int OK = 0; // OK
public static final int ERROR = 1; // "generic" error
public static final int BAD_OPERATION = 2; // bad user input or alike
public static final int CANCELED = 3; // user canceled
protected int doExecute(LocalContext context) throws Exception {
if (!context.interactive) {
System.out.println("This tool works only in interactive mode!");
System.out.println("Tool purpose is to configure password management on developer workstations.");
System.out.println(
"Note: Generated configuration can be moved/copied to headless environments, if configured as such.");
return BAD_OPERATION;
}
context.header = new ArrayList<>();
context.style = new AttributedStyle();
context.addInHeader(
context.style.italic().bold().foreground(Colors.rgbColor("green")),
"Maven Encryption " + CLIReportingUtils.showVersionMinimal());
context.addInHeader("Tool for secure password management on workstations.");
context.addInHeader("This tool is part of Apache Maven 4 distribution.");
context.addInHeader("");
try {
Thread executeThread = Thread.currentThread();
terminal.handle(Terminal.Signal.INT, signal -> executeThread.interrupt());
ConsolePrompt.UiConfig config;
if (terminal.getType().equals(Terminal.TYPE_DUMB)
|| terminal.getType().equals(Terminal.TYPE_DUMB_COLOR)) {
System.out.println(terminal.getName() + ": " + terminal.getType());
throw new IllegalStateException("Dumb terminal detected.\nThis tool requires real terminal to work!\n"
+ "Note: On Windows Jansi or JNA library must be included in classpath.");
} else if (OSUtils.IS_WINDOWS) {
config = new ConsolePrompt.UiConfig(">", "( )", "(x)", "( )");
} else {
config = new ConsolePrompt.UiConfig("", "", "", "");
}
config.setCancellableFirstPrompt(true);
context.reader = LineReaderBuilder.builder().terminal(terminal).build();
context.prompt = new ConsolePrompt(context.reader, terminal, config);
if (context.invokerRequest.options().goals().isEmpty()
|| context.invokerRequest.options().goals().get().size() != 1) {
return badGoalsErrorMessage("No goal or multiple goals specified, specify only one goal.", context);
}
String goalName = context.invokerRequest.options().goals().get().get(0);
Goal goal = context.goals.get(goalName);
if (goal == null) {
return badGoalsErrorMessage("Unknown goal: " + goalName, context);
}
return goal.execute(context);
} catch (InterruptedException | InterruptedIOException | UserInterruptException e) {
System.out.println("Goal canceled by user.");
return CANCELED;
} catch (Exception e) {
if (context.invokerRequest.options().showErrors().orElse(false)) {
context.logger.error(e.getMessage(), e);
} else {
context.logger.error(e.getMessage());
}
return ERROR;
}
}
protected int badGoalsErrorMessage(String message, LocalContext context) {
System.out.println(message);
System.out.println("Supported goals are: " + String.join(", ", context.goals.keySet()));
System.out.println("Use -h to display help.");
return BAD_OPERATION;
}
}

View File

@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc;
/**
* The mvnenc tool goal.
*/
public interface Goal {
int execute(DefaultEncryptInvoker.LocalContext context) throws Exception;
}

View File

@ -0,0 +1,94 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc.goals;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.ERROR;
/**
* The support class for goal implementations that requires valid/workable config.
*/
public abstract class ConfiguredGoalSupport extends GoalSupport {
protected ConfiguredGoalSupport(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) {
super(messageBuilderFactory, secDispatcher);
}
@Override
public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception {
if (!validateConfiguration()) {
logger.error(messageBuilderFactory
.builder()
.error("Maven Encryption is not configured, run `mvnenc init` first.")
.build());
return ERROR;
}
return doExecute(context);
}
protected boolean validateConfiguration() {
SecDispatcher.ValidationResponse response = secDispatcher.validateConfiguration();
if (!response.isValid() || logger.isDebugEnabled()) {
dumpResponse("", response);
}
return response.isValid();
}
protected void dumpResponse(String indent, SecDispatcher.ValidationResponse response) {
logger.info(
response.isValid()
? messageBuilderFactory
.builder()
.success("{}Configuration validation of {}: {}")
.build()
: messageBuilderFactory
.builder()
.failure("{}Configuration validation of {}: {}")
.build(),
indent,
response.getSource(),
response.isValid() ? "VALID" : "INVALID");
for (Map.Entry<SecDispatcher.ValidationResponse.Level, List<String>> entry :
response.getReport().entrySet()) {
Consumer<String> consumer =
s -> logger.info(messageBuilderFactory.builder().info(s).build());
if (entry.getKey() == SecDispatcher.ValidationResponse.Level.ERROR) {
consumer = s ->
logger.error(messageBuilderFactory.builder().error(s).build());
} else if (entry.getKey() == SecDispatcher.ValidationResponse.Level.WARNING) {
consumer = s ->
logger.warn(messageBuilderFactory.builder().warning(s).build());
}
for (String line : entry.getValue()) {
consumer.accept(indent + " " + line);
}
}
for (SecDispatcher.ValidationResponse subsystem : response.getSubsystems()) {
dumpResponse(indent + " ", subsystem);
}
}
protected abstract int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception;
}

View File

@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc.goals;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.BAD_OPERATION;
import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
/**
* The "decrypt" goal.
*/
@Singleton
@Named("decrypt")
public class Decrypt extends ConfiguredGoalSupport {
@Inject
public Decrypt(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) {
super(messageBuilderFactory, secDispatcher);
}
@Override
protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception {
String encrypted = context.reader.readLine("Enter the password to decrypt: ");
if (secDispatcher.isAnyEncryptedString(encrypted)) {
logger.info(secDispatcher.decrypt(encrypted));
return OK;
} else {
logger.error("Malformed encrypted string");
return BAD_OPERATION;
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc.goals;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
/**
* The "diag" goal.
*/
@Singleton
@Named("diag")
public class Diag extends ConfiguredGoalSupport {
@Inject
public Diag(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) {
super(messageBuilderFactory, secDispatcher);
}
@Override
protected int doExecute(DefaultEncryptInvoker.LocalContext context) {
dumpResponse("", secDispatcher.validateConfiguration());
return OK;
}
}

View File

@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc.goals;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
/**
* The "encrypt" goal.
*/
@Singleton
@Named("encrypt")
public class Encrypt extends ConfiguredGoalSupport {
@Inject
public Encrypt(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) {
super(messageBuilderFactory, secDispatcher);
}
@Override
protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception {
String cleartext = context.reader.readLine("Enter the password to encrypt: ", '*');
logger.info(secDispatcher.encrypt(cleartext, null));
return OK;
}
}

View File

@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc.goals;
import java.io.IOException;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.cling.invoker.mvnenc.Goal;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The support class for goal implementations.
*/
public abstract class GoalSupport implements Goal {
protected final Logger logger = LoggerFactory.getLogger(getClass());
protected final MessageBuilderFactory messageBuilderFactory;
protected final SecDispatcher secDispatcher;
protected GoalSupport(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) {
this.messageBuilderFactory = messageBuilderFactory;
this.secDispatcher = secDispatcher;
}
protected boolean configExists() throws IOException {
return secDispatcher.readConfiguration(false) != null;
}
}

View File

@ -0,0 +1,275 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.cling.invoker.mvnenc.goals;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import org.codehaus.plexus.components.secdispatcher.model.Config;
import org.codehaus.plexus.components.secdispatcher.model.ConfigProperty;
import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
import org.jline.consoleui.elements.ConfirmChoice;
import org.jline.consoleui.prompt.ConfirmResult;
import org.jline.consoleui.prompt.ConsolePrompt;
import org.jline.consoleui.prompt.PromptResultItemIF;
import org.jline.consoleui.prompt.builder.ListPromptBuilder;
import org.jline.consoleui.prompt.builder.PromptBuilder;
import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import org.jline.utils.Colors;
import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.BAD_OPERATION;
import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
/**
* The "init" goal.
*/
@Singleton
@Named("init")
public class Init extends GoalSupport {
private static final String NONE = "__none__";
@Inject
public Init(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) {
super(messageBuilderFactory, secDispatcher);
}
@Override
public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception {
context.addInHeader(context.style.italic().bold().foreground(Colors.rgbColor("yellow")), "goal: init");
context.addInHeader("");
ConsolePrompt prompt = context.prompt;
boolean force = context.invokerRequest.options().force().orElse(false);
boolean yes = context.invokerRequest.options().yes().orElse(false);
if (configExists() && !force) {
System.out.println(messageBuilderFactory
.builder()
.error("Error: configuration exist. Use --force if you want to reset existing configuration."));
return BAD_OPERATION;
}
SettingsSecurity config = secDispatcher.readConfiguration(true);
// reset config
config.setDefaultDispatcher(null);
config.getConfigurations().clear();
Map<String, PromptResultItemIF> result = prompt.prompt(
context.header, dispatcherPrompt(prompt.getPromptBuilder()).build());
if (result == null) {
throw new InterruptedException();
}
if (NONE.equals(result.get("defaultDispatcher").getResult())) {
logger.warn(messageBuilderFactory
.builder()
.warning(
"Maven4 SecDispatcher disabled; Maven3 fallback may still work, use `mvnenc diag` to check")
.build());
secDispatcher.writeConfiguration(config);
return OK;
}
config.setDefaultDispatcher(result.get("defaultDispatcher").getResult());
DispatcherMeta meta = secDispatcher.availableDispatchers().stream()
.filter(d -> Objects.equals(config.getDefaultDispatcher(), d.name()))
.findFirst()
.orElseThrow();
if (!meta.fields().isEmpty()) {
result = prompt.prompt(
context.header,
configureDispatcher(context, meta, prompt.getPromptBuilder())
.build());
if (result == null) {
throw new InterruptedException();
}
List<Map.Entry<String, PromptResultItemIF>> editables = result.entrySet().stream()
.filter(e -> e.getValue().getResult().contains("$"))
.toList();
if (!editables.isEmpty()) {
context.addInHeader("");
context.addInHeader("Please customize the editable value:");
Map<String, PromptResultItemIF> editMap;
for (Map.Entry<String, PromptResultItemIF> editable : editables) {
String template = editable.getValue().getResult();
String prefix = template.substring(0, template.indexOf("$"));
editMap = prompt.prompt(
context.header,
prompt.getPromptBuilder()
.createInputPrompt()
.name("edit")
.message(template)
.addCompleter(new Completer() {
@Override
public void complete(
LineReader reader, ParsedLine line, List<Candidate> candidates) {
if (!line.line().startsWith(prefix)) {
candidates.add(
new Candidate(prefix, prefix, null, null, null, null, false));
}
}
})
.addPrompt()
.build());
if (editMap == null) {
throw new InterruptedException();
}
result.put(editable.getKey(), editMap.get("edit"));
}
}
Config dispatcherConfig = new Config();
dispatcherConfig.setName(meta.name());
for (DispatcherMeta.Field field : meta.fields()) {
ConfigProperty property = new ConfigProperty();
property.setName(field.getKey());
property.setValue(result.get(field.getKey()).getResult());
dispatcherConfig.addProperty(property);
}
if (!dispatcherConfig.getProperties().isEmpty()) {
config.addConfiguration(dispatcherConfig);
}
}
if (yes) {
secDispatcher.writeConfiguration(config);
} else {
context.addInHeader("");
context.addInHeader("Values set:");
context.addInHeader("defaultDispatcher=" + config.getDefaultDispatcher());
for (Config c : config.getConfigurations()) {
context.addInHeader(" dispatcherName=" + c.getName());
for (ConfigProperty cp : c.getProperties()) {
context.addInHeader(" " + cp.getName() + "=" + cp.getValue());
}
}
result = prompt.prompt(
context.header, confirmPrompt(prompt.getPromptBuilder()).build());
ConfirmResult confirm = (ConfirmResult) result.get("confirm");
if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) {
logger.info(messageBuilderFactory
.builder()
.info("Writing out the configuration...")
.build());
secDispatcher.writeConfiguration(config);
} else {
logger.warn(messageBuilderFactory
.builder()
.warning("Values not accepted; not saving configuration.")
.build());
return BAD_OPERATION;
}
}
return OK;
}
protected PromptBuilder confirmPrompt(PromptBuilder promptBuilder) {
promptBuilder
.createConfirmPromp()
.name("confirm")
.message("Are values above correct?")
.defaultValue(ConfirmChoice.ConfirmationValue.YES)
.addPrompt();
return promptBuilder;
}
protected PromptBuilder dispatcherPrompt(PromptBuilder promptBuilder) {
ListPromptBuilder listPromptBuilder = promptBuilder
.createListPrompt()
.name("defaultDispatcher")
.message("Which dispatcher you want to use as default?");
listPromptBuilder
.newItem()
.name(NONE)
.text("None (disable MavenSecDispatcher)")
.add();
for (DispatcherMeta meta : secDispatcher.availableDispatchers()) {
if (!meta.isHidden()) {
listPromptBuilder
.newItem()
.name(meta.name())
.text(meta.displayName())
.add();
}
}
listPromptBuilder.addPrompt();
return promptBuilder;
}
private PromptBuilder configureDispatcher(
DefaultEncryptInvoker.LocalContext context, DispatcherMeta dispatcherMeta, PromptBuilder promptBuilder)
throws Exception {
context.addInHeader(
context.style.italic().bold().foreground(Colors.rgbColor("yellow")),
"Configure " + dispatcherMeta.displayName());
context.addInHeader("");
for (DispatcherMeta.Field field : dispatcherMeta.fields()) {
String fieldKey = field.getKey();
String fieldDescription = "Configure " + fieldKey + ": " + field.getDescription();
if (field.getOptions().isPresent()) {
// list options
ListPromptBuilder listPromptBuilder =
promptBuilder.createListPrompt().name(fieldKey).message(fieldDescription);
for (DispatcherMeta.Field option : field.getOptions().get()) {
listPromptBuilder
.newItem()
.name(
option.getDefaultValue().isPresent()
? option.getDefaultValue().get()
: option.getKey())
.text(option.getDescription())
.add();
}
listPromptBuilder.addPrompt();
} else if (field.getDefaultValue().isPresent()) {
// input w/ def value
promptBuilder
.createInputPrompt()
.name(fieldKey)
.message(fieldDescription)
.defaultValue(field.getDefaultValue().get())
.addPrompt();
} else {
// ? plain input?
promptBuilder
.createInputPrompt()
.name(fieldKey)
.message(fieldDescription)
.addPrompt();
}
}
return promptBuilder;
}
}

View File

@ -214,10 +214,15 @@ public class DefaultRepositorySystemSessionFactory implements RepositorySystemSe
decrypt.setProxies(request.getProxies());
decrypt.setServers(request.getServers());
SettingsDecryptionResult decrypted = settingsDecrypter.decrypt(decrypt);
if (logger.isDebugEnabled()) {
for (SettingsProblem problem : decrypted.getProblems()) {
logger.debug(problem.getMessage(), problem.getException());
for (SettingsProblem problem : decrypted.getProblems()) {
if (problem.getSeverity() == SettingsProblem.Severity.WARNING) {
logger.warn(problem.getMessage());
} else if (problem.getSeverity() == SettingsProblem.Severity.ERROR) {
logger.error(
problem.getMessage(),
request.isShowErrors()
? problem.getException()
: problem.getException().getMessage());
}
}

View File

@ -88,10 +88,6 @@ under the License.
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-sec-dispatcher</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-cipher</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-interpolation</artifactId>

View File

@ -42,6 +42,18 @@ under the License.
<groupId>org.jline</groupId>
<artifactId>jline-reader</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-style</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-builtins</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-console-ui</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-terminal-jni</artifactId>

View File

@ -62,10 +62,6 @@ under the License.
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-sec-dispatcher</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-cipher</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>

View File

@ -22,6 +22,7 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -58,28 +59,52 @@ public class DefaultSettingsDecrypter implements SettingsDecrypter {
for (Server server : request.getServers()) {
server = server.clone();
try {
server.setPassword(decrypt(server.getPassword()));
} catch (SecDispatcherException e) {
problems.add(new DefaultSettingsProblem(
"Failed to decrypt password for server " + server.getId() + ": " + e.getMessage(),
Severity.ERROR,
"server: " + server.getId(),
-1,
-1,
e));
String password = server.getPassword();
if (securityDispatcher.isAnyEncryptedString(password)) {
try {
if (securityDispatcher.isLegacyEncryptedString(password)) {
problems.add(new DefaultSettingsProblem(
"Legacy/insecurely encrypted password detected for server " + server.getId(),
Severity.WARNING,
"server: " + server.getId(),
-1,
-1,
null));
}
server.setPassword(securityDispatcher.decrypt(password));
} catch (SecDispatcherException | IOException e) {
problems.add(new DefaultSettingsProblem(
"Failed to decrypt password for server " + server.getId() + ": " + e.getMessage(),
Severity.ERROR,
"server: " + server.getId(),
-1,
-1,
e));
}
}
try {
server.setPassphrase(decrypt(server.getPassphrase()));
} catch (SecDispatcherException e) {
problems.add(new DefaultSettingsProblem(
"Failed to decrypt passphrase for server " + server.getId() + ": " + e.getMessage(),
Severity.ERROR,
"server: " + server.getId(),
-1,
-1,
e));
String passphrase = server.getPassphrase();
if (securityDispatcher.isAnyEncryptedString(passphrase)) {
try {
if (securityDispatcher.isLegacyEncryptedString(passphrase)) {
problems.add(new DefaultSettingsProblem(
"Legacy/insecurely encrypted passphrase detected for server " + server.getId(),
Severity.WARNING,
"server: " + server.getId(),
-1,
-1,
null));
}
server.setPassphrase(securityDispatcher.decrypt(passphrase));
} catch (SecDispatcherException | IOException e) {
problems.add(new DefaultSettingsProblem(
"Failed to decrypt passphrase for server " + server.getId() + ": " + e.getMessage(),
Severity.ERROR,
"server: " + server.getId(),
-1,
-1,
e));
}
}
servers.add(server);
@ -88,16 +113,28 @@ public class DefaultSettingsDecrypter implements SettingsDecrypter {
List<Proxy> proxies = new ArrayList<>();
for (Proxy proxy : request.getProxies()) {
try {
proxy.setPassword(decrypt(proxy.getPassword()));
} catch (SecDispatcherException e) {
problems.add(new DefaultSettingsProblem(
"Failed to decrypt password for proxy " + proxy.getId() + ": " + e.getMessage(),
Severity.ERROR,
"proxy: " + proxy.getId(),
-1,
-1,
e));
String password = proxy.getPassword();
if (securityDispatcher.isAnyEncryptedString(password)) {
try {
if (securityDispatcher.isLegacyEncryptedString(password)) {
problems.add(new DefaultSettingsProblem(
"Legacy/insecurely encrypted password detected for proxy " + proxy.getId(),
Severity.WARNING,
"proxy: " + proxy.getId(),
-1,
-1,
null));
}
proxy.setPassword(securityDispatcher.decrypt(password));
} catch (SecDispatcherException | IOException e) {
problems.add(new DefaultSettingsProblem(
"Failed to decrypt password for proxy " + proxy.getId() + ": " + e.getMessage(),
Severity.ERROR,
"proxy: " + proxy.getId(),
-1,
-1,
e));
}
}
proxies.add(proxy);
@ -105,8 +142,4 @@ public class DefaultSettingsDecrypter implements SettingsDecrypter {
return new DefaultSettingsDecryptionResult(servers, proxies, problems);
}
private String decrypt(String str) throws SecDispatcherException {
return (str == null) ? null : securityDispatcher.decrypt(str);
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.settings.crypto;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import org.apache.maven.api.Constants;
import org.codehaus.plexus.components.secdispatcher.Dispatcher;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import org.codehaus.plexus.components.secdispatcher.internal.DefaultSecDispatcher;
/**
* This class implements "Maven specific" {@link SecDispatcher}.
*
* @deprecated since 4.0.0
*/
@Named
@Singleton
@Deprecated(since = "4.0.0")
public class MavenSecDispatcher extends DefaultSecDispatcher {
private static final String FILE_NAME = "settings-security4.xml";
@Inject
public MavenSecDispatcher(Map<String, Dispatcher> dispatchers) {
super(dispatchers, configurationFile());
}
private static Path configurationFile() {
String mavenUserConf = System.getProperty(Constants.MAVEN_USER_CONF);
if (mavenUserConf != null) {
return Paths.get(mavenUserConf, FILE_NAME);
}
// this means we are in UT or alike
return Paths.get(System.getProperty("user.home"), ".m2", FILE_NAME);
}
}

23
pom.xml
View File

@ -167,7 +167,6 @@ under the License.
<assertjVersion>3.26.3</assertjVersion>
<asmVersion>9.7.1</asmVersion>
<byteBuddyVersion>1.15.3</byteBuddyVersion>
<cipherVersion>3.0.0</cipherVersion>
<classWorldsVersion>2.8.0</classWorldsVersion>
<commonsCliVersion>1.9.0</commonsCliVersion>
<guiceVersion>6.0.0</guiceVersion>
@ -186,7 +185,7 @@ under the License.
<plexusTestingVersion>1.4.0</plexusTestingVersion>
<plexusXmlVersion>4.0.4</plexusXmlVersion>
<resolverVersion>2.0.1</resolverVersion>
<securityDispatcherVersion>3.0.0</securityDispatcherVersion>
<securityDispatcherVersion>4.0.1</securityDispatcherVersion>
<sisuVersion>0.9.0.M3</sisuVersion>
<slf4jVersion>2.0.16</slf4jVersion>
<stax2ApiVersion>4.2.2</stax2ApiVersion>
@ -475,6 +474,21 @@ under the License.
<artifactId>jline-reader</artifactId>
<version>${jlineVersion}</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-style</artifactId>
<version>${jlineVersion}</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-builtins</artifactId>
<version>${jlineVersion}</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-console-ui</artifactId>
<version>${jlineVersion}</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-terminal-ffm</artifactId>
@ -590,11 +604,6 @@ under the License.
<artifactId>plexus-sec-dispatcher</artifactId>
<version>${securityDispatcherVersion}</version>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-cipher</artifactId>
<version>${cipherVersion}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.woodstox</groupId>
<artifactId>woodstox-core</artifactId>