From 44851138a2ae0f63dca0ae133886a9cf32f257fa Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 12:21:47 +0530 Subject: [PATCH 01/12] fix: Keep existing Aviator session and admin config when validation fails AbstractSessionLoginCommand destroyed an existing session before login() ran. If Aviator token or admin-config validation then failed (for example because the server could not be reached), the previous valid session or configuration was lost. Add the isReplaceExistingSessionBeforeLogin() hook. The default keeps the current behavior for all other modules. The Aviator user session login and admin-config create commands opt out, so they validate first and only replace the stored descriptor after validation succeeds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cmd/AviatorAdminConfigCreateCommand.java | 6 + .../cmd/AviatorUserSessionLoginCommand.java | 6 + .../cli/cmd/AbstractSessionLoginCommand.java | 15 +- .../cmd/AbstractSessionLoginCommandTest.java | 206 ++++++++++++++++++ 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLoginCommandTest.java 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..d2de06ffb5c 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 @@ -47,6 +47,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(); 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..435d98b1f9b 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 @@ -49,6 +49,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(); 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..00e4f8f1624 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,15 @@ private void logoutIfSessionExists(String sessionName) { protected abstract void logoutBeforeNewLogin(String sessionName, D sessionDescriptor); protected abstract D login(String sessionName); protected void testAuthenticatedConnection(String sessionName) {} + + /** + * Returns whether an existing session with the same name must be logged out and + * destroyed before {@link #login(String)} is called. Subclasses can return false if + * login() only validates the new session and the old session needs no remote logout. + * The existing session then stays in place if login() fails, and + * {@code AbstractSessionHelper.save()} replaces it if login() succeeds. + */ + 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"; + } + } +} From 18887fe6089240af4611e29395df3bd8698b4ce5 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 12:24:12 +0530 Subject: [PATCH 02/12] chore: Allow fcli exceptions to supply a safe DEBUG log description Add AbstractFcliException.getSafeDiagnosticString(). The default returns null, so FcliExecutionExceptionHandler keeps logging the full throwable at DEBUG level. An exception whose causes can hold sensitive text can return a safe description instead; the handler then logs only that description. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../exception/AbstractFcliException.java | 10 ++ .../FcliExecutionExceptionHandler.java | 13 ++- .../FcliExecutionExceptionHandlerTest.java | 97 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java index 070a0ae3be6..0b1d4419bf2 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java @@ -58,4 +58,14 @@ public AbstractFcliException(String message, Throwable cause) { } public abstract String getStackTraceString(); + + /** + * Returns a safe description of this exception for DEBUG logging, or null to log the + * full throwable as before. Override only if this exception or its causes can hold + * sensitive text; the returned value must not contain exception messages, URLs, + * headers, or credentials. + */ + public String getSafeDiagnosticString() { + return null; + } } diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java index baddffb1d89..5193d466ae8 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java @@ -33,7 +33,7 @@ public int handleExecutionException(Exception ex, CommandLine commandLine, Parse public int handleException(Exception ex, CommandLine commandLine) { if ( LOG.isDebugEnabled() ) { - LOG.debug("fcli terminating with exception", ex); + logDebug(ex); } var formattedException = FcliExceptionHelper.formatException(ex); if ( formattedException!=null ) { @@ -44,6 +44,17 @@ public int handleException(Exception ex, CommandLine commandLine) { return getExitCode(ex, commandLine); } + private static void logDebug(Exception ex) { + var safeDiagnostic = ex instanceof AbstractFcliException fcliException + ? fcliException.getSafeDiagnosticString() + : null; + if ( safeDiagnostic==null ) { + LOG.debug("fcli terminating with exception", ex); + } else { + LOG.debug("fcli terminating with exception: {}", safeDiagnostic); + } + } + private int getExitCode(Exception ex, CommandLine commandLine) { if ( ex instanceof AbstractFcliException ) { var exitCode = ((AbstractFcliException)ex).exitCode(); diff --git a/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java b/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java new file mode 100644 index 00000000000..0579d870fa8 --- /dev/null +++ b/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java @@ -0,0 +1,97 @@ +/* + * 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.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import picocli.CommandLine; +import picocli.CommandLine.Help.Ansi; +import picocli.CommandLine.Model.CommandSpec; + +/** + * Checks the default DEBUG logging of {@link FcliExecutionExceptionHandler}. The safe + * diagnostic path is tested with a real overriding exception in fcli-aviator-common + * (AviatorConnectivityExceptionTest), because test sources cannot extend the + * {@code @Reflectable} exception classes without the compile-only annotation library. + */ +class FcliExecutionExceptionHandlerTest { + 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 defaultSafeDiagnosticStringIsNull() { + assertNull(new FcliSimpleException("Plain failure").getSafeDiagnosticString()); + } + + @Test + void logsThrowableForFcliExceptionsWithoutSafeDiagnosticString() { + var err = new StringWriter(); + + int exitCode = FcliExecutionExceptionHandler.INSTANCE.handleException(new FcliSimpleException("Plain failure"), createCommandLine(err)); + + assertEquals(1, exitCode); + assertTrue(err.toString().contains("Plain failure")); + var event = singleEvent(); + assertNotNull(event.getThrowableProxy()); + assertEquals("fcli terminating with exception", event.getFormattedMessage()); + } + + @Test + void logsThrowableForNonFcliExceptions() { + FcliExecutionExceptionHandler.INSTANCE.handleException(new IllegalStateException("Unexpected"), createCommandLine(new StringWriter())); + + assertNotNull(singleEvent().getThrowableProxy()); + } + + private ILoggingEvent singleEvent() { + assertEquals(1, appender.list.size()); + return appender.list.get(0); + } + + 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; + } +} From 8c2e7df25194b3140cb9a18a34b31048a15278da Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 12:27:14 +0530 Subject: [PATCH 03/12] chore: Add Aviator connectivity failure classification Add the types for consistent Aviator connectivity failure reporting: - AviatorRequestPolicy: ORDINARY (fail fast, shared messages) or UNCHANGED (current behavior) - AviatorConnectionContext/AviatorEndpoint: safe route summary without URLs or credentials - AviatorConnectivityFailureKind: one message per failure kind - AviatorConnectivityFailureClassifier: maps UNAVAILABLE/DEADLINE_EXCEEDED to a kind using only exception types, never status descriptions or messages; TLS failures (SSLException, CertificateException) keep their current handling - AviatorConnectivityException: prints only the user message and supplies a safe DEBUG description The grpc package does not depend on the diagnose package. The types are not used yet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AviatorConnectivityException.java | 71 ++++++++++ .../grpc/AviatorConnectionContext.java | 51 ++++++++ .../AviatorConnectivityFailureClassifier.java | 78 +++++++++++ .../grpc/AviatorConnectivityFailureKind.java | 76 +++++++++++ .../cli/aviator/grpc/AviatorEndpoint.java | 39 ++++++ .../aviator/grpc/AviatorRequestPolicy.java | 40 ++++++ .../AviatorConnectivityExceptionTest.java | 113 ++++++++++++++++ .../grpc/AviatorConnectionContextTest.java | 110 ++++++++++++++++ ...atorConnectivityFailureClassifierTest.java | 122 ++++++++++++++++++ 9 files changed, 700 insertions(+) create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityException.java create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectionContext.java create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKind.java create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorEndpoint.java create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorRequestPolicy.java create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityExceptionTest.java create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectionContextTest.java create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java 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..04127fd6413 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityException.java @@ -0,0 +1,71 @@ +/* + * 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 java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * Reports an Aviator connectivity failure with a short user message. Terminal output + * shows only the message; DEBUG logs show only safe metadata. The original cause stays + * attached for in-memory use, but it is never written to output or logs. + */ +public final class AviatorConnectivityException extends AviatorSimpleException { + private static final long serialVersionUID = 1L; + private static final int MAX_CAUSE_TYPES = 10; + + private final String kind; + private final String operation; + private final String statusCode; + private final String route; + private final String endpoint; + + /** + * @param message safe user message + * @param kind failure kind name + * @param operation internal operation label + * @param statusCode gRPC status code name + * @param route "direct" or "proxy" + * @param endpoint sanitized host and port of the first TCP peer + * @param cause original failure; it is never written to output or logs + */ + public AviatorConnectivityException(String message, String kind, String operation, + String statusCode, String route, String endpoint, Throwable cause) { + super(message, cause); + this.kind = kind; + this.operation = operation; + this.statusCode = statusCode; + this.route = route; + this.endpoint = endpoint; + } + + public String getKind() { + return kind; + } + + @Override + public String getStackTraceString() { + return getMessage(); + } + + @Override + public String getSafeDiagnosticString() { + var causeTypes = ExceptionUtils.getThrowableList(getCause()).stream() + .limit(MAX_CAUSE_TYPES) + .map(t -> t.getClass().getName()) + .collect(Collectors.joining(" <- ")); + return String.format("Aviator connectivity failure: kind=%s, operation=%s, status=%s, route=%s, endpoint=%s, causes=[%s]", + kind, operation, statusCode, route, endpoint, causeTypes); + } +} 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..9616b8d6b90 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectionContext.java @@ -0,0 +1,51 @@ +/* + * 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; + +/** + * Safe summary of the route that an Aviator client uses. It holds only host names and + * ports; it never holds URLs, proxy credentials, or proxy descriptors. + * + * @param target the Aviator server endpoint + * @param proxy the HTTP CONNECT proxy endpoint, or null for a direct connection + */ +record AviatorConnectionContext(AviatorEndpoint target, AviatorEndpoint proxy) { + 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; + } + + /** Returns the first TCP peer of the client: the proxy if one is used, else the target. */ + AviatorEndpoint firstHop() { + return isProxied() ? proxy : target; + } + + String route() { + return isProxied() ? "proxy" : "direct"; + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java new file mode 100644 index 00000000000..f27440153ae --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java @@ -0,0 +1,78 @@ +/* + * 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 com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.CONNECT_TIMEOUT; +import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.DEADLINE_EXCEEDED; +import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.DIRECT_REFUSED; +import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.DNS_FAILURE; +import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.PROXY_NEGOTIATION_FAILED; +import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.PROXY_REFUSED; +import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.PROXY_REJECTED; +import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.UNAVAILABLE; + +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; +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; +import io.grpc.netty.shaded.io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException; +import io.grpc.netty.shaded.io.netty.handler.proxy.ProxyConnectException; + +/** + * Maps a failed unary gRPC call to an {@link AviatorConnectivityFailureKind}, based only on + * the status code and the exception types in the cause chain. It never reads status + * descriptions or exception messages. + */ +final class AviatorConnectivityFailureClassifier { + private AviatorConnectivityFailureClassifier() {} + + /** + * Returns the failure kind, or an empty Optional if the failure is not a connectivity + * failure (other status codes, or TLS failures, which keep their current handling). + */ + 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 ( has(e, SSLException.class) || has(e, CertificateException.class) ) { + return Optional.empty(); + } + return Optional.of(classifyTransport(e, context, code)); + } + + // The order is significant: all Netty connect and proxy exceptions extend ConnectException. + private static AviatorConnectivityFailureKind classifyTransport(Throwable e, AviatorConnectionContext context, Status.Code code) { + if ( has(e, HttpProxyConnectException.class) ) { return PROXY_REJECTED; } + if ( has(e, ProxyConnectException.class) ) { return 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 ( has(e, ConnectException.class) ) { return context.isProxied() ? PROXY_REFUSED : DIRECT_REFUSED; } + if ( has(e, UnknownHostException.class) || has(e, UnresolvedAddressException.class) ) { return DNS_FAILURE; } + return code==Status.Code.DEADLINE_EXCEEDED ? DEADLINE_EXCEEDED : UNAVAILABLE; + } + + private static boolean has(Throwable e, Class type) { + return ExceptionUtils.indexOfType(e, type) >= 0; + } +} 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..8c26aaae09e --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKind.java @@ -0,0 +1,76 @@ +/* + * 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.Optional; + +/** + * Kinds of Aviator connectivity failure, each with one user-facing message. + * To add a kind, add a constant here and one rule in {@link AviatorConnectivityFailureClassifier}. + */ +enum AviatorConnectivityFailureKind { + PROXY_REJECTED(EndpointRole.PROXY, + "Proxy %s rejected the connection to the Aviator server", + "Check the proxy credentials and the proxy access rules"), + PROXY_NEGOTIATION_FAILED(EndpointRole.PROXY, + "Could not open a tunnel to the Aviator server through proxy %s", + "Check that the proxy is reachable and allows connections to the Aviator server"), + CONNECT_TIMEOUT(EndpointRole.FIRST_HOP, + "Connection to %s timed out", + "Check network access, firewall rules, and proxy settings"), + PROXY_REFUSED(EndpointRole.PROXY, + "Connection to proxy %s was refused", + "Check that the proxy is running and that its host and port are correct"), + DIRECT_REFUSED(EndpointRole.TARGET, + "Connection to Aviator server %s was refused", + "Check the Aviator URL and that the Aviator service is available"), + DNS_FAILURE(EndpointRole.NONE, + "Could not resolve a host name for the Aviator connection", + "Check the Aviator URL, the proxy host, and the DNS settings"), + UNAVAILABLE(EndpointRole.NONE, + "The Aviator server is unavailable or cannot be reached", + "Check the Aviator URL, network access, and proxy settings, then try again"), + DEADLINE_EXCEEDED(EndpointRole.NONE, + "The Aviator request timed out", + "Check network access and proxy settings; if they are correct, try again later"); + + private final EndpointRole endpointRole; + private final String summaryTemplate; + private final String guidance; + + AviatorConnectivityFailureKind(EndpointRole endpointRole, String summaryTemplate, String guidance) { + this.endpointRole = endpointRole; + this.summaryTemplate = summaryTemplate; + this.guidance = guidance; + } + + String formatMessage(AviatorConnectionContext context) { + var summary = endpointRole.resolve(context) + .map(endpoint -> String.format(summaryTemplate, endpoint.display())) + .orElse(summaryTemplate); + return summary + "\n" + guidance; + } + + private enum EndpointRole { + TARGET, PROXY, FIRST_HOP, NONE; + + Optional resolve(AviatorConnectionContext context) { + return Optional.ofNullable(switch (this) { + case TARGET -> context.target(); + case PROXY -> context.proxy(); + case FIRST_HOP -> context.firstHop(); + case NONE -> null; + }); + } + } +} 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..be42561531e --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorEndpoint.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * Host and port that are safe to show to users and to write to logs. + * Control characters in the host name are replaced. + */ +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/AviatorRequestPolicy.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorRequestPolicy.java new file mode 100644 index 00000000000..3226761d93b --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorRequestPolicy.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * Selects how unary Aviator gRPC requests behave when the connection fails. + * Code that uses this enum reads the flags; it does not compare with specific constants. + */ +public enum AviatorRequestPolicy { + /** Fail fast on connection failures and report them with the shared connectivity messages. */ + ORDINARY(false, true), + /** Current behavior: wait for the channel to be ready and use the existing status mapping. */ + UNCHANGED(true, false); + + private final boolean waitForReady; + private final boolean classifyConnectivityFailures; + + AviatorRequestPolicy(boolean waitForReady, boolean classifyConnectivityFailures) { + this.waitForReady = waitForReady; + this.classifyConnectivityFailures = classifyConnectivityFailures; + } + + public boolean isWaitForReady() { + return waitForReady; + } + + public boolean isClassifyConnectivityFailures() { + return classifyConnectivityFailures; + } +} 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..299235a5aea --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/exception/AviatorConnectivityExceptionTest.java @@ -0,0 +1,113 @@ +/* + * 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.assertTrue; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.ConnectException; + +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.common.exception.FcliExceptionHelper; +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.core.read.ListAppender; +import io.grpc.Status; +import picocli.CommandLine; +import picocli.CommandLine.Help.Ansi; +import picocli.CommandLine.Model.CommandSpec; + +class AviatorConnectivityExceptionTest { + private static final String SECRET = "sentinel-secret-value"; + private static final String MESSAGE = "Connection to proxy localhost:33444 was refused\n" + + "Check that the proxy is running and that its host and port are correct"; + + 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 stackTraceStringIsOnlyTheMessage() { + var exception = createException(); + + assertEquals(MESSAGE, exception.getStackTraceString()); + assertEquals(MESSAGE, FcliExceptionHelper.formatException(exception)); + } + + @Test + void safeDiagnosticStringHasMetadataButNoSecrets() { + var diagnostic = createException().getSafeDiagnosticString(); + + assertTrue(diagnostic.contains("kind=PROXY_REFUSED")); + assertTrue(diagnostic.contains("status=UNAVAILABLE")); + assertTrue(diagnostic.contains("route=proxy")); + assertTrue(diagnostic.contains("endpoint=localhost:33444")); + assertTrue(diagnostic.contains("java.net.ConnectException")); + assertFalse(diagnostic.contains(SECRET)); + } + + @Test + void handlerPrintsOnlyMessageAndLogsOnlySafeDiagnostic() { + var err = new StringWriter(); + var commandLine = new CommandLine(CommandSpec.create()); + commandLine.setErr(new PrintWriter(err)); + commandLine.setColorScheme(CommandLine.Help.defaultColorScheme(Ansi.OFF)); + + int exitCode = FcliExecutionExceptionHandler.INSTANCE.handleException(createException(), commandLine); + + assertEquals(1, exitCode); + assertEquals(MESSAGE, err.toString().trim()); + assertFalse(err.toString().contains("Exception")); + assertEquals(1, appender.list.size()); + var event = appender.list.get(0); + assertNull(event.getThrowableProxy()); + assertTrue(event.getFormattedMessage().contains("kind=PROXY_REFUSED")); + assertFalse(event.getFormattedMessage().contains(SECRET)); + } + + private static AviatorConnectivityException createException() { + var transportCause = new ConnectException("Connection refused: proxy-user:" + SECRET + "@localhost"); + transportCause.addSuppressed(new IllegalStateException("suppressed " + SECRET)); + var statusException = Status.UNAVAILABLE + .withDescription("io exception " + SECRET) + .withCause(transportCause) + .asRuntimeException(); + return new AviatorConnectivityException(MESSAGE, "PROXY_REFUSED", "listApplications", + "UNAVAILABLE", "proxy", "localhost:33444", statusException); + } +} 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..a11165dc8ce --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectionContextTest.java @@ -0,0 +1,110 @@ +/* + * 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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +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 requestPolicyFlags() { + assertFalse(AviatorRequestPolicy.ORDINARY.isWaitForReady()); + assertTrue(AviatorRequestPolicy.ORDINARY.isClassifyConnectivityFailures()); + assertTrue(AviatorRequestPolicy.UNCHANGED.isWaitForReady()); + assertFalse(AviatorRequestPolicy.UNCHANGED.isClassifyConnectivityFailures()); + } + + @Test + void createsDirectContextFromPlan() { + var context = AviatorConnectionContext.of(plan(Optional.empty())); + + assertFalse(context.isProxied()); + assertNull(context.proxy()); + assertEquals("aviator.example.com:443", context.firstHop().display()); + assertEquals("direct", context.route()); + } + + @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.firstHop().display()); + assertEquals("proxy", context.route()); + 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()); + } + + @ParameterizedTest + @EnumSource(AviatorConnectivityFailureKind.class) + void messagesHaveSummaryAndGuidanceWithoutTrailingPeriod(AviatorConnectivityFailureKind kind) { + var context = new AviatorConnectionContext(new AviatorEndpoint("aviator.example.com", 443), new AviatorEndpoint("localhost", 33444)); + var lines = kind.formatMessage(context).split("\n"); + + assertEquals(2, lines.length); + assertFalse(lines[0].contains("%s")); + assertFalse(lines[0].endsWith(".")); + assertFalse(lines[1].endsWith(".")); + } + + @Test + void proxyRefusedMessageNamesTheProxy() { + var context = new AviatorConnectionContext(new AviatorEndpoint("aviator.example.com", 443), new AviatorEndpoint("localhost", 33444)); + + assertEquals("Connection to proxy localhost:33444 was refused\n" + + "Check that the proxy is running and that its host and port are correct", + AviatorConnectivityFailureKind.PROXY_REFUSED.formatMessage(context)); + } + + 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/AviatorConnectivityFailureClassifierTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java new file mode 100644 index 00000000000..9c1338b72b5 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java @@ -0,0 +1,122 @@ +/* + * 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 java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; +import java.security.cert.CertificateException; +import java.util.Optional; + +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 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 AviatorConnectivityFailureClassifierTest { + 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)); + } + + @Test + void classifiesConnectExceptionAsDirectRefusalWhenNotProxied() { + var cause = new ConnectException("Connection refused"); + 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 ignoresTlsFailures() { + assertEquals(Optional.empty(), classify(Status.UNAVAILABLE, new SSLHandshakeException("handshake"), 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 doesNotReadExceptionMessages() { + var cause = new ConnectException("Connection refused: openssl-handshake_failure.example/10.0.0.1:443"); + assertEquals(Optional.of(AviatorConnectivityFailureKind.DIRECT_REFUSED), classify(Status.UNAVAILABLE, cause, DIRECT)); + } + + private static Optional classify(Status status, Throwable cause, AviatorConnectionContext context) { + var exception = new StatusRuntimeException(cause==null ? status : status.withCause(cause)); + return AviatorConnectivityFailureClassifier.classify(exception, context); + } +} From c9a5e11ede9181a9baae4d1fd555032b0829f49e Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 12:31:00 +0530 Subject: [PATCH 04/12] chore: Support fail-fast request policy in Aviator gRPC client GrpcUtil.executeGrpcCall() now accepts an AviatorRequestPolicy and a connection context. With the ORDINARY policy, unary calls run without wait-for-ready (through a per-call interceptor, because gRPC 1.76 stubs have no withoutWaitForReady()), and UNAVAILABLE/DEADLINE_EXCEEDED failures become AviatorConnectivityException. The 30-second deadline, the shared stubs, and streaming calls do not change. AviatorGrpcClient and AviatorGrpcClientHelper get overloads that accept the policy. All existing constructors and createClient() methods keep the UNCHANGED policy, so no caller changes behavior yet. The existing status mapping is extracted into toAviatorException() without changes to messages or logging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/aviator/grpc/AviatorGrpcClient.java | 58 +++-- .../aviator/grpc/AviatorGrpcClientHelper.java | 20 +- .../fortify/cli/aviator/grpc/GrpcUtil.java | 152 ++++++++---- .../AviatorGrpcClientConnectivityTest.java | 233 ++++++++++++++++++ .../cli/aviator/grpc/GrpcUtilTest.java | 59 +++++ 5 files changed, 449 insertions(+), 73 deletions(-) create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientConnectivityTest.java 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..5055fb4fa9e 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; @@ -75,6 +76,7 @@ import io.grpc.DecompressorRegistry; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.stub.AbstractBlockingStub; public class AviatorGrpcClient implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(AviatorGrpcClient.class); @@ -92,10 +94,26 @@ public class AviatorGrpcClient implements AutoCloseable { private final java.util.concurrent.ExecutorService processingExecutor; private final long pingIntervalSeconds; private final java.util.concurrent.ScheduledExecutorService pingScheduler; + private final AviatorRequestPolicy requestPolicy; + private final AviatorConnectionContext connectionContext; final AtomicBoolean isShutdown = new AtomicBoolean(false); public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) { + this(channel, defaultTimeoutSeconds, logger, pingIntervalSeconds, AviatorRequestPolicy.UNCHANGED, null); + } + + /** + * Creates a client with an explicit request policy. A connection context is required if the + * policy classifies connectivity failures, because the messages name the failed endpoint. + */ + AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds, + AviatorRequestPolicy requestPolicy, AviatorConnectionContext connectionContext) { LOG.debug("Initializing AviatorGrpcClient with ManagedChannel"); + this.requestPolicy = Objects.requireNonNull(requestPolicy, "requestPolicy"); + if ( requestPolicy.isClassifyConnectivityFailures() ) { + Objects.requireNonNull(connectionContext, "connectionContext"); + } + this.connectionContext = connectionContext; this.logger = logger; this.channel = channel; this.asyncStub = AuditorServiceGrpc.newStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady(); @@ -132,6 +150,10 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv this(channel, defaultTimeoutSeconds, logger, 30); } + private , T, R> R execute(S stub, GrpcUtil.GrpcCall call, T request, String operation) { + return GrpcUtil.executeGrpcCall(stub, call, request, operation, requestPolicy, connectionContext); + } + public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle, List customPriorityOrder, ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) { @@ -189,27 +211,27 @@ 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 execute(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 execute(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 execute(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 execute(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 execute(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplication, request, Constants.OP_GET_APP); } public Application getApplicationByToken(String token, String appName) { @@ -217,14 +239,14 @@ 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 execute(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplicationByToken, request, Constants.OP_GET_APP_BY_TOKEN); } public long getDefaultQuota(String token) { GetDefaultQuotaRequest request = GetDefaultQuotaRequest.newBuilder() .setToken(token) .build(); - GetDefaultQuotaResponse response = GrpcUtil.executeGrpcCall(blockingStub, + GetDefaultQuotaResponse response = execute(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getDefaultQuota, request, Constants.OP_GET_DEFAULT_QUOTA); return response.getDefaultQuota(); @@ -241,7 +263,7 @@ public void validateAdminSession(String tenantName, String signature, String mes .setSignature(signature) .setMessage(message) .build(); - GrpcUtil.executeGrpcCall( + execute( blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::validateAdminSession, request, @@ -250,18 +272,18 @@ 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 = execute(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 execute(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 execute(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::listTokens, request, Constants.OP_LIST_TOKENS); } public ListTokensResponse listTokensByDeveloper(String tenantName, String developerEmail, String signature, String message) { @@ -270,38 +292,38 @@ 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 execute(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 execute(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 execute(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 execute(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 execute(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 = execute(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 = execute(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..bb4aa53eb1c 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.UNCHANGED); + } + + 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); + /** Builds a client from a connection plan; tests use this to supply a proxy without environment variables. */ + 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) { @@ -172,7 +182,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.UNCHANGED); + } + + 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/GrpcUtil.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/GrpcUtil.java index 1fc5785ca33..a0012cc04b1 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 @@ -26,6 +26,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; @@ -37,6 +38,11 @@ import com.fortify.cli.aviator.fpr.utils.SourceCodeEnricher; import com.fortify.cli.aviator.util.Constants; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.stub.AbstractBlockingStub; @@ -50,61 +56,30 @@ interface GrpcCall { R call(S stub, T request) throws StatusRuntimeException; } + // gRPC 1.76 stubs have no withoutWaitForReady(); this interceptor clears the flag per call. + private static final ClientInterceptor WITHOUT_WAIT_FOR_READY = new ClientInterceptor() { + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions.withoutWaitForReady()); + } + }; + static , T, R> R executeGrpcCall(S stub, GrpcCall call, T request, String operation) { + return executeGrpcCall(stub, call, request, operation, AviatorRequestPolicy.UNCHANGED, null); + } + + /** + * Executes a unary call with the default deadline. The policy selects wait-for-ready and + * connectivity failure handling; the context must be non-null if the policy classifies failures. + */ + static , T, R> R executeGrpcCall(S stub, GrpcCall call, T request, String operation, + AviatorRequestPolicy policy, AviatorConnectionContext context) { try { - S stubWithDeadline = stub.withDeadlineAfter(Constants.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); - return call.call(stubWithDeadline, request); + return call.call(configureStub(stub, policy), 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, operation, policy, context) + .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 +87,79 @@ static , T, R> R executeGrpcCall(S stub, GrpcC } } + private static > S configureStub(S stub, AviatorRequestPolicy policy) { + var stubWithDeadline = stub.withDeadlineAfter(Constants.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + return policy.isWaitForReady() ? stubWithDeadline : stubWithDeadline.withInterceptors(WITHOUT_WAIT_FOR_READY); + } + + private static Optional toConnectivityException(StatusRuntimeException e, String operation, + AviatorRequestPolicy policy, AviatorConnectionContext context) { + if ( !policy.isClassifyConnectivityFailures() ) { + return Optional.empty(); + } + return AviatorConnectivityFailureClassifier.classify(e, context) + .map(kind -> newConnectivityException(kind, operation, e, context)); + } + + private static AviatorConnectivityException newConnectivityException(AviatorConnectivityFailureKind kind, String operation, + StatusRuntimeException e, AviatorConnectionContext context) { + return new AviatorConnectivityException(kind.formatMessage(context), kind.name(), operation, + e.getStatus().getCode().name(), context.route(), context.firstHop().display(), e); + } + + 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("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 + ); + return 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); + 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/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..ee83dc18df5 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientConnectivityTest.java @@ -0,0 +1,233 @@ +/* + * 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.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.nio.charset.StandardCharsets; +import java.time.Duration; +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 com.fortify.aviator.application.ApplicationByTenantName; +import com.fortify.aviator.application.ApplicationList; +import com.fortify.aviator.application.ApplicationServiceGrpc; +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.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 ordinary-policy 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 plaintextChannel; + + @AfterEach + void tearDown() throws Exception { + if ( proxyServer!=null ) { proxyServer.close(); } + if ( plaintextChannel!=null ) { plaintextChannel.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("DIRECT_REFUSED", exception.getKind()); + assertTrue(exception.getMessage().startsWith("Connection to Aviator server 127.0.0.1:" + port + " was refused")); + } + + @Test + void proxyRefusalFailsFastWithProxyRefusedMessage() throws Exception { + int proxyPort = unusedPort(); + + var exception = callAndExpectConnectivityFailure(proxiedPlan(proxyPort)); + + assertEquals("PROXY_REFUSED", exception.getKind()); + assertEquals("Connection to proxy 127.0.0.1:" + proxyPort + " was refused\n" + + "Check that the proxy is running and that its host and port are correct", exception.getMessage()); + assertFalse(exception.getSafeDiagnosticString().contains(PROXY_SECRET)); + } + + @Test + void proxyAuthenticationRejectionIsReportedAsProxyRejection() throws Exception { + proxyServer = new ServerSocket(0, 50, InetAddress.getLoopbackAddress()); + startRejectingProxy(proxyServer); + + var exception = callAndExpectConnectivityFailure(proxiedPlan(proxyServer.getLocalPort())); + + assertEquals("PROXY_REJECTED", exception.getKind()); + assertFalse(exception.getMessage().contains("sentinel-realm")); + assertFalse(exception.getSafeDiagnosticString().contains("sentinel-realm")); + assertFalse(exception.getSafeDiagnosticString().contains(PROXY_SECRET)); + } + + @Test + void ordinaryPolicyDisablesWaitForReadyAndKeepsDeadline() throws Exception { + var captured = new AtomicReference(); + var client = createPlaintextClient(AviatorRequestPolicy.ORDINARY, captured); + + var result = client.listApplication("tenant", "signature", "message"); + + assertTrue(result.isEmpty()); + assertFalse(captured.get().isWaitForReady()); + assertDeadlineAtMostDefault(captured.get()); + client.close(); + } + + @Test + void unchangedPolicyKeepsWaitForReadyAndDeadline() throws Exception { + var captured = new AtomicReference(); + var client = createPlaintextClient(AviatorRequestPolicy.UNCHANGED, captured); + + var result = client.listApplication("tenant", "signature", "message"); + + assertTrue(result.isEmpty()); + assertTrue(captured.get().isWaitForReady()); + assertDeadlineAtMostDefault(captured.get()); + client.close(); + } + + private static AviatorConnectivityException callAndExpectConnectivityFailure(AviatorConnectionPlan plan) { + try ( var client = AviatorGrpcClientHelper.createClient(plan, null, 30, AviatorRequestPolicy.ORDINARY) ) { + 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); + 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()) + .build() + .start(); + plaintextChannel = 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(plaintextChannel, 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 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/GrpcUtilTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/GrpcUtilTest.java index 09285bae9e9..b1ea45b5521 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,18 +13,31 @@ 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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.ConnectException; import java.util.List; import java.util.Set; 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 io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; + class GrpcUtilTest { @Test @@ -58,4 +71,50 @@ void shouldClassifySkippedServerResponseWithoutInspectingMessage() { assertEquals(AuditSkipReason.SKIPPED_BY_AVIATOR, auditResponse.getAuditSkipReason()); } + + @Test + void unchangedPolicyKeepsCurrentUnavailableMapping() { + var e = assertThrows(AviatorTechnicalException.class, + () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("refused")), AviatorRequestPolicy.UNCHANGED)); + + assertTrue(e.getMessage().startsWith("A technical error occurred on the server while performing the test operation")); + } + + @Test + void ordinaryPolicyClassifiesUnavailable() { + var e = assertThrows(AviatorConnectivityException.class, + () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("refused")), AviatorRequestPolicy.ORDINARY)); + + assertEquals("DIRECT_REFUSED", e.getKind()); + } + + @Test + void ordinaryPolicyKeepsUnimplementedContractForOlderServerLoginFallback() { + var e = assertThrows(AviatorTechnicalException.class, + () -> execute(Status.UNIMPLEMENTED, AviatorRequestPolicy.ORDINARY)); + + var cause = assertInstanceOf(StatusRuntimeException.class, e.getCause()); + assertEquals(Status.Code.UNIMPLEMENTED, cause.getStatus().getCode()); + } + + @Test + void ordinaryPolicyKeepsApplicationStatusMapping() { + var e = assertThrows(AviatorSimpleException.class, + () -> execute(Status.NOT_FOUND.withDescription("app"), AviatorRequestPolicy.ORDINARY)); + + 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(ApplicationServiceGrpc.newBlockingStub(channel), + (stub, request) -> { throw status.asRuntimeException(); }, + "request", "test", policy, context); + } finally { + channel.shutdownNow(); + } + } } \ No newline at end of file From 15c56608165138bf437e887500e5c6a688018b45 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 12:34:28 +0530 Subject: [PATCH 05/12] feat: Fail fast with clear messages when Aviator commands cannot connect Aviator app, token, and entitlement commands, 'aviator session login', and 'aviator admin-config create' now use the ORDINARY request policy. When the Aviator server or the configured proxy refuses the connection, these commands now fail at once instead of waiting until the 30-second request deadline, and print a short message, for example: Connection to proxy localhost:33444 was refused Check that the proxy is running and that its host and port are correct All admin commands get the policy through AbstractAviatorAdminSessionOutputCommand.createGrpcClient(). AviatorUserSessionHelper.validateToken() and AviatorAdminConfigHelper.validateConfig() get overloads with a policy; the existing overloads keep the UNCHANGED policy, so connection diagnostics, audit, and correlation commands keep their current behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cmd/AviatorAdminConfigCreateCommand.java | 3 +- .../helper/AviatorAdminConfigHelper.java | 7 +- ...tractAviatorAdminSessionOutputCommand.java | 11 ++ .../cmd/AviatorUserSessionLoginCommand.java | 4 +- .../user/helper/AviatorUserSessionHelper.java | 7 +- .../cmd/AviatorAppAddEntitlementCommand.java | 3 +- .../app/cli/cmd/AviatorAppCreateCommand.java | 3 +- .../app/cli/cmd/AviatorAppDeleteCommand.java | 3 +- .../app/cli/cmd/AviatorAppGetCommand.java | 3 +- .../app/cli/cmd/AviatorAppListCommand.java | 3 +- .../app/cli/cmd/AviatorAppUpdateCommand.java | 3 +- ...ractAviatorSastEntitlementListCommand.java | 3 +- .../AviatorEntitlementListDastCommand.java | 3 +- .../cli/cmd/AviatorTokenCreateCommand.java | 3 +- .../cli/cmd/AviatorTokenDeleteCommand.java | 3 +- .../cli/cmd/AviatorTokenListCommand.java | 3 +- .../cli/cmd/AviatorTokenRevokeCommand.java | 3 +- .../cli/cmd/AviatorTokenValidateCommand.java | 3 +- ...viatorOrdinaryCommandConnectivityTest.java | 139 ++++++++++++++++++ 19 files changed, 180 insertions(+), 30 deletions(-) create mode 100644 fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/_common/output/cli/cmd/AviatorOrdinaryCommandConnectivityTest.java 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 d2de06ffb5c..ea655a3fc1d 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; @@ -61,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.ORDINARY); 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..2d2de5a6ad0 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.UNCHANGED); + } + + 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..c5660925364 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,12 @@ public final JsonNode getJsonNode() { protected abstract JsonNode getJsonNode(AviatorAdminConfigDescriptor configDescriptor); public abstract boolean isSingular(); + + /** + * Creates a gRPC client that fails fast on connectivity failures and reports them with + * the shared Aviator connectivity messages. Callers must close the client. + */ + protected final AviatorGrpcClient createGrpcClient(AviatorAdminConfigDescriptor configDescriptor) { + return AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl(), AviatorRequestPolicy.ORDINARY); + } } \ 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 435d98b1f9b..33e061dccf1 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; @@ -62,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.ORDINARY).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..d3edc48a1ac 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.UNCHANGED); + } + + 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..e0ee92558c8 --- /dev/null +++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/_common/output/cli/cmd/AviatorOrdinaryCommandConnectivityTest.java @@ -0,0 +1,139 @@ +/* + * 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 java.util.List; + +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.common.cli.util.FcliExecutionContextHolder; +import com.fortify.cli.common.output.cli.mixin.IOutputHelper; + +/** + * Checks that ordinary 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); + private static final List PROXY_ENV_VARS = + List.of("https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY"); + + // 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 { + // The client reads proxy environment variables; a configured proxy would change the result + assumeTrue(PROXY_ENV_VARS.stream().allMatch(name -> System.getenv(name)==null), + "Skipped because a proxy environment variable is set"); + FcliExecutionContextHolder.pushNew(); + try ( var socket = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()) ) { + closedPortUrl = "127.0.0.1:" + socket.getLocalPort(); + } + } + + @AfterEach + void tearDown() { + FcliExecutionContextHolder.pop(); + } + + @Test + void adminCommandBaseClassCreatesOrdinaryClient() { + 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 userSessionTokenValidationFailsFastWithOrdinaryPolicy() { + assertFailsFast(() -> AviatorUserSessionHelper.instance() + .validateToken(closedPortUrl, syntheticToken(), AviatorRequestPolicy.ORDINARY)); + } + + 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("was refused"), 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; + } + } +} From 17ff153f68d06df4aafbf5da9d3213249f54b953 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 12:35:56 +0530 Subject: [PATCH 06/12] fix: Don't show credentials from the Aviator URL in messages and logs parseTarget() and normalizeUrl() included the raw Aviator URL in error messages, in a WARN log entry, and in UnsupportedAviatorUrlSchemeException (which connection diagnostics copy into their evidence). A URL with user info, such as https://user:password@host, exposed the password. These places now show the URL with the user-info part replaced by ***. URLs without user info are shown unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aviator/grpc/AviatorGrpcClientHelper.java | 32 +++++++++++++++--- .../grpc/AviatorGrpcClientHelperTest.java | 33 +++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) 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 bb4aa53eb1c..4ce0d3b1552 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 @@ -89,27 +89,28 @@ 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); + throw new AviatorSimpleException("Aviator URL format is invalid. Expected 'host:port' or a valid target string. Provided URL: " + safeUrl, e); } 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); } @@ -131,7 +132,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; @@ -140,6 +141,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()) 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")); + } } From 5534a7c7875525bed085ca95a5f911db9b1865d5 Mon Sep 17 00:00:00 2001 From: Kittu Varma Date: Thu, 22 Oct 2026 09:51:02 +0000 Subject: [PATCH 07/12] chore: Simplify Aviator connectivity handling --- .../AviatorConnectivityException.java | 31 +--- .../grpc/AviatorConnectionContext.java | 17 +-- .../AviatorConnectivityFailureClassifier.java | 24 ++-- .../grpc/AviatorConnectivityFailureKind.java | 83 ++++------- .../cli/aviator/grpc/AviatorEndpoint.java | 5 +- .../cli/aviator/grpc/AviatorGrpcClient.java | 89 +++++++----- .../aviator/grpc/AviatorGrpcClientHelper.java | 8 +- .../aviator/grpc/AviatorRequestPolicy.java | 38 ++--- .../fortify/cli/aviator/grpc/GrpcUtil.java | 49 ++----- .../AviatorConnectivityExceptionTest.java | 7 +- .../grpc/AviatorConnectionContextTest.java | 10 -- ...atorConnectivityFailureClassifierTest.java | 17 ++- .../AviatorGrpcClientConnectivityTest.java | 136 +++++++++++++++--- .../cli/aviator/grpc/GrpcUtilTest.java | 20 +-- .../cmd/AviatorAdminConfigCreateCommand.java | 2 +- .../helper/AviatorAdminConfigHelper.java | 2 +- ...tractAviatorAdminSessionOutputCommand.java | 7 +- .../cmd/AviatorUserSessionLoginCommand.java | 2 +- .../user/helper/AviatorUserSessionHelper.java | 2 +- ...viatorOrdinaryCommandConnectivityTest.java | 17 +-- .../exception/AbstractFcliException.java | 6 +- .../cli/cmd/AbstractSessionLoginCommand.java | 7 +- .../FcliExecutionExceptionHandlerTest.java | 12 +- 23 files changed, 299 insertions(+), 292 deletions(-) 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 index 04127fd6413..88de5a8dc99 100644 --- 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 @@ -16,38 +16,17 @@ import org.apache.commons.lang3.exception.ExceptionUtils; -/** - * Reports an Aviator connectivity failure with a short user message. Terminal output - * shows only the message; DEBUG logs show only safe metadata. The original cause stays - * attached for in-memory use, but it is never written to output or logs. - */ +/** Keeps transport cause text out of CLI output and DEBUG logs. */ public final class AviatorConnectivityException extends AviatorSimpleException { private static final long serialVersionUID = 1L; private static final int MAX_CAUSE_TYPES = 10; private final String kind; - private final String operation; - private final String statusCode; - private final String route; - private final String endpoint; - /** - * @param message safe user message - * @param kind failure kind name - * @param operation internal operation label - * @param statusCode gRPC status code name - * @param route "direct" or "proxy" - * @param endpoint sanitized host and port of the first TCP peer - * @param cause original failure; it is never written to output or logs - */ - public AviatorConnectivityException(String message, String kind, String operation, - String statusCode, String route, String endpoint, Throwable cause) { + /** Message and kind must be safe for display. */ + public AviatorConnectivityException(String message, String kind, Throwable cause) { super(message, cause); this.kind = kind; - this.operation = operation; - this.statusCode = statusCode; - this.route = route; - this.endpoint = endpoint; } public String getKind() { @@ -65,7 +44,7 @@ public String getSafeDiagnosticString() { .limit(MAX_CAUSE_TYPES) .map(t -> t.getClass().getName()) .collect(Collectors.joining(" <- ")); - return String.format("Aviator connectivity failure: kind=%s, operation=%s, status=%s, route=%s, endpoint=%s, causes=[%s]", - kind, operation, statusCode, route, endpoint, causeTypes); + return String.format("Aviator connectivity failure: kind=%s, message=%s, causes=[%s]", + kind, getMessage().replace("\n", " / "), causeTypes); } } 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 index 9616b8d6b90..0cd5ba1998c 100644 --- 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 @@ -16,14 +16,16 @@ import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan; +import io.grpc.CallOptions; + /** - * Safe summary of the route that an Aviator client uses. It holds only host names and - * ports; it never holds URLs, proxy credentials, or proxy descriptors. - * - * @param target the Aviator server endpoint - * @param proxy the HTTP CONNECT proxy endpoint, or null for a direct connection + * 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"); } @@ -40,12 +42,7 @@ boolean isProxied() { return proxy!=null; } - /** Returns the first TCP peer of the client: the proxy if one is used, else the target. */ AviatorEndpoint firstHop() { return isProxied() ? proxy : target; } - - String route() { - return isProxied() ? "proxy" : "direct"; - } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java index f27440153ae..feab4bb4a96 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java @@ -37,18 +37,10 @@ import io.grpc.netty.shaded.io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException; import io.grpc.netty.shaded.io.netty.handler.proxy.ProxyConnectException; -/** - * Maps a failed unary gRPC call to an {@link AviatorConnectivityFailureKind}, based only on - * the status code and the exception types in the cause chain. It never reads status - * descriptions or exception messages. - */ final class AviatorConnectivityFailureClassifier { private AviatorConnectivityFailureClassifier() {} - /** - * Returns the failure kind, or an empty Optional if the failure is not a connectivity - * failure (other status codes, or TLS failures, which keep their current handling). - */ + /** Leaves TLS failures and application statuses to the existing exception handler. */ static Optional classify(StatusRuntimeException e, AviatorConnectionContext context) { var code = e.getStatus().getCode(); if ( code!=Status.Code.UNAVAILABLE && code!=Status.Code.DEADLINE_EXCEEDED ) { @@ -67,11 +59,23 @@ private static AviatorConnectivityFailureKind classifyTransport(Throwable e, Avi 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 ( has(e, ConnectException.class) ) { return context.isProxied() ? PROXY_REFUSED : DIRECT_REFUSED; } + if ( isConnectionRefused(e) ) { return context.isProxied() ? PROXY_REFUSED : DIRECT_REFUSED; } if ( has(e, UnknownHostException.class) || has(e, UnresolvedAddressException.class) ) { return DNS_FAILURE; } return code==Status.Code.DEADLINE_EXCEEDED ? DEADLINE_EXCEEDED : UNAVAILABLE; } + // 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: no further information".equalsIgnoreCase(message) + || "connect(..) failed: Connection refused".equalsIgnoreCase(message) + || "finishConnect(..) failed: Connection refused".equalsIgnoreCase(message)); + } + private static boolean has(Throwable e, Class type) { return ExceptionUtils.indexOfType(e, type) >= 0; } 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 index 8c26aaae09e..602385a8dfb 100644 --- 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 @@ -12,65 +12,34 @@ */ package com.fortify.cli.aviator.grpc; -import java.util.Optional; - -/** - * Kinds of Aviator connectivity failure, each with one user-facing message. - * To add a kind, add a constant here and one rule in {@link AviatorConnectivityFailureClassifier}. - */ enum AviatorConnectivityFailureKind { - PROXY_REJECTED(EndpointRole.PROXY, - "Proxy %s rejected the connection to the Aviator server", - "Check the proxy credentials and the proxy access rules"), - PROXY_NEGOTIATION_FAILED(EndpointRole.PROXY, - "Could not open a tunnel to the Aviator server through proxy %s", - "Check that the proxy is reachable and allows connections to the Aviator server"), - CONNECT_TIMEOUT(EndpointRole.FIRST_HOP, - "Connection to %s timed out", - "Check network access, firewall rules, and proxy settings"), - PROXY_REFUSED(EndpointRole.PROXY, - "Connection to proxy %s was refused", - "Check that the proxy is running and that its host and port are correct"), - DIRECT_REFUSED(EndpointRole.TARGET, - "Connection to Aviator server %s was refused", - "Check the Aviator URL and that the Aviator service is available"), - DNS_FAILURE(EndpointRole.NONE, - "Could not resolve a host name for the Aviator connection", - "Check the Aviator URL, the proxy host, and the DNS settings"), - UNAVAILABLE(EndpointRole.NONE, - "The Aviator server is unavailable or cannot be reached", - "Check the Aviator URL, network access, and proxy settings, then try again"), - DEADLINE_EXCEEDED(EndpointRole.NONE, - "The Aviator request timed out", - "Check network access and proxy settings; if they are correct, try again later"); - - private final EndpointRole endpointRole; - private final String summaryTemplate; - private final String guidance; - - AviatorConnectivityFailureKind(EndpointRole endpointRole, String summaryTemplate, String guidance) { - this.endpointRole = endpointRole; - this.summaryTemplate = summaryTemplate; - this.guidance = guidance; - } + PROXY_REJECTED, + PROXY_NEGOTIATION_FAILED, + CONNECT_TIMEOUT, + PROXY_REFUSED, + DIRECT_REFUSED, + DNS_FAILURE, + UNAVAILABLE, + DEADLINE_EXCEEDED; String formatMessage(AviatorConnectionContext context) { - var summary = endpointRole.resolve(context) - .map(endpoint -> String.format(summaryTemplate, endpoint.display())) - .orElse(summaryTemplate); - return summary + "\n" + guidance; - } - - private enum EndpointRole { - TARGET, PROXY, FIRST_HOP, NONE; - - Optional resolve(AviatorConnectionContext context) { - return Optional.ofNullable(switch (this) { - case TARGET -> context.target(); - case PROXY -> context.proxy(); - case FIRST_HOP -> context.firstHop(); - case NONE -> null; - }); - } + return switch (this) { + case PROXY_REJECTED -> "Proxy " + context.proxy().display() + " rejected the connection to the Aviator server\n" + + "Check the proxy credentials, access rules, and whether the Aviator service is available"; + case PROXY_NEGOTIATION_FAILED -> "Could not open a tunnel to the Aviator server through proxy " + + context.proxy().display() + "\nCheck that the proxy is reachable and allows connections to the Aviator server"; + case CONNECT_TIMEOUT -> "Connection to " + context.firstHop().display() + + " timed out\nCheck network access, firewall rules, and proxy settings"; + case PROXY_REFUSED -> "Connection to proxy " + context.proxy().display() + + " was refused\nCheck that the proxy is running and that its host and port are correct"; + case DIRECT_REFUSED -> "Connection to Aviator server " + context.target().display() + + " was refused\nCheck the Aviator URL and that the Aviator service is available"; + case DNS_FAILURE -> "Could not resolve a host name for the Aviator connection\n" + + "Check the Aviator URL, the proxy host, and the DNS settings"; + case UNAVAILABLE -> "The Aviator server is unavailable or cannot be reached\n" + + "Check the Aviator URL, network access, and proxy settings, then try again"; + case DEADLINE_EXCEEDED -> "The Aviator request timed out\n" + + "Check network access and proxy settings; if they are correct, 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 index be42561531e..234500c611e 100644 --- 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 @@ -12,10 +12,7 @@ */ package com.fortify.cli.aviator.grpc; -/** - * Host and port that are safe to show to users and to write to logs. - * Control characters in the host name are replaced. - */ +/** Replaces control characters in host names before display. */ record AviatorEndpoint(String host, int port) { private static final String UNKNOWN_HOST = "unknown"; 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 5055fb4fa9e..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 @@ -76,7 +76,6 @@ import io.grpc.DecompressorRegistry; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; -import io.grpc.stub.AbstractBlockingStub; public class AviatorGrpcClient implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(AviatorGrpcClient.class); @@ -94,33 +93,32 @@ public class AviatorGrpcClient implements AutoCloseable { private final java.util.concurrent.ExecutorService processingExecutor; private final long pingIntervalSeconds; private final java.util.concurrent.ScheduledExecutorService pingScheduler; - private final AviatorRequestPolicy requestPolicy; - private final AviatorConnectionContext connectionContext; final AtomicBoolean isShutdown = new AtomicBoolean(false); public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) { - this(channel, defaultTimeoutSeconds, logger, pingIntervalSeconds, AviatorRequestPolicy.UNCHANGED, null); + this(channel, defaultTimeoutSeconds, logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY, null); } - /** - * Creates a client with an explicit request policy. A connection context is required if the - * policy classifies connectivity failures, because the messages name the failed endpoint. - */ + /** 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"); - this.requestPolicy = Objects.requireNonNull(requestPolicy, "requestPolicy"); - if ( requestPolicy.isClassifyConnectivityFailures() ) { - Objects.requireNonNull(connectionContext, "connectionContext"); - } - this.connectionContext = connectionContext; + 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") @@ -150,10 +148,6 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv this(channel, defaultTimeoutSeconds, logger, 30); } - private , T, R> R execute(S stub, GrpcUtil.GrpcCall call, T request, String operation) { - return GrpcUtil.executeGrpcCall(stub, call, request, operation, requestPolicy, connectionContext); - } - public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle, List customPriorityOrder, ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) { @@ -211,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 execute(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 execute(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 execute(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 execute(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 execute(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) { @@ -239,14 +238,15 @@ public Application getApplicationByToken(String token, String appName) { .setToken(token) .setAppName(appName) .build(); - return execute(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) { GetDefaultQuotaRequest request = GetDefaultQuotaRequest.newBuilder() .setToken(token) .build(); - GetDefaultQuotaResponse response = execute(blockingStub, + GetDefaultQuotaResponse response = GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getDefaultQuota, request, Constants.OP_GET_DEFAULT_QUOTA); return response.getDefaultQuota(); @@ -263,7 +263,7 @@ public void validateAdminSession(String tenantName, String signature, String mes .setSignature(signature) .setMessage(message) .build(); - execute( + GrpcUtil.executeGrpcCall( blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::validateAdminSession, request, @@ -272,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 = execute(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 execute(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 execute(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) { @@ -292,38 +295,46 @@ public ListTokensResponse listTokensByDeveloper(String tenantName, String develo requestBuilder.setDeveloperEmail(developerEmail); } ListTokensByDeveloperRequest request = requestBuilder.build(); - return execute(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 execute(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 execute(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 execute(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 execute(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 = execute(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 = execute(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 4ce0d3b1552..947db4192f8 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,7 +45,7 @@ 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.UNCHANGED); + return createClient(url, logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY); } public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, long pingIntervalSeconds, @@ -56,7 +56,7 @@ public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, return createClient(createConnectionPlan(url), logger, pingIntervalSeconds, policy); } - /** Builds a client from a connection plan; tests use this to supply a proxy without environment variables. */ + /** 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(); @@ -71,7 +71,7 @@ static AviatorGrpcClient createClient(AviatorConnectionPlan connectionPlan, IAvi LOG.debug("Port specified, using NettyChannelBuilder.forAddress: {}:{}", target.host(), target.port()); } - var channel = configureBuilder(builder, proxyDescriptor).build(); + var channel = policy.configureChannel(configureBuilder(builder, proxyDescriptor)).build(); return new AviatorGrpcClient(channel, Constants.DEFAULT_TIMEOUT_SECONDS, logger, pingIntervalSeconds, policy, AviatorConnectionContext.of(connectionPlan)); } @@ -204,7 +204,7 @@ static ProxiedSocketAddress toProxiedSocketAddress(SocketAddress targetAddress, } public static AviatorGrpcClient createClient(String url) throws AviatorSimpleException { - return createClient(url, AviatorRequestPolicy.UNCHANGED); + return createClient(url, AviatorRequestPolicy.WAIT_FOR_READY); } public static AviatorGrpcClient createClient(String url, AviatorRequestPolicy policy) throws AviatorSimpleException { 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 index 3226761d93b..e6944e6742a 100644 --- 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 @@ -12,29 +12,33 @@ */ package com.fortify.cli.aviator.grpc; -/** - * Selects how unary Aviator gRPC requests behave when the connection fails. - * Code that uses this enum reads the flags; it does not compare with specific constants. - */ +import java.util.Objects; + +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.channel.ChannelOption; +import io.grpc.stub.AbstractBlockingStub; + +/** Unary request behavior; audit and diagnostic clients use {@link #WAIT_FOR_READY}. */ public enum AviatorRequestPolicy { - /** Fail fast on connection failures and report them with the shared connectivity messages. */ - ORDINARY(false, true), - /** Current behavior: wait for the channel to be ready and use the existing status mapping. */ - UNCHANGED(true, false); + FAIL_FAST, + WAIT_FOR_READY; - private final boolean waitForReady; - private final boolean classifyConnectivityFailures; + private static final int CONNECT_TIMEOUT_MILLIS = 5_000; - AviatorRequestPolicy(boolean waitForReady, boolean classifyConnectivityFailures) { - this.waitForReady = waitForReady; - this.classifyConnectivityFailures = classifyConnectivityFailures; + boolean isWaitForReady() { + return this == WAIT_FOR_READY; } - public boolean isWaitForReady() { - return waitForReady; + /** 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")); } - public boolean isClassifyConnectivityFailures() { - return classifyConnectivityFailures; + NettyChannelBuilder configureChannel(NettyChannelBuilder builder) { + // TCP establishment has a shorter budget than a request to a connected server. + return isWaitForReady() ? builder : builder.withOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS); } } 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 a0012cc04b1..3534520db96 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,7 +12,9 @@ */ 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; @@ -38,11 +40,6 @@ import com.fortify.cli.aviator.fpr.utils.SourceCodeEnricher; import com.fortify.cli.aviator.util.Constants; -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.stub.AbstractBlockingStub; @@ -56,29 +53,11 @@ interface GrpcCall { R call(S stub, T request) throws StatusRuntimeException; } - // gRPC 1.76 stubs have no withoutWaitForReady(); this interceptor clears the flag per call. - private static final ClientInterceptor WITHOUT_WAIT_FOR_READY = new ClientInterceptor() { - @Override - public ClientCall interceptCall(MethodDescriptor method, - CallOptions callOptions, Channel next) { - return next.newCall(method, callOptions.withoutWaitForReady()); - } - }; - static , T, R> R executeGrpcCall(S stub, GrpcCall call, T request, String operation) { - return executeGrpcCall(stub, call, request, operation, AviatorRequestPolicy.UNCHANGED, null); - } - - /** - * Executes a unary call with the default deadline. The policy selects wait-for-ready and - * connectivity failure handling; the context must be non-null if the policy classifies failures. - */ - static , T, R> R executeGrpcCall(S stub, GrpcCall call, T request, String operation, - AviatorRequestPolicy policy, AviatorConnectionContext context) { try { - return call.call(configureStub(stub, policy), request); + return call.call(stub.withDeadlineAfter(Constants.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS), request); } catch (StatusRuntimeException e) { - throw toConnectivityException(e, operation, policy, context) + 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."; @@ -87,24 +66,12 @@ static , T, R> R executeGrpcCall(S stub, GrpcC } } - private static > S configureStub(S stub, AviatorRequestPolicy policy) { - var stubWithDeadline = stub.withDeadlineAfter(Constants.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); - return policy.isWaitForReady() ? stubWithDeadline : stubWithDeadline.withInterceptors(WITHOUT_WAIT_FOR_READY); - } - - private static Optional toConnectivityException(StatusRuntimeException e, String operation, - AviatorRequestPolicy policy, AviatorConnectionContext context) { - if ( !policy.isClassifyConnectivityFailures() ) { + private static Optional toConnectivityException(StatusRuntimeException e, AviatorConnectionContext context) { + if ( context == null ) { return Optional.empty(); } return AviatorConnectivityFailureClassifier.classify(e, context) - .map(kind -> newConnectivityException(kind, operation, e, context)); - } - - private static AviatorConnectivityException newConnectivityException(AviatorConnectivityFailureKind kind, String operation, - StatusRuntimeException e, AviatorConnectionContext context) { - return new AviatorConnectivityException(kind.formatMessage(context), kind.name(), operation, - e.getStatus().getCode().name(), context.route(), context.firstHop().display(), e); + .map(kind -> new AviatorConnectivityException(kind.formatMessage(context), kind.name(), e)); } private static RuntimeException toAviatorException(StatusRuntimeException e, String operation) { 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 index 299235a5aea..defab13c002 100644 --- 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 @@ -74,9 +74,7 @@ void safeDiagnosticStringHasMetadataButNoSecrets() { var diagnostic = createException().getSafeDiagnosticString(); assertTrue(diagnostic.contains("kind=PROXY_REFUSED")); - assertTrue(diagnostic.contains("status=UNAVAILABLE")); - assertTrue(diagnostic.contains("route=proxy")); - assertTrue(diagnostic.contains("endpoint=localhost:33444")); + assertTrue(diagnostic.contains("proxy localhost:33444")); assertTrue(diagnostic.contains("java.net.ConnectException")); assertFalse(diagnostic.contains(SECRET)); } @@ -107,7 +105,6 @@ private static AviatorConnectivityException createException() { .withDescription("io exception " + SECRET) .withCause(transportCause) .asRuntimeException(); - return new AviatorConnectivityException(MESSAGE, "PROXY_REFUSED", "listApplications", - "UNAVAILABLE", "proxy", "localhost:33444", statusException); + return new AviatorConnectivityException(MESSAGE, "PROXY_REFUSED", statusException); } } 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 index a11165dc8ce..27522467dd3 100644 --- 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 @@ -31,14 +31,6 @@ class AviatorConnectionContextTest { private static final String PROXY_PASSWORD = "sentinel-proxy-password"; - @Test - void requestPolicyFlags() { - assertFalse(AviatorRequestPolicy.ORDINARY.isWaitForReady()); - assertTrue(AviatorRequestPolicy.ORDINARY.isClassifyConnectivityFailures()); - assertTrue(AviatorRequestPolicy.UNCHANGED.isWaitForReady()); - assertFalse(AviatorRequestPolicy.UNCHANGED.isClassifyConnectivityFailures()); - } - @Test void createsDirectContextFromPlan() { var context = AviatorConnectionContext.of(plan(Optional.empty())); @@ -46,7 +38,6 @@ void createsDirectContextFromPlan() { assertFalse(context.isProxied()); assertNull(context.proxy()); assertEquals("aviator.example.com:443", context.firstHop().display()); - assertEquals("direct", context.route()); } @Test @@ -60,7 +51,6 @@ void createsProxiedContextWithoutCredentials() { assertTrue(context.isProxied()); assertEquals("localhost:33444", context.firstHop().display()); - assertEquals("proxy", context.route()); assertFalse(context.toString().contains(PROXY_PASSWORD)); assertFalse(context.toString().contains("sentinel-user")); } diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java index 9c1338b72b5..b9679dd1958 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java @@ -110,8 +110,23 @@ void ignoresNestedCertificateFailures() { } @Test - void doesNotReadExceptionMessages() { + 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)); } 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 index ee83dc18df5..951bb43785b 100644 --- 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 @@ -15,6 +15,7 @@ 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; @@ -25,18 +26,32 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.exception.ExceptionUtils; 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; @@ -52,9 +67,11 @@ import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import io.grpc.stub.StreamObserver; +import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.socket.nio.NioSocketChannel; /** - * Checks ordinary-policy behavior against real Netty transports on the loopback interface. + * Checks connectivity behavior against real Netty transports on the loopback interface. */ class AviatorGrpcClientConnectivityTest { private static final Duration FAIL_FAST_LIMIT = Duration.ofSeconds(5); @@ -80,7 +97,8 @@ void directRefusalFailsFastWithDirectRefusedMessage() throws Exception { var exception = callAndExpectConnectivityFailure(plan); - assertEquals("DIRECT_REFUSED", exception.getKind()); + assertEquals("DIRECT_REFUSED", exception.getKind(), + () -> ExceptionUtils.getRootCause(exception).toString()); assertTrue(exception.getMessage().startsWith("Connection to Aviator server 127.0.0.1:" + port + " was refused")); } @@ -110,33 +128,82 @@ void proxyAuthenticationRejectionIsReportedAsProxyRejection() throws Exception { } @Test - void ordinaryPolicyDisablesWaitForReadyAndKeepsDeadline() throws Exception { - var captured = new AtomicReference(); - var client = createPlaintextClient(AviatorRequestPolicy.ORDINARY, captured); - - var result = client.listApplication("tenant", "signature", "message"); + @Timeout(15) + void pendingTcpConnectUsesNettyTimeoutBeforeRequestDeadline() throws Exception { + var eventLoop = new NioEventLoopGroup(1); + var builder = NettyChannelBuilder.forAddress("127.0.0.1", 443).usePlaintext() + .eventLoopGroup(eventLoop).channelFactory(PendingConnectChannel::new); + plaintextChannel = AviatorRequestPolicy.FAIL_FAST.configureChannel(builder).build(); + var context = new AviatorConnectionContext(new AviatorEndpoint("127.0.0.1", 443), null); + long started = System.nanoTime(); + try ( var client = new AviatorGrpcClient(plaintextChannel, 30, null, 30, AviatorRequestPolicy.FAIL_FAST, context) ) { + var exception = assertThrows(AviatorConnectivityException.class, + () -> client.listApplication("tenant", "signature", "message")); + assertEquals("CONNECT_TIMEOUT", exception.getKind()); + assertTrue(exception.getMessage().contains("timed out")); + assertFalse(exception.getMessage().contains("refused")); + } finally { + eventLoop.shutdownGracefully(0, 5, TimeUnit.SECONDS).sync(); + } + var elapsed = Duration.ofNanos(System.nanoTime() - started); + assertTrue(elapsed.compareTo(Duration.ofSeconds(4)) >= 0, "Connect timer fired too early: " + elapsed); + assertTrue(elapsed.compareTo(Duration.ofSeconds(10)) < 0, "Connect timer fired too late: " + elapsed); + } - assertTrue(result.isEmpty()); - assertFalse(captured.get().isWaitForReady()); - assertDeadlineAtMostDefault(captured.get()); - client.close(); + /** Simulates a pending TCP connection; Netty itself schedules and raises the timeout. */ + private static final class PendingConnectChannel extends NioSocketChannel { + @Override + protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) { + return false; + } } @Test - void unchangedPolicyKeepsWaitForReadyAndDeadline() throws Exception { - var captured = new AtomicReference(); - var client = createPlaintextClient(AviatorRequestPolicy.UNCHANGED, captured); + @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")); + assertEquals("PROXY_NEGOTIATION_FAILED", exception.getKind()); + assertFalse(exception.getMessage().contains("refused")); + assertFalse(exception.getSafeDiagnosticString().contains(PROXY_SECRET)); + } + assertTrue(Duration.ofNanos(System.nanoTime() - started).compareTo(Duration.ofSeconds(15)) < 0); + } - var result = client.listApplication("tenant", "signature", "message"); + @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)); + } + } - assertTrue(result.isEmpty()); - assertTrue(captured.get().isWaitForReady()); - assertDeadlineAtMostDefault(captured.get()); - client.close(); + 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.ORDINARY) ) { + 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")); @@ -158,6 +225,9 @@ private static AviatorConnectionPlan proxiedPlan(int proxyPort) { 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(); plaintextChannel = NettyChannelBuilder.forAddress(InetAddress.getLoopbackAddress().getHostAddress(), grpcServer.getPort()) @@ -216,6 +286,32 @@ public void listApplications(ApplicationByTenantName 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; 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 b1ea45b5521..19d5113ea32 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 @@ -73,34 +73,34 @@ void shouldClassifySkippedServerResponseWithoutInspectingMessage() { } @Test - void unchangedPolicyKeepsCurrentUnavailableMapping() { + void waitForReadyPolicyKeepsCurrentUnavailableMapping() { var e = assertThrows(AviatorTechnicalException.class, - () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("refused")), AviatorRequestPolicy.UNCHANGED)); + () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("Connection refused")), AviatorRequestPolicy.WAIT_FOR_READY)); assertTrue(e.getMessage().startsWith("A technical error occurred on the server while performing the test operation")); } @Test - void ordinaryPolicyClassifiesUnavailable() { + void failFastPolicyClassifiesUnavailable() { var e = assertThrows(AviatorConnectivityException.class, - () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("refused")), AviatorRequestPolicy.ORDINARY)); + () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("Connection refused")), AviatorRequestPolicy.FAIL_FAST)); assertEquals("DIRECT_REFUSED", e.getKind()); } @Test - void ordinaryPolicyKeepsUnimplementedContractForOlderServerLoginFallback() { + void failFastPolicyKeepsUnimplementedContractForOlderServerLoginFallback() { var e = assertThrows(AviatorTechnicalException.class, - () -> execute(Status.UNIMPLEMENTED, AviatorRequestPolicy.ORDINARY)); + () -> execute(Status.UNIMPLEMENTED, AviatorRequestPolicy.FAIL_FAST)); var cause = assertInstanceOf(StatusRuntimeException.class, e.getCause()); assertEquals(Status.Code.UNIMPLEMENTED, cause.getStatus().getCode()); } @Test - void ordinaryPolicyKeepsApplicationStatusMapping() { + void failFastPolicyKeepsApplicationStatusMapping() { var e = assertThrows(AviatorSimpleException.class, - () -> execute(Status.NOT_FOUND.withDescription("app"), AviatorRequestPolicy.ORDINARY)); + () -> execute(Status.NOT_FOUND.withDescription("app"), AviatorRequestPolicy.FAIL_FAST)); assertFalse(e instanceof AviatorConnectivityException); assertTrue(e.getMessage().contains("not found")); @@ -110,9 +110,9 @@ 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(ApplicationServiceGrpc.newBlockingStub(channel), + GrpcUtil.executeGrpcCall(policy.configureNewStub(ApplicationServiceGrpc.newBlockingStub(channel), context), (stub, request) -> { throw status.asRuntimeException(); }, - "request", "test", policy, context); + "request", "test"); } finally { channel.shutdownNow(); } 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 ea655a3fc1d..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 @@ -62,7 +62,7 @@ protected AviatorAdminConfigDescriptor login(String configName) { configCreateOptions.getTenant(), privateKeyContents); LOG.info("Validating Aviator admin configuration with the Aviator server..."); - configHelper.validateConfig(configDescriptor, AviatorRequestPolicy.ORDINARY); + 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 2d2de5a6ad0..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 @@ -39,7 +39,7 @@ protected Class getSessionDescriptorType() { } public void validateConfig(AviatorAdminConfigDescriptor configDescriptor) { - validateConfig(configDescriptor, AviatorRequestPolicy.UNCHANGED); + validateConfig(configDescriptor, AviatorRequestPolicy.WAIT_FOR_READY); } public void validateConfig(AviatorAdminConfigDescriptor configDescriptor, AviatorRequestPolicy policy) { 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 c5660925364..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 @@ -37,11 +37,8 @@ public final JsonNode getJsonNode() { public abstract boolean isSingular(); - /** - * Creates a gRPC client that fails fast on connectivity failures and reports them with - * the shared Aviator connectivity messages. Callers must close the client. - */ + /** The caller must close the client. */ protected final AviatorGrpcClient createGrpcClient(AviatorAdminConfigDescriptor configDescriptor) { - return AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl(), AviatorRequestPolicy.ORDINARY); + 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 33e061dccf1..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 @@ -64,7 +64,7 @@ protected AviatorUserSessionDescriptor login(String sessionName) { LOG.info("Validating Aviator user token with the server..."); try { var validationResponse = sessionHelper.validateToken(sessionLoginOptions.getAviatorUrl(), resolvedToken, - AviatorRequestPolicy.ORDINARY).response(); + 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 d3edc48a1ac..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 @@ -43,7 +43,7 @@ public AviatorUserTokenValidationResult validateToken(AviatorUserSessionDescript } public AviatorUserTokenValidationResult validateToken(String aviatorUrl, String token) { - return validateToken(aviatorUrl, token, AviatorRequestPolicy.UNCHANGED); + return validateToken(aviatorUrl, token, AviatorRequestPolicy.WAIT_FOR_READY); } public AviatorUserTokenValidationResult validateToken(String aviatorUrl, String token, AviatorRequestPolicy policy) { 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 index e0ee92558c8..1d7c364b34c 100644 --- 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 @@ -23,7 +23,6 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Base64; -import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -38,18 +37,17 @@ 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 ordinary Aviator commands fail fast with the shared connectivity messages. + * 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); - private static final List PROXY_ENV_VARS = - List.of("https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY"); // Static, because some fcli helpers cache paths below the fcli data directory in static fields @TempDir static Path fcliDataDir; @@ -73,13 +71,12 @@ static void restoreFcliDataDir() { @BeforeEach void setUp() throws IOException { - // The client reads proxy environment variables; a configured proxy would change the result - assumeTrue(PROXY_ENV_VARS.stream().allMatch(name -> System.getenv(name)==null), - "Skipped because a proxy environment variable is set"); 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 @@ -88,7 +85,7 @@ void tearDown() { } @Test - void adminCommandBaseClassCreatesOrdinaryClient() { + void adminCommandBaseClassCreatesFailFastClient() { var descriptor = new AviatorAdminConfigDescriptor(closedPortUrl, "tenant", "unused-private-key"); var command = new TestAdminCommand(); @@ -100,9 +97,9 @@ void adminCommandBaseClassCreatesOrdinaryClient() { } @Test - void userSessionTokenValidationFailsFastWithOrdinaryPolicy() { + void userSessionTokenValidationFailsFastWithFailFastPolicy() { assertFailsFast(() -> AviatorUserSessionHelper.instance() - .validateToken(closedPortUrl, syntheticToken(), AviatorRequestPolicy.ORDINARY)); + .validateToken(closedPortUrl, syntheticToken(), AviatorRequestPolicy.FAIL_FAST)); } private static String syntheticToken() { diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java index 0b1d4419bf2..9f0ff0663ef 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java @@ -60,10 +60,8 @@ public AbstractFcliException(String message, Throwable cause) { public abstract String getStackTraceString(); /** - * Returns a safe description of this exception for DEBUG logging, or null to log the - * full throwable as before. Override only if this exception or its causes can hold - * sensitive text; the returned value must not contain exception messages, URLs, - * headers, or credentials. + * Returns a safe DEBUG description, or null to log the full throwable. + * Overrides must exclude credentials and untrusted transport messages. */ public String getSafeDiagnosticString() { 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 00e4f8f1624..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 @@ -67,11 +67,8 @@ private void logoutIfSessionExists(String sessionName) { protected void testAuthenticatedConnection(String sessionName) {} /** - * Returns whether an existing session with the same name must be logged out and - * destroyed before {@link #login(String)} is called. Subclasses can return false if - * login() only validates the new session and the old session needs no remote logout. - * The existing session then stays in place if login() fails, and - * {@code AbstractSessionHelper.save()} replaces it if login() succeeds. + * 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/exception/FcliExecutionExceptionHandlerTest.java b/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java index 0579d870fa8..68ad39c71e3 100644 --- a/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java +++ b/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java @@ -14,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.PrintWriter; @@ -34,10 +33,8 @@ import picocli.CommandLine.Model.CommandSpec; /** - * Checks the default DEBUG logging of {@link FcliExecutionExceptionHandler}. The safe - * diagnostic path is tested with a real overriding exception in fcli-aviator-common - * (AviatorConnectivityExceptionTest), because test sources cannot extend the - * {@code @Reflectable} exception classes without the compile-only annotation library. + * AviatorConnectivityExceptionTest covers the safe logging path. This module's test + * classpath lacks the annotation library needed to extend @Reflectable exceptions. */ class FcliExecutionExceptionHandlerTest { private final Logger logger = (Logger)LoggerFactory.getLogger(FcliExecutionExceptionHandler.class); @@ -58,11 +55,6 @@ void tearDown() { logger.setLevel(previousLevel); } - @Test - void defaultSafeDiagnosticStringIsNull() { - assertNull(new FcliSimpleException("Plain failure").getSafeDiagnosticString()); - } - @Test void logsThrowableForFcliExceptionsWithoutSafeDiagnosticString() { var err = new StringWriter(); From 8c13c04db87710563182aef2dac431c581c8e134 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 23:38:55 +0530 Subject: [PATCH 08/12] chore: streamline Aviator connectivity failure handling Consolidate classification and inline messages, retain the existing connection timeout, and recognize Windows connection refusals. Protect sensitive causes in terminal output and malformed URL errors. Add TLS CONNECT proxy and address-fallback coverage with focused test helpers. Validation: 927 tests passed across common-core, aviator-common, and aviator; git diff --check passed. --- .../AviatorConnectivityException.java | 5 + .../AviatorConnectivityFailureClassifier.java | 82 ---------- .../grpc/AviatorConnectivityFailureKind.java | 58 +++++++ .../aviator/grpc/AviatorGrpcClientHelper.java | 7 +- .../aviator/grpc/AviatorRequestPolicy.java | 8 - .../fortify/cli/aviator/grpc/GrpcUtil.java | 2 +- .../AviatorConnectivityExceptionTest.java | 23 ++- ...> AviatorConnectivityFailureKindTest.java} | 24 ++- .../AviatorGrpcClientConnectivityTest.java | 151 +++++++++++++----- .../com/fortify/cli/aviator/grpc/README.md | 4 + .../cli/aviator/grpc/localhost-cert.pem | 19 +++ .../cli/aviator/grpc/localhost-key.pem | 28 ++++ .../exception/AbstractFcliException.java | 5 + .../common/exception/FcliExceptionHelper.java | 4 + 14 files changed, 282 insertions(+), 138 deletions(-) delete mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java rename fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/{AviatorConnectivityFailureClassifierTest.java => AviatorConnectivityFailureKindTest.java} (86%) create mode 100644 fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md create mode 100644 fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem create mode 100644 fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem 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 index 88de5a8dc99..89ed297f60b 100644 --- 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 @@ -33,6 +33,11 @@ public String getKind() { return kind; } + @Override + public boolean isCauseSensitive() { + return true; + } + @Override public String getStackTraceString() { return getMessage(); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java deleted file mode 100644 index feab4bb4a96..00000000000 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifier.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.CONNECT_TIMEOUT; -import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.DEADLINE_EXCEEDED; -import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.DIRECT_REFUSED; -import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.DNS_FAILURE; -import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.PROXY_NEGOTIATION_FAILED; -import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.PROXY_REFUSED; -import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.PROXY_REJECTED; -import static com.fortify.cli.aviator.grpc.AviatorConnectivityFailureKind.UNAVAILABLE; - -import java.net.ConnectException; -import java.net.UnknownHostException; -import java.nio.channels.UnresolvedAddressException; -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; -import io.grpc.netty.shaded.io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException; -import io.grpc.netty.shaded.io.netty.handler.proxy.ProxyConnectException; - -final class AviatorConnectivityFailureClassifier { - private AviatorConnectivityFailureClassifier() {} - - /** Leaves TLS failures and application statuses to the existing exception handler. */ - 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 ( has(e, SSLException.class) || has(e, CertificateException.class) ) { - return Optional.empty(); - } - return Optional.of(classifyTransport(e, context, code)); - } - - // The order is significant: all Netty connect and proxy exceptions extend ConnectException. - private static AviatorConnectivityFailureKind classifyTransport(Throwable e, AviatorConnectionContext context, Status.Code code) { - if ( has(e, HttpProxyConnectException.class) ) { return PROXY_REJECTED; } - if ( has(e, ProxyConnectException.class) ) { return 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 code==Status.Code.DEADLINE_EXCEEDED ? DEADLINE_EXCEEDED : UNAVAILABLE; - } - - // 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: no further information".equalsIgnoreCase(message) - || "connect(..) failed: Connection refused".equalsIgnoreCase(message) - || "finishConnect(..) failed: Connection refused".equalsIgnoreCase(message)); - } - - private static boolean has(Throwable e, Class type) { - return ExceptionUtils.indexOfType(e, type) >= 0; - } -} 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 index 602385a8dfb..446b204c3db 100644 --- 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 @@ -12,6 +12,23 @@ */ package com.fortify.cli.aviator.grpc; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; +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; +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, @@ -22,6 +39,47 @@ enum AviatorConnectivityFailureKind { UNAVAILABLE, DEADLINE_EXCEEDED; + /** Leaves TLS failures and application statuses to the existing exception handler. */ + 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 ( has(e, SSLException.class) || has(e, CertificateException.class) ) { + return Optional.empty(); + } + return Optional.of(classifyTransport(e, context, code)); + } + + // The order is significant: all Netty connect and proxy exceptions extend ConnectException. + private static AviatorConnectivityFailureKind classifyTransport(Throwable e, AviatorConnectionContext context, Status.Code code) { + if ( has(e, HttpProxyConnectException.class) ) { return PROXY_REJECTED; } + if ( has(e, ProxyConnectException.class) ) { return 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 code==Status.Code.DEADLINE_EXCEEDED ? DEADLINE_EXCEEDED : UNAVAILABLE; + } + + // 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 type) { + return ExceptionUtils.indexOfType(e, type) >= 0; + } + String formatMessage(AviatorConnectionContext context) { return switch (this) { case PROXY_REJECTED -> "Proxy " + context.proxy().display() + " rejected the connection to the Aviator server\n" 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 947db4192f8..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 @@ -71,7 +71,7 @@ static AviatorGrpcClient createClient(AviatorConnectionPlan connectionPlan, IAvi LOG.debug("Port specified, using NettyChannelBuilder.forAddress: {}:{}", target.host(), target.port()); } - var channel = policy.configureChannel(configureBuilder(builder, proxyDescriptor)).build(); + var channel = configureBuilder(builder, proxyDescriptor).build(); return new AviatorGrpcClient(channel, Constants.DEFAULT_TIMEOUT_SECONDS, logger, pingIntervalSeconds, policy, AviatorConnectionContext.of(connectionPlan)); } @@ -93,8 +93,9 @@ public static ParsedTarget parseTarget(String 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: " + safeUrl, 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(); 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 index e6944e6742a..257a71b4165 100644 --- 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 @@ -14,8 +14,6 @@ import java.util.Objects; -import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; -import io.grpc.netty.shaded.io.netty.channel.ChannelOption; import io.grpc.stub.AbstractBlockingStub; /** Unary request behavior; audit and diagnostic clients use {@link #WAIT_FOR_READY}. */ @@ -23,8 +21,6 @@ public enum AviatorRequestPolicy { FAIL_FAST, WAIT_FOR_READY; - private static final int CONNECT_TIMEOUT_MILLIS = 5_000; - boolean isWaitForReady() { return this == WAIT_FOR_READY; } @@ -37,8 +33,4 @@ > S configureNewStub(S stub, AviatorConnection return stub.withOption(AviatorConnectionContext.CALL_OPTION, Objects.requireNonNull(context, "connectionContext")); } - NettyChannelBuilder configureChannel(NettyChannelBuilder builder) { - // TCP establishment has a shorter budget than a request to a connected server. - return isWaitForReady() ? builder : builder.withOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS); - } } 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 3534520db96..48a2417bdab 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 @@ -70,7 +70,7 @@ private static Optional toConnectivityException(StatusRuntimeE if ( context == null ) { return Optional.empty(); } - return AviatorConnectivityFailureClassifier.classify(e, context) + return AviatorConnectivityFailureKind.classify(e, context) .map(kind -> new AviatorConnectivityException(kind.formatMessage(context), kind.name(), e)); } 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 index defab13c002..0b73ed84fbc 100644 --- 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 @@ -15,6 +15,7 @@ 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.io.PrintWriter; @@ -26,12 +27,14 @@ import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; +import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; import com.fortify.cli.common.exception.FcliExceptionHelper; 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 io.grpc.Status; import picocli.CommandLine; @@ -98,11 +101,29 @@ void handlerPrintsOnlyMessageAndLogsOnlySafeDiagnostic() { assertFalse(event.getFormattedMessage().contains(SECRET)); } + @Test + void malformedUrlDoesNotExposeCredentialsInOutputOrLogs() { + var exception = assertThrows(AviatorSimpleException.class, + () -> AviatorGrpcClientHelper.parseTarget("https://user:" + SECRET + "@aviator.invalid/%zz")); + var err = new StringWriter(); + var commandLine = new CommandLine(CommandSpec.create()); + commandLine.setErr(new PrintWriter(err)); + + assertEquals(1, FcliExecutionExceptionHandler.INSTANCE.handleException(exception, commandLine)); + + 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 AviatorConnectivityException createException() { var transportCause = new ConnectException("Connection refused: proxy-user:" + SECRET + "@localhost"); transportCause.addSuppressed(new IllegalStateException("suppressed " + SECRET)); var statusException = Status.UNAVAILABLE - .withDescription("io exception " + SECRET) + .withDescription("PKIX path building failed: " + SECRET) .withCause(transportCause) .asRuntimeException(); return new AviatorConnectivityException(MESSAGE, "PROXY_REFUSED", statusException); diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKindTest.java similarity index 86% rename from fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java rename to fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKindTest.java index b9679dd1958..aabe60be76a 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureClassifierTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorConnectivityFailureKindTest.java @@ -13,6 +13,8 @@ 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; @@ -25,6 +27,7 @@ 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; @@ -33,7 +36,7 @@ import io.grpc.netty.shaded.io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException; import io.grpc.netty.shaded.io.netty.handler.proxy.ProxyConnectException; -class AviatorConnectivityFailureClassifierTest { +class AviatorConnectivityFailureKindTest { private static final AviatorConnectionContext DIRECT = new AviatorConnectionContext(new AviatorEndpoint("aviator.example.com", 443), null); private static final AviatorConnectionContext PROXIED = @@ -63,9 +66,10 @@ void classifiesConnectExceptionAsProxyRefusalWhenProxied() { assertEquals(Optional.of(AviatorConnectivityFailureKind.PROXY_REFUSED), classify(Status.UNAVAILABLE, cause, PROXIED)); } - @Test - void classifiesConnectExceptionAsDirectRefusalWhenNotProxied() { - var cause = new ConnectException("Connection refused"); + @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)); } @@ -130,8 +134,18 @@ void recognizesRefusalInsideNettyAnnotatedCause() { assertEquals(Optional.of(AviatorConnectivityFailureKind.DIRECT_REFUSED), classify(Status.UNAVAILABLE, cause, DIRECT)); } + @ParameterizedTest + @EnumSource(AviatorConnectivityFailureKind.class) + void everyFailureHasAnActionableMessage(AviatorConnectivityFailureKind kind) { + var message = kind.formatMessage(PROXIED); + assertEquals(2, message.lines().count()); + assertTrue(message.contains("\nCheck ")); + assertFalse(message.contains("%s")); + assertFalse(message.contains("Exception")); + } + private static Optional classify(Status status, Throwable cause, AviatorConnectionContext context) { var exception = new StatusRuntimeException(cause==null ? status : status.withCause(cause)); - return AviatorConnectivityFailureClassifier.classify(exception, context); + 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 index 951bb43785b..e2eb473de5b 100644 --- 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 @@ -26,10 +26,14 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; -import java.net.SocketAddress; +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.Executors; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -62,13 +66,14 @@ 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.GrpcSslContexts; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import io.grpc.stub.StreamObserver; -import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup; -import io.grpc.netty.shaded.io.netty.channel.socket.nio.NioSocketChannel; /** * Checks connectivity behavior against real Netty transports on the loopback interface. @@ -80,12 +85,12 @@ class AviatorGrpcClientConnectivityTest { private ServerSocket proxyServer; private Server grpcServer; - private ManagedChannel plaintextChannel; + private ManagedChannel testChannel; @AfterEach void tearDown() throws Exception { if ( proxyServer!=null ) { proxyServer.close(); } - if ( plaintextChannel!=null ) { plaintextChannel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } + if ( testChannel!=null ) { testChannel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } if ( grpcServer!=null ) { grpcServer.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } } @@ -127,37 +132,6 @@ void proxyAuthenticationRejectionIsReportedAsProxyRejection() throws Exception { assertFalse(exception.getSafeDiagnosticString().contains(PROXY_SECRET)); } - @Test - @Timeout(15) - void pendingTcpConnectUsesNettyTimeoutBeforeRequestDeadline() throws Exception { - var eventLoop = new NioEventLoopGroup(1); - var builder = NettyChannelBuilder.forAddress("127.0.0.1", 443).usePlaintext() - .eventLoopGroup(eventLoop).channelFactory(PendingConnectChannel::new); - plaintextChannel = AviatorRequestPolicy.FAIL_FAST.configureChannel(builder).build(); - var context = new AviatorConnectionContext(new AviatorEndpoint("127.0.0.1", 443), null); - long started = System.nanoTime(); - try ( var client = new AviatorGrpcClient(plaintextChannel, 30, null, 30, AviatorRequestPolicy.FAIL_FAST, context) ) { - var exception = assertThrows(AviatorConnectivityException.class, - () -> client.listApplication("tenant", "signature", "message")); - assertEquals("CONNECT_TIMEOUT", exception.getKind()); - assertTrue(exception.getMessage().contains("timed out")); - assertFalse(exception.getMessage().contains("refused")); - } finally { - eventLoop.shutdownGracefully(0, 5, TimeUnit.SECONDS).sync(); - } - var elapsed = Duration.ofNanos(System.nanoTime() - started); - assertTrue(elapsed.compareTo(Duration.ofSeconds(4)) >= 0, "Connect timer fired too early: " + elapsed); - assertTrue(elapsed.compareTo(Duration.ofSeconds(10)) < 0, "Connect timer fired too late: " + elapsed); - } - - /** Simulates a pending TCP connection; Netty itself schedules and raises the timeout. */ - private static final class PendingConnectChannel extends NioSocketChannel { - @Override - protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) { - return false; - } - } - @Test @Timeout(20) void stalledProxyNegotiationEndsBeforeRequestDeadline() throws Exception { @@ -196,6 +170,107 @@ void healthyUnaryServicesUseSelectedPolicyAndStreamsKeepWaiting(AviatorRequestPo } } + @Test + @Timeout(20) + void healthyTlsConnectionThroughConnectProxySucceeds() throws Exception { + startTlsServer(); + proxyServer = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")); + var executor = Executors.newFixedThreadPool(2); + try { + var tunnel = startConnectTunnel(executor); + try ( var client = createProxiedTlsClient() ) { + assertTrue(client.listApplication("tenant", "signature", "message").isEmpty()); + } + tunnel.get(10, TimeUnit.SECONDS); + } finally { + proxyServer.close(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(15, TimeUnit.SECONDS)); + } + } + + private void startTlsServer() throws IOException { + // Public test fixtures, trusted only by this test client. + try ( var certificate = getClass().getResourceAsStream("localhost-cert.pem"); + var key = getClass().getResourceAsStream("localhost-key.pem") ) { + grpcServer = NettyServerBuilder.forAddress(new InetSocketAddress("127.0.0.1", 0)) + .sslContext(GrpcSslContexts.forServer(certificate, key).build()) + .addService(new EmptyApplicationService()).build().start(); + } + } + + private AviatorGrpcClient createProxiedTlsClient() throws IOException { + var proxy = ProxyDescriptor.builder().proxyHost("127.0.0.1").proxyPort(proxyServer.getLocalPort()).build(); + try ( var certificate = getClass().getResourceAsStream("localhost-cert.pem") ) { + testChannel = NettyChannelBuilder.forAddress("localhost", grpcServer.getPort()) + .sslContext(GrpcSslContexts.forClient().trustManager(certificate).build()) + .proxyDetector(address -> AviatorGrpcClientHelper.toProxiedSocketAddress(address, proxy)) + .build(); + } + var context = new AviatorConnectionContext(new AviatorEndpoint("localhost", grpcServer.getPort()), + new AviatorEndpoint("127.0.0.1", proxyServer.getLocalPort())); + return new AviatorGrpcClient(testChannel, 30, null, 30, AviatorRequestPolicy.FAIL_FAST, context); + } + + private Future startConnectTunnel(ExecutorService executor) { + return executor.submit(() -> { + try ( var downstream = proxyServer.accept(); + var upstream = new Socket("127.0.0.1", grpcServer.getPort()) ) { + downstream.setSoTimeout(10_000); + upstream.setSoTimeout(10_000); + readRequestHeaders(downstream.getInputStream()); + downstream.getOutputStream().write("HTTP/1.1 200 Connection Established\r\n\r\n" + .getBytes(StandardCharsets.US_ASCII)); + downstream.getOutputStream().flush(); + var response = executor.submit(() -> { + upstream.getInputStream().transferTo(downstream.getOutputStream()); + downstream.shutdownOutput(); + return null; + }); + downstream.getInputStream().transferTo(upstream.getOutputStream()); + upstream.shutdownOutput(); + response.get(10, TimeUnit.SECONDS); + } + return null; + }); + } + + @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); @@ -230,12 +305,12 @@ private AviatorGrpcClient createPlaintextClient(AviatorRequestPolicy policy, Ato .addService(new EmptyDastEntitlementService()) .build() .start(); - plaintextChannel = NettyChannelBuilder.forAddress(InetAddress.getLoopbackAddress().getHostAddress(), grpcServer.getPort()) + 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(plaintextChannel, 30, null, 30, policy, context); + return new AviatorGrpcClient(testChannel, 30, null, 30, policy, context); } private static void assertDeadlineAtMostDefault(CallOptions callOptions) { diff --git a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md new file mode 100644 index 00000000000..8ed41a434da --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md @@ -0,0 +1,4 @@ +The certificate and private key in this directory are public test fixtures for the +local TLS/CONNECT proxy test. They are not credentials for any deployed service. +The certificate covers `localhost` and `127.0.0.1`; the test client trusts only this +certificate. No production trust store is changed. diff --git a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem new file mode 100644 index 00000000000..6e5a247ebb6 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJzCCAg+gAwIBAgIUJV46i5ZV+22z1ciDUue4RrZJA34wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyMjE2NDE1OVoYDzIxMjYw +OTI4MTY0MTU5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC0v80zI9oOCJD89jFFL0S3M7K3lrIsIahugYwXvqs5 +ptFgVshu1loYd+7Yi396BWN3IbQZtkSOVimSa+/sE6m9MyAF2CIo2+nL6T/ase57 +6gy4W42nox8EDBl1NpREv7gRcsQGTjWhxaJHJy5WD7g0SEyu6CoDzk5gcL8v9gu4 +H/4ViOoQjGPzeRjKuYEau9ZD6oIM7FwzVqLmKMdw9EI53jFWzrZiWsWCmZ6wj3fG +yyenkaaZT6K7ct7ACnV4SVGPOD8fQWV+8iUu/owRIbZEpF5/i95Du+1//f9PAEgz +nJtxcfwPcljBO53p+Bh3S2hw3HZM10xJssOtH1xT7XutAgMBAAGjbzBtMB0GA1Ud +DgQWBBQ0yo1JfIwQoykeCR9VSnjUImy3fDAfBgNVHSMEGDAWgBQ0yo1JfIwQoyke +CR9VSnjUImy3fDAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z +dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAq2rziu/YQP9woqsmgwtPhqWA1XA3 +UdTNNkH1e5qtOe7mqWTDAPwjFN5ALL10SgWpe8mNMwz7GSNvr15Mt0dK/k7uVSQv +NDIXu4V+bR0GL65Rlb4aA3Mk1nImZMzvz/E/KzSV6TH2X3d/mbZCpphAMSzkIlYf +0ZX3d3D+AT77kal5MUD5DtsRkLjgO5GtzBVP8yptN5He8RBA66NkvMWDv4RcoZnc +D8w7qF4ehGSWI5Rfc4g0uS8xTK4+KESs5GHOZasV6ZFDjJc9vyEEuzMs58HEGjO3 +Mv9E8/GZblBgLgI2zRjAJGlD5AyqmgcO9mUbEgJMPGTeBBbeemjrZSVDiw== +-----END CERTIFICATE----- diff --git a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem new file mode 100644 index 00000000000..f111ea382fe --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0v80zI9oOCJD8 +9jFFL0S3M7K3lrIsIahugYwXvqs5ptFgVshu1loYd+7Yi396BWN3IbQZtkSOVimS +a+/sE6m9MyAF2CIo2+nL6T/ase576gy4W42nox8EDBl1NpREv7gRcsQGTjWhxaJH +Jy5WD7g0SEyu6CoDzk5gcL8v9gu4H/4ViOoQjGPzeRjKuYEau9ZD6oIM7FwzVqLm +KMdw9EI53jFWzrZiWsWCmZ6wj3fGyyenkaaZT6K7ct7ACnV4SVGPOD8fQWV+8iUu +/owRIbZEpF5/i95Du+1//f9PAEgznJtxcfwPcljBO53p+Bh3S2hw3HZM10xJssOt +H1xT7XutAgMBAAECggEBAIr6GzZiL6ywq4LFbbbrlGNlDdqZtV+3dYGafHRZd+B5 +Qu5Xa//FMzszJGQ+RoM1F3inYoqOmgHzgD2EXtLff5jpcYrs178g8JCIRT/LpjqD +CMrMIowuIZAEGaMFl1WRMF/CJbVPd29uvcsF9lvX9GPG36dw/bU1cRA4QNkIz5P/ +QJ7IXLez/qLed8T4SfePpOiDIQWyvIn8QQg3hOJCl11poGJWVvmrDaC4FJCdIQ4z +lJjxKeiP612/Ol1Y+PspuGKafM4GoNPEdnicHJ9tifItqBam5Ywh9fkyHtAMAG4Y +lwILbqUu5RWIiRAEDYyqjJLxLOXa0ypmuJeKlcwW3cECgYEA8ATFnxFuOEXzo8HX ++4WrwvBuO/sudYJbppn+iNxBJI3zAmCyYK/iZnWCw6ATDrjhzeVW896Z5n+U4bgR +0YwDgQ6RVMrVxaROu0QmOhUUWok/MS31EQfrEHafMa/aF1dtK3/i4/AAvJN4Snfb +jTApxJCmmQT2U86GHjYX0L6ue/ECgYEAwMjBUwiAqP5bqy7e9OMAUGkXdpEDlgXp +2DhZYkAlcwe/7lh3VgmYwviY0VuIh0unZ5WlYneugaUPssPJhjKUgprnBW98Y9vu +rUA8gIWvpAFiEQkHEdJ/zqDOxGzvzI4BQILNPNmeJw0IYQr3VVEOyTk2hUKD82O6 +wcAmIHOtZ30CgYAlfNlQ6uwIJgTTwub1Bf+5xsuw2p57BA1EjDDRN3WFPBaEfI3f +xBjcX+Y4n7vaRM4f1n4qQM5QzgR27MT7sZ6qfZf9YmDZKbySSyadU8WGDoGsML1i +F5aDT8O1gv3RP3pxnNvXNhknWvEi0G2eM7OSW/VpWPkQReV0uoYluGKQQQKBgAx4 +8Ij+s9Q7IJMipSoSxaJuKiZxm1nJaCek/aNtov6eMbyp3I6kcE2s57YjxeF684mw +FNMaCyA4HZvjLtaPzyvj+a5QWCvdvsNWtJNcEtD5he60NtDgXYRwwu/SXHFPfqU9 +0Sluqty76x7cJsROSjaajPxyITdN7Hc8hzryQPXBAoGBAOZESShlexeoz234HtY3 +kEcOmpJp9thc26IL5uAZorGTOQTTXz4PB+qybQMvLdReAMoHxqB9oRQW6ZNgq3X6 ++2yP46Ev8650ZOe+Y21Zc813mOZc/vnHtUi1ZD/jwi6Pm+iTDfsmfLDI8LPpKrYI +5emblvatzsmHLJUNVGU6hCZI +-----END PRIVATE KEY----- diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java index 9f0ff0663ef..c18c38048f0 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java @@ -59,6 +59,11 @@ public AbstractFcliException(String message, Throwable cause) { public abstract String getStackTraceString(); + /** Whether terminal output must use this exception's formatting without inspecting its causes. */ + public boolean isCauseSensitive() { + return false; + } + /** * Returns a safe DEBUG description, or null to log the full throwable. * Overrides must exclude credentials and untrusted transport messages. diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java index 2d28f437904..eac5cb6346c 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java @@ -41,6 +41,10 @@ private FcliExceptionHelper() { */ public static String formatException(Exception e) { if ( e==null ) { return null; } + // Sensitive causes must not enter the message-based PKIX formatter. + if ( e instanceof AbstractFcliException fcliException && fcliException.isCauseSensitive() ) { + return fcliException.getStackTraceString(); + } if ( isPkixRelatedException(e) ) { return formatPkixRelatedException(e); } From efd73a1321b8b723acd2876f31ec3f3092f3dc75 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Fri, 23 Oct 2026 09:47:05 +0530 Subject: [PATCH 09/12] chore: Remove the Aviator TLS proxy test and its checked-in key --- .../AviatorGrpcClientConnectivityTest.java | 69 ------------------- .../com/fortify/cli/aviator/grpc/README.md | 4 -- .../cli/aviator/grpc/localhost-cert.pem | 19 ----- .../cli/aviator/grpc/localhost-key.pem | 28 -------- 4 files changed, 120 deletions(-) delete mode 100644 fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md delete mode 100644 fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem delete mode 100644 fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem 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 index e2eb473de5b..b4b09ffc991 100644 --- 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 @@ -31,9 +31,6 @@ import java.time.Duration; import java.util.List; import java.util.Optional; -import java.util.concurrent.Executors; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -71,7 +68,6 @@ 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.GrpcSslContexts; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import io.grpc.stub.StreamObserver; @@ -170,71 +166,6 @@ void healthyUnaryServicesUseSelectedPolicyAndStreamsKeepWaiting(AviatorRequestPo } } - @Test - @Timeout(20) - void healthyTlsConnectionThroughConnectProxySucceeds() throws Exception { - startTlsServer(); - proxyServer = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")); - var executor = Executors.newFixedThreadPool(2); - try { - var tunnel = startConnectTunnel(executor); - try ( var client = createProxiedTlsClient() ) { - assertTrue(client.listApplication("tenant", "signature", "message").isEmpty()); - } - tunnel.get(10, TimeUnit.SECONDS); - } finally { - proxyServer.close(); - executor.shutdownNow(); - assertTrue(executor.awaitTermination(15, TimeUnit.SECONDS)); - } - } - - private void startTlsServer() throws IOException { - // Public test fixtures, trusted only by this test client. - try ( var certificate = getClass().getResourceAsStream("localhost-cert.pem"); - var key = getClass().getResourceAsStream("localhost-key.pem") ) { - grpcServer = NettyServerBuilder.forAddress(new InetSocketAddress("127.0.0.1", 0)) - .sslContext(GrpcSslContexts.forServer(certificate, key).build()) - .addService(new EmptyApplicationService()).build().start(); - } - } - - private AviatorGrpcClient createProxiedTlsClient() throws IOException { - var proxy = ProxyDescriptor.builder().proxyHost("127.0.0.1").proxyPort(proxyServer.getLocalPort()).build(); - try ( var certificate = getClass().getResourceAsStream("localhost-cert.pem") ) { - testChannel = NettyChannelBuilder.forAddress("localhost", grpcServer.getPort()) - .sslContext(GrpcSslContexts.forClient().trustManager(certificate).build()) - .proxyDetector(address -> AviatorGrpcClientHelper.toProxiedSocketAddress(address, proxy)) - .build(); - } - var context = new AviatorConnectionContext(new AviatorEndpoint("localhost", grpcServer.getPort()), - new AviatorEndpoint("127.0.0.1", proxyServer.getLocalPort())); - return new AviatorGrpcClient(testChannel, 30, null, 30, AviatorRequestPolicy.FAIL_FAST, context); - } - - private Future startConnectTunnel(ExecutorService executor) { - return executor.submit(() -> { - try ( var downstream = proxyServer.accept(); - var upstream = new Socket("127.0.0.1", grpcServer.getPort()) ) { - downstream.setSoTimeout(10_000); - upstream.setSoTimeout(10_000); - readRequestHeaders(downstream.getInputStream()); - downstream.getOutputStream().write("HTTP/1.1 200 Connection Established\r\n\r\n" - .getBytes(StandardCharsets.US_ASCII)); - downstream.getOutputStream().flush(); - var response = executor.submit(() -> { - upstream.getInputStream().transferTo(downstream.getOutputStream()); - downstream.shutdownOutput(); - return null; - }); - downstream.getInputStream().transferTo(upstream.getOutputStream()); - upstream.shutdownOutput(); - response.get(10, TimeUnit.SECONDS); - } - return null; - }); - } - @Test @Timeout(15) @SuppressWarnings("deprecation") // Per-channel resolver keeps the fixture out of the global registry. diff --git a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md deleted file mode 100644 index 8ed41a434da..00000000000 --- a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/README.md +++ /dev/null @@ -1,4 +0,0 @@ -The certificate and private key in this directory are public test fixtures for the -local TLS/CONNECT proxy test. They are not credentials for any deployed service. -The certificate covers `localhost` and `127.0.0.1`; the test client trusts only this -certificate. No production trust store is changed. diff --git a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem deleted file mode 100644 index 6e5a247ebb6..00000000000 --- a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDJzCCAg+gAwIBAgIUJV46i5ZV+22z1ciDUue4RrZJA34wDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyMjE2NDE1OVoYDzIxMjYw -OTI4MTY0MTU5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQC0v80zI9oOCJD89jFFL0S3M7K3lrIsIahugYwXvqs5 -ptFgVshu1loYd+7Yi396BWN3IbQZtkSOVimSa+/sE6m9MyAF2CIo2+nL6T/ase57 -6gy4W42nox8EDBl1NpREv7gRcsQGTjWhxaJHJy5WD7g0SEyu6CoDzk5gcL8v9gu4 -H/4ViOoQjGPzeRjKuYEau9ZD6oIM7FwzVqLmKMdw9EI53jFWzrZiWsWCmZ6wj3fG -yyenkaaZT6K7ct7ACnV4SVGPOD8fQWV+8iUu/owRIbZEpF5/i95Du+1//f9PAEgz -nJtxcfwPcljBO53p+Bh3S2hw3HZM10xJssOtH1xT7XutAgMBAAGjbzBtMB0GA1Ud -DgQWBBQ0yo1JfIwQoykeCR9VSnjUImy3fDAfBgNVHSMEGDAWgBQ0yo1JfIwQoyke -CR9VSnjUImy3fDAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z -dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAq2rziu/YQP9woqsmgwtPhqWA1XA3 -UdTNNkH1e5qtOe7mqWTDAPwjFN5ALL10SgWpe8mNMwz7GSNvr15Mt0dK/k7uVSQv -NDIXu4V+bR0GL65Rlb4aA3Mk1nImZMzvz/E/KzSV6TH2X3d/mbZCpphAMSzkIlYf -0ZX3d3D+AT77kal5MUD5DtsRkLjgO5GtzBVP8yptN5He8RBA66NkvMWDv4RcoZnc -D8w7qF4ehGSWI5Rfc4g0uS8xTK4+KESs5GHOZasV6ZFDjJc9vyEEuzMs58HEGjO3 -Mv9E8/GZblBgLgI2zRjAJGlD5AyqmgcO9mUbEgJMPGTeBBbeemjrZSVDiw== ------END CERTIFICATE----- diff --git a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem b/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem deleted file mode 100644 index f111ea382fe..00000000000 --- a/fcli-core/fcli-aviator-common/src/test/resources/com/fortify/cli/aviator/grpc/localhost-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0v80zI9oOCJD8 -9jFFL0S3M7K3lrIsIahugYwXvqs5ptFgVshu1loYd+7Yi396BWN3IbQZtkSOVimS -a+/sE6m9MyAF2CIo2+nL6T/ase576gy4W42nox8EDBl1NpREv7gRcsQGTjWhxaJH -Jy5WD7g0SEyu6CoDzk5gcL8v9gu4H/4ViOoQjGPzeRjKuYEau9ZD6oIM7FwzVqLm -KMdw9EI53jFWzrZiWsWCmZ6wj3fGyyenkaaZT6K7ct7ACnV4SVGPOD8fQWV+8iUu -/owRIbZEpF5/i95Du+1//f9PAEgznJtxcfwPcljBO53p+Bh3S2hw3HZM10xJssOt -H1xT7XutAgMBAAECggEBAIr6GzZiL6ywq4LFbbbrlGNlDdqZtV+3dYGafHRZd+B5 -Qu5Xa//FMzszJGQ+RoM1F3inYoqOmgHzgD2EXtLff5jpcYrs178g8JCIRT/LpjqD -CMrMIowuIZAEGaMFl1WRMF/CJbVPd29uvcsF9lvX9GPG36dw/bU1cRA4QNkIz5P/ -QJ7IXLez/qLed8T4SfePpOiDIQWyvIn8QQg3hOJCl11poGJWVvmrDaC4FJCdIQ4z -lJjxKeiP612/Ol1Y+PspuGKafM4GoNPEdnicHJ9tifItqBam5Ywh9fkyHtAMAG4Y -lwILbqUu5RWIiRAEDYyqjJLxLOXa0ypmuJeKlcwW3cECgYEA8ATFnxFuOEXzo8HX -+4WrwvBuO/sudYJbppn+iNxBJI3zAmCyYK/iZnWCw6ATDrjhzeVW896Z5n+U4bgR -0YwDgQ6RVMrVxaROu0QmOhUUWok/MS31EQfrEHafMa/aF1dtK3/i4/AAvJN4Snfb -jTApxJCmmQT2U86GHjYX0L6ue/ECgYEAwMjBUwiAqP5bqy7e9OMAUGkXdpEDlgXp -2DhZYkAlcwe/7lh3VgmYwviY0VuIh0unZ5WlYneugaUPssPJhjKUgprnBW98Y9vu -rUA8gIWvpAFiEQkHEdJ/zqDOxGzvzI4BQILNPNmeJw0IYQr3VVEOyTk2hUKD82O6 -wcAmIHOtZ30CgYAlfNlQ6uwIJgTTwub1Bf+5xsuw2p57BA1EjDDRN3WFPBaEfI3f -xBjcX+Y4n7vaRM4f1n4qQM5QzgR27MT7sZ6qfZf9YmDZKbySSyadU8WGDoGsML1i -F5aDT8O1gv3RP3pxnNvXNhknWvEi0G2eM7OSW/VpWPkQReV0uoYluGKQQQKBgAx4 -8Ij+s9Q7IJMipSoSxaJuKiZxm1nJaCek/aNtov6eMbyp3I6kcE2s57YjxeF684mw -FNMaCyA4HZvjLtaPzyvj+a5QWCvdvsNWtJNcEtD5he60NtDgXYRwwu/SXHFPfqU9 -0Sluqty76x7cJsROSjaajPxyITdN7Hc8hzryQPXBAoGBAOZESShlexeoz234HtY3 -kEcOmpJp9thc26IL5uAZorGTOQTTXz4PB+qybQMvLdReAMoHxqB9oRQW6ZNgq3X6 -+2yP46Ev8650ZOe+Y21Zc813mOZc/vnHtUi1ZD/jwi6Pm+iTDfsmfLDI8LPpKrYI -5emblvatzsmHLJUNVGU6hCZI ------END PRIVATE KEY----- From 052812e96d34ea6e08cfc2583a2a39900a31b34a Mon Sep 17 00:00:00 2001 From: kireetivar Date: Fri, 23 Oct 2026 11:16:32 +0530 Subject: [PATCH 10/12] chore: Don't attach transport causes to Aviator connectivity exceptions The message is safe to show, so the standard fcli simple exception output is enough. Remove the cause-sensitive and safe-diagnostic hooks from AbstractFcliException, and log only cause types at DEBUG level. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AviatorConnectivityException.java | 46 ++-------- .../fortify/cli/aviator/grpc/GrpcUtil.java | 13 ++- .../AviatorConnectivityExceptionTest.java | 60 +++---------- .../AviatorGrpcClientConnectivityTest.java | 19 ++-- .../cli/aviator/grpc/GrpcUtilTest.java | 4 +- .../exception/AbstractFcliException.java | 13 --- .../common/exception/FcliExceptionHelper.java | 4 - .../FcliExecutionExceptionHandler.java | 13 +-- .../FcliExecutionExceptionHandlerTest.java | 89 ------------------- 9 files changed, 44 insertions(+), 217 deletions(-) delete mode 100644 fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java 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 index 89ed297f60b..6df5f9db285 100644 --- 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 @@ -12,44 +12,16 @@ */ package com.fortify.cli.aviator._common.exception; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.exception.ExceptionUtils; - -/** Keeps transport cause text out of CLI output and DEBUG logs. */ +/** + * 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; - private static final int MAX_CAUSE_TYPES = 10; - - private final String kind; - - /** Message and kind must be safe for display. */ - public AviatorConnectivityException(String message, String kind, Throwable cause) { - super(message, cause); - this.kind = kind; - } - - public String getKind() { - return kind; - } - - @Override - public boolean isCauseSensitive() { - return true; - } - - @Override - public String getStackTraceString() { - return getMessage(); - } - @Override - public String getSafeDiagnosticString() { - var causeTypes = ExceptionUtils.getThrowableList(getCause()).stream() - .limit(MAX_CAUSE_TYPES) - .map(t -> t.getClass().getName()) - .collect(Collectors.joining(" <- ")); - return String.format("Aviator connectivity failure: kind=%s, message=%s, causes=[%s]", - kind, getMessage().replace("\n", " / "), causeTypes); + 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/GrpcUtil.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/GrpcUtil.java index 48a2417bdab..be79c0d651e 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 @@ -18,6 +18,7 @@ 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; @@ -71,7 +72,17 @@ private static Optional toConnectivityException(StatusRuntimeE return Optional.empty(); } return AviatorConnectivityFailureKind.classify(e, context) - .map(kind -> new AviatorConnectivityException(kind.formatMessage(context), kind.name(), e)); + .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) { 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 index 0b73ed84fbc..0e7f83292aa 100644 --- 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 @@ -16,11 +16,9 @@ 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.io.PrintWriter; import java.io.StringWriter; -import java.net.ConnectException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -28,7 +26,6 @@ import org.slf4j.LoggerFactory; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; -import com.fortify.cli.common.exception.FcliExceptionHelper; import com.fortify.cli.common.exception.FcliExecutionExceptionHandler; import ch.qos.logback.classic.Level; @@ -36,15 +33,12 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ThrowableProxyUtil; import ch.qos.logback.core.read.ListAppender; -import io.grpc.Status; import picocli.CommandLine; import picocli.CommandLine.Help.Ansi; import picocli.CommandLine.Model.CommandSpec; class AviatorConnectivityExceptionTest { private static final String SECRET = "sentinel-secret-value"; - private static final String MESSAGE = "Connection to proxy localhost:33444 was refused\n" - + "Check that the proxy is running and that its host and port are correct"; private final Logger logger = (Logger)LoggerFactory.getLogger(FcliExecutionExceptionHandler.class); private final ListAppender appender = new ListAppender<>(); @@ -65,40 +59,15 @@ void tearDown() { } @Test - void stackTraceStringIsOnlyTheMessage() { - var exception = createException(); - - assertEquals(MESSAGE, exception.getStackTraceString()); - assertEquals(MESSAGE, FcliExceptionHelper.formatException(exception)); - } - - @Test - void safeDiagnosticStringHasMetadataButNoSecrets() { - var diagnostic = createException().getSafeDiagnosticString(); - - assertTrue(diagnostic.contains("kind=PROXY_REFUSED")); - assertTrue(diagnostic.contains("proxy localhost:33444")); - assertTrue(diagnostic.contains("java.net.ConnectException")); - assertFalse(diagnostic.contains(SECRET)); - } - - @Test - void handlerPrintsOnlyMessageAndLogsOnlySafeDiagnostic() { + void handlerPrintsTheMessageWithoutCause() { var err = new StringWriter(); - var commandLine = new CommandLine(CommandSpec.create()); - commandLine.setErr(new PrintWriter(err)); - commandLine.setColorScheme(CommandLine.Help.defaultColorScheme(Ansi.OFF)); + var exception = new AviatorConnectivityException("Proxy localhost:33444 refused the connection; check the proxy host and port"); - int exitCode = FcliExecutionExceptionHandler.INSTANCE.handleException(createException(), commandLine); + int exitCode = FcliExecutionExceptionHandler.INSTANCE.handleException(exception, createCommandLine(err)); assertEquals(1, exitCode); - assertEquals(MESSAGE, err.toString().trim()); - assertFalse(err.toString().contains("Exception")); - assertEquals(1, appender.list.size()); - var event = appender.list.get(0); - assertNull(event.getThrowableProxy()); - assertTrue(event.getFormattedMessage().contains("kind=PROXY_REFUSED")); - assertFalse(event.getFormattedMessage().contains(SECRET)); + assertNull(exception.getCause()); + assertFalse(err.toString().contains("Caused by")); } @Test @@ -106,10 +75,8 @@ void malformedUrlDoesNotExposeCredentialsInOutputOrLogs() { var exception = assertThrows(AviatorSimpleException.class, () -> AviatorGrpcClientHelper.parseTarget("https://user:" + SECRET + "@aviator.invalid/%zz")); var err = new StringWriter(); - var commandLine = new CommandLine(CommandSpec.create()); - commandLine.setErr(new PrintWriter(err)); - assertEquals(1, FcliExecutionExceptionHandler.INSTANCE.handleException(exception, commandLine)); + assertEquals(1, FcliExecutionExceptionHandler.INSTANCE.handleException(exception, createCommandLine(err))); assertNull(exception.getCause()); assertFalse(err.toString().contains(SECRET)); @@ -119,13 +86,10 @@ void malformedUrlDoesNotExposeCredentialsInOutputOrLogs() { assertFalse(ThrowableProxyUtil.asString(event.getThrowableProxy()).contains(SECRET)); } - private static AviatorConnectivityException createException() { - var transportCause = new ConnectException("Connection refused: proxy-user:" + SECRET + "@localhost"); - transportCause.addSuppressed(new IllegalStateException("suppressed " + SECRET)); - var statusException = Status.UNAVAILABLE - .withDescription("PKIX path building failed: " + SECRET) - .withCause(transportCause) - .asRuntimeException(); - return new AviatorConnectivityException(MESSAGE, "PROXY_REFUSED", statusException); + 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/AviatorGrpcClientConnectivityTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/AviatorGrpcClientConnectivityTest.java index b4b09ffc991..7e54dc2423e 100644 --- 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 @@ -34,7 +34,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -98,9 +97,8 @@ void directRefusalFailsFastWithDirectRefusedMessage() throws Exception { var exception = callAndExpectConnectivityFailure(plan); - assertEquals("DIRECT_REFUSED", exception.getKind(), - () -> ExceptionUtils.getRootCause(exception).toString()); - assertTrue(exception.getMessage().startsWith("Connection to Aviator server 127.0.0.1:" + port + " was refused")); + assertTrue(exception.getMessage().startsWith("Connection to Aviator server 127.0.0.1:" + port + " was refused"), + exception.getMessage()); } @Test @@ -109,10 +107,8 @@ void proxyRefusalFailsFastWithProxyRefusedMessage() throws Exception { var exception = callAndExpectConnectivityFailure(proxiedPlan(proxyPort)); - assertEquals("PROXY_REFUSED", exception.getKind()); assertEquals("Connection to proxy 127.0.0.1:" + proxyPort + " was refused\n" + "Check that the proxy is running and that its host and port are correct", exception.getMessage()); - assertFalse(exception.getSafeDiagnosticString().contains(PROXY_SECRET)); } @Test @@ -122,10 +118,9 @@ void proxyAuthenticationRejectionIsReportedAsProxyRejection() throws Exception { var exception = callAndExpectConnectivityFailure(proxiedPlan(proxyServer.getLocalPort())); - assertEquals("PROXY_REJECTED", exception.getKind()); + assertTrue(exception.getMessage().startsWith("Proxy 127.0.0.1:" + proxyServer.getLocalPort() + " rejected"), + exception.getMessage()); assertFalse(exception.getMessage().contains("sentinel-realm")); - assertFalse(exception.getSafeDiagnosticString().contains("sentinel-realm")); - assertFalse(exception.getSafeDiagnosticString().contains(PROXY_SECRET)); } @Test @@ -139,9 +134,7 @@ void stalledProxyNegotiationEndsBeforeRequestDeadline() throws Exception { null, 30, AviatorRequestPolicy.FAIL_FAST) ) { var exception = assertThrows(AviatorConnectivityException.class, () -> client.listApplication("tenant", "signature", "message")); - assertEquals("PROXY_NEGOTIATION_FAILED", exception.getKind()); - assertFalse(exception.getMessage().contains("refused")); - assertFalse(exception.getSafeDiagnosticString().contains(PROXY_SECRET)); + assertTrue(exception.getMessage().startsWith("Could not open a tunnel"), exception.getMessage()); } assertTrue(Duration.ofNanos(System.nanoTime() - started).compareTo(Duration.ofSeconds(15)) < 0); } @@ -215,6 +208,8 @@ private static AviatorConnectivityException callAndExpectConnectivityFailure(Avi () -> 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; } } 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 19d5113ea32..07b0ea6675b 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 @@ -15,6 +15,7 @@ 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; @@ -85,7 +86,8 @@ void failFastPolicyClassifiesUnavailable() { var e = assertThrows(AviatorConnectivityException.class, () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("Connection refused")), AviatorRequestPolicy.FAIL_FAST)); - assertEquals("DIRECT_REFUSED", e.getKind()); + assertTrue(e.getMessage().startsWith("Connection to Aviator server 127.0.0.1:1 was refused"), e.getMessage()); + assertNull(e.getCause()); } @Test diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java index c18c38048f0..070a0ae3be6 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/AbstractFcliException.java @@ -58,17 +58,4 @@ public AbstractFcliException(String message, Throwable cause) { } public abstract String getStackTraceString(); - - /** Whether terminal output must use this exception's formatting without inspecting its causes. */ - public boolean isCauseSensitive() { - return false; - } - - /** - * Returns a safe DEBUG description, or null to log the full throwable. - * Overrides must exclude credentials and untrusted transport messages. - */ - public String getSafeDiagnosticString() { - return null; - } } diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java index eac5cb6346c..2d28f437904 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExceptionHelper.java @@ -41,10 +41,6 @@ private FcliExceptionHelper() { */ public static String formatException(Exception e) { if ( e==null ) { return null; } - // Sensitive causes must not enter the message-based PKIX formatter. - if ( e instanceof AbstractFcliException fcliException && fcliException.isCauseSensitive() ) { - return fcliException.getStackTraceString(); - } if ( isPkixRelatedException(e) ) { return formatPkixRelatedException(e); } diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java index 5193d466ae8..baddffb1d89 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandler.java @@ -33,7 +33,7 @@ public int handleExecutionException(Exception ex, CommandLine commandLine, Parse public int handleException(Exception ex, CommandLine commandLine) { if ( LOG.isDebugEnabled() ) { - logDebug(ex); + LOG.debug("fcli terminating with exception", ex); } var formattedException = FcliExceptionHelper.formatException(ex); if ( formattedException!=null ) { @@ -44,17 +44,6 @@ public int handleException(Exception ex, CommandLine commandLine) { return getExitCode(ex, commandLine); } - private static void logDebug(Exception ex) { - var safeDiagnostic = ex instanceof AbstractFcliException fcliException - ? fcliException.getSafeDiagnosticString() - : null; - if ( safeDiagnostic==null ) { - LOG.debug("fcli terminating with exception", ex); - } else { - LOG.debug("fcli terminating with exception: {}", safeDiagnostic); - } - } - private int getExitCode(Exception ex, CommandLine commandLine) { if ( ex instanceof AbstractFcliException ) { var exitCode = ((AbstractFcliException)ex).exitCode(); diff --git a/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java b/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java deleted file mode 100644 index 68ad39c71e3..00000000000 --- a/fcli-core/fcli-common-core/src/test/java/com/fortify/cli/common/exception/FcliExecutionExceptionHandlerTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.exception; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -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 ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; -import picocli.CommandLine; -import picocli.CommandLine.Help.Ansi; -import picocli.CommandLine.Model.CommandSpec; - -/** - * AviatorConnectivityExceptionTest covers the safe logging path. This module's test - * classpath lacks the annotation library needed to extend @Reflectable exceptions. - */ -class FcliExecutionExceptionHandlerTest { - 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 logsThrowableForFcliExceptionsWithoutSafeDiagnosticString() { - var err = new StringWriter(); - - int exitCode = FcliExecutionExceptionHandler.INSTANCE.handleException(new FcliSimpleException("Plain failure"), createCommandLine(err)); - - assertEquals(1, exitCode); - assertTrue(err.toString().contains("Plain failure")); - var event = singleEvent(); - assertNotNull(event.getThrowableProxy()); - assertEquals("fcli terminating with exception", event.getFormattedMessage()); - } - - @Test - void logsThrowableForNonFcliExceptions() { - FcliExecutionExceptionHandler.INSTANCE.handleException(new IllegalStateException("Unexpected"), createCommandLine(new StringWriter())); - - assertNotNull(singleEvent().getThrowableProxy()); - } - - private ILoggingEvent singleEvent() { - assertEquals(1, appender.list.size()); - return appender.list.get(0); - } - - 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; - } -} From 34ff21a606935a8f457ac14eb7141bc2a3f0af5d Mon Sep 17 00:00:00 2001 From: kireetivar Date: Fri, 23 Oct 2026 11:18:53 +0530 Subject: [PATCH 11/12] fix: Show short single-line Aviator connectivity messages Name the failing proxy or Aviator server, give one remediation, and state the 30-second request limit. Add a TLS failure kind for non-certificate TLS errors; certificate failures keep the fcli PKIX trust-store guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../grpc/AviatorConnectionContext.java | 4 -- .../grpc/AviatorConnectivityFailureKind.java | 64 +++++++++++++------ .../grpc/AviatorConnectionContextTest.java | 27 +------- .../AviatorConnectivityFailureKindTest.java | 53 ++++++++++++--- .../AviatorGrpcClientConnectivityTest.java | 9 +-- .../cli/aviator/grpc/GrpcUtilTest.java | 15 ++++- ...viatorOrdinaryCommandConnectivityTest.java | 2 +- 7 files changed, 110 insertions(+), 64 deletions(-) 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 index 0cd5ba1998c..e2746ef4d64 100644 --- 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 @@ -41,8 +41,4 @@ static AviatorConnectionContext of(AviatorConnectionPlan plan) { boolean isProxied() { return proxy!=null; } - - AviatorEndpoint firstHop() { - return isProxied() ? proxy : target; - } } 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 index 446b204c3db..05b6776be16 100644 --- 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 @@ -15,6 +15,7 @@ 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; @@ -22,6 +23,8 @@ 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; @@ -36,31 +39,48 @@ enum AviatorConnectivityFailureKind { PROXY_REFUSED, DIRECT_REFUSED, DNS_FAILURE, + TLS_FAILURE, UNAVAILABLE, DEADLINE_EXCEEDED; - /** Leaves TLS failures and application statuses to the existing exception handler. */ + /** + * 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 ( has(e, SSLException.class) || has(e, CertificateException.class) ) { + 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) { - if ( has(e, HttpProxyConnectException.class) ) { return PROXY_REJECTED; } - if ( has(e, ProxyConnectException.class) ) { return PROXY_NEGOTIATION_FAILED; } + 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 code==Status.Code.DEADLINE_EXCEEDED ? DEADLINE_EXCEEDED : UNAVAILABLE; + return neutral; } // Java has no refusal-specific exception type. Match complete known reasons, never @@ -81,23 +101,25 @@ private static boolean has(Throwable e, Class type) { } 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 the Aviator server\n" - + "Check the proxy credentials, access rules, and whether the Aviator service is available"; - case PROXY_NEGOTIATION_FAILED -> "Could not open a tunnel to the Aviator server through proxy " - + context.proxy().display() + "\nCheck that the proxy is reachable and allows connections to the Aviator server"; - case CONNECT_TIMEOUT -> "Connection to " + context.firstHop().display() - + " timed out\nCheck network access, firewall rules, and proxy settings"; - case PROXY_REFUSED -> "Connection to proxy " + context.proxy().display() - + " was refused\nCheck that the proxy is running and that its host and port are correct"; - case DIRECT_REFUSED -> "Connection to Aviator server " + context.target().display() - + " was refused\nCheck the Aviator URL and that the Aviator service is available"; - case DNS_FAILURE -> "Could not resolve a host name for the Aviator connection\n" - + "Check the Aviator URL, the proxy host, and the DNS settings"; - case UNAVAILABLE -> "The Aviator server is unavailable or cannot be reached\n" - + "Check the Aviator URL, network access, and proxy settings, then try again"; - case DEADLINE_EXCEEDED -> "The Aviator request timed out\n" - + "Check network access and proxy settings; if they are correct, try again later"; + 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/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 index 27522467dd3..1062082a3b4 100644 --- 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 @@ -21,8 +21,6 @@ import java.util.Optional; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.ParsedTarget; @@ -37,7 +35,7 @@ void createsDirectContextFromPlan() { assertFalse(context.isProxied()); assertNull(context.proxy()); - assertEquals("aviator.example.com:443", context.firstHop().display()); + assertEquals("aviator.example.com:443", context.target().display()); } @Test @@ -50,7 +48,7 @@ void createsProxiedContextWithoutCredentials() { var context = AviatorConnectionContext.of(plan(Optional.of(proxy))); assertTrue(context.isProxied()); - assertEquals("localhost:33444", context.firstHop().display()); + assertEquals("localhost:33444", context.proxy().display()); assertFalse(context.toString().contains(PROXY_PASSWORD)); assertFalse(context.toString().contains("sentinel-user")); } @@ -72,27 +70,6 @@ void displaysIpv6Hosts() { assertEquals("[::1]:443", new AviatorEndpoint("[::1]", 443).display()); } - @ParameterizedTest - @EnumSource(AviatorConnectivityFailureKind.class) - void messagesHaveSummaryAndGuidanceWithoutTrailingPeriod(AviatorConnectivityFailureKind kind) { - var context = new AviatorConnectionContext(new AviatorEndpoint("aviator.example.com", 443), new AviatorEndpoint("localhost", 33444)); - var lines = kind.formatMessage(context).split("\n"); - - assertEquals(2, lines.length); - assertFalse(lines[0].contains("%s")); - assertFalse(lines[0].endsWith(".")); - assertFalse(lines[1].endsWith(".")); - } - - @Test - void proxyRefusedMessageNamesTheProxy() { - var context = new AviatorConnectionContext(new AviatorEndpoint("aviator.example.com", 443), new AviatorEndpoint("localhost", 33444)); - - assertEquals("Connection to proxy localhost:33444 was refused\n" - + "Check that the proxy is running and that its host and port are correct", - AviatorConnectivityFailureKind.PROXY_REFUSED.formatMessage(context)); - } - 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 index aabe60be76a..61d26cc4ac5 100644 --- 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 @@ -19,9 +19,11 @@ 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; @@ -88,8 +90,27 @@ void usesNeutralKindsWithoutTransportEvidence() { } @Test - void ignoresTlsFailures() { - assertEquals(Optional.empty(), classify(Status.UNAVAILABLE, new SSLHandshakeException("handshake"), DIRECT)); + 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 @@ -136,12 +157,28 @@ void recognizesRefusalInsideNettyAnnotatedCause() { @ParameterizedTest @EnumSource(AviatorConnectivityFailureKind.class) - void everyFailureHasAnActionableMessage(AviatorConnectivityFailureKind kind) { - var message = kind.formatMessage(PROXIED); - assertEquals(2, message.lines().count()); - assertTrue(message.contains("\nCheck ")); - assertFalse(message.contains("%s")); - assertFalse(message.contains("Exception")); + 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) { 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 index 7e54dc2423e..3332c0c62a7 100644 --- 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 @@ -97,7 +97,7 @@ void directRefusalFailsFastWithDirectRefusedMessage() throws Exception { var exception = callAndExpectConnectivityFailure(plan); - assertTrue(exception.getMessage().startsWith("Connection to Aviator server 127.0.0.1:" + port + " was refused"), + assertEquals("Aviator server 127.0.0.1:" + port + " refused the connection; check the Aviator URL", exception.getMessage()); } @@ -107,8 +107,8 @@ void proxyRefusalFailsFastWithProxyRefusedMessage() throws Exception { var exception = callAndExpectConnectivityFailure(proxiedPlan(proxyPort)); - assertEquals("Connection to proxy 127.0.0.1:" + proxyPort + " was refused\n" - + "Check that the proxy is running and that its host and port are correct", exception.getMessage()); + assertEquals("Proxy 127.0.0.1:" + proxyPort + " refused the connection; check the proxy host and port", + exception.getMessage()); } @Test @@ -134,7 +134,8 @@ void stalledProxyNegotiationEndsBeforeRequestDeadline() throws Exception { null, 30, AviatorRequestPolicy.FAIL_FAST) ) { var exception = assertThrows(AviatorConnectivityException.class, () -> client.listApplication("tenant", "signature", "message")); - assertTrue(exception.getMessage().startsWith("Could not open a tunnel"), exception.getMessage()); + 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); } 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 07b0ea6675b..9de64025922 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 @@ -20,9 +20,12 @@ 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; @@ -34,6 +37,7 @@ 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; @@ -86,10 +90,19 @@ void failFastPolicyClassifiesUnavailable() { var e = assertThrows(AviatorConnectivityException.class, () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("Connection refused")), AviatorRequestPolicy.FAIL_FAST)); - assertTrue(e.getMessage().startsWith("Connection to Aviator server 127.0.0.1:1 was refused"), e.getMessage()); + 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, 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 index 1d7c364b34c..e6884ad1a1d 100644 --- 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 @@ -114,7 +114,7 @@ private static void assertFailsFast(Executable executable) { 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("was refused"), exception.getMessage()); + assertTrue(exception.getMessage().contains("refused the connection"), exception.getMessage()); } private static final class TestAdminCommand extends AbstractAviatorAdminSessionOutputCommand { From 16a68cc4f9aca0f3ebae999db5db965d8647cae8 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Fri, 23 Oct 2026 11:20:02 +0530 Subject: [PATCH 12/12] chore: Put remediation before 'contact support' in Aviator gRPC errors The generic message no longer blames the server for failures that can be client-side, names the gRPC status, and tells the user how to log details. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/com/fortify/cli/aviator/grpc/GrpcUtil.java | 4 ++-- .../com/fortify/cli/aviator/grpc/GrpcUtilTest.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) 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 be79c0d651e..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 @@ -111,7 +111,7 @@ private static RuntimeException toAviatorException(StatusRuntimeException e, Str 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("The server's resource limits were exceeded during %s. Please try again later or contact support. Server details: %s", operation, serverDescription)); + 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" + @@ -131,7 +131,7 @@ private static RuntimeException toAviatorException(StatusRuntimeException e, Str 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 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); 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 9de64025922..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 @@ -82,7 +82,9 @@ void waitForReadyPolicyKeepsCurrentUnavailableMapping() { var e = assertThrows(AviatorTechnicalException.class, () -> execute(Status.UNAVAILABLE.withCause(new ConnectException("Connection refused")), AviatorRequestPolicy.WAIT_FOR_READY)); - assertTrue(e.getMessage().startsWith("A technical error occurred on the server while performing the test operation")); + 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 @@ -112,6 +114,15 @@ void failFastPolicyKeepsUnimplementedContractForOlderServerLoginFallback() { 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,