diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityException.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityException.java
new file mode 100644
index 00000000000..6df5f9db285
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator._common.exception;
+
+/**
+ * Aviator connection failure with a message that is safe to show.
+ *
+ * The transport cause is not attached: its text can contain proxy credentials or
+ * host details, and fcli prints and logs the cause chain of simple exceptions.
+ */
+public final class AviatorConnectivityException extends AviatorSimpleException {
+ private static final long serialVersionUID = 1L;
+
+ public AviatorConnectivityException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectionContext.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectionContext.java
new file mode 100644
index 00000000000..e2746ef4d64
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectionContext.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.grpc;
+
+import java.util.Objects;
+
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan;
+
+import io.grpc.CallOptions;
+
+/**
+ * Endpoints retained for error messages, without proxy credentials.
+ * @param proxy null for a direct connection
+ */
+record AviatorConnectionContext(AviatorEndpoint target, AviatorEndpoint proxy) {
+ // Present only on fail-fast unary stubs; absence preserves the existing error mapping.
+ static final CallOptions.Key CALL_OPTION = CallOptions.Key.create("aviator-connectivity");
+
+ AviatorConnectionContext {
+ Objects.requireNonNull(target, "target");
+ }
+
+ static AviatorConnectionContext of(AviatorConnectionPlan plan) {
+ var target = new AviatorEndpoint(plan.target().host(), plan.effectivePort());
+ var proxy = plan.proxyDescriptor()
+ .map(d -> new AviatorEndpoint(d.getProxyHost(), d.getProxyPort()))
+ .orElse(null);
+ return new AviatorConnectionContext(target, proxy);
+ }
+
+ boolean isProxied() {
+ return proxy!=null;
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKind.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKind.java
new file mode 100644
index 00000000000..05b6776be16
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKind.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.grpc;
+
+import java.net.ConnectException;
+import java.net.UnknownHostException;
+import java.nio.channels.UnresolvedAddressException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
+import java.util.Optional;
+
+import javax.net.ssl.SSLException;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import com.fortify.cli.aviator.util.Constants;
+
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.netty.shaded.io.netty.channel.ConnectTimeoutException;
+import io.grpc.netty.shaded.io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException;
+import io.grpc.netty.shaded.io.netty.handler.proxy.ProxyConnectException;
+
+/** Shared classification and messages for ordinary unary request failures. */
+enum AviatorConnectivityFailureKind {
+ PROXY_REJECTED,
+ PROXY_NEGOTIATION_FAILED,
+ CONNECT_TIMEOUT,
+ PROXY_REFUSED,
+ DIRECT_REFUSED,
+ DNS_FAILURE,
+ TLS_FAILURE,
+ UNAVAILABLE,
+ DEADLINE_EXCEEDED;
+
+ /**
+ * Leaves certificate failures to the fcli PKIX trust-store guidance, and application
+ * statuses to the existing exception mapping.
+ */
+ static Optional classify(StatusRuntimeException e, AviatorConnectionContext context) {
+ var code = e.getStatus().getCode();
+ if ( code!=Status.Code.UNAVAILABLE && code!=Status.Code.DEADLINE_EXCEEDED ) {
+ return Optional.empty();
+ }
+ if ( isCertificateFailure(e) ) {
+ return Optional.empty();
+ }
+ if ( has(e, SSLException.class) ) {
+ return Optional.of(TLS_FAILURE);
+ }
+ return Optional.of(classifyTransport(e, context, code));
+ }
+
+ // JDK PKIX and host name failures always carry one of these causes; other
+ // handshake failures, such as timeouts or protocol mismatches, are TLS_FAILURE.
+ private static boolean isCertificateFailure(Throwable e) {
+ return has(e, CertificateException.class) || has(e, CertPathValidatorException.class);
+ }
+
+ // The order is significant: all Netty connect and proxy exceptions extend ConnectException.
+ private static AviatorConnectivityFailureKind classifyTransport(Throwable e, AviatorConnectionContext context, Status.Code code) {
+ var neutral = code==Status.Code.DEADLINE_EXCEEDED ? DEADLINE_EXCEEDED : UNAVAILABLE;
+ if ( has(e, ProxyConnectException.class) ) {
+ // A JVM or GRPC_PROXY_EXP proxy can be active without an fcli proxy; fcli cannot name it.
+ if ( !context.isProxied() ) { return neutral; }
+ return has(e, HttpProxyConnectException.class) ? PROXY_REJECTED : PROXY_NEGOTIATION_FAILED;
+ }
+ if ( has(e, ConnectTimeoutException.class) ) { return CONNECT_TIMEOUT; }
+ // With an HTTP CONNECT proxy, the proxy is the only TCP peer of the client;
+ // a target refusal arrives as HttpProxyConnectException instead.
+ if ( isConnectionRefused(e) ) { return context.isProxied() ? PROXY_REFUSED : DIRECT_REFUSED; }
+ if ( has(e, UnknownHostException.class) || has(e, UnresolvedAddressException.class) ) { return DNS_FAILURE; }
+ return neutral;
+ }
+
+ // Java has no refusal-specific exception type. Match complete known reasons, never
+ // substrings that could come from a host name; unknown/localized reasons stay neutral.
+ private static boolean isConnectionRefused(Throwable e) {
+ return ExceptionUtils.getThrowableList(e).stream()
+ .filter(ConnectException.class::isInstance)
+ .map(Throwable::getMessage)
+ .anyMatch(message -> "Connection refused".equalsIgnoreCase(message)
+ || "Connection refused: getsockopt".equalsIgnoreCase(message)
+ || "Connection refused: no further information".equalsIgnoreCase(message)
+ || "connect(..) failed: Connection refused".equalsIgnoreCase(message)
+ || "finishConnect(..) failed: Connection refused".equalsIgnoreCase(message));
+ }
+
+ private static boolean has(Throwable e, Class extends Throwable> type) {
+ return ExceptionUtils.indexOfType(e, type) >= 0;
+ }
+
+ String formatMessage(AviatorConnectionContext context) {
+ var server = "Aviator server " + context.target().display();
+ var proxy = context.isProxied() ? "proxy " + context.proxy().display() : null;
+ return switch (this) {
+ case PROXY_REJECTED -> "Proxy " + context.proxy().display() + " rejected the connection to " + server
+ + "; check the proxy credentials and access rules";
+ case PROXY_NEGOTIATION_FAILED -> "Could not connect to " + server + " through " + proxy
+ + "; check that the proxy allows HTTPS connections to the Aviator server";
+ case CONNECT_TIMEOUT -> "Timed out connecting to " + (proxy==null ? server : proxy)
+ + "; check network access and firewall rules";
+ case PROXY_REFUSED -> "Proxy " + context.proxy().display() + " refused the connection; check the proxy host and port";
+ case DIRECT_REFUSED -> server + " refused the connection; check the Aviator URL";
+ case DNS_FAILURE -> proxy==null
+ ? "Could not resolve the host name of " + server + "; check the Aviator URL"
+ : "Could not resolve the host name of " + proxy + " or " + server + "; check the proxy settings and the Aviator URL";
+ case TLS_FAILURE -> "Could not set up a TLS connection to " + server
+ + "; check that the Aviator URL uses the HTTPS port of the Aviator server";
+ case UNAVAILABLE -> "Cannot reach " + server + "; check the Aviator URL, network access, and proxy settings";
+ case DEADLINE_EXCEEDED -> server + " did not respond within " + Constants.DEFAULT_TIMEOUT_SECONDS
+ + " seconds; check network access and proxy settings, or try again later";
+ };
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorEndpoint.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorEndpoint.java
new file mode 100644
index 00000000000..234500c611e
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorEndpoint.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.grpc;
+
+/** Replaces control characters in host names before display. */
+record AviatorEndpoint(String host, int port) {
+ private static final String UNKNOWN_HOST = "unknown";
+
+ AviatorEndpoint {
+ host = sanitize(host);
+ }
+
+ String display() {
+ var isUnbracketedIpv6 = host.contains(":") && !host.startsWith("[");
+ return isUnbracketedIpv6 ? "[" + host + "]:" + port : host + ":" + port;
+ }
+
+ private static String sanitize(String host) {
+ if ( host==null || host.isBlank() ) {
+ return UNKNOWN_HOST;
+ }
+ var result = new StringBuilder(host.length());
+ host.codePoints().forEach(c -> result.appendCodePoint(Character.isISOControl(c) ? '?' : c));
+ return result.toString();
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java
index 9903b9a72b6..6e99f9b1f67 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java
@@ -14,6 +14,7 @@
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
@@ -95,14 +96,29 @@ public class AviatorGrpcClient implements AutoCloseable {
final AtomicBoolean isShutdown = new AtomicBoolean(false);
public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) {
+ this(channel, defaultTimeoutSeconds, logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY, null);
+ }
+
+ /** Fail-fast clients require a connection context for error messages. */
+ AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds,
+ AviatorRequestPolicy requestPolicy, AviatorConnectionContext connectionContext) {
LOG.debug("Initializing AviatorGrpcClient with ManagedChannel");
+ Objects.requireNonNull(requestPolicy, "requestPolicy");
this.logger = logger;
this.channel = channel;
this.asyncStub = AuditorServiceGrpc.newStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
- this.blockingStub = ApplicationServiceGrpc.newBlockingStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
- this.tokenServiceBlockingStub = TokenServiceGrpc.newBlockingStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
- this.entitlementServiceBlockingStub = EntitlementServiceGrpc.newBlockingStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
- this.dastEntitlementServiceBlockingStub = DastEntitlementServiceGrpc.newBlockingStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
+ this.blockingStub = requestPolicy.configureNewStub(ApplicationServiceGrpc.newBlockingStub(channel)
+ .withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE)
+ .withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE), connectionContext);
+ this.tokenServiceBlockingStub = requestPolicy.configureNewStub(TokenServiceGrpc.newBlockingStub(channel)
+ .withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE)
+ .withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE), connectionContext);
+ this.entitlementServiceBlockingStub = requestPolicy.configureNewStub(EntitlementServiceGrpc.newBlockingStub(channel)
+ .withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE)
+ .withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE), connectionContext);
+ this.dastEntitlementServiceBlockingStub = requestPolicy.configureNewStub(DastEntitlementServiceGrpc.newBlockingStub(channel)
+ .withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE)
+ .withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE), connectionContext);
this.correlationAsyncStub = CorrelationServiceGrpc.newStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
this.dastAuditAsyncStub = DastAuditServiceGrpc.newStub(channel)
.withCompression("gzip")
@@ -189,27 +205,32 @@ public void close() {
public Application createApplication(String name, String tenantName, String signature, String message) {
CreateApplicationRequest request = CreateApplicationRequest.newBuilder().setName(name).setTenantName(tenantName).setSignature(signature).setMessage(message).build();
- return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::createApplication, request, Constants.OP_CREATE_APP);
+ return GrpcUtil.executeGrpcCall(blockingStub,
+ ApplicationServiceGrpc.ApplicationServiceBlockingStub::createApplication, request, Constants.OP_CREATE_APP);
}
public Application updateApplication(String projectId, String newName, String signature, String message, String tenantName) {
UpdateApplicationRequest request = UpdateApplicationRequest.newBuilder().setId(Long.parseLong(projectId)).setName(newName).setTenantName(tenantName).setSignature(signature).setMessage(message).build();
- return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::updateApplication, request, Constants.OP_UPDATE_APP);
+ return GrpcUtil.executeGrpcCall(blockingStub,
+ ApplicationServiceGrpc.ApplicationServiceBlockingStub::updateApplication, request, Constants.OP_UPDATE_APP);
}
public Application addEntitlement(String projectId, String signature, String message, String tenantName) {
ApplicationById request = ApplicationById.newBuilder().setId(Long.parseLong(projectId)).setSignature(signature).setMessage(message).setTenantName(tenantName).build();
- return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::addEntitlement, request, Constants.OP_ADD_APP_ENTITLEMENT);
+ return GrpcUtil.executeGrpcCall(blockingStub,
+ ApplicationServiceGrpc.ApplicationServiceBlockingStub::addEntitlement, request, Constants.OP_ADD_APP_ENTITLEMENT);
}
public ApplicationResponseMessage deleteApplication(String projectId, String signature, String message, String tenantName) {
ApplicationById request = ApplicationById.newBuilder().setId(Long.parseLong(projectId)).setSignature(signature).setMessage(message).setTenantName(tenantName).build();
- return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::deleteApplication, request, Constants.OP_DELETE_APP);
+ return GrpcUtil.executeGrpcCall(blockingStub,
+ ApplicationServiceGrpc.ApplicationServiceBlockingStub::deleteApplication, request, Constants.OP_DELETE_APP);
}
public Application getApplication(String projectId, String signature, String message, String tenantName) {
ApplicationById request = ApplicationById.newBuilder().setId(Long.parseLong(projectId)).setSignature(signature).setMessage(message).setTenantName(tenantName).build();
- return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplication, request, Constants.OP_GET_APP);
+ return GrpcUtil.executeGrpcCall(blockingStub,
+ ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplication, request, Constants.OP_GET_APP);
}
public Application getApplicationByToken(String token, String appName) {
@@ -217,7 +238,8 @@ public Application getApplicationByToken(String token, String appName) {
.setToken(token)
.setAppName(appName)
.build();
- return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplicationByToken, request, Constants.OP_GET_APP_BY_TOKEN);
+ return GrpcUtil.executeGrpcCall(blockingStub,
+ ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplicationByToken, request, Constants.OP_GET_APP_BY_TOKEN);
}
public long getDefaultQuota(String token) {
@@ -250,18 +272,21 @@ public void validateAdminSession(String tenantName, String signature, String mes
public List listApplication(String tenantName, String signature, String message) {
ApplicationByTenantName request = ApplicationByTenantName.newBuilder().setName(tenantName).setSignature(signature).setMessage(message).build();
- ApplicationList applicationList = GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::listApplications, request, Constants.OP_LIST_APPS);
+ ApplicationList applicationList = GrpcUtil.executeGrpcCall(blockingStub,
+ ApplicationServiceGrpc.ApplicationServiceBlockingStub::listApplications, request, Constants.OP_LIST_APPS);
return applicationList.getApplicationsList();
}
public TokenGenerationResponse generateToken(String email, String tokenName, String signature, String message, String tenantName, String endDate) {
TokenGenerationRequest request = TokenGenerationRequest.newBuilder().setEmail(email != null ? email : "").setCustomTokenName(tokenName != null ? tokenName : "").setRequestSignature(signature).setEndDate(com.fortify.cli.aviator.util.StringUtil.isEmpty(endDate) ? "" : endDate).setMessage(message).setTenantName(tenantName).build();
- return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::generateToken, request, Constants.OP_GENERATE_TOKEN);
+ return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub,
+ TokenServiceGrpc.TokenServiceBlockingStub::generateToken, request, Constants.OP_GENERATE_TOKEN);
}
public ListTokensResponse listTokens(String email, String tenantName, String signature, String message) {
ListTokensRequest request = ListTokensRequest.newBuilder().setRequestSignature(signature).setMessage(message).setTenantName(tenantName).setIgnorePagination(true).build();
- return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::listTokens, request, Constants.OP_LIST_TOKENS);
+ return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub,
+ TokenServiceGrpc.TokenServiceBlockingStub::listTokens, request, Constants.OP_LIST_TOKENS);
}
public ListTokensResponse listTokensByDeveloper(String tenantName, String developerEmail, String signature, String message) {
@@ -270,38 +295,46 @@ public ListTokensResponse listTokensByDeveloper(String tenantName, String develo
requestBuilder.setDeveloperEmail(developerEmail);
}
ListTokensByDeveloperRequest request = requestBuilder.build();
- return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::listTokensByDeveloper, request, Constants.OP_LIST_TOKENS_BY_DEVELOPER);
+ return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub,
+ TokenServiceGrpc.TokenServiceBlockingStub::listTokensByDeveloper, request, Constants.OP_LIST_TOKENS_BY_DEVELOPER);
}
public RevokeTokenResponse revokeToken(String token, String email, String tenantName, String signature, String message) {
RevokeTokenRequest request = RevokeTokenRequest.newBuilder().setToken(token).setTenantName(tenantName).setRequestSignature(signature).setMessage(message).build();
- return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::revokeToken, request, Constants.OP_REVOKE_TOKEN);
+ return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub,
+ TokenServiceGrpc.TokenServiceBlockingStub::revokeToken, request, Constants.OP_REVOKE_TOKEN);
}
public DeleteTokenResponse deleteToken(String token, String email, String tenantName, String signature, String message) {
DeleteTokenRequest request = DeleteTokenRequest.newBuilder().setToken(token).setTenantName(tenantName).setRequestSignature(signature).setMessage(message).build();
- return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::deleteToken, request, Constants.OP_DELETE_TOKEN);
+ return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub,
+ TokenServiceGrpc.TokenServiceBlockingStub::deleteToken, request, Constants.OP_DELETE_TOKEN);
}
public TokenValidationResponse validateToken(String token, String tenantName, String signature, String message) {
TokenValidationRequest request = TokenValidationRequest.newBuilder().setToken(token).setTenantName(tenantName).setRequestSignature(signature).setMessage(message).build();
- return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::validateToken, request, Constants.OP_VALIDATE_TOKEN);
+ return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub,
+ TokenServiceGrpc.TokenServiceBlockingStub::validateToken, request, Constants.OP_VALIDATE_TOKEN);
}
public TokenValidationResponse validateUserToken(String token, String tenantName) {
ValidateUserTokenRequest request = ValidateUserTokenRequest.newBuilder().setToken(token).setTenantName(tenantName != null ? tenantName : "").build();
- return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::validateUserToken, request, Constants.OP_VALIDATE_USER_TOKEN);
+ return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub,
+ TokenServiceGrpc.TokenServiceBlockingStub::validateUserToken, request, Constants.OP_VALIDATE_USER_TOKEN);
}
public List listEntitlements(String tenantName, String signature, String message) {
ListEntitlementsByTenantRequest request = ListEntitlementsByTenantRequest.newBuilder().setTenantName(tenantName).setSignature(signature).setMessage(message).build();
- ListEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(entitlementServiceBlockingStub, EntitlementServiceGrpc.EntitlementServiceBlockingStub::listEntitlementsByTenant, request, Constants.OP_LIST_ENTITLEMENTS);
+ ListEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(entitlementServiceBlockingStub,
+ EntitlementServiceGrpc.EntitlementServiceBlockingStub::listEntitlementsByTenant, request, Constants.OP_LIST_ENTITLEMENTS);
return response.getEntitlementsList();
}
public List listDastEntitlements(String tenantName, String signature, String message) {
ListDastEntitlementsByTenantRequest request = ListDastEntitlementsByTenantRequest.newBuilder().setTenantName(tenantName).setSignature(signature).setMessage(message).build();
- ListDastEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(dastEntitlementServiceBlockingStub, DastEntitlementServiceGrpc.DastEntitlementServiceBlockingStub::listDastEntitlementsByTenant, request, Constants.OP_LIST_DAST_ENTITLEMENTS);
+ ListDastEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(dastEntitlementServiceBlockingStub,
+ DastEntitlementServiceGrpc.DastEntitlementServiceBlockingStub::listDastEntitlementsByTenant,
+ request, Constants.OP_LIST_DAST_ENTITLEMENTS);
return response.getEntitlementsList();
}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelper.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelper.java
index d01c9d04586..c6045123392 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelper.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelper.java
@@ -45,11 +45,20 @@ public class AviatorGrpcClientHelper {
private static final String AVIATOR_MODULE = "aviator";
public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, long pingIntervalSeconds) throws AviatorSimpleException {
+ return createClient(url, logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY);
+ }
+
+ public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, long pingIntervalSeconds,
+ AviatorRequestPolicy policy) throws AviatorSimpleException {
if (url == null || url.trim().isEmpty()) {
throw new AviatorSimpleException("Aviator URL cannot be null or empty.");
}
+ return createClient(createConnectionPlan(url), logger, pingIntervalSeconds, policy);
+ }
- var connectionPlan = createConnectionPlan(url);
+ /** Accepts an explicit route so callers need not resolve proxy settings again. */
+ static AviatorGrpcClient createClient(AviatorConnectionPlan connectionPlan, IAviatorLogger logger, long pingIntervalSeconds,
+ AviatorRequestPolicy policy) {
var proxyDescriptor = connectionPlan.proxyDescriptor();
var target = connectionPlan.target();
var builder = target.port() == null
@@ -63,7 +72,8 @@ public static AviatorGrpcClient createClient(String url, IAviatorLogger logger,
}
var channel = configureBuilder(builder, proxyDescriptor).build();
- return new AviatorGrpcClient(channel, Constants.DEFAULT_TIMEOUT_SECONDS, logger, pingIntervalSeconds);
+ return new AviatorGrpcClient(channel, Constants.DEFAULT_TIMEOUT_SECONDS, logger, pingIntervalSeconds,
+ policy, AviatorConnectionContext.of(connectionPlan));
}
public static AviatorConnectionPlan createConnectionPlan(String url) {
@@ -79,27 +89,29 @@ public static AviatorConnectionPlan createConnectionPlan(String url) {
public static ParsedTarget parseTarget(String url) {
var normalizedUrl = normalizeUrl(url);
+ var safeUrl = toSafeUrlForDisplay(url);
URI uri;
try {
uri = URI.create(normalizedUrl);
- } catch (Exception e) {
- throw new AviatorSimpleException("Aviator URL format is invalid. Expected 'host:port' or a valid target string. Provided URL: " + url, e);
+ } catch (IllegalArgumentException e) {
+ // URI parsing errors include the original URL, which may contain credentials.
+ throw new AviatorSimpleException("Invalid Aviator URL; expected a host and optional port: " + safeUrl);
}
var host = uri.getHost();
if ( host==null || host.isBlank() ) {
- throw new AviatorSimpleException("Aviator URL is invalid: Host cannot be empty. Provided URL: " + url);
+ throw new AviatorSimpleException("Aviator URL is invalid: Host cannot be empty. Provided URL: " + safeUrl);
}
if ( uri.getPath()!=null && !uri.getPath().isBlank() && !"/".equals(uri.getPath()) ) {
- LOG.warn("WARN: URL contained a path ('{}'), using only the host/port part. Full URL: {}", uri.getPath(), url);
+ LOG.warn("WARN: URL contained a path ('{}'), using only the host/port part. Full URL: {}", uri.getPath(), safeUrl);
}
var port = uri.getPort();
if ( port==-1 ) {
return new ParsedTarget(host, null);
} else if (port <= 0 || port > 65535) {
- throw new AviatorSimpleException("Aviator URL is invalid: Invalid port number '"+port+"'. Provided URL: " + url);
+ throw new AviatorSimpleException("Aviator URL is invalid: Invalid port number '"+port+"'. Provided URL: " + safeUrl);
} else {
return new ParsedTarget(host, port);
}
@@ -121,7 +133,7 @@ public static String normalizeUrl(String url) {
var schemeSeparator = trimmed.indexOf("://");
var scheme = trimmed.substring(0, schemeSeparator);
if ( !"https".equalsIgnoreCase(scheme) ) {
- throw new UnsupportedAviatorUrlSchemeException(scheme, url);
+ throw new UnsupportedAviatorUrlSchemeException(scheme, toSafeUrlForDisplay(url));
}
if ( "https".equals(scheme) ) {
return trimmed;
@@ -130,6 +142,27 @@ public static String normalizeUrl(String url) {
return "https"+trimmed.substring(schemeSeparator);
}
+ /**
+ * Returns the URL with any user-info part (for example {@code user:password@}) replaced by
+ * {@code ***}, so that the URL can be shown in messages and logs. Other URLs stay unchanged.
+ */
+ static String toSafeUrlForDisplay(String url) {
+ if ( url==null ) {
+ return null;
+ }
+ var schemeSeparator = url.indexOf("://");
+ var authorityStart = schemeSeparator<0 ? 0 : schemeSeparator+3;
+ var authorityEnd = authorityStart;
+ while ( authorityEnd proxyDescriptor) {
var configuredBuilder = builder
.sslContext(createSslContext())
@@ -172,7 +205,11 @@ static ProxiedSocketAddress toProxiedSocketAddress(SocketAddress targetAddress,
}
public static AviatorGrpcClient createClient(String url) throws AviatorSimpleException {
- return createClient(url, null, Constants.DEFAULT_PING_INTERVAL_SECONDS);
+ return createClient(url, AviatorRequestPolicy.WAIT_FOR_READY);
+ }
+
+ public static AviatorGrpcClient createClient(String url, AviatorRequestPolicy policy) throws AviatorSimpleException {
+ return createClient(url, null, Constants.DEFAULT_PING_INTERVAL_SECONDS, policy);
}
public record AviatorConnectionPlan(
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorRequestPolicy.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorRequestPolicy.java
new file mode 100644
index 00000000000..257a71b4165
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorRequestPolicy.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.grpc;
+
+import java.util.Objects;
+
+import io.grpc.stub.AbstractBlockingStub;
+
+/** Unary request behavior; audit and diagnostic clients use {@link #WAIT_FOR_READY}. */
+public enum AviatorRequestPolicy {
+ FAIL_FAST,
+ WAIT_FOR_READY;
+
+ boolean isWaitForReady() {
+ return this == WAIT_FOR_READY;
+ }
+
+ /** Requires a fresh stub: gRPC's default is fail-fast, and wait-for-ready cannot be unset through the stub API. */
+ > S configureNewStub(S stub, AviatorConnectionContext context) {
+ if ( isWaitForReady() ) {
+ return stub.withWaitForReady();
+ }
+ return stub.withOption(AviatorConnectionContext.CALL_OPTION, Objects.requireNonNull(context, "connectionContext"));
+ }
+
+}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/GrpcUtil.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/GrpcUtil.java
index 1fc5785ca33..6e561935143 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/GrpcUtil.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/GrpcUtil.java
@@ -12,10 +12,13 @@
*/
package com.fortify.cli.aviator.grpc;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
+import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,6 +29,7 @@
import com.fortify.aviator.grpc.Fragment;
import com.fortify.aviator.grpc.IssueData;
import com.fortify.aviator.grpc.StackTraceElementList;
+import com.fortify.cli.aviator._common.exception.AviatorConnectivityException;
import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
import com.fortify.cli.aviator._common.exception.AviatorTechnicalException;
import com.fortify.cli.aviator.audit.model.AuditResponse;
@@ -52,59 +56,10 @@ interface GrpcCall {
static , T, R> R executeGrpcCall(S stub, GrpcCall call, T request, String operation) {
try {
- S stubWithDeadline = stub.withDeadlineAfter(Constants.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
- return call.call(stubWithDeadline, request);
+ return call.call(stub.withDeadlineAfter(Constants.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS), request);
} catch (StatusRuntimeException e) {
- Status status = e.getStatus();
- String serverDescription = status.getDescription() != null && !status.getDescription().isBlank()
- ? status.getDescription()
- : "No additional details were provided by the server.";
-
- switch (status.getCode()) {
- case INVALID_ARGUMENT:
- throw new AviatorSimpleException(String.format("Invalid input for %s. The server reported: %s. Please check the provided arguments.", operation, serverDescription));
- case NOT_FOUND:
- throw new AviatorSimpleException(String.format("The requested resource was not found during %s. The server reported: %s. Please verify the name or ID is correct.", operation, serverDescription));
- case ALREADY_EXISTS:
- throw new AviatorSimpleException(String.format("Cannot perform %s because a resource with the same identifier already exists. The server reported: %s.", operation, serverDescription));
- case FAILED_PRECONDITION:
- throw new AviatorSimpleException(String.format("The %s operation could not be completed because a required condition was not met. The server reported: %s.", operation, serverDescription));
-
- case PERMISSION_DENIED:
- if (serverDescription.toLowerCase().contains("invalid signature")) {
- throw new AviatorSimpleException("Permission Denied: Invalid signature. Please verify the private key in your admin configuration is correct and corresponds to the public key registered with Aviator.");
- } else {
- throw new AviatorSimpleException(String.format("Permission Denied for %s. You may not have the required roles for this action. Server details: %s", operation, serverDescription));
- }
- case UNAUTHENTICATED:
- throw new AviatorSimpleException("Authentication Failed: The token or credentials used are invalid or expired. Please log in again using 'fcli aviator session login' or verify your admin configuration.");
-
- case RESOURCE_EXHAUSTED:
- throw new AviatorSimpleException(String.format("The server's resource limits were exceeded during %s. Please try again later or contact support. Server details: %s", operation, serverDescription));
- case DEADLINE_EXCEEDED:
- String timeoutMessage = String.format(
- "The %s operation timed out because the server did not respond in time.\n\n" +
- "Please check the following:\n" +
- " 1. The Aviator URL in your configuration is correct and reachable.\n" +
- " 2. Your network connection is stable and any firewalls or proxies are properly configured.\n\n" +
- "If the URL and network are correct, the server may be experiencing high load. Please try again later.",
- operation
- );
- throw new AviatorSimpleException(timeoutMessage);
-
- case CANCELLED:
- case UNKNOWN:
- case ABORTED:
- case UNIMPLEMENTED:
- case INTERNAL:
- case UNAVAILABLE:
- case DATA_LOSS:
- default:
- String techMessage = String.format("A technical error occurred on the server while performing the %s operation. Please check the fcli logs for details or contact support if the issue persists.", operation);
- String logMessage = String.format("gRPC call for '%s' failed with status %s (%s): %s", operation, status.getCode(), status.getCode().name(), serverDescription);
- LOG.error(logMessage, e);
- throw new AviatorTechnicalException(techMessage, e);
- }
+ throw toConnectivityException(e, stub.getCallOptions().getOption(AviatorConnectionContext.CALL_OPTION))
+ .orElseGet(() -> toAviatorException(e, operation));
} catch (Exception e) {
String errorMessage = "An unexpected client-side error occurred during the " + operation + " operation.";
LOG.error(errorMessage, e);
@@ -112,6 +67,77 @@ static , T, R> R executeGrpcCall(S stub, GrpcC
}
}
+ private static Optional toConnectivityException(StatusRuntimeException e, AviatorConnectionContext context) {
+ if ( context == null ) {
+ return Optional.empty();
+ }
+ return AviatorConnectivityFailureKind.classify(e, context)
+ .map(kind -> {
+ // Cause messages can contain credentials, so log only the cause types.
+ LOG.debug("Aviator connectivity failure: kind={}, causes=[{}]", kind, getCauseTypes(e));
+ return new AviatorConnectivityException(kind.formatMessage(context));
+ });
+ }
+
+ private static String getCauseTypes(Throwable e) {
+ return ExceptionUtils.getThrowableList(e).stream()
+ .map(t -> t.getClass().getName())
+ .collect(Collectors.joining(" <- "));
+ }
+
+ private static RuntimeException toAviatorException(StatusRuntimeException e, String operation) {
+ Status status = e.getStatus();
+ String serverDescription = status.getDescription() != null && !status.getDescription().isBlank()
+ ? status.getDescription()
+ : "No additional details were provided by the server.";
+
+ switch (status.getCode()) {
+ case INVALID_ARGUMENT:
+ return new AviatorSimpleException(String.format("Invalid input for %s. The server reported: %s. Please check the provided arguments.", operation, serverDescription));
+ case NOT_FOUND:
+ return new AviatorSimpleException(String.format("The requested resource was not found during %s. The server reported: %s. Please verify the name or ID is correct.", operation, serverDescription));
+ case ALREADY_EXISTS:
+ return new AviatorSimpleException(String.format("Cannot perform %s because a resource with the same identifier already exists. The server reported: %s.", operation, serverDescription));
+ case FAILED_PRECONDITION:
+ return new AviatorSimpleException(String.format("The %s operation could not be completed because a required condition was not met. The server reported: %s.", operation, serverDescription));
+
+ case PERMISSION_DENIED:
+ if (serverDescription.toLowerCase().contains("invalid signature")) {
+ return new AviatorSimpleException("Permission Denied: Invalid signature. Please verify the private key in your admin configuration is correct and corresponds to the public key registered with Aviator.");
+ } else {
+ return new AviatorSimpleException(String.format("Permission Denied for %s. You may not have the required roles for this action. Server details: %s", operation, serverDescription));
+ }
+ case UNAUTHENTICATED:
+ return new AviatorSimpleException("Authentication Failed: The token or credentials used are invalid or expired. Please log in again using 'fcli aviator session login' or verify your admin configuration.");
+
+ case RESOURCE_EXHAUSTED:
+ return new AviatorSimpleException(String.format("Aviator server resource limits were exceeded during %s (server details: %s); try again later, and contact support if the problem continues", operation, serverDescription));
+ case DEADLINE_EXCEEDED:
+ String timeoutMessage = String.format(
+ "The %s operation timed out because the server did not respond in time.\n\n" +
+ "Please check the following:\n" +
+ " 1. The Aviator URL in your configuration is correct and reachable.\n" +
+ " 2. Your network connection is stable and any firewalls or proxies are properly configured.\n\n" +
+ "If the URL and network are correct, the server may be experiencing high load. Please try again later.",
+ operation
+ );
+ return new AviatorSimpleException(timeoutMessage);
+
+ case CANCELLED:
+ case UNKNOWN:
+ case ABORTED:
+ case UNIMPLEMENTED:
+ case INTERNAL:
+ case UNAVAILABLE:
+ case DATA_LOSS:
+ default:
+ String techMessage = String.format("The Aviator %s operation failed with status %s; check the Aviator URL and network access, run the command again with --log-level DEBUG to log details, and contact support if the problem continues", operation, status.getCode());
+ String logMessage = String.format("gRPC call for '%s' failed with status %s (%s): %s", operation, status.getCode(), status.getCode().name(), serverDescription);
+ LOG.error(logMessage, e);
+ return new AviatorTechnicalException(techMessage, e);
+ }
+ }
+
static AuditRequest convertToAuditRequest(UserPrompt userPrompt, String streamId, String requestId) {
List stackTraceElementLists = new ArrayList<>();
if (userPrompt.getStackTrace() != null) {
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityExceptionTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityExceptionTest.java
new file mode 100644
index 00000000000..0e7f83292aa
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityExceptionTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator._common.exception;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
+import com.fortify.cli.common.exception.FcliExecutionExceptionHandler;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.ThrowableProxyUtil;
+import ch.qos.logback.core.read.ListAppender;
+import picocli.CommandLine;
+import picocli.CommandLine.Help.Ansi;
+import picocli.CommandLine.Model.CommandSpec;
+
+class AviatorConnectivityExceptionTest {
+ private static final String SECRET = "sentinel-secret-value";
+
+ private final Logger logger = (Logger)LoggerFactory.getLogger(FcliExecutionExceptionHandler.class);
+ private final ListAppender appender = new ListAppender<>();
+ private Level previousLevel;
+
+ @BeforeEach
+ void setUp() {
+ previousLevel = logger.getLevel();
+ logger.setLevel(Level.DEBUG);
+ appender.start();
+ logger.addAppender(appender);
+ }
+
+ @AfterEach
+ void tearDown() {
+ logger.detachAppender(appender);
+ logger.setLevel(previousLevel);
+ }
+
+ @Test
+ void handlerPrintsTheMessageWithoutCause() {
+ var err = new StringWriter();
+ var exception = new AviatorConnectivityException("Proxy localhost:33444 refused the connection; check the proxy host and port");
+
+ int exitCode = FcliExecutionExceptionHandler.INSTANCE.handleException(exception, createCommandLine(err));
+
+ assertEquals(1, exitCode);
+ assertNull(exception.getCause());
+ assertFalse(err.toString().contains("Caused by"));
+ }
+
+ @Test
+ void malformedUrlDoesNotExposeCredentialsInOutputOrLogs() {
+ var exception = assertThrows(AviatorSimpleException.class,
+ () -> AviatorGrpcClientHelper.parseTarget("https://user:" + SECRET + "@aviator.invalid/%zz"));
+ var err = new StringWriter();
+
+ assertEquals(1, FcliExecutionExceptionHandler.INSTANCE.handleException(exception, createCommandLine(err)));
+
+ assertNull(exception.getCause());
+ assertFalse(err.toString().contains(SECRET));
+ assertEquals(1, appender.list.size());
+ var event = appender.list.get(0);
+ assertFalse(event.getFormattedMessage().contains(SECRET));
+ assertFalse(ThrowableProxyUtil.asString(event.getThrowableProxy()).contains(SECRET));
+ }
+
+ private static CommandLine createCommandLine(StringWriter err) {
+ var commandLine = new CommandLine(CommandSpec.create());
+ commandLine.setErr(new PrintWriter(err));
+ commandLine.setColorScheme(CommandLine.Help.defaultColorScheme(Ansi.OFF));
+ return commandLine;
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectionContextTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectionContextTest.java
new file mode 100644
index 00000000000..1062082a3b4
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectionContextTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan;
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.ParsedTarget;
+import com.fortify.cli.common.http.proxy.helper.ProxyDescriptor;
+
+class AviatorConnectionContextTest {
+ private static final String PROXY_PASSWORD = "sentinel-proxy-password";
+
+ @Test
+ void createsDirectContextFromPlan() {
+ var context = AviatorConnectionContext.of(plan(Optional.empty()));
+
+ assertFalse(context.isProxied());
+ assertNull(context.proxy());
+ assertEquals("aviator.example.com:443", context.target().display());
+ }
+
+ @Test
+ void createsProxiedContextWithoutCredentials() {
+ var proxy = ProxyDescriptor.builder()
+ .proxyHost("localhost").proxyPort(33444)
+ .proxyUser("sentinel-user").proxyPassword(PROXY_PASSWORD.toCharArray())
+ .build();
+
+ var context = AviatorConnectionContext.of(plan(Optional.of(proxy)));
+
+ assertTrue(context.isProxied());
+ assertEquals("localhost:33444", context.proxy().display());
+ assertFalse(context.toString().contains(PROXY_PASSWORD));
+ assertFalse(context.toString().contains("sentinel-user"));
+ }
+
+ @Test
+ void requiresTarget() {
+ assertThrows(NullPointerException.class, () -> new AviatorConnectionContext(null, null));
+ }
+
+ @Test
+ void replacesControlCharactersInHostNames() {
+ assertEquals("evil?host:1", new AviatorEndpoint("evil\u001bhost", 1).display());
+ assertEquals("unknown:1", new AviatorEndpoint(null, 1).display());
+ }
+
+ @Test
+ void displaysIpv6Hosts() {
+ assertEquals("[::1]:443", new AviatorEndpoint("::1", 443).display());
+ assertEquals("[::1]:443", new AviatorEndpoint("[::1]", 443).display());
+ }
+
+ private static AviatorConnectionPlan plan(Optional proxy) {
+ return new AviatorConnectionPlan("aviator.example.com", "https://aviator.example.com",
+ new ParsedTarget("aviator.example.com", null), 443, proxy);
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKindTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKindTest.java
new file mode 100644
index 00000000000..61d26cc4ac5
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKindTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.ConnectException;
+import java.net.UnknownHostException;
+import java.nio.channels.UnresolvedAddressException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
+import java.util.Optional;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.netty.shaded.io.netty.channel.ConnectTimeoutException;
+import io.grpc.netty.shaded.io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.grpc.netty.shaded.io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException;
+import io.grpc.netty.shaded.io.netty.handler.proxy.ProxyConnectException;
+
+class AviatorConnectivityFailureKindTest {
+ private static final AviatorConnectionContext DIRECT =
+ new AviatorConnectionContext(new AviatorEndpoint("aviator.example.com", 443), null);
+ private static final AviatorConnectionContext PROXIED =
+ new AviatorConnectionContext(new AviatorEndpoint("aviator.example.com", 443), new AviatorEndpoint("localhost", 33444));
+
+ @Test
+ void classifiesProxyRejectionBeforeOtherConnectExceptions() {
+ var cause = new HttpProxyConnectException("status: 407", new DefaultHttpHeaders());
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.PROXY_REJECTED), classify(Status.UNAVAILABLE, cause, PROXIED));
+ }
+
+ @Test
+ void classifiesOtherProxyFailuresAsNegotiationFailure() {
+ var cause = new ProxyConnectException("timeout");
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.PROXY_NEGOTIATION_FAILED), classify(Status.UNAVAILABLE, cause, PROXIED));
+ }
+
+ @Test
+ void classifiesConnectTimeoutBeforeRefusal() {
+ var cause = new ConnectTimeoutException("connection timed out");
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.CONNECT_TIMEOUT), classify(Status.UNAVAILABLE, cause, DIRECT));
+ }
+
+ @Test
+ void classifiesConnectExceptionAsProxyRefusalWhenProxied() {
+ var cause = new ConnectException("Connection refused");
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.PROXY_REFUSED), classify(Status.UNAVAILABLE, cause, PROXIED));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"Connection refused", "Connection refused: getsockopt", "Connection refused: no further information"})
+ void classifiesConnectExceptionAsDirectRefusalWhenNotProxied(String reason) {
+ var cause = new ConnectException(reason);
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.DIRECT_REFUSED), classify(Status.UNAVAILABLE, cause, DIRECT));
+ }
+
+ @Test
+ void classifiesNameResolutionFailures() {
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.DNS_FAILURE),
+ classify(Status.UNAVAILABLE, new UnknownHostException("host"), DIRECT));
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.DNS_FAILURE),
+ classify(Status.UNAVAILABLE, new UnresolvedAddressException(), PROXIED));
+ }
+
+ @Test
+ void usesNeutralKindsWithoutTransportEvidence() {
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.UNAVAILABLE), classify(Status.UNAVAILABLE, null, DIRECT));
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.DEADLINE_EXCEEDED), classify(Status.DEADLINE_EXCEEDED, null, DIRECT));
+ }
+
+ @Test
+ void leavesCertificateFailuresToPkixGuidance() {
+ var pkix = new SSLHandshakeException("PKIX path building failed");
+ pkix.initCause(new CertificateException("untrusted"));
+ assertEquals(Optional.empty(), classify(Status.UNAVAILABLE, pkix, DIRECT));
+ assertEquals(Optional.empty(), classify(Status.UNAVAILABLE, new CertPathValidatorException("untrusted"), DIRECT));
+ }
+
+ @Test
+ void classifiesOtherTlsFailures() {
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.TLS_FAILURE),
+ classify(Status.UNAVAILABLE, new SSLException("Unsupported or unrecognized SSL message"), PROXIED));
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.TLS_FAILURE),
+ classify(Status.UNAVAILABLE, new SSLHandshakeException("handshake timed out"), DIRECT));
+ }
+
+ @Test
+ void usesNeutralKindForProxyFailuresWithoutFcliProxy() {
+ var cause = new HttpProxyConnectException("status: 407", new DefaultHttpHeaders());
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.UNAVAILABLE), classify(Status.UNAVAILABLE, cause, DIRECT));
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.DEADLINE_EXCEEDED),
+ classify(Status.DEADLINE_EXCEEDED, new ProxyConnectException("timeout"), DIRECT));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Status.Code.class, names = {"UNAVAILABLE", "DEADLINE_EXCEEDED"}, mode = EnumSource.Mode.EXCLUDE)
+ void ignoresOtherStatusCodes(Status.Code code) {
+ assertEquals(Optional.empty(), classify(Status.fromCode(code), new ConnectException("Connection refused"), DIRECT));
+ }
+
+ @Test
+ void terminatesOnCyclicCauseChain() {
+ var first = new RuntimeException("first");
+ var second = new RuntimeException("second", first);
+ first.initCause(second);
+
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.UNAVAILABLE), classify(Status.UNAVAILABLE, first, DIRECT));
+ }
+
+ @Test
+ void ignoresNestedCertificateFailures() {
+ var cause = new RuntimeException(new CertificateException("untrusted"));
+ assertEquals(Optional.empty(), classify(Status.UNAVAILABLE, cause, DIRECT));
+ }
+
+ @Test
+ void doesNotInferRefusalFromAnAmbiguousConnectException() {
+ var cause = new ConnectException("Connection refused: openssl-handshake_failure.example/10.0.0.1:443");
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.UNAVAILABLE), classify(Status.UNAVAILABLE, cause, DIRECT));
+ }
+
+ @Test
+ void doesNotMisreportNetworkOrUnknownConnectFailuresAsRefusal() {
+ for ( var reason : new String[] {"Network is unreachable", "Connection timed out", "Permission denied", null} ) {
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.UNAVAILABLE),
+ classify(Status.UNAVAILABLE, new ConnectException(reason), DIRECT));
+ }
+ }
+
+ @Test
+ void recognizesRefusalInsideNettyAnnotatedCause() {
+ var cause = new ConnectException("Connection refused: openssl-handshake_failure.example/10.0.0.1:443");
+ cause.initCause(new ConnectException("Connection refused"));
+ assertEquals(Optional.of(AviatorConnectivityFailureKind.DIRECT_REFUSED), classify(Status.UNAVAILABLE, cause, DIRECT));
+ }
+
+ @ParameterizedTest
+ @EnumSource(AviatorConnectivityFailureKind.class)
+ void everyFailureHasASingleLineActionableMessage(AviatorConnectivityFailureKind kind) {
+ for ( var context : new AviatorConnectionContext[] {DIRECT, PROXIED} ) {
+ if ( !context.isProxied() && kind.name().startsWith("PROXY_") ) { continue; }
+ var message = kind.formatMessage(context);
+ assertEquals(1, message.lines().count(), message);
+ assertTrue(message.contains("; check ") || message.contains("; try "), message);
+ assertFalse(message.endsWith("."), message);
+ assertFalse(message.contains("null"), message);
+ }
+ }
+
+ @Test
+ void messagesNameTheFailingEndpoint() {
+ assertEquals("Proxy localhost:33444 refused the connection; check the proxy host and port",
+ AviatorConnectivityFailureKind.PROXY_REFUSED.formatMessage(PROXIED));
+ assertEquals("Aviator server aviator.example.com:443 refused the connection; check the Aviator URL",
+ AviatorConnectivityFailureKind.DIRECT_REFUSED.formatMessage(DIRECT));
+ assertEquals("Timed out connecting to proxy localhost:33444; check network access and firewall rules",
+ AviatorConnectivityFailureKind.CONNECT_TIMEOUT.formatMessage(PROXIED));
+ assertEquals("Aviator server aviator.example.com:443 did not respond within 30 seconds; "
+ + "check network access and proxy settings, or try again later",
+ AviatorConnectivityFailureKind.DEADLINE_EXCEEDED.formatMessage(DIRECT));
+ }
+
+ private static Optional classify(Status status, Throwable cause, AviatorConnectionContext context) {
+ var exception = new StatusRuntimeException(cause==null ? status : status.withCause(cause));
+ return AviatorConnectivityFailureKind.classify(exception, context);
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientConnectivityTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientConnectivityTest.java
new file mode 100644
index 00000000000..3332c0c62a7
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientConnectivityTest.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+import com.fortify.aviator.application.ApplicationByTenantName;
+import com.fortify.aviator.application.ApplicationList;
+import com.fortify.aviator.application.ApplicationServiceGrpc;
+import com.fortify.aviator.dastentitlement.DastEntitlementServiceGrpc;
+import com.fortify.aviator.dastentitlement.ListDastEntitlementsByTenantRequest;
+import com.fortify.aviator.dastentitlement.ListDastEntitlementsByTenantResponse;
+import com.fortify.aviator.entitlement.EntitlementServiceGrpc;
+import com.fortify.aviator.entitlement.ListEntitlementsByTenantRequest;
+import com.fortify.aviator.entitlement.ListEntitlementsByTenantResponse;
+import com.fortify.grpc.token.ListTokensRequest;
+import com.fortify.grpc.token.ListTokensResponse;
+import com.fortify.grpc.token.TokenServiceGrpc;
+import com.fortify.cli.aviator._common.exception.AviatorConnectivityException;
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan;
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.ParsedTarget;
+import com.fortify.cli.common.http.proxy.helper.ProxyDescriptor;
+
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ClientInterceptor;
+import io.grpc.ManagedChannel;
+import io.grpc.EquivalentAddressGroup;
+import io.grpc.NameResolver;
+import io.grpc.MethodDescriptor;
+import io.grpc.Server;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import io.grpc.stub.StreamObserver;
+
+/**
+ * Checks connectivity behavior against real Netty transports on the loopback interface.
+ */
+class AviatorGrpcClientConnectivityTest {
+ private static final Duration FAIL_FAST_LIMIT = Duration.ofSeconds(5);
+ private static final String PROXY_SECRET = "sentinel-proxy-secret";
+ private static final String TARGET_HOST = "aviator.example.invalid";
+
+ private ServerSocket proxyServer;
+ private Server grpcServer;
+ private ManagedChannel testChannel;
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if ( proxyServer!=null ) { proxyServer.close(); }
+ if ( testChannel!=null ) { testChannel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); }
+ if ( grpcServer!=null ) { grpcServer.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); }
+ }
+
+ @Test
+ void directRefusalFailsFastWithDirectRefusedMessage() throws Exception {
+ int port = unusedPort();
+ var plan = new AviatorConnectionPlan("127.0.0.1:" + port, "https://127.0.0.1:" + port,
+ new ParsedTarget("127.0.0.1", port), port, Optional.empty());
+
+ var exception = callAndExpectConnectivityFailure(plan);
+
+ assertEquals("Aviator server 127.0.0.1:" + port + " refused the connection; check the Aviator URL",
+ exception.getMessage());
+ }
+
+ @Test
+ void proxyRefusalFailsFastWithProxyRefusedMessage() throws Exception {
+ int proxyPort = unusedPort();
+
+ var exception = callAndExpectConnectivityFailure(proxiedPlan(proxyPort));
+
+ assertEquals("Proxy 127.0.0.1:" + proxyPort + " refused the connection; check the proxy host and port",
+ exception.getMessage());
+ }
+
+ @Test
+ void proxyAuthenticationRejectionIsReportedAsProxyRejection() throws Exception {
+ proxyServer = new ServerSocket(0, 50, InetAddress.getLoopbackAddress());
+ startRejectingProxy(proxyServer);
+
+ var exception = callAndExpectConnectivityFailure(proxiedPlan(proxyServer.getLocalPort()));
+
+ assertTrue(exception.getMessage().startsWith("Proxy 127.0.0.1:" + proxyServer.getLocalPort() + " rejected"),
+ exception.getMessage());
+ assertFalse(exception.getMessage().contains("sentinel-realm"));
+ }
+
+ @Test
+ @Timeout(20)
+ void stalledProxyNegotiationEndsBeforeRequestDeadline() throws Exception {
+ proxyServer = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
+ // Leave the TCP connection in the listen backlog without answering CONNECT. Netty's real
+ // proxy handshake timer must end the call, rather than the RPC deadline.
+ var started = System.nanoTime();
+ try ( var client = AviatorGrpcClientHelper.createClient(proxiedPlan(proxyServer.getLocalPort()),
+ null, 30, AviatorRequestPolicy.FAIL_FAST) ) {
+ var exception = assertThrows(AviatorConnectivityException.class,
+ () -> client.listApplication("tenant", "signature", "message"));
+ assertTrue(exception.getMessage().startsWith("Could not connect to Aviator server " + TARGET_HOST + ":443 through proxy"),
+ exception.getMessage());
+ }
+ assertTrue(Duration.ofNanos(System.nanoTime() - started).compareTo(Duration.ofSeconds(15)) < 0);
+ }
+
+ @ParameterizedTest
+ @EnumSource(AviatorRequestPolicy.class)
+ void healthyUnaryServicesUseSelectedPolicyAndStreamsKeepWaiting(AviatorRequestPolicy policy) throws Exception {
+ var captured = new AtomicReference();
+ try ( var client = createPlaintextClient(policy, captured) ) {
+ assertTrue(client.listApplication("tenant", "signature", "message").isEmpty());
+ assertCallPolicy(policy, captured.get());
+ assertEquals(ListTokensResponse.getDefaultInstance(), client.listTokens("email", "tenant", "signature", "message"));
+ assertCallPolicy(policy, captured.get());
+ assertTrue(client.listEntitlements("tenant", "signature", "message").isEmpty());
+ assertCallPolicy(policy, captured.get());
+ assertTrue(client.listDastEntitlements("tenant", "signature", "message").isEmpty());
+ assertCallPolicy(policy, captured.get());
+ assertTrue(client.getCorrelationAsyncStub().getCallOptions().isWaitForReady());
+ assertTrue(client.getDastAuditAsyncStub().getCallOptions().isWaitForReady());
+ assertNull(client.getCorrelationAsyncStub().getCallOptions().getOption(AviatorConnectionContext.CALL_OPTION));
+ assertNull(client.getDastAuditAsyncStub().getCallOptions().getOption(AviatorConnectionContext.CALL_OPTION));
+ }
+ }
+
+ @Test
+ @Timeout(15)
+ @SuppressWarnings("deprecation") // Per-channel resolver keeps the fixture out of the global registry.
+ void refusedFirstAddressFallsBackToHealthyAddress() throws Exception {
+ grpcServer = NettyServerBuilder.forAddress(new InetSocketAddress("127.0.0.1", 0))
+ .addService(new EmptyApplicationService()).build().start();
+ var refused = new InetSocketAddress("127.0.0.1", unusedPort());
+ var healthy = new InetSocketAddress("127.0.0.1", grpcServer.getPort());
+ testChannel = NettyChannelBuilder.forTarget("fixture:///localhost").usePlaintext()
+ .nameResolverFactory(new NameResolver.Factory() {
+ @Override
+ public String getDefaultScheme() { return "fixture"; }
+
+ @Override
+ public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
+ return new NameResolver() {
+ @Override
+ public String getServiceAuthority() { return "localhost"; }
+
+ @Override
+ public void start(Listener2 listener) {
+ listener.onResult(ResolutionResult.newBuilder().setAddresses(List.of(
+ new EquivalentAddressGroup(refused), new EquivalentAddressGroup(healthy))).build());
+ }
+
+ @Override
+ public void shutdown() {}
+ };
+ }
+ }).build();
+ var context = new AviatorConnectionContext(new AviatorEndpoint("localhost", grpcServer.getPort()), null);
+ try ( var client = new AviatorGrpcClient(testChannel, 30, null, 30, AviatorRequestPolicy.FAIL_FAST, context) ) {
+ assertTrue(client.listApplication("tenant", "signature", "message").isEmpty());
+ }
+ }
+
+ private static void assertCallPolicy(AviatorRequestPolicy policy, CallOptions options) {
+ assertEquals(policy.isWaitForReady(), options.isWaitForReady());
+ assertEquals(!policy.isWaitForReady(), options.getOption(AviatorConnectionContext.CALL_OPTION) != null);
+ assertDeadlineAtMostDefault(options);
+ }
+
+ private static AviatorConnectivityException callAndExpectConnectivityFailure(AviatorConnectionPlan plan) {
+ try ( var client = AviatorGrpcClientHelper.createClient(plan, null, 30, AviatorRequestPolicy.FAIL_FAST) ) {
+ long start = System.nanoTime();
+ var exception = assertThrows(AviatorConnectivityException.class,
+ () -> client.listApplication("tenant", "signature", "message"));
+ var elapsed = Duration.ofNanos(System.nanoTime() - start);
+ assertTrue(elapsed.compareTo(FAIL_FAST_LIMIT) < 0, "Call took " + elapsed);
+ assertNull(exception.getCause());
+ assertFalse(exception.getMessage().contains(PROXY_SECRET));
+ return exception;
+ }
+ }
+
+ private static AviatorConnectionPlan proxiedPlan(int proxyPort) {
+ var proxy = ProxyDescriptor.builder()
+ .proxyHost("127.0.0.1").proxyPort(proxyPort)
+ .proxyUser("sentinel-user").proxyPassword(PROXY_SECRET.toCharArray())
+ .build();
+ return new AviatorConnectionPlan(TARGET_HOST, "https://" + TARGET_HOST,
+ new ParsedTarget(TARGET_HOST, null), 443, Optional.of(proxy));
+ }
+
+ private AviatorGrpcClient createPlaintextClient(AviatorRequestPolicy policy, AtomicReference captured) throws IOException {
+ grpcServer = NettyServerBuilder.forAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0))
+ .addService(new EmptyApplicationService())
+ .addService(new EmptyTokenService())
+ .addService(new EmptyEntitlementService())
+ .addService(new EmptyDastEntitlementService())
+ .build()
+ .start();
+ testChannel = NettyChannelBuilder.forAddress(InetAddress.getLoopbackAddress().getHostAddress(), grpcServer.getPort())
+ .usePlaintext()
+ .intercept(new CallOptionsCapture(captured))
+ .build();
+ var context = new AviatorConnectionContext(new AviatorEndpoint("127.0.0.1", grpcServer.getPort()), null);
+ return new AviatorGrpcClient(testChannel, 30, null, 30, policy, context);
+ }
+
+ private static void assertDeadlineAtMostDefault(CallOptions callOptions) {
+ assertNotNull(callOptions.getDeadline());
+ long remainingSeconds = callOptions.getDeadline().timeRemaining(TimeUnit.SECONDS);
+ assertTrue(remainingSeconds <= 30 && remainingSeconds > 20, "Deadline remaining: " + remainingSeconds);
+ }
+
+ private static int unusedPort() throws IOException {
+ try ( var socket = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()) ) {
+ return socket.getLocalPort();
+ }
+ }
+
+ private static void startRejectingProxy(ServerSocket server) {
+ var thread = new Thread(() -> {
+ while ( !server.isClosed() ) {
+ try ( Socket socket = server.accept() ) {
+ readRequestHeaders(socket.getInputStream());
+ OutputStream out = socket.getOutputStream();
+ out.write(("HTTP/1.1 407 Proxy Authentication Required\r\n"
+ + "Proxy-Authenticate: Basic realm=\"sentinel-realm\"\r\n"
+ + "Content-Length: 0\r\n\r\n").getBytes(StandardCharsets.US_ASCII));
+ out.flush();
+ } catch ( IOException e ) {
+ // Server socket closed at test end
+ }
+ }
+ }, "test-rejecting-proxy");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ private static void readRequestHeaders(InputStream in) throws IOException {
+ int matched = 0;
+ byte[] terminator = {'\r', '\n', '\r', '\n'};
+ int b;
+ while ( matched responseObserver) {
+ responseObserver.onNext(ApplicationList.getDefaultInstance());
+ responseObserver.onCompleted();
+ }
+ }
+
+ private static final class EmptyTokenService extends TokenServiceGrpc.TokenServiceImplBase {
+ @Override
+ public void listTokens(ListTokensRequest request, StreamObserver observer) {
+ observer.onNext(ListTokensResponse.getDefaultInstance());
+ observer.onCompleted();
+ }
+ }
+
+ private static final class EmptyEntitlementService extends EntitlementServiceGrpc.EntitlementServiceImplBase {
+ @Override
+ public void listEntitlementsByTenant(ListEntitlementsByTenantRequest request,
+ StreamObserver observer) {
+ observer.onNext(ListEntitlementsByTenantResponse.getDefaultInstance());
+ observer.onCompleted();
+ }
+ }
+
+ private static final class EmptyDastEntitlementService extends DastEntitlementServiceGrpc.DastEntitlementServiceImplBase {
+ @Override
+ public void listDastEntitlementsByTenant(ListDastEntitlementsByTenantRequest request,
+ StreamObserver observer) {
+ observer.onNext(ListDastEntitlementsByTenantResponse.getDefaultInstance());
+ observer.onCompleted();
+ }
+ }
+
+ private static final class CallOptionsCapture implements ClientInterceptor {
+ private final AtomicReference captured;
+
+ private CallOptionsCapture(AtomicReference captured) {
+ this.captured = captured;
+ }
+
+ @Override
+ public ClientCall interceptCall(MethodDescriptor method,
+ CallOptions callOptions, Channel next) {
+ captured.set(callOptions);
+ return next.newCall(method, callOptions);
+ }
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelperTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelperTest.java
index 91ceb215f87..d3f60b9a2e5 100644
--- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelperTest.java
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientHelperTest.java
@@ -13,6 +13,7 @@
package com.fortify.cli.aviator.grpc;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -23,8 +24,10 @@
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.ValueSource;
+import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
import com.fortify.cli.aviator._common.exception.UnsupportedAviatorUrlSchemeException;
import com.fortify.cli.common.http.proxy.helper.ProxyDescriptor;
@@ -118,4 +121,34 @@ void shouldIgnoreUnsupportedSocketAddressTypes() {
assertNull(proxied);
}
+
+ @ParameterizedTest
+ @CsvSource({
+ "https://user:sentinel-secret@aviator.invalid:443/path, https://***@aviator.invalid:443/path",
+ "user:sentinel-secret@aviator.invalid:443, ***@aviator.invalid:443",
+ "mttp://sentinel-secret@aviator.invalid, mttp://***@aviator.invalid",
+ "https://aviator.invalid:443/a@b, https://aviator.invalid:443/a@b",
+ "aviator.invalid:443, aviator.invalid:443"
+ })
+ void shouldRemoveUserInfoFromDisplayedUrl(String url, String expected) {
+ assertEquals(expected, AviatorGrpcClientHelper.toSafeUrlForDisplay(url));
+ }
+
+ @Test
+ void shouldNotExposeUserInfoInInvalidPortMessage() {
+ var ex = assertThrows(AviatorSimpleException.class,
+ () -> AviatorGrpcClientHelper.parseTarget("https://user:sentinel-secret@aviator.invalid:99999"));
+
+ assertFalse(ex.getMessage().contains("sentinel-secret"));
+ assertTrue(ex.getMessage().contains("***@aviator.invalid:99999"));
+ }
+
+ @Test
+ void shouldNotExposeUserInfoInUnsupportedSchemeException() {
+ var ex = assertThrows(UnsupportedAviatorUrlSchemeException.class,
+ () -> AviatorGrpcClientHelper.parseTarget("http://user:sentinel-secret@aviator.invalid"));
+
+ assertFalse(ex.getMessage().contains("sentinel-secret"));
+ assertFalse(ex.getProvidedUrl().contains("sentinel-secret"));
+ }
}
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/GrpcUtilTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/GrpcUtilTest.java
index 09285bae9e9..434e2154c0a 100644
--- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/GrpcUtilTest.java
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/GrpcUtilTest.java
@@ -13,17 +13,35 @@
package com.fortify.cli.aviator.grpc;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.net.ConnectException;
+import java.security.cert.CertificateException;
import java.util.List;
import java.util.Set;
+import javax.net.ssl.SSLHandshakeException;
+
import org.junit.jupiter.api.Test;
+import com.fortify.aviator.application.ApplicationServiceGrpc;
import com.fortify.aviator.grpc.AuditorResponse;
+import com.fortify.cli.aviator._common.exception.AviatorConnectivityException;
+import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+import com.fortify.cli.aviator._common.exception.AviatorTechnicalException;
import com.fortify.cli.aviator.audit.model.AuditResponse;
import com.fortify.cli.aviator.audit.model.AuditResponse.AuditSkipReason;
import com.fortify.cli.aviator.audit.model.File;
import com.fortify.cli.aviator.audit.model.UserPrompt;
+import com.fortify.cli.common.exception.FcliExceptionHelper;
+
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
class GrpcUtilTest {
@@ -58,4 +76,71 @@ void shouldClassifySkippedServerResponseWithoutInspectingMessage() {
assertEquals(AuditSkipReason.SKIPPED_BY_AVIATOR, auditResponse.getAuditSkipReason());
}
+
+ @Test
+ void waitForReadyPolicyKeepsCurrentUnavailableMapping() {
+ var e = assertThrows(AviatorTechnicalException.class,
+ () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("Connection refused")), AviatorRequestPolicy.WAIT_FOR_READY));
+
+ assertEquals("The Aviator test operation failed with status UNAVAILABLE; check the Aviator URL and network access, "
+ + "run the command again with --log-level DEBUG to log details, and contact support if the problem continues",
+ e.getMessage());
+ }
+
+ @Test
+ void failFastPolicyClassifiesUnavailable() {
+ var e = assertThrows(AviatorConnectivityException.class,
+ () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("Connection refused")), AviatorRequestPolicy.FAIL_FAST));
+
+ assertEquals("Aviator server 127.0.0.1:1 refused the connection; check the Aviator URL", e.getMessage());
+ assertNull(e.getCause());
+ }
+
+ @Test
+ void failFastPolicyKeepsCertificateCauseForPkixGuidance() {
+ var e = assertThrows(AviatorTechnicalException.class,
+ () -> execute(Status.UNAVAILABLE.withCause(new SSLHandshakeException("PKIX path building failed")
+ .initCause(new CertificateException("untrusted"))), AviatorRequestPolicy.FAIL_FAST));
+
+ assertTrue(FcliExceptionHelper.formatException(e).contains("PKIX SSL certificate validation failed"));
+ }
+
+ @Test
+ void failFastPolicyKeepsUnimplementedContractForOlderServerLoginFallback() {
+ var e = assertThrows(AviatorTechnicalException.class,
+ () -> execute(Status.UNIMPLEMENTED, AviatorRequestPolicy.FAIL_FAST));
+
+ var cause = assertInstanceOf(StatusRuntimeException.class, e.getCause());
+ assertEquals(Status.Code.UNIMPLEMENTED, cause.getStatus().getCode());
+ }
+
+ @Test
+ void resourceExhaustedPutsRemediationBeforeSupport() {
+ var e = assertThrows(AviatorSimpleException.class,
+ () -> execute(Status.RESOURCE_EXHAUSTED.withDescription("quota"), AviatorRequestPolicy.FAIL_FAST));
+
+ assertEquals("Aviator server resource limits were exceeded during test (server details: quota); "
+ + "try again later, and contact support if the problem continues", e.getMessage());
+ }
+
+ @Test
+ void failFastPolicyKeepsApplicationStatusMapping() {
+ var e = assertThrows(AviatorSimpleException.class,
+ () -> execute(Status.NOT_FOUND.withDescription("app"), AviatorRequestPolicy.FAIL_FAST));
+
+ assertFalse(e instanceof AviatorConnectivityException);
+ assertTrue(e.getMessage().contains("not found"));
+ }
+
+ private static void execute(Status status, AviatorRequestPolicy policy) {
+ var channel = NettyChannelBuilder.forAddress("127.0.0.1", 1).usePlaintext().build();
+ try {
+ var context = new AviatorConnectionContext(new AviatorEndpoint("127.0.0.1", 1), null);
+ GrpcUtil.executeGrpcCall(policy.configureNewStub(ApplicationServiceGrpc.newBlockingStub(channel), context),
+ (stub, request) -> { throw status.asRuntimeException(); },
+ "request", "test");
+ } finally {
+ channel.shutdownNow();
+ }
+ }
}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/cli/cmd/AviatorAdminConfigCreateCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/cli/cmd/AviatorAdminConfigCreateCommand.java
index d4ed177ca91..0bbd54e5601 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/cli/cmd/AviatorAdminConfigCreateCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/cli/cmd/AviatorAdminConfigCreateCommand.java
@@ -19,6 +19,7 @@
import com.fortify.cli.aviator._common.config.admin.cli.mixin.AviatorAdminConfigNameArgGroup;
import com.fortify.cli.aviator._common.config.admin.helper.AviatorAdminConfigDescriptor;
import com.fortify.cli.aviator._common.config.admin.helper.AviatorAdminConfigHelper;
+import com.fortify.cli.aviator.grpc.AviatorRequestPolicy;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.session.cli.cmd.AbstractSessionLoginCommand;
import com.fortify.cli.common.session.cli.mixin.ISessionNameSupplier;
@@ -47,6 +48,12 @@ public ISessionNameSupplier getSessionNameSupplier() {
protected void logoutBeforeNewLogin(String configName, AviatorAdminConfigDescriptor configDescriptor) {
}
+ @Override
+ protected boolean isReplaceExistingSessionBeforeLogin() {
+ // login() validates the configuration first; keep the existing configuration if validation fails
+ return false;
+ }
+
@Override
protected AviatorAdminConfigDescriptor login(String configName) {
String privateKeyContents = configCreateOptions.getPrivateKeyResolver().getPrivateKeyContents();
@@ -55,7 +62,7 @@ protected AviatorAdminConfigDescriptor login(String configName) {
configCreateOptions.getTenant(),
privateKeyContents);
LOG.info("Validating Aviator admin configuration with the Aviator server...");
- configHelper.validateConfig(configDescriptor);
+ configHelper.validateConfig(configDescriptor, AviatorRequestPolicy.FAIL_FAST);
LOG.info("Aviator admin configuration validated successfully.");
return configDescriptor;
}
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/helper/AviatorAdminConfigHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/helper/AviatorAdminConfigHelper.java
index e7039f7e94b..ada7e58dff1 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/helper/AviatorAdminConfigHelper.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/config/admin/helper/AviatorAdminConfigHelper.java
@@ -15,6 +15,7 @@
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
+import com.fortify.cli.aviator.grpc.AviatorRequestPolicy;
import com.fortify.cli.common.session.helper.AbstractSessionHelper;
public class AviatorAdminConfigHelper extends AbstractSessionHelper {
@@ -38,7 +39,11 @@ protected Class getSessionDescriptorType() {
}
public void validateConfig(AviatorAdminConfigDescriptor configDescriptor) {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ validateConfig(configDescriptor, AviatorRequestPolicy.WAIT_FOR_READY);
+ }
+
+ public void validateConfig(AviatorAdminConfigDescriptor configDescriptor, AviatorRequestPolicy policy) {
+ try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl(), policy)) {
String[] messageAndSignature = AviatorSignatureUtils.createMessageAndSignature(configDescriptor, configDescriptor.getTenant());
String message = messageAndSignature[0];
String signature = messageAndSignature[1];
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/output/cli/cmd/AbstractAviatorAdminSessionOutputCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/output/cli/cmd/AbstractAviatorAdminSessionOutputCommand.java
index 12ad4a13068..3742f89aea5 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/output/cli/cmd/AbstractAviatorAdminSessionOutputCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/output/cli/cmd/AbstractAviatorAdminSessionOutputCommand.java
@@ -15,6 +15,9 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fortify.cli.aviator._common.config.admin.cli.mixin.AviatorAdminConfigDescriptorSupplier;
import com.fortify.cli.aviator._common.config.admin.helper.AviatorAdminConfigDescriptor;
+import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
+import com.fortify.cli.aviator.grpc.AviatorRequestPolicy;
import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand;
import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier;
@@ -33,4 +36,9 @@ public final JsonNode getJsonNode() {
protected abstract JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor);
public abstract boolean isSingular();
+
+ /** The caller must close the client. */
+ protected final AviatorGrpcClient createGrpcClient(AviatorAdminConfigDescriptor configDescriptor) {
+ return AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl(), AviatorRequestPolicy.FAIL_FAST);
+ }
}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/cli/cmd/AviatorUserSessionLoginCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/cli/cmd/AviatorUserSessionLoginCommand.java
index 42f93eb2c10..afa45505416 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/cli/cmd/AviatorUserSessionLoginCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/cli/cmd/AviatorUserSessionLoginCommand.java
@@ -25,6 +25,7 @@
import com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionDescriptor;
import com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionHelper;
import com.fortify.cli.aviator._common.util.AviatorJwtUtils;
+import com.fortify.cli.aviator.grpc.AviatorRequestPolicy;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.session.cli.cmd.AbstractSessionLoginCommand;
@@ -49,6 +50,12 @@ public class AviatorUserSessionLoginCommand extends AbstractSessionLoginCommand<
@Override
protected void logoutBeforeNewLogin(String sessionName, AviatorUserSessionDescriptor sessionDescriptor) {}
+ @Override
+ protected boolean isReplaceExistingSessionBeforeLogin() {
+ // login() validates the token first; keep the existing session if validation fails
+ return false;
+ }
+
@Override
protected AviatorUserSessionDescriptor login(String sessionName) {
String resolvedToken = tokenResolver.getToken();
@@ -56,7 +63,8 @@ protected AviatorUserSessionDescriptor login(String sessionName) {
LOG.info("Validating Aviator user token with the server...");
try {
- var validationResponse = sessionHelper.validateToken(sessionLoginOptions.getAviatorUrl(), resolvedToken).response();
+ var validationResponse = sessionHelper.validateToken(sessionLoginOptions.getAviatorUrl(), resolvedToken,
+ AviatorRequestPolicy.FAIL_FAST).response();
if (!validationResponse.getValid()) {
String errorMsg = validationResponse.getErrorMessage();
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/helper/AviatorUserSessionHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/helper/AviatorUserSessionHelper.java
index dbed50341e3..5031a227993 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/helper/AviatorUserSessionHelper.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/_common/session/user/helper/AviatorUserSessionHelper.java
@@ -14,6 +14,7 @@
import com.fortify.cli.aviator._common.util.AviatorJwtUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
+import com.fortify.cli.aviator.grpc.AviatorRequestPolicy;
import com.fortify.cli.common.session.helper.AbstractSessionHelper;
import com.fortify.grpc.token.TokenValidationResponse;
@@ -42,8 +43,12 @@ public AviatorUserTokenValidationResult validateToken(AviatorUserSessionDescript
}
public AviatorUserTokenValidationResult validateToken(String aviatorUrl, String token) {
+ return validateToken(aviatorUrl, token, AviatorRequestPolicy.WAIT_FOR_READY);
+ }
+
+ public AviatorUserTokenValidationResult validateToken(String aviatorUrl, String token, AviatorRequestPolicy policy) {
var tenantName = AviatorJwtUtils.extractTenantNameFromToken(token);
- try (var client = AviatorGrpcClientHelper.createClient(aviatorUrl)) {
+ try (var client = AviatorGrpcClientHelper.createClient(aviatorUrl, policy)) {
var response = client.validateUserToken(token, tenantName);
return new AviatorUserTokenValidationResult(tenantName, response);
}
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppAddEntitlementCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppAddEntitlementCommand.java
index a3436bc9e94..5978d488555 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppAddEntitlementCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppAddEntitlementCommand.java
@@ -21,7 +21,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
@@ -37,7 +36,7 @@ public class AviatorAppAddEntitlementCommand extends AbstractAviatorAdminSession
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = AviatorSignatureUtils.createMessageAndSignature(
configDescriptor,
configDescriptor.getTenant(),
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppCreateCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppCreateCommand.java
index 72380a8b9e5..a72e1d6bf33 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppCreateCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppCreateCommand.java
@@ -23,7 +23,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin;
@@ -42,7 +41,7 @@ public class AviatorAppCreateCommand extends AbstractAviatorAdminSessionOutputCo
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor);
Application createdApplication = createApplication(client, configDescriptor, messageAndSignature);
LOG.info("Application '{}' created successfully for tenant: {}", applicationName, configDescriptor.getTenant());
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppDeleteCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppDeleteCommand.java
index eb0c13fe327..fbcca2b56ca 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppDeleteCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppDeleteCommand.java
@@ -25,7 +25,6 @@
import com.fortify.cli.aviator._common.output.cli.cmd.AbstractAviatorAdminSessionOutputCommand;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import lombok.Getter;
@@ -41,7 +40,7 @@ public class AviatorAppDeleteCommand extends AbstractAviatorAdminSessionOutputCo
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor);
ApplicationResponseMessage response = deleteApplication(client, configDescriptor, messageAndSignature);
JsonNode result = processDeleteResponse(response);
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppGetCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppGetCommand.java
index b1f12d10984..006ea270168 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppGetCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppGetCommand.java
@@ -29,7 +29,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import lombok.Getter;
@@ -46,7 +45,7 @@ public class AviatorAppGetCommand extends AbstractAviatorAdminSessionOutputComma
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor);
Application application = getApplication(client, configDescriptor, messageAndSignature);
JsonNode response = processGetApplicationResponse(AviatorGrpcUtils.grpcToJsonNode(application));
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppListCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppListCommand.java
index 094f3dc91b7..7595274cf9b 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppListCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppListCommand.java
@@ -30,7 +30,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import lombok.Getter;
@@ -45,7 +44,7 @@ public class AviatorAppListCommand extends AbstractAviatorAdminSessionOutputComm
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor);
List applications = listApplications(client, configDescriptor, messageAndSignature);
return formatApplicationsArray(applications, configDescriptor.getTenant());
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppUpdateCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppUpdateCommand.java
index da0adf98f6c..7e7e034c0b5 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppUpdateCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/app/cli/cmd/AviatorAppUpdateCommand.java
@@ -29,7 +29,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
@@ -49,7 +48,7 @@ public class AviatorAppUpdateCommand extends AbstractAviatorAdminSessionOutputCo
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor);
Application updatedApplication = updateApplication(client, configDescriptor, messageAndSignature);
JsonNode response = processUpdateApplicationResponse(AviatorGrpcUtils.grpcToJsonNode(updatedApplication));
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AbstractAviatorSastEntitlementListCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AbstractAviatorSastEntitlementListCommand.java
index 5f9bfa4ea51..79500af4b48 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AbstractAviatorSastEntitlementListCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AbstractAviatorSastEntitlementListCommand.java
@@ -26,7 +26,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
/**
* Abstract base class for SAST entitlement list commands. Provides the shared
@@ -38,7 +37,7 @@ public abstract class AbstractAviatorSastEntitlementListCommand extends Abstract
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
var messageAndSignature = AviatorSignatureUtils.createMessageAndSignature(configDescriptor, configDescriptor.getTenant());
String message = messageAndSignature[0];
String signature = messageAndSignature[1];
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AviatorEntitlementListDastCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AviatorEntitlementListDastCommand.java
index 9e619dc1075..5cd8fe81fbd 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AviatorEntitlementListDastCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/entitlement/cli/cmd/AviatorEntitlementListDastCommand.java
@@ -27,7 +27,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import lombok.Getter;
import picocli.CommandLine.Command;
@@ -40,7 +39,7 @@ public class AviatorEntitlementListDastCommand extends AbstractAviatorAdminSessi
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
var messageAndSignature = AviatorSignatureUtils.createMessageAndSignature(configDescriptor, configDescriptor.getTenant());
String message = messageAndSignature[0];
String signature = messageAndSignature[1];
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenCreateCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenCreateCommand.java
index dee46793140..33b924138aa 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenCreateCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenCreateCommand.java
@@ -32,7 +32,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.aviator.util.FileUtil;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
@@ -57,7 +56,7 @@ public class AviatorTokenCreateCommand extends AbstractAviatorAdminSessionOutput
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor);
TokenGenerationResponse response = generateToken(client, configDescriptor, messageAndSignature);
return processTokenResponse(response);
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenDeleteCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenDeleteCommand.java
index aa2aa814b24..aef69d516b8 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenDeleteCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenDeleteCommand.java
@@ -25,7 +25,6 @@
import com.fortify.cli.aviator._common.session.user.cli.mixin.AviatorUserTokenResolverMixin;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.grpc.token.DeleteTokenResponse;
@@ -45,7 +44,7 @@ public class AviatorTokenDeleteCommand extends AbstractAviatorAdminSessionOutput
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
String tokenToDelete = tokenResolver.getToken();
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor, tokenToDelete);
DeleteTokenResponse response = deleteToken(client, configDescriptor, messageAndSignature, tokenToDelete);
return processDeleteResponse(response, tokenToDelete);
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenListCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenListCommand.java
index 08f450ebe20..c03df4cf811 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenListCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenListCommand.java
@@ -29,7 +29,6 @@
import com.fortify.cli.aviator._common.util.AviatorGrpcUtils;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.aviator.util.StringUtil;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.grpc.token.ListTokensResponse;
@@ -50,7 +49,7 @@ public class AviatorTokenListCommand extends AbstractAviatorAdminSessionOutputCo
@Override
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
ArrayNode tokensArray = fetchAllTokens(client, configDescriptor);
logTokenCount(tokensArray.size());
return tokensArray;
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenRevokeCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenRevokeCommand.java
index e85fc311334..258fa698b5c 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenRevokeCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenRevokeCommand.java
@@ -25,7 +25,6 @@
import com.fortify.cli.aviator._common.session.user.cli.mixin.AviatorUserTokenResolverMixin;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.grpc.token.RevokeTokenResponse;
@@ -45,7 +44,7 @@ public class AviatorTokenRevokeCommand extends AbstractAviatorAdminSessionOutput
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
String tokenToRevoke = tokenResolver.getToken();
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor, tokenToRevoke);
RevokeTokenResponse response = revokeToken(client, configDescriptor, messageAndSignature, tokenToRevoke);
return processRevokeResponse(response, tokenToRevoke);
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenValidateCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenValidateCommand.java
index 5b682f3af97..84c798d42ee 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenValidateCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/token/cli/cmd/AviatorTokenValidateCommand.java
@@ -22,7 +22,6 @@
import com.fortify.cli.aviator._common.session.user.cli.mixin.AviatorUserTokenResolverMixin;
import com.fortify.cli.aviator._common.util.AviatorSignatureUtils;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
-import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.grpc.token.TokenValidationResponse;
@@ -39,7 +38,7 @@ public class AviatorTokenValidateCommand extends AbstractAviatorAdminSessionOutp
protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) throws AviatorSimpleException, AviatorTechnicalException {
String tokenToValidate = tokenResolver.getToken();
- try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl())) {
+ try (AviatorGrpcClient client = createGrpcClient(configDescriptor)) {
String[] messageAndSignature = createMessageAndSignature(configDescriptor, tokenToValidate);
TokenValidationResponse response = validateToken(client, configDescriptor, messageAndSignature, tokenToValidate);
return createResponseNode(response);
diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/_common/output/cli/cmd/AviatorOrdinaryCommandConnectivityTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/_common/output/cli/cmd/AviatorOrdinaryCommandConnectivityTest.java
new file mode 100644
index 00000000000..e6884ad1a1d
--- /dev/null
+++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/_common/output/cli/cmd/AviatorOrdinaryCommandConnectivityTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator._common.output.cli.cmd;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+
+import org.junit.jupiter.api.AfterAll;
+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.function.Executable;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fortify.cli.aviator._common.config.admin.helper.AviatorAdminConfigDescriptor;
+import com.fortify.cli.aviator._common.exception.AviatorConnectivityException;
+import com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionHelper;
+import com.fortify.cli.aviator.grpc.AviatorRequestPolicy;
+import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
+import com.fortify.cli.common.cli.util.FcliExecutionContextHolder;
+import com.fortify.cli.common.output.cli.mixin.IOutputHelper;
+
+/**
+ * Checks that fail-fast Aviator commands fail fast with the shared connectivity messages.
+ * Each case targets a closed loopback port, so no Aviator server is needed.
+ */
+class AviatorOrdinaryCommandConnectivityTest {
+ private static final String FCLI_DATA_DIR_PROPERTY = "fcli.env.FCLI_DATA_DIR";
+ private static final Duration FAIL_FAST_LIMIT = Duration.ofSeconds(5);
+
+ // Static, because some fcli helpers cache paths below the fcli data directory in static fields
+ @TempDir static Path fcliDataDir;
+ private static String previousFcliDataDir;
+ private String closedPortUrl;
+
+ @BeforeAll
+ static void setUpFcliDataDir() {
+ previousFcliDataDir = System.getProperty(FCLI_DATA_DIR_PROPERTY);
+ System.setProperty(FCLI_DATA_DIR_PROPERTY, fcliDataDir.toString());
+ }
+
+ @AfterAll
+ static void restoreFcliDataDir() {
+ if ( previousFcliDataDir==null ) {
+ System.clearProperty(FCLI_DATA_DIR_PROPERTY);
+ } else {
+ System.setProperty(FCLI_DATA_DIR_PROPERTY, previousFcliDataDir);
+ }
+ }
+
+ @BeforeEach
+ void setUp() throws IOException {
+ FcliExecutionContextHolder.pushNew();
+ try ( var socket = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()) ) {
+ closedPortUrl = "127.0.0.1:" + socket.getLocalPort();
+ }
+ assumeTrue(AviatorGrpcClientHelper.createConnectionPlan(closedPortUrl).proxyDescriptor().isEmpty(),
+ "This test requires a direct loopback route (configure NO_PROXY for 127.0.0.1)");
+ }
+
+ @AfterEach
+ void tearDown() {
+ FcliExecutionContextHolder.pop();
+ }
+
+ @Test
+ void adminCommandBaseClassCreatesFailFastClient() {
+ var descriptor = new AviatorAdminConfigDescriptor(closedPortUrl, "tenant", "unused-private-key");
+ var command = new TestAdminCommand();
+
+ assertFailsFast(() -> {
+ try ( var client = command.createGrpcClient(descriptor) ) {
+ client.listApplication("tenant", "signature", "message");
+ }
+ });
+ }
+
+ @Test
+ void userSessionTokenValidationFailsFastWithFailFastPolicy() {
+ assertFailsFast(() -> AviatorUserSessionHelper.instance()
+ .validateToken(closedPortUrl, syntheticToken(), AviatorRequestPolicy.FAIL_FAST));
+ }
+
+ private static String syntheticToken() {
+ var encoder = Base64.getUrlEncoder().withoutPadding();
+ return encoder.encodeToString("{\"alg\":\"none\"}".getBytes(StandardCharsets.UTF_8))
+ + "." + encoder.encodeToString("{\"tenantName\":\"tenant\"}".getBytes(StandardCharsets.UTF_8))
+ + ".signature";
+ }
+
+ private static void assertFailsFast(Executable executable) {
+ long start = System.nanoTime();
+ var exception = assertThrows(AviatorConnectivityException.class, executable);
+ var elapsed = Duration.ofNanos(System.nanoTime() - start);
+ assertTrue(elapsed.compareTo(FAIL_FAST_LIMIT) < 0, "Call took " + elapsed);
+ assertTrue(exception.getMessage().contains("refused the connection"), exception.getMessage());
+ }
+
+ private static final class TestAdminCommand extends AbstractAviatorAdminSessionOutputCommand {
+ @Override
+ protected JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor) {
+ return null;
+ }
+
+ @Override
+ public boolean isSingular() {
+ return true;
+ }
+
+ @Override
+ public IOutputHelper getOutputHelper() {
+ return null;
+ }
+ }
+}
diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommand.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommand.java
index ce93114f719..8ca29538306 100644
--- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommand.java
+++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommand.java
@@ -28,7 +28,9 @@ public JsonNode getJsonNode() {
var sessionNameSupplier = getSessionNameSupplier();
String sessionName = sessionNameSupplier==null?"default":sessionNameSupplier.getSessionName();
var sessionHelper = getSessionHelper();
- logoutIfSessionExists(sessionName);
+ if ( isReplaceExistingSessionBeforeLogin() ) {
+ logoutIfSessionExists(sessionName);
+ }
D sessionDescriptor = login(sessionName);
sessionHelper.save(sessionName, sessionDescriptor);
testAuthenticatedConnection(sessionName);
@@ -63,4 +65,12 @@ private void logoutIfSessionExists(String sessionName) {
protected abstract void logoutBeforeNewLogin(String sessionName, D sessionDescriptor);
protected abstract D login(String sessionName);
protected void testAuthenticatedConnection(String sessionName) {}
+
+ /**
+ * Return false when login only validates credentials and the old session needs no remote logout.
+ * This keeps the existing session if validation fails; save replaces it after success.
+ */
+ protected boolean isReplaceExistingSessionBeforeLogin() {
+ return true;
+ }
}
diff --git a/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommandTest.java b/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommandTest.java
new file mode 100644
index 00000000000..7c0356ed398
--- /dev/null
+++ b/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommandTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.common.session.cli.cmd;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Path;
+import java.util.Date;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fortify.cli.common.cli.util.FcliExecutionContextHolder;
+import com.fortify.cli.common.exception.FcliSimpleException;
+import com.fortify.cli.common.output.cli.mixin.IOutputHelper;
+import com.fortify.cli.common.session.cli.mixin.ISessionNameSupplier;
+import com.fortify.cli.common.session.helper.AbstractSessionHelper;
+import com.fortify.cli.common.session.helper.ISessionDescriptor;
+
+class AbstractSessionLoginCommandTest {
+ private static final String FCLI_DATA_DIR_PROPERTY = "fcli.env.FCLI_DATA_DIR";
+ private static final String SESSION_NAME = "test-session";
+
+ @TempDir Path fcliDataDir;
+ private String previousFcliDataDir;
+ private final TestSessionHelper sessionHelper = new TestSessionHelper();
+
+ @BeforeEach
+ void setUp() {
+ previousFcliDataDir = System.getProperty(FCLI_DATA_DIR_PROPERTY);
+ System.setProperty(FCLI_DATA_DIR_PROPERTY, fcliDataDir.toString());
+ FcliExecutionContextHolder.pushNew();
+ }
+
+ @AfterEach
+ void tearDown() {
+ FcliExecutionContextHolder.pop();
+ if ( previousFcliDataDir==null ) {
+ System.clearProperty(FCLI_DATA_DIR_PROPERTY);
+ } else {
+ System.setProperty(FCLI_DATA_DIR_PROPERTY, previousFcliDataDir);
+ }
+ }
+
+ @Test
+ void defaultLifecycleDestroysExistingSessionBeforeLogin() {
+ sessionHelper.save(SESSION_NAME, new TestSessionDescriptor("old"));
+ var command = new TestLoginCommand(sessionHelper, true, null);
+
+ assertThrows(FcliSimpleException.class, command::getJsonNode);
+
+ assertFalse(sessionHelper.exists(SESSION_NAME));
+ assertEquals(1, command.logoutCount);
+ }
+
+ @Test
+ void optOutKeepsExistingSessionWhenLoginFails() {
+ sessionHelper.save(SESSION_NAME, new TestSessionDescriptor("old"));
+ var command = new TestLoginCommand(sessionHelper, false, null);
+
+ assertThrows(FcliSimpleException.class, command::getJsonNode);
+
+ assertTrue(sessionHelper.exists(SESSION_NAME));
+ assertEquals("old", sessionHelper.get(SESSION_NAME, true).getValue());
+ assertEquals(0, command.logoutCount);
+ }
+
+ @Test
+ void optOutCreatesNoSessionWhenLoginFailsForNewName() {
+ var command = new TestLoginCommand(sessionHelper, false, null);
+
+ assertThrows(FcliSimpleException.class, command::getJsonNode);
+
+ assertFalse(sessionHelper.exists(SESSION_NAME));
+ }
+
+ @Test
+ void optOutReplacesExistingSessionWhenLoginSucceeds() {
+ sessionHelper.save(SESSION_NAME, new TestSessionDescriptor("old"));
+ var command = new TestLoginCommand(sessionHelper, false, new TestSessionDescriptor("new"));
+
+ command.getJsonNode();
+
+ assertEquals("new", sessionHelper.get(SESSION_NAME, true).getValue());
+ assertEquals(0, command.logoutCount);
+ }
+
+ private static final class TestLoginCommand extends AbstractSessionLoginCommand {
+ private final TestSessionHelper sessionHelper;
+ private final boolean replaceExistingSessionBeforeLogin;
+ private final TestSessionDescriptor loginResult;
+ private int logoutCount;
+
+ private TestLoginCommand(TestSessionHelper sessionHelper, boolean replaceExistingSessionBeforeLogin,
+ TestSessionDescriptor loginResult) {
+ this.sessionHelper = sessionHelper;
+ this.replaceExistingSessionBeforeLogin = replaceExistingSessionBeforeLogin;
+ this.loginResult = loginResult;
+ }
+
+ @Override
+ public ISessionNameSupplier getSessionNameSupplier() {
+ return () -> SESSION_NAME;
+ }
+
+ @Override
+ protected void logoutBeforeNewLogin(String sessionName, TestSessionDescriptor sessionDescriptor) {
+ logoutCount++;
+ }
+
+ @Override
+ protected TestSessionDescriptor login(String sessionName) {
+ if ( loginResult==null ) {
+ throw new FcliSimpleException("Validation failed");
+ }
+ return loginResult;
+ }
+
+ @Override
+ protected boolean isReplaceExistingSessionBeforeLogin() {
+ return replaceExistingSessionBeforeLogin;
+ }
+
+ @Override
+ protected AbstractSessionHelper getSessionHelper() {
+ return sessionHelper;
+ }
+
+ @Override
+ public IOutputHelper getOutputHelper() {
+ return null;
+ }
+ }
+
+ private static final class TestSessionHelper extends AbstractSessionHelper {
+ @Override
+ public String getType() {
+ return "test-login";
+ }
+
+ @Override
+ protected String getLoginCmd() {
+ return "fcli test session login";
+ }
+
+ @Override
+ protected Class getSessionDescriptorType() {
+ return TestSessionDescriptor.class;
+ }
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static final class TestSessionDescriptor implements ISessionDescriptor {
+ private String value;
+
+ public TestSessionDescriptor() {}
+
+ TestSessionDescriptor(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String getUrlDescriptor() {
+ return "test";
+ }
+
+ @Override
+ public Date getCreatedDate() {
+ return new Date();
+ }
+
+ @Override
+ public Date getExpiryDate() {
+ return null;
+ }
+
+ @Override
+ public String getType() {
+ return "test-login";
+ }
+ }
+}