diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/UnsupportedAviatorUrlSchemeException.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/UnsupportedAviatorUrlSchemeException.java
new file mode 100644
index 00000000000..07cb8e97da8
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/UnsupportedAviatorUrlSchemeException.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * User-facing rejection of a non-{@code https} Aviator target scheme.
+ *
+ * Typed so diagnose can map endpoint UX without parsing exception message text.
+ */
+public class UnsupportedAviatorUrlSchemeException extends AviatorSimpleException {
+ private static final long serialVersionUID = 1L;
+
+ public static final String STAGE_SUMMARY = "Unsupported URL scheme";
+ public static final String STAGE_GUIDANCE = "Use a supported Aviator URL (https://host[:port])";
+
+ private final String scheme;
+ private final String providedUrl;
+
+ public UnsupportedAviatorUrlSchemeException(String scheme, String providedUrl) {
+ super(STAGE_SUMMARY+" '"+scheme+"'. "+STAGE_GUIDANCE+". Provided URL: "+providedUrl);
+ this.scheme = scheme;
+ this.providedUrl = providedUrl;
+ }
+
+ public String getScheme() {
+ return scheme;
+ }
+
+ public String getProvidedUrl() {
+ return providedUrl;
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnostics.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnostics.java
index 69bdb94428c..820839f2d8f 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnostics.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnostics.java
@@ -16,10 +16,12 @@
import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
+import java.util.StringJoiner;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.aviator._common.exception.AviatorBugException;
import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+import com.fortify.cli.aviator._common.exception.UnsupportedAviatorUrlSchemeException;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan;
import com.fortify.cli.common.json.JsonHelper;
@@ -44,14 +46,14 @@ public AviatorConnectionDiagnostics(IAviatorDiagnosticProbe probe) {
}
public AviatorDiagnosticReport diagnose(String url, int timeoutSeconds, String sourceType) {
+ var report = new AviatorDiagnosticReport();
+ report.begin(AviatorDiagnosticStage.ENDPOINT);
try {
- return diagnose(AviatorGrpcClientHelper.createConnectionPlan(url), timeoutSeconds, sourceType);
+ return diagnoseValidated(report, AviatorGrpcClientHelper.createConnectionPlan(url), timeoutSeconds,
+ sourceType);
} catch (AviatorSimpleException e) {
- var report = new AviatorDiagnosticReport();
- report.fail(AviatorDiagnosticStage.ENDPOINT,
- "Endpoint is invalid", "Use a valid Aviator host name and optional port",
- AviatorDiagnosticEvidence.errorEvidence(e));
- skipAfter(report, null, AviatorDiagnosticStage.ENDPOINT, "endpoint configuration failed");
+ failEndpoint(report, e);
+ skipAfter(report, null, AviatorDiagnosticStage.ENDPOINT, "endpoint validation failed");
return report;
}
}
@@ -61,8 +63,15 @@ public AviatorDiagnosticReport diagnose(String url, int timeoutSeconds, String s
*/
public AviatorDiagnosticReport diagnose(AviatorConnectionPlan connectionPlan, int timeoutSeconds, String sourceType) {
var report = new AviatorDiagnosticReport();
+ report.begin(AviatorDiagnosticStage.ENDPOINT);
+ return diagnoseValidated(report, connectionPlan, timeoutSeconds, sourceType);
+ }
+
+ private AviatorDiagnosticReport diagnoseValidated(AviatorDiagnosticReport report,
+ AviatorConnectionPlan connectionPlan, int timeoutSeconds, String sourceType) {
report.pass(AviatorDiagnosticStage.ENDPOINT,
- "Endpoint is valid", "No action required", endpointEvidence(connectionPlan, sourceType));
+ "Endpoint is valid: "+connectionPlan.normalizedUrl(), "No action required",
+ endpointEvidence(connectionPlan, sourceType));
if (!runDns(report, connectionPlan)) {
skipAfter(report, connectionPlan, AviatorDiagnosticStage.DNS, "DNS resolution failed");
@@ -79,7 +88,23 @@ public AviatorDiagnosticReport diagnose(AviatorConnectionPlan connectionPlan, in
return report;
}
+ private static void failEndpoint(AviatorDiagnosticReport report, AviatorSimpleException e) {
+ var evidence = AviatorDiagnosticEvidence.errorEvidence(e);
+ if (e instanceof UnsupportedAviatorUrlSchemeException schemeFailure) {
+ evidence.put("scheme", schemeFailure.getScheme());
+ evidence.put("providedUrl", schemeFailure.getProvidedUrl());
+ report.fail(AviatorDiagnosticStage.ENDPOINT,
+ UnsupportedAviatorUrlSchemeException.STAGE_SUMMARY,
+ UnsupportedAviatorUrlSchemeException.STAGE_GUIDANCE,
+ evidence);
+ return;
+ }
+ report.fail(AviatorDiagnosticStage.ENDPOINT,
+ "Endpoint is invalid", "Use a valid Aviator host name and optional port", evidence);
+ }
+
private boolean runDns(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan) {
+ report.begin(AviatorDiagnosticStage.DNS);
try {
var evidence = JsonHelper.getObjectMapper().createObjectNode();
addAddresses(evidence, "resolvedAddresses", probe.resolve(connectionPlan.target().host()));
@@ -87,7 +112,8 @@ private boolean runDns(AviatorDiagnosticReport report, AviatorConnectionPlan con
var proxy = connectionPlan.proxyDescriptor().get();
addAddresses(evidence, "proxyResolvedAddresses", probe.resolve(proxy.getProxyHost()));
}
- report.pass(AviatorDiagnosticStage.DNS, "Host name resolved", "No action required", evidence);
+ report.pass(AviatorDiagnosticStage.DNS,
+ "Host name resolved: "+addresses(evidence, "resolvedAddresses"), "No action required", evidence);
return true;
} catch (IOException e) {
report.fail(AviatorDiagnosticStage.DNS,
@@ -105,13 +131,15 @@ private boolean runDns(AviatorDiagnosticReport report, AviatorConnectionPlan con
* handshake failure. Do not fold TCP into the tunnel solely to avoid a double connect.
*/
private boolean runTcp(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan, int timeoutSeconds) {
+ report.begin(AviatorDiagnosticStage.TCP);
var proxyDescriptor = connectionPlan.proxyDescriptor();
var nextHopHost = proxyDescriptor.map(proxy -> proxy.getProxyHost()).orElse(connectionPlan.target().host());
var nextHopPort = proxyDescriptor.map(proxy -> proxy.getProxyPort()).orElse(connectionPlan.effectivePort());
var evidence = nextHopEvidence(nextHopHost, nextHopPort, proxyDescriptor.isPresent());
try {
probe.connect(nextHopHost, nextHopPort, timeoutSeconds);
- report.pass(AviatorDiagnosticStage.TCP, "TCP connection opened", "No action required", evidence);
+ report.pass(AviatorDiagnosticStage.TCP,
+ "TCP connection opened to "+nextHopHost+":"+nextHopPort, "No action required", evidence);
return true;
} catch (Exception e) {
putError(evidence, e);
@@ -132,6 +160,9 @@ private boolean runTcp(AviatorDiagnosticReport report, AviatorConnectionPlan con
*/
private boolean runTunnelStages(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan,
int timeoutSeconds) {
+ var hasProxy = connectionPlan.proxyDescriptor().isPresent();
+ // First stage that will be recorded from this shared tunnel session.
+ report.begin(hasProxy ? AviatorDiagnosticStage.PROXY : AviatorDiagnosticStage.TLS);
var tunnel = probe.probeTunnel(connectionPlan, timeoutSeconds);
if (tunnel instanceof AviatorTunnelResult.ProxyConnectFailed failed) {
appendProxyFailure(report, connectionPlan, failed);
@@ -139,6 +170,9 @@ private boolean runTunnelStages(AviatorDiagnosticReport report, AviatorConnectio
return false;
}
appendProxyPassIfConfigured(report, connectionPlan, tunnel);
+ if (hasProxy) {
+ report.begin(AviatorDiagnosticStage.TLS);
+ }
if (tunnel instanceof AviatorTunnelResult.TlsFailed failed) {
appendTlsFailure(report, connectionPlan, failed);
skipAfter(report, connectionPlan, AviatorDiagnosticStage.TLS, "TLS handshake failed");
@@ -167,7 +201,8 @@ private void appendProxyPassIfConfigured(AviatorDiagnosticReport report,
var evidence = JsonHelper.getObjectMapper().createObjectNode();
evidence.put("proxyConnectStatus", tunnel.proxyConnectStatus());
putProxyEvidence(evidence, connectionPlan);
- report.pass(AviatorDiagnosticStage.PROXY, "Proxy CONNECT succeeded", "No action required", evidence);
+ report.pass(AviatorDiagnosticStage.PROXY,
+ "Proxy CONNECT succeeded through "+proxyEndpoint(evidence), "No action required", evidence);
}
private void appendTlsSuccess(AviatorDiagnosticReport report, AviatorTunnelResult.TlsSucceeded ok) {
@@ -180,10 +215,11 @@ private void appendTlsSuccess(AviatorDiagnosticReport report, AviatorTunnelResul
evidence.put("tlsPhase", AviatorTlsPhase.HANDSHAKE.id());
if (!"h2".equals(ok.applicationProtocol())) {
report.warn(AviatorDiagnosticStage.TLS,
- "TLS works, but HTTP/2 was not enabled",
+ tlsSummary("TLS works, but HTTP/2 was not enabled", ok),
"Allow ALPN h2 through the proxy or gateway to aviator-grpc-server", true, evidence);
} else {
- report.pass(AviatorDiagnosticStage.TLS, "TLS and HTTP/2 are available", "No action required", evidence);
+ report.pass(AviatorDiagnosticStage.TLS,
+ tlsSummary("TLS and HTTP/2 are available", ok), "No action required", evidence);
}
}
@@ -207,6 +243,7 @@ private void appendTlsFailure(AviatorDiagnosticReport report, AviatorConnectionP
}
private void runGrpc(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan, int timeoutSeconds) {
+ report.begin(AviatorDiagnosticStage.GRPC);
try {
applyGrpc(report, probe.probeGrpc(connectionPlan.originalUrl(), timeoutSeconds));
} catch (Exception e) {
@@ -220,10 +257,11 @@ private void applyGrpc(AviatorDiagnosticReport report, AviatorGrpcReachabilityRe
if (grpc.pattern() != null) {
evidence.put("pattern", grpc.pattern().wireId());
}
+ var summary = grpc.stageSummary();
if (grpc.stagePass()) {
- report.pass(AviatorDiagnosticStage.GRPC, grpc.stageSummary(), grpc.stageGuidance(), evidence);
+ report.pass(AviatorDiagnosticStage.GRPC, summary, grpc.stageGuidance(), evidence);
} else {
- report.fail(AviatorDiagnosticStage.GRPC, grpc.stageSummary(), grpc.stageGuidance(), evidence);
+ report.fail(AviatorDiagnosticStage.GRPC, summary, grpc.stageGuidance(), evidence);
}
}
@@ -275,7 +313,7 @@ private static void skipAfter(AviatorDiagnosticReport report, AviatorConnectionP
if (stage == AviatorDiagnosticStage.PROXY && !hasProxy) {
continue;
}
- report.skipWarn(stage, "Skipped because " + reason,
+ report.skipWarn(stage, reason,
"Resolve the previous failed required stage first",
AviatorDiagnosticEvidence.empty());
}
@@ -285,4 +323,18 @@ private static void addAddresses(ObjectNode evidence, String fieldName, InetAddr
var array = evidence.putArray(fieldName);
Arrays.stream(addresses).map(InetAddress::getHostAddress).forEach(array::add);
}
+
+ private static String addresses(ObjectNode evidence, String fieldName) {
+ var result = new StringJoiner(", ");
+ evidence.withArray(fieldName).forEach(address -> result.add(address.asText()));
+ return result.toString();
+ }
+
+ private static String proxyEndpoint(ObjectNode evidence) {
+ return evidence.path("proxyHost").asText()+":"+evidence.path("proxyPort").asInt();
+ }
+
+ private static String tlsSummary(String summary, AviatorTunnelResult.TlsSucceeded result) {
+ return summary+": "+result.protocol()+", ALPN "+result.applicationProtocol();
+ }
}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticReport.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticReport.java
index 3e2a96c63cd..64a3544ba50 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticReport.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticReport.java
@@ -16,22 +16,77 @@
import java.util.Collections;
import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.common.json.JsonHelper;
/**
- * Owns stage order and collects diagnostic rows for one diagnose run.
+ * Owns stage order, collects diagnostic rows, and emits support log lines for one diagnose run.
+ *
+ * Call {@link #begin(AviatorDiagnosticStage)} (or the string overload) before running a stage;
+ * {@code pass}/{@code fail}/{@code warn}/{@code skip*} record the row and log the outcome.
+ * Skip APIs take a structured {@code reason} so log formatting never parses summary English.
*/
public final class AviatorDiagnosticReport {
+ private static final Logger LOG = LoggerFactory.getLogger(AviatorDiagnosticReport.class);
+
private final List stages = new ArrayList<>();
public int nextOrder() {
return stages.size() + 1;
}
+ /** DEBUG start line before a stage is executed (not for pure skip rows). */
+ public void begin(AviatorDiagnosticStage stage) {
+ LOG.debug("Starting {} diagnostic", stage.displayName());
+ }
+
+ /** DEBUG start line for product stages (token/admin) that are not transport enums. */
+ public void begin(String stageId) {
+ LOG.debug("Starting {} diagnostic", AviatorDiagnosticStage.displayNameFor(stageId));
+ }
+
public void add(AviatorDiagnosticStageResult result) {
stages.add(result);
+ logStageOutcome(result, false, null);
+ }
+
+ private void addSkip(AviatorDiagnosticStageResult result, String reason) {
+ stages.add(result);
+ logStageOutcome(result, true, reason);
+ }
+
+ private static void logStageOutcome(AviatorDiagnosticStageResult result, boolean skipped, String skipReason) {
+ var name = AviatorDiagnosticStage.displayNameFor(result.stage());
+ if (skipped) {
+ LOG.info("{} skipped because {}", name, nullToEmpty(skipReason));
+ return;
+ }
+ switch (result.status()) {
+ case PASS -> LOG.info("{}: PASS - {}", name, nullToEmpty(result.summary()));
+ case FAIL -> LOG.error("{}: FAIL - {}", name, failDetail(result));
+ case WARN -> LOG.info("{}: WARN - {}", name, nullToEmpty(result.summary()));
+ }
+ }
+
+ private static String failDetail(AviatorDiagnosticStageResult result) {
+ var summary = nullToEmpty(result.summary());
+ var evidence = result.evidence();
+ if (evidence == null || !evidence.hasNonNull("exceptionMessage")) {
+ return summary;
+ }
+ var exceptionMessage = evidence.get("exceptionMessage").asText();
+ if (exceptionMessage == null || exceptionMessage.isBlank()) {
+ return summary;
+ }
+ return summary+" ("+exceptionMessage+")";
+ }
+
+ private static String nullToEmpty(String value) {
+ return value == null ? "" : value;
}
public void pass(AviatorDiagnosticStage stage, String summary, String guidance, ObjectNode evidence) {
@@ -43,11 +98,12 @@ public void fail(AviatorDiagnosticStage stage, String summary, String guidance,
}
/**
- * Transport skip WARN. {@code required=true} documents the stage as part of the required
- * pipeline; WARN itself never drives process exit (only required FAIL does).
+ * Required transport skip WARN. Builds summary {@code Skipped because {reason}} and logs
+ * as a skip without parsing summary text.
*/
- public void skipWarn(AviatorDiagnosticStage stage, String summary, String guidance, ObjectNode evidence) {
- add(AviatorDiagnosticStageResult.warn(nextOrder(), stage, summary, guidance, true, evidence));
+ public void skipWarn(AviatorDiagnosticStage stage, String reason, String guidance, ObjectNode evidence) {
+ addSkip(AviatorDiagnosticStageResult.warn(nextOrder(), stage, skipSummary(reason), guidance, true, evidence),
+ reason);
}
public void warn(AviatorDiagnosticStage stage, String summary, String guidance, boolean required,
@@ -63,8 +119,16 @@ public void optionalFail(String stage, String description, String summary, Strin
add(AviatorDiagnosticStageResult.optionalFail(nextOrder(), stage, description, summary, guidance, evidence));
}
- public void optionalSkipWarn(String stage, String description, String summary, String guidance, ObjectNode evidence) {
- add(AviatorDiagnosticStageResult.warn(nextOrder(), stage, description, summary, guidance, false, evidence));
+ /**
+ * Optional product-stage skip WARN. Builds summary {@code Skipped because {reason}}.
+ */
+ public void optionalSkipWarn(String stage, String description, String reason, String guidance, ObjectNode evidence) {
+ addSkip(AviatorDiagnosticStageResult.warn(nextOrder(), stage, description, skipSummary(reason), guidance, false,
+ evidence), reason);
+ }
+
+ private static String skipSummary(String reason) {
+ return "Skipped because "+reason;
}
public List stages() {
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticStage.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticStage.java
index c0cabd7a8af..d4cdab1c173 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticStage.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDiagnosticStage.java
@@ -20,19 +20,22 @@
* string stage ids from the connection diagnose helper only.
*/
public enum AviatorDiagnosticStage {
- ENDPOINT("endpoint", "Aviator endpoint configuration validation"),
- DNS("dns", "DNS resolution"),
- TCP("tcp", "TCP connectivity"),
- PROXY("proxy", "HTTP proxy CONNECT"),
- TLS("tls", "TLS handshake"),
- GRPC("grpc", "gRPC request/response reachability");
+ ENDPOINT("endpoint", "Aviator endpoint configuration validation", "Endpoint"),
+ DNS("dns", "DNS resolution", "DNS"),
+ TCP("tcp", "TCP connectivity", "TCP"),
+ PROXY("proxy", "HTTP proxy CONNECT", "Proxy"),
+ TLS("tls", "TLS handshake", "TLS"),
+ GRPC("grpc", "gRPC request/response reachability", "gRPC");
private final String id;
private final String description;
+ /** Human-readable label for log lines (e.g. {@code Endpoint}, {@code gRPC}). */
+ private final String displayName;
- AviatorDiagnosticStage(String id, String description) {
+ AviatorDiagnosticStage(String id, String description, String displayName) {
this.id = id;
this.description = description;
+ this.displayName = displayName;
}
public String id() {
@@ -42,4 +45,21 @@ public String id() {
public String description() {
return description;
}
+
+ public String displayName() {
+ return displayName;
+ }
+
+ /** Resolve wire id to display name; falls back to a simple capitalization of {@code stageId}. */
+ public static String displayNameFor(String stageId) {
+ if (stageId == null || stageId.isBlank()) {
+ return "Stage";
+ }
+ for (var stage : values()) {
+ if (stage.id.equals(stageId)) {
+ return stage.displayName;
+ }
+ }
+ return Character.toUpperCase(stageId.charAt(0)) + stageId.substring(1);
+ }
}
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 3ae94b7a1c5..4c5c9791a70 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
@@ -91,7 +91,7 @@ public class AviatorGrpcClient implements AutoCloseable {
final AtomicBoolean isShutdown = new AtomicBoolean(false);
public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) {
- LOG.info("Initializing AviatorGrpcClient with ManagedChannel");
+ LOG.debug("Initializing AviatorGrpcClient with ManagedChannel");
this.logger = logger;
this.channel = channel;
this.asyncStub = AuditorServiceGrpc.newStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
@@ -116,7 +116,7 @@ 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.info("Initialized AviatorGrpcClient - Host: {}, Port: {}", host, port);
+ LOG.debug("Initialized AviatorGrpcClient - Host: {}, Port: {}", host, port);
}
public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger) {
@@ -172,7 +172,7 @@ public void close() {
}
}
- LOG.info("Client closed");
+ LOG.debug("Client closed");
}
public Application createApplication(String name, String tenantName, String signature, String message) {
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 39672b9f650..d01c9d04586 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
@@ -24,8 +24,10 @@
import org.slf4j.LoggerFactory;
import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+import com.fortify.cli.aviator._common.exception.UnsupportedAviatorUrlSchemeException;
import com.fortify.cli.aviator.config.IAviatorLogger;
import com.fortify.cli.aviator.util.Constants;
+import com.fortify.cli.common.http.UrlSchemes;
import com.fortify.cli.common.http.proxy.helper.ProxyDescriptor;
import com.fortify.cli.common.http.proxy.helper.ProxyHelper;
import com.fortify.cli.common.http.ssl.trust.FcliTrustManager;
@@ -103,12 +105,29 @@ public static ParsedTarget parseTarget(String url) {
}
}
+ /**
+ * Normalizes an Aviator target to an https URL.
+ *
+ * - Scheme-less input ({@code host} or {@code host:port}) gets {@code https://} prepended.
+ * - {@code https} (any casing) is accepted and canonicalized to lowercase {@code https}.
+ * - Any other scheme ({@code http}, {@code ftp}, typos like {@code mttp}, …) is rejected.
+ *
+ */
public static String normalizeUrl(String url) {
var trimmed = url.trim();
- if ( trimmed.matches("^[a-zA-Z][a-zA-Z0-9+\\-.]*://.*$") ) {
+ if ( !UrlSchemes.hasScheme(trimmed) ) {
+ return "https://"+trimmed;
+ }
+ var schemeSeparator = trimmed.indexOf("://");
+ var scheme = trimmed.substring(0, schemeSeparator);
+ if ( !"https".equalsIgnoreCase(scheme) ) {
+ throw new UnsupportedAviatorUrlSchemeException(scheme, url);
+ }
+ if ( "https".equals(scheme) ) {
return trimmed;
}
- return "https://"+trimmed;
+ // Canonicalize scheme casing (HTTPS://host → https://host)
+ return "https"+trimmed.substring(schemeSeparator);
}
private static NettyChannelBuilder configureBuilder(NettyChannelBuilder builder, Optional proxyDescriptor) {
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnosticsTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnosticsTest.java
index 68a12df6d2b..50fb7bdf946 100644
--- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnosticsTest.java
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/diagnose/AviatorConnectionDiagnosticsTest.java
@@ -23,6 +23,7 @@
import org.junit.jupiter.api.Test;
+import com.fortify.cli.aviator._common.exception.UnsupportedAviatorUrlSchemeException;
import com.fortify.cli.aviator.diagnose.support.ConfigurableDiagnosticProbe;
import com.fortify.cli.aviator.diagnose.support.OfflineConnectionPlan;
@@ -35,7 +36,9 @@ void shouldSkipDependentStagesWhenEndpointIsInvalid() {
assertEquals(5, results.size());
assertStage(results.get(0), AviatorDiagnosticStage.ENDPOINT, AviatorDiagnosticStatus.FAIL);
+ assertEquals("Endpoint is invalid", results.get(0).summary());
assertStage(results.get(1), AviatorDiagnosticStage.DNS, AviatorDiagnosticStatus.WARN);
+ assertTrue(results.get(1).summary().contains("endpoint validation failed"));
assertStage(results.get(2), AviatorDiagnosticStage.TCP, AviatorDiagnosticStatus.WARN);
assertStage(results.get(3), AviatorDiagnosticStage.TLS, AviatorDiagnosticStatus.WARN);
assertStage(results.get(4), AviatorDiagnosticStage.GRPC, AviatorDiagnosticStatus.WARN);
@@ -43,6 +46,52 @@ void shouldSkipDependentStagesWhenEndpointIsInvalid() {
assertFalse(probe.tunnelCalled);
}
+ @Test
+ void shouldFailEndpointForUnsupportedUrlScheme() {
+ var probe = new ConfigurableDiagnosticProbe();
+ var report = new AviatorConnectionDiagnostics(probe).diagnose("mttp://vacant", 5, "url");
+ var results = report.stages();
+
+ assertEquals(5, results.size());
+ assertStage(results.get(0), AviatorDiagnosticStage.ENDPOINT, AviatorDiagnosticStatus.FAIL);
+ assertEquals(UnsupportedAviatorUrlSchemeException.STAGE_SUMMARY, results.get(0).summary());
+ assertEquals(UnsupportedAviatorUrlSchemeException.STAGE_GUIDANCE, results.get(0).guidance());
+ assertEquals("mttp", results.get(0).evidence().path("scheme").asText());
+ assertEquals("mttp://vacant", results.get(0).evidence().path("providedUrl").asText());
+ assertStage(results.get(1), AviatorDiagnosticStage.DNS, AviatorDiagnosticStatus.WARN);
+ assertEquals("Skipped because endpoint validation failed", results.get(1).summary());
+ assertStage(results.get(2), AviatorDiagnosticStage.TCP, AviatorDiagnosticStatus.WARN);
+ assertStage(results.get(3), AviatorDiagnosticStage.TLS, AviatorDiagnosticStatus.WARN);
+ assertStage(results.get(4), AviatorDiagnosticStage.GRPC, AviatorDiagnosticStatus.WARN);
+ assertTrue(report.hasRequiredFailure());
+ assertFalse(probe.tunnelCalled);
+ assertFalse(probe.grpcCalled);
+ }
+
+ @Test
+ void shouldFailEndpointForFtpSchemeEvenWithValidLookingHost() {
+ var probe = new ConfigurableDiagnosticProbe();
+ var report = new AviatorConnectionDiagnostics(probe)
+ .diagnose("ftp://aviator-qa01.example.com", 5, "url");
+
+ assertStage(report.stages().get(0), AviatorDiagnosticStage.ENDPOINT, AviatorDiagnosticStatus.FAIL);
+ assertEquals(UnsupportedAviatorUrlSchemeException.STAGE_SUMMARY, report.stages().get(0).summary());
+ assertEquals("ftp", report.stages().get(0).evidence().path("scheme").asText());
+ assertTrue(report.hasRequiredFailure());
+ assertFalse(probe.tunnelCalled);
+ }
+
+ @Test
+ void shouldFailEndpointForHttpScheme() {
+ var probe = new ConfigurableDiagnosticProbe();
+ var report = new AviatorConnectionDiagnostics(probe).diagnose("http://aviator.invalid", 5, "url");
+
+ assertStage(report.stages().get(0), AviatorDiagnosticStage.ENDPOINT, AviatorDiagnosticStatus.FAIL);
+ assertEquals(UnsupportedAviatorUrlSchemeException.STAGE_SUMMARY, report.stages().get(0).summary());
+ assertEquals("http", report.stages().get(0).evidence().path("scheme").asText());
+ assertFalse(probe.tunnelCalled);
+ }
+
@Test
void shouldSkipTransportStagesWhenDnsFails() {
var probe = new ConfigurableDiagnosticProbe();
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 019b91ce691..91ceb215f87 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
@@ -15,12 +15,17 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
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.InetSocketAddress;
import java.net.SocketAddress;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import com.fortify.cli.aviator._common.exception.UnsupportedAviatorUrlSchemeException;
import com.fortify.cli.common.http.proxy.helper.ProxyDescriptor;
import io.grpc.HttpConnectProxiedSocketAddress;
@@ -64,6 +69,26 @@ void shouldCreateConnectionPlanWithExplicitPort() {
assertEquals(8443, plan.effectivePort());
}
+ @Test
+ void shouldCanonicalizeHttpsSchemeCasing() {
+ assertEquals("https://aviator.invalid", AviatorGrpcClientHelper.normalizeUrl("HTTPS://aviator.invalid"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "mttp://vacant",
+ "ftp://aviator.invalid",
+ "http://aviator.invalid",
+ "grpc://aviator.invalid"
+ })
+ void shouldRejectNonHttpsSchemes(String url) {
+ var ex = assertThrows(UnsupportedAviatorUrlSchemeException.class,
+ () -> AviatorGrpcClientHelper.createConnectionPlan(url));
+ assertEquals(url.substring(0, url.indexOf("://")), ex.getScheme());
+ assertEquals(url, ex.getProvidedUrl());
+ assertTrue(ex.getMessage().contains(UnsupportedAviatorUrlSchemeException.STAGE_SUMMARY));
+ }
+
@Test
void shouldBuildHttpConnectProxyAddressWithCredentials() {
var proxy = ProxyDescriptor.builder()
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java
index 0cf8d1397fe..0f66987d9d5 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java
@@ -77,9 +77,10 @@ public void visitAdmin(CredentialRequest.Admin admin) {
/** Skip when gRPC did not respond; otherwise run {@code validate}. */
private void runCredentialStage(AviatorDiagnosticReport report, String stage, String description,
Runnable validate) {
+ report.begin(stage);
if (!report.hasGrpcStagePass()) {
report.optionalSkipWarn(stage, description,
- "Credential check skipped",
+ "gRPC did not respond",
"Fix the gRPC connection first", AviatorDiagnosticEvidence.empty());
return;
}
@@ -99,7 +100,7 @@ private void validateUserToken(AviatorDiagnosticReport report, String aviatorUrl
evidence.put("tenantNamePresent", validationResult.tenantName() != null);
if (validationResult.response().getValid()) {
report.optionalPass(STAGE_TOKEN, STAGE_TOKEN_DESCRIPTION,
- "Aviator token is valid", "No action required", evidence);
+ "Aviator token is valid for the requested tenant", "No action required", evidence);
return;
}
var errorMessage = validationResult.response().getErrorMessage();
@@ -124,7 +125,7 @@ private void validateAdminConfig(AviatorDiagnosticReport report, AviatorAdminCon
var evidence = AviatorDiagnosticEvidence.empty();
evidence.put("tenant", configDescriptor.getTenant());
report.optionalPass(STAGE_ADMIN, STAGE_ADMIN_DESCRIPTION,
- "Aviator admin credentials are valid", "No action required", evidence);
+ "Aviator admin credentials are valid for tenant "+configDescriptor.getTenant(), "No action required", evidence);
} catch (RuntimeException e) {
rethrowIfBug(e);
report.optionalFail(STAGE_ADMIN, STAGE_ADMIN_DESCRIPTION,
diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/http/UrlSchemes.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/http/UrlSchemes.java
new file mode 100644
index 00000000000..e1db79769fc
--- /dev/null
+++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/http/UrlSchemes.java
@@ -0,0 +1,28 @@
+/*
+ * 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.http;
+
+/**
+ * Shared URL scheme detection (RFC 3986 scheme production, lenient for CLI inputs).
+ * Policy after detection (prepend https, reject non-https, …) stays with the caller.
+ */
+public final class UrlSchemes {
+ private static final String SCHEME_PREFIX = "^[a-zA-Z][a-zA-Z0-9+\\-.]*://.*$";
+
+ private UrlSchemes() {}
+
+ /** True when {@code url} starts with a scheme and {@code ://}. */
+ public static boolean hasScheme(String url) {
+ return url != null && url.matches(SCHEME_PREFIX);
+ }
+}
diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/http/proxy/helper/ProxyHelper.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/http/proxy/helper/ProxyHelper.java
index 79e8515e085..d7bd1a3390e 100644
--- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/http/proxy/helper/ProxyHelper.java
+++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/http/proxy/helper/ProxyHelper.java
@@ -26,6 +26,7 @@
import org.slf4j.LoggerFactory;
import com.fortify.cli.common.exception.FcliSimpleException;
+import com.fortify.cli.common.http.UrlSchemes;
import com.fortify.cli.common.util.FcliDataHelper;
import kong.unirest.UnirestInstance;
@@ -75,16 +76,12 @@ static URI parseTargetUri(String targetUrlString) {
private static String normalizeTargetUrl(String targetUrlString) {
var trimmed = StringUtils.trimToEmpty(targetUrlString);
- if ( !hasScheme(trimmed) ) {
+ if ( !UrlSchemes.hasScheme(trimmed) ) {
return "https://"+trimmed;
}
return trimmed;
}
- private static boolean hasScheme(String url) {
- return url.matches("^[a-zA-Z][a-zA-Z0-9+\\-.]*://.*$");
- }
-
static Optional getProxyEnvVarName(String targetScheme, Map env) {
return getProxyEnvVarCandidates(targetScheme).stream()
.filter(envVarName->StringUtils.isNotBlank(env.get(envVarName)))
@@ -108,7 +105,7 @@ private static ProxyDescriptor getProxyDescriptorFromEnvVar(String envVarName, S
private static String normalizeProxyUri(String envVarName, String proxyString) {
var trimmed = StringUtils.trimToEmpty(proxyString);
- if ( hasScheme(trimmed) ) {
+ if ( UrlSchemes.hasScheme(trimmed) ) {
return trimmed;
}
if ( envVarName.toLowerCase(Locale.ROOT).startsWith("https_") ) {