Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4485113
fix: Keep existing Aviator session and admin config when validation f…
kireetivar Oct 22, 2026
18887fe
chore: Allow fcli exceptions to supply a safe DEBUG log description
kireetivar Oct 22, 2026
8c2e7df
chore: Add Aviator connectivity failure classification
kireetivar Oct 22, 2026
c9a5e11
chore: Support fail-fast request policy in Aviator gRPC client
kireetivar Oct 22, 2026
15c5660
feat: Fail fast with clear messages when Aviator commands cannot connect
kireetivar Oct 22, 2026
17ff153
fix: Don't show credentials from the Aviator URL in messages and logs
kireetivar Oct 22, 2026
5534a7c
chore: Simplify Aviator connectivity handling
kireetivar Oct 22, 2026
8c13c04
chore: streamline Aviator connectivity failure handling
kireetivar Oct 22, 2026
efd73a1
chore: Remove the Aviator TLS proxy test and its checked-in key
kireetivar Oct 23, 2026
052812e
chore: Don't attach transport causes to Aviator connectivity exceptions
kireetivar Oct 23, 2026
34ff21a
fix: Show short single-line Aviator connectivity messages
kireetivar Oct 23, 2026
16a68cc
chore: Put remediation before 'contact support' in Aviator gRPC errors
kireetivar Oct 23, 2026
ffdd743
chore: Sort imports in Aviator connectivity tests
kireetivar Oct 26, 2026
5565017
chore: Clarify session replacement hook and test the inherited default
kireetivar Oct 26, 2026
811b3aa
fix: Clarify Aviator connectivity failures and initialize test context
kireetivar Oct 26, 2026
706b235
fix: Keep sensitive Aviator URL details out of messages and logs
kireetivar Oct 26, 2026
004dafe
chore: Make Aviator diagnostic request policy explicit
kireetivar Oct 26, 2026
6fe6862
chore: Document Aviator connectivity and session contracts
kireetivar Oct 26, 2026
7864e14
fix: Avoid assuming local deadline expiry for Aviator timeouts
kireetivar Oct 26, 2026
1dbb5b8
chore: Remove unrelated Aviator request call wrapping
kireetivar Oct 27, 2026
4febc64
docs: Remove misplaced Aviator repository usage section
kireetivar Oct 27, 2026
2c14112
refactor: Require an explicit Aviator unary request policy
kireetivar Oct 27, 2026
27db567
fix: Bound Aviator stream initialization to 30 seconds
kireetivar Oct 27, 2026
10cadf6
fix(aviator): add stream initialization timeout that cancels the RPC …
kireetivar Oct 28, 2026
1e28582
docs: Restore trailing blank line in repo-usage.md
kireetivar Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.fortify.cli.aviator.fpr.processor.AuditProcessor;
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.aviator.util.Constants;
import com.fortify.cli.aviator.util.ResourceUtil;
import com.fortify.cli.aviator.util.StringUtil;
Expand Down Expand Up @@ -169,7 +170,7 @@ public AuditOutcome performAudit(Map<String, AuditResponse> auditResponses) {
if (promptsToAudit.isEmpty()) {
options.getLogger().progress("Audit skipped - no issues to process after filtering.");
} else {
try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(options.getUrl(), options.getLogger(), DEFAULT_PING_INTERVAL_SECONDS)) {
try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(options.getUrl(), options.getLogger(), DEFAULT_PING_INTERVAL_SECONDS, AviatorRequestPolicy.WAIT_FOR_READY)) {
CompletableFuture<Map<String, AuditResponse>> future =
client.processBatchRequests(promptsToAudit, projectName, fprInfo.getBuildId(), SSCApplicationName,
SSCApplicationVersion, options.getToken(), options.getFprHandle(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.fortify.cli.aviator._common.exception.AviatorBugException;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan;
import com.fortify.cli.aviator.grpc.AviatorRequestPolicy;
import com.fortify.cli.common.http.ssl.trust.FcliTrustManager;

import io.grpc.Status;
Expand Down Expand Up @@ -140,7 +141,7 @@ private static AviatorTunnelResult.TlsSucceeded toTlsSucceeded(SSLSocket sslSock

@Override
public AviatorGrpcReachabilityResult probeGrpc(String url, int timeoutSeconds) throws Exception {
try (var client = AviatorGrpcClientHelper.createClient(url)) {
try (var client = AviatorGrpcClientHelper.createClient(url, AviatorRequestPolicy.WAIT_FOR_READY)) {
client.probeGetDefaultQuota(timeoutSeconds);
return AviatorGrpcReachabilityResult.responseReceived("OK", "Default quota response received");
} catch (StatusRuntimeException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AviatorConnectionContext> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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 io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.netty.shaded.io.netty.channel.ConnectTimeoutException;
Comment thread
kireetivar marked this conversation as resolved.
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<AviatorConnectivityFailureKind> classify(StatusRuntimeException e, AviatorConnectionContext context) {
var code = e.getStatus().getCode();
if ( code!=Status.Code.UNAVAILABLE && code!=Status.Code.DEADLINE_EXCEEDED ) {
Comment thread
kireetivar marked this conversation as resolved.
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) {
Comment thread
kireetivar marked this conversation as resolved.
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 -> server + " is unavailable or could not be reached"
+ "; try again later, or check the Aviator URL, network access, and proxy settings";
case DEADLINE_EXCEEDED -> "The Aviator request timed out; try again later"
+ " (the server may be busy, or network/proxy delays may be involved)";
};
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -71,10 +72,7 @@
import com.fortify.grpc.token.TokenValidationResponse;
import com.fortify.grpc.token.ValidateUserTokenRequest;

import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class AviatorGrpcClient implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(AviatorGrpcClient.class);
Expand All @@ -94,15 +92,26 @@ public class AviatorGrpcClient implements AutoCloseable {
private final java.util.concurrent.ScheduledExecutorService pingScheduler;
final AtomicBoolean isShutdown = new AtomicBoolean(false);

public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) {
/** 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")
Expand All @@ -123,15 +132,6 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv
});
}

public AviatorGrpcClient(String host, int port, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) {
this(ManagedChannelBuilder.forAddress(host, port).useTransportSecurity().maxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).keepAliveTime(30, TimeUnit.SECONDS).keepAliveTimeout(10, TimeUnit.SECONDS).keepAliveWithoutCalls(true).enableRetry().compressorRegistry(CompressorRegistry.getDefaultInstance()).decompressorRegistry(DecompressorRegistry.getDefaultInstance()).build(), defaultTimeoutSeconds, logger, pingIntervalSeconds);
LOG.debug("Initialized AviatorGrpcClient - Host: {}, Port: {}", host, port);
}

public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger) {
this(channel, defaultTimeoutSeconds, logger, 30);
}

public CompletableFuture<Map<String, AuditResponse>> processBatchRequests(Queue<UserPrompt> requests, String projectName,
String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle,
List<String> customPriorityOrder, ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) {
Expand Down
Loading
Loading