From 44851138a2ae0f63dca0ae133886a9cf32f257fa Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 22 Oct 2026 12:21:47 +0530 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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, From ffdd7432ee58280a630cc1eb4a1f88b5fc3f68c7 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Mon, 26 Oct 2026 10:52:21 +0530 Subject: [PATCH 13/25] chore: Sort imports in Aviator connectivity tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../grpc/AviatorGrpcClientConnectivityTest.java | 10 +++++----- .../cmd/AviatorOrdinaryCommandConnectivityTest.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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 3332c0c62a7..77a0cb637fc 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 @@ -49,22 +49,22 @@ import com.fortify.aviator.entitlement.EntitlementServiceGrpc; import com.fortify.aviator.entitlement.ListEntitlementsByTenantRequest; import com.fortify.aviator.entitlement.ListEntitlementsByTenantResponse; -import com.fortify.grpc.token.ListTokensRequest; -import com.fortify.grpc.token.ListTokensResponse; -import com.fortify.grpc.token.TokenServiceGrpc; import com.fortify.cli.aviator._common.exception.AviatorConnectivityException; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.ParsedTarget; import com.fortify.cli.common.http.proxy.helper.ProxyDescriptor; +import com.fortify.grpc.token.ListTokensRequest; +import com.fortify.grpc.token.ListTokensResponse; +import com.fortify.grpc.token.TokenServiceGrpc; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; import io.grpc.EquivalentAddressGroup; -import io.grpc.NameResolver; +import io.grpc.ManagedChannel; import io.grpc.MethodDescriptor; +import io.grpc.NameResolver; import io.grpc.Server; import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; 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 e6884ad1a1d..080e58a62cb 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 @@ -36,8 +36,8 @@ import com.fortify.cli.aviator._common.config.admin.helper.AviatorAdminConfigDescriptor; import com.fortify.cli.aviator._common.exception.AviatorConnectivityException; import com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionHelper; -import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; +import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.common.cli.util.FcliExecutionContextHolder; import com.fortify.cli.common.output.cli.mixin.IOutputHelper; From 556501731086c6167a381d69b1683fd73de0e941 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Mon, 26 Oct 2026 11:12:59 +0530 Subject: [PATCH 14/25] chore: Clarify session replacement hook and test the inherited default Rename the hook to shouldReplaceExistingSessionBeforeLogin and use a separate opt-out test subclass so the default lifecycle test exercises the production default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cmd/AviatorAdminConfigCreateCommand.java | 2 +- .../cmd/AviatorUserSessionLoginCommand.java | 2 +- .../cli/cmd/AbstractSessionLoginCommand.java | 4 +-- .../cmd/AbstractSessionLoginCommandTest.java | 32 +++++++++++-------- 4 files changed, 22 insertions(+), 18 deletions(-) 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 0bbd54e5601..5e1b0d6ee82 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 @@ -49,7 +49,7 @@ protected void logoutBeforeNewLogin(String configName, AviatorAdminConfigDescrip } @Override - protected boolean isReplaceExistingSessionBeforeLogin() { + protected boolean shouldReplaceExistingSessionBeforeLogin() { // login() validates the configuration first; keep the existing configuration if validation fails return false; } 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 afa45505416..a6e2e3379d3 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 @@ -51,7 +51,7 @@ public class AviatorUserSessionLoginCommand extends AbstractSessionLoginCommand< protected void logoutBeforeNewLogin(String sessionName, AviatorUserSessionDescriptor sessionDescriptor) {} @Override - protected boolean isReplaceExistingSessionBeforeLogin() { + protected boolean shouldReplaceExistingSessionBeforeLogin() { // login() validates the token first; keep the existing session if validation fails return false; } 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 8ca29538306..21aca304612 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,7 @@ public JsonNode getJsonNode() { var sessionNameSupplier = getSessionNameSupplier(); String sessionName = sessionNameSupplier==null?"default":sessionNameSupplier.getSessionName(); var sessionHelper = getSessionHelper(); - if ( isReplaceExistingSessionBeforeLogin() ) { + if ( shouldReplaceExistingSessionBeforeLogin() ) { logoutIfSessionExists(sessionName); } D sessionDescriptor = login(sessionName); @@ -70,7 +70,7 @@ protected void testAuthenticatedConnection(String sessionName) {} * Return false when login only validates credentials and the old session needs no remote logout. * This keeps the existing session if validation fails; save replaces it after success. */ - protected boolean isReplaceExistingSessionBeforeLogin() { + protected boolean shouldReplaceExistingSessionBeforeLogin() { 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 index 7c0356ed398..a274fd50f19 100644 --- 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 @@ -61,7 +61,8 @@ void tearDown() { @Test void defaultLifecycleDestroysExistingSessionBeforeLogin() { sessionHelper.save(SESSION_NAME, new TestSessionDescriptor("old")); - var command = new TestLoginCommand(sessionHelper, true, null); + assertTrue(sessionHelper.exists(SESSION_NAME)); + var command = new TestLoginCommand(sessionHelper, null); assertThrows(FcliSimpleException.class, command::getJsonNode); @@ -72,7 +73,7 @@ void defaultLifecycleDestroysExistingSessionBeforeLogin() { @Test void optOutKeepsExistingSessionWhenLoginFails() { sessionHelper.save(SESSION_NAME, new TestSessionDescriptor("old")); - var command = new TestLoginCommand(sessionHelper, false, null); + TestLoginCommand command = new OptOutTestLoginCommand(sessionHelper, null); assertThrows(FcliSimpleException.class, command::getJsonNode); @@ -83,7 +84,7 @@ void optOutKeepsExistingSessionWhenLoginFails() { @Test void optOutCreatesNoSessionWhenLoginFailsForNewName() { - var command = new TestLoginCommand(sessionHelper, false, null); + TestLoginCommand command = new OptOutTestLoginCommand(sessionHelper, null); assertThrows(FcliSimpleException.class, command::getJsonNode); @@ -93,7 +94,7 @@ void optOutCreatesNoSessionWhenLoginFailsForNewName() { @Test void optOutReplacesExistingSessionWhenLoginSucceeds() { sessionHelper.save(SESSION_NAME, new TestSessionDescriptor("old")); - var command = new TestLoginCommand(sessionHelper, false, new TestSessionDescriptor("new")); + TestLoginCommand command = new OptOutTestLoginCommand(sessionHelper, new TestSessionDescriptor("new")); command.getJsonNode(); @@ -101,16 +102,13 @@ void optOutReplacesExistingSessionWhenLoginSucceeds() { assertEquals(0, command.logoutCount); } - private static final class TestLoginCommand extends AbstractSessionLoginCommand { + private static 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) { + private TestLoginCommand(TestSessionHelper sessionHelper, TestSessionDescriptor loginResult) { this.sessionHelper = sessionHelper; - this.replaceExistingSessionBeforeLogin = replaceExistingSessionBeforeLogin; this.loginResult = loginResult; } @@ -132,11 +130,6 @@ protected TestSessionDescriptor login(String sessionName) { return loginResult; } - @Override - protected boolean isReplaceExistingSessionBeforeLogin() { - return replaceExistingSessionBeforeLogin; - } - @Override protected AbstractSessionHelper getSessionHelper() { return sessionHelper; @@ -148,6 +141,17 @@ public IOutputHelper getOutputHelper() { } } + private static final class OptOutTestLoginCommand extends TestLoginCommand { + private OptOutTestLoginCommand(TestSessionHelper sessionHelper, TestSessionDescriptor loginResult) { + super(sessionHelper, loginResult); + } + + @Override + protected boolean shouldReplaceExistingSessionBeforeLogin() { + return false; + } + } + private static final class TestSessionHelper extends AbstractSessionHelper { @Override public String getType() { From 811b3aa20b748958ff59c412608243b79edfb94b Mon Sep 17 00:00:00 2001 From: kireetivar Date: Mon, 26 Oct 2026 14:52:18 +0530 Subject: [PATCH 15/25] fix: Clarify Aviator connectivity failures and initialize test context Keep unavailable responses neutral and mention server load for request deadlines. Cover safe cause-free statuses and preserve factory request policies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../grpc/AviatorConnectivityFailureKind.java | 5 +- .../AviatorConnectivityFailureKindTest.java | 5 +- .../AviatorGrpcClientConnectivityTest.java | 66 ++++++++++++++++++- .../cli/aviator/grpc/GrpcUtilTest.java | 36 ++++++++++ 4 files changed, 106 insertions(+), 6 deletions(-) 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 05b6776be16..aade41234a5 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 @@ -117,9 +117,10 @@ String formatMessage(AviatorConnectionContext context) { : "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 UNAVAILABLE -> server + " is unavailable or could not be reached" + + "; try again later, or check the Aviator URL, network access, and proxy settings"; case DEADLINE_EXCEEDED -> server + " did not respond within " + Constants.DEFAULT_TIMEOUT_SECONDS - + " seconds; check network access and proxy settings, or try again later"; + + " seconds; try again later because the server may be busy, or check network access and proxy settings"; }; } } 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 61d26cc4ac5..274e8b79c64 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 @@ -177,8 +177,11 @@ void messagesNameTheFailingEndpoint() { 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", + + "try again later because the server may be busy, or check network access and proxy settings", AviatorConnectivityFailureKind.DEADLINE_EXCEEDED.formatMessage(DIRECT)); + assertEquals("Aviator server aviator.example.com:443 is unavailable or could not be reached; " + + "try again later, or check the Aviator URL, network access, and proxy settings", + AviatorConnectivityFailureKind.UNAVAILABLE.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 77a0cb637fc..fbaf18df1a2 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 @@ -14,10 +14,12 @@ 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.IOException; import java.io.InputStream; @@ -35,10 +37,12 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; 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 org.junit.jupiter.params.provider.ValueSource; import com.fortify.aviator.application.ApplicationByTenantName; import com.fortify.aviator.application.ApplicationList; @@ -52,6 +56,7 @@ 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.cli.util.FcliExecutionContextHolder; import com.fortify.cli.common.http.proxy.helper.ProxyDescriptor; import com.fortify.grpc.token.ListTokensRequest; import com.fortify.grpc.token.ListTokensResponse; @@ -66,8 +71,10 @@ import io.grpc.MethodDescriptor; import io.grpc.NameResolver; import io.grpc.Server; +import io.grpc.Status; import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.AbstractBlockingStub; import io.grpc.stub.StreamObserver; /** @@ -82,15 +89,26 @@ class AviatorGrpcClientConnectivityTest { private Server grpcServer; private ManagedChannel testChannel; + @BeforeEach + void setUp() { + FcliExecutionContextHolder.pushNew(); + } + @AfterEach void tearDown() throws Exception { - if ( proxyServer!=null ) { proxyServer.close(); } - if ( testChannel!=null ) { testChannel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } - if ( grpcServer!=null ) { grpcServer.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } + try { + if ( proxyServer!=null ) { proxyServer.close(); } + if ( testChannel!=null ) { testChannel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } + if ( grpcServer!=null ) { grpcServer.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } + } finally { + FcliExecutionContextHolder.pop(); + } } @Test void directRefusalFailsFastWithDirectRefusedMessage() throws Exception { + assumeTrue(System.getenv("GRPC_PROXY_EXP")==null && System.getProperty("https.proxyHost")==null, + "This test requires a direct loopback route without a JVM or gRPC proxy"); 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()); @@ -101,6 +119,23 @@ void directRefusalFailsFastWithDirectRefusedMessage() throws Exception { exception.getMessage()); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void publicClientFactoriesKeepWaitForReady(boolean withLoggerAndPing) throws Exception { + try ( var client = withLoggerAndPing + ? AviatorGrpcClientHelper.createClient("127.0.0.1:1", null, 30) + : AviatorGrpcClientHelper.createClient("127.0.0.1:1") ) { + for ( var name : List.of("blockingStub", "tokenServiceBlockingStub", + "entitlementServiceBlockingStub", "dastEntitlementServiceBlockingStub") ) { + var field = AviatorGrpcClient.class.getDeclaredField(name); + field.setAccessible(true); + var stub = assertInstanceOf(AbstractBlockingStub.class, field.get(client)); + assertTrue(stub.getCallOptions().isWaitForReady(), name); + assertNull(stub.getCallOptions().getOption(AviatorConnectionContext.CALL_OPTION), name); + } + } + } + @Test void proxyRefusalFailsFastWithProxyRefusedMessage() throws Exception { int proxyPort = unusedPort(); @@ -169,6 +204,7 @@ void refusedFirstAddressFallsBackToHealthyAddress() throws Exception { 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() + .proxyDetector(address -> null) .nameResolverFactory(new NameResolver.Factory() { @Override public String getDefaultScheme() { return "fixture"; } @@ -202,6 +238,29 @@ private static void assertCallPolicy(AviatorRequestPolicy policy, CallOptions op assertDeadlineAtMostDefault(options); } + @Test + void serverSentUnavailableUsesNeutralMessageWithoutServerDetails() throws Exception { + grpcServer = NettyServerBuilder.forAddress(new InetSocketAddress("127.0.0.1", 0)) + .addService(new ApplicationServiceGrpc.ApplicationServiceImplBase() { + @Override + public void listApplications(ApplicationByTenantName request, StreamObserver observer) { + observer.onError(Status.UNAVAILABLE.withDescription("sentinel-maintenance-details").asRuntimeException()); + } + }).build().start(); + testChannel = NettyChannelBuilder.forAddress("127.0.0.1", grpcServer.getPort()) + .proxyDetector(address -> null).usePlaintext().build(); + var context = new AviatorConnectionContext(new AviatorEndpoint("127.0.0.1", grpcServer.getPort()), null); + try ( var client = new AviatorGrpcClient(testChannel, 30, null, 30, AviatorRequestPolicy.FAIL_FAST, context) ) { + var e = assertThrows(AviatorConnectivityException.class, + () -> client.listApplication("tenant", "signature", "message")); + + assertTrue(e.getMessage().contains("is unavailable or could not be reached")); + assertFalse(e.getMessage().contains("refused")); + assertFalse(e.getMessage().contains("sentinel-maintenance-details")); + assertNull(e.getCause()); + } + } + private static AviatorConnectivityException callAndExpectConnectivityFailure(AviatorConnectionPlan plan) { try ( var client = AviatorGrpcClientHelper.createClient(plan, null, 30, AviatorRequestPolicy.FAIL_FAST) ) { long start = System.nanoTime(); @@ -233,6 +292,7 @@ private AviatorGrpcClient createPlaintextClient(AviatorRequestPolicy policy, Ato .build() .start(); testChannel = NettyChannelBuilder.forAddress(InetAddress.getLoopbackAddress().getHostAddress(), grpcServer.getPort()) + .proxyDetector(address -> null) .usePlaintext() .intercept(new CallOptionsCapture(captured)) .build(); 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 434e2154c0a..430c5a6a7e7 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 @@ -27,6 +27,9 @@ import javax.net.ssl.SSLHandshakeException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.slf4j.LoggerFactory; import com.fortify.aviator.application.ApplicationServiceGrpc; import com.fortify.aviator.grpc.AuditorResponse; @@ -39,12 +42,45 @@ import com.fortify.cli.aviator.audit.model.UserPrompt; import com.fortify.cli.common.exception.FcliExceptionHelper; +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 io.grpc.StatusRuntimeException; import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; class GrpcUtilTest { + @ParameterizedTest + @EnumSource(value = Status.Code.class, names = {"UNAVAILABLE", "DEADLINE_EXCEEDED"}) + void causeFreeStatusesKeepSafeOutputAndLogs(Status.Code code) { + var secret = "sentinel-server-description"; + var logger = (Logger)LoggerFactory.getLogger(GrpcUtil.class); + var previousLevel = logger.getLevel(); + var appender = new ListAppender(); + appender.start(); + logger.addAppender(appender); + logger.setLevel(Level.DEBUG); + try { + var e = assertThrows(AviatorConnectivityException.class, + () -> execute(Status.fromCode(code).withDescription(secret), AviatorRequestPolicy.FAIL_FAST)); + + assertNull(e.getCause()); + assertFalse(FcliExceptionHelper.formatException(e).contains(secret)); + assertTrue(e.getMessage().contains(code==Status.Code.UNAVAILABLE + ? "is unavailable or could not be reached" : "server may be busy")); + assertFalse(e.getMessage().contains("refused")); + assertEquals(1, appender.list.size()); + assertFalse(appender.list.get(0).getFormattedMessage().contains(secret)); + assertNull(appender.list.get(0).getThrowableProxy()); + } finally { + logger.detachAppender(appender); + logger.setLevel(previousLevel); + appender.stop(); + } + } + @Test void shouldPreserveResolvedProgrammingLanguages() { File file = new File(); From 706b2352745a3d0bd4cea7e3d76d59ec16d04fa2 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Mon, 26 Oct 2026 14:52:24 +0530 Subject: [PATCH 16/25] fix: Keep sensitive Aviator URL details out of messages and logs Omit ignored paths, queries, fragments, and userinfo contents from display URLs. Remove the unused client factory overload and reuse connection-plan validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aviator/grpc/AviatorGrpcClientHelper.java | 30 +++++------ .../grpc/AviatorGrpcClientHelperTest.java | 54 ++++++++++++++++++- 2 files changed, 67 insertions(+), 17 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 c6045123392..3d8ea4ac92c 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 @@ -44,16 +44,12 @@ public class AviatorGrpcClientHelper { private static final Logger LOG = LoggerFactory.getLogger(AviatorGrpcClientHelper.class); private static final String AVIATOR_MODULE = "aviator"; + /** + * Keeps wait-for-ready for audit and streaming callers. + * Ordinary unary callers must select {@link AviatorRequestPolicy#FAIL_FAST} explicitly. + */ public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, long pingIntervalSeconds) throws AviatorSimpleException { - return createClient(url, logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY); - } - - public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, long pingIntervalSeconds, - AviatorRequestPolicy policy) throws AviatorSimpleException { - if (url == null || url.trim().isEmpty()) { - throw new AviatorSimpleException("Aviator URL cannot be null or empty."); - } - return createClient(createConnectionPlan(url), logger, pingIntervalSeconds, policy); + return createClient(createConnectionPlan(url), logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY); } /** Accepts an explicit route so callers need not resolve proxy settings again. */ @@ -104,7 +100,7 @@ public static ParsedTarget parseTarget(String url) { } 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(), safeUrl); + LOG.warn("WARN: Ignoring path in Aviator URL; using only the host and port"); } var port = uri.getPort(); @@ -143,8 +139,8 @@ public static String normalizeUrl(String url) { } /** - * 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. + * Returns only the URL scheme and authority, with user-info replaced by {@code ***}. + * Paths, queries, and fragments are omitted because they can contain credentials. */ static String toSafeUrlForDisplay(String url) { if ( url==null ) { @@ -158,9 +154,9 @@ static String toSafeUrlForDisplay(String url) { } var userInfoEnd = url.lastIndexOf('@', authorityEnd-1); if ( userInfoEnd proxyDescriptor) { @@ -204,12 +200,16 @@ static ProxiedSocketAddress toProxiedSocketAddress(SocketAddress targetAddress, return builder.build(); } + /** + * Keeps wait-for-ready for existing audit and diagnostic callers. + * Ordinary unary callers must select {@link AviatorRequestPolicy#FAIL_FAST} explicitly. + */ public static AviatorGrpcClient createClient(String url) throws AviatorSimpleException { return createClient(url, AviatorRequestPolicy.WAIT_FOR_READY); } public static AviatorGrpcClient createClient(String url, AviatorRequestPolicy policy) throws AviatorSimpleException { - return createClient(url, null, Constants.DEFAULT_PING_INTERVAL_SECONDS, policy); + return createClient(createConnectionPlan(url), null, Constants.DEFAULT_PING_INTERVAL_SECONDS, policy); } public record AviatorConnectionPlan( 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 d3f60b9a2e5..5407d8e9fdc 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 @@ -21,19 +21,68 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.List; 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 org.slf4j.LoggerFactory; 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; +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.HttpConnectProxiedSocketAddress; class AviatorGrpcClientHelperTest { + private static final List URL_MARKERS = + List.of("sentinel-userinfo", "sentinel-path", "sentinel-query", "sentinel-fragment"); + + @Test + void shouldOmitIgnoredUrlDetailsFromWarnings() { + var logger = (Logger)LoggerFactory.getLogger(AviatorGrpcClientHelper.class); + var previousLevel = logger.getLevel(); + var appender = new ListAppender(); + appender.start(); + logger.addAppender(appender); + logger.setLevel(Level.WARN); + try { + var target = AviatorGrpcClientHelper.parseTarget( + "https://user:sentinel-userinfo@aviator.invalid:8443/sentinel-path?token=sentinel-query#sentinel-fragment"); + + assertEquals("aviator.invalid", target.host()); + assertEquals(8443, target.port()); + assertEquals(1, appender.list.size()); + var event = appender.list.get(0); + assertEquals(Level.WARN, event.getLevel()); + assertTrue(event.getFormattedMessage().contains("Ignoring path")); + URL_MARKERS.forEach(marker -> assertFalse(event.getFormattedMessage().contains(marker), marker)); + assertNull(event.getThrowableProxy()); + } finally { + logger.detachAppender(appender); + logger.setLevel(previousLevel); + appender.stop(); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "https://user:sentinel-userinfo@aviator.invalid:99999/sentinel-path?token=sentinel-query#sentinel-fragment", + "https://user:sentinel-userinfo@aviator.invalid/sentinel-path/%zz?token=sentinel-query#sentinel-fragment", + "http://user:sentinel-userinfo@aviator.invalid/sentinel-path?token=sentinel-query#sentinel-fragment" + }) + void shouldOmitUrlDetailsFromInvalidTargetMessages(String url) { + var exception = assertThrows(AviatorSimpleException.class, () -> AviatorGrpcClientHelper.parseTarget(url)); + + URL_MARKERS.forEach(marker -> assertFalse(exception.getMessage().contains(marker), marker)); + assertNull(exception.getCause()); + } + @Test void shouldParseSchemeLessHostAndPort() { var parsed = AviatorGrpcClientHelper.parseTarget("aviator.invalid:443"); @@ -124,10 +173,11 @@ void shouldIgnoreUnsupportedSocketAddressTypes() { @ParameterizedTest @CsvSource({ - "https://user:sentinel-secret@aviator.invalid:443/path, https://***@aviator.invalid:443/path", + "https://user:sentinel-secret@aviator.invalid:443/path, https://***@aviator.invalid:443", "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", + "https://aviator.invalid:443/a@b, https://aviator.invalid:443", + "https://aviator.invalid?token=sentinel-query#sentinel-fragment, https://aviator.invalid", "aviator.invalid:443, aviator.invalid:443" }) void shouldRemoveUserInfoFromDisplayedUrl(String url, String expected) { From 004dafe5d3ee62ab11e705141e0ec366ac4231a5 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Mon, 26 Oct 2026 14:52:29 +0530 Subject: [PATCH 17/25] chore: Make Aviator diagnostic request policy explicit Pass WAIT_FOR_READY explicitly for diagnostic credential validation and remove unused validation overloads. Preserve ordinary fail-fast calls and existing validator interfaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../config/admin/helper/AviatorAdminConfigHelper.java | 4 ---- .../session/user/helper/AviatorUserSessionHelper.java | 8 -------- .../helper/AviatorConnectionDiagnoseHelper.java | 6 ++++-- 3 files changed, 4 insertions(+), 14 deletions(-) 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 ada7e58dff1..c79f5773294 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 @@ -38,10 +38,6 @@ protected Class getSessionDescriptorType() { return AviatorAdminConfigDescriptor.class; } - public void validateConfig(AviatorAdminConfigDescriptor configDescriptor) { - validateConfig(configDescriptor, AviatorRequestPolicy.WAIT_FOR_READY); - } - public void validateConfig(AviatorAdminConfigDescriptor configDescriptor, AviatorRequestPolicy policy) { try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl(), policy)) { String[] messageAndSignature = AviatorSignatureUtils.createMessageAndSignature(configDescriptor, configDescriptor.getTenant()); 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 5031a227993..0ef65f56498 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 @@ -38,14 +38,6 @@ protected Class getSessionDescriptorType() { return AviatorUserSessionDescriptor.class; } - public AviatorUserTokenValidationResult validateToken(AviatorUserSessionDescriptor sessionDescriptor) { - return validateToken(sessionDescriptor.getAviatorUrl(), sessionDescriptor.getAviatorToken()); - } - - public AviatorUserTokenValidationResult validateToken(String aviatorUrl, String token) { - return validateToken(aviatorUrl, token, AviatorRequestPolicy.WAIT_FOR_READY); - } - public AviatorUserTokenValidationResult validateToken(String aviatorUrl, String token, AviatorRequestPolicy policy) { var tenantName = AviatorJwtUtils.extractTenantNameFromToken(token); try (var client = AviatorGrpcClientHelper.createClient(aviatorUrl, policy)) { diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java index 0f66987d9d5..f309a869d5f 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/connection/helper/AviatorConnectionDiagnoseHelper.java @@ -24,6 +24,7 @@ import com.fortify.cli.aviator.diagnose.AviatorDiagnosticEvidence; import com.fortify.cli.aviator.diagnose.AviatorDiagnosticReport; import com.fortify.cli.aviator.diagnose.AviatorDiagnosticStageResult; +import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.common.exception.FcliBugException; import lombok.RequiredArgsConstructor; @@ -48,8 +49,9 @@ public class AviatorConnectionDiagnoseHelper { public AviatorConnectionDiagnoseHelper() { this(new AviatorConnectionDiagnostics(), - (url, token) -> AviatorUserSessionHelper.instance().validateToken(url, token), - descriptor -> AviatorAdminConfigHelper.instance().validateConfig(descriptor)); + // Preserve diagnostic status mapping instead of ordinary fail-fast messages. + (url, token) -> AviatorUserSessionHelper.instance().validateToken(url, token, AviatorRequestPolicy.WAIT_FOR_READY), + descriptor -> AviatorAdminConfigHelper.instance().validateConfig(descriptor, AviatorRequestPolicy.WAIT_FOR_READY)); } public DiagnoseRunResult diagnose(AviatorConnectionDiagnoseSource source, int timeoutSeconds) { From 6fe6862b91f807c22f4a0ccbdb6ef6710254a5ca Mon Sep 17 00:00:00 2001 From: kireetivar Date: Mon, 26 Oct 2026 14:52:35 +0530 Subject: [PATCH 18/25] chore: Document Aviator connectivity and session contracts Explain fail-fast call options, unchanged deadlines, and the pre-save session preservation boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc-resources/repo-usage.md | 11 +++++++++++ .../java/com/fortify/cli/aviator/grpc/GrpcUtil.java | 5 +++++ .../session/cli/cmd/AbstractSessionLoginCommand.java | 5 +++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc-resources/repo-usage.md b/doc-resources/repo-usage.md index 9117e3cd8ef..1de942e5389 100644 --- a/doc-resources/repo-usage.md +++ b/doc-resources/repo-usage.md @@ -11,3 +11,14 @@ Published images are available at [Docker Hub: fortifydocker/fcli](https://hub.d For more information about available Docker images and usage instructions, see the [fcli-docker repository](https://github.com/fortify/fcli-docker). +## Aviator Connectivity + +Ordinary Aviator admin commands and credential validation calls do not wait for a failed connection to recover. +A confirmed connection failure produces an actionable message. +An unavailable response can indicate a connection problem or a temporary server problem. +A deadline can also indicate a busy server. +The request deadline remains 30 seconds. + +Audit, streaming, correlation, and connection diagnostics keep their existing wait behavior. +Failed user-login or admin-config validation preserves the existing session or configuration. +This protection applies to validation before saving, not failures after saving. 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 6e561935143..c3992cd289f 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 @@ -54,6 +54,11 @@ interface GrpcCall { R call(S stub, T request) throws StatusRuntimeException; } + /** + * Applies the 30-second deadline to each unary call. + * Fail-fast stubs carry {@link AviatorConnectionContext#CALL_OPTION} for safe connectivity classification. + * Stubs without that option retain the existing status mapping. + */ static , T, R> R executeGrpcCall(S stub, GrpcCall call, T request, String operation) { try { return call.call(stub.withDeadlineAfter(Constants.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS), request); 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 21aca304612..cc7a6ba0d0f 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,8 +67,9 @@ private void logoutIfSessionExists(String sessionName) { protected void testAuthenticatedConnection(String sessionName) {} /** - * Return false when login only validates credentials and the old session needs no remote logout. - * This keeps the existing session if validation fails; save replaces it after success. + * Return false only when no remote logout is needed and {@link #login(String)} preserves the existing session until success. + * The session helper saves the new session after {@code login} returns. + * {@link #testAuthenticatedConnection(String)} runs after saving; failures there do not restore the previous session. */ protected boolean shouldReplaceExistingSessionBeforeLogin() { return true; From 7864e143acedfc9c5c26a71c67a554800658801d Mon Sep 17 00:00:00 2001 From: Kireeti Varma <81861747+kireetivar@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:01:08 +0530 Subject: [PATCH 19/25] fix: Avoid assuming local deadline expiry for Aviator timeouts Use neutral deadline guidance and cover an immediate server-returned DEADLINE_EXCEEDED response with a real Netty regression test. Keep the existing 30-second request deadline unchanged. --- .../grpc/AviatorConnectivityFailureKind.java | 6 ++--- .../AviatorConnectivityFailureKindTest.java | 4 +-- .../AviatorGrpcClientConnectivityTest.java | 27 +++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) 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 aade41234a5..0bb534fe118 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 @@ -23,8 +23,6 @@ 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; @@ -119,8 +117,8 @@ String formatMessage(AviatorConnectionContext context) { + "; check that the Aviator URL uses the HTTPS port of the Aviator server"; case UNAVAILABLE -> server + " is unavailable or could not be reached" + "; try again later, or check the Aviator URL, network access, and proxy settings"; - case DEADLINE_EXCEEDED -> server + " did not respond within " + Constants.DEFAULT_TIMEOUT_SECONDS - + " seconds; try again later because the server may be busy, or check network access and proxy settings"; + case DEADLINE_EXCEEDED -> "The Aviator request timed out; try again later" + + " (the server may be busy, or network/proxy delays may be involved)"; }; } } 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 274e8b79c64..d7cef4fd268 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 @@ -176,8 +176,8 @@ void messagesNameTheFailingEndpoint() { 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; " - + "try again later because the server may be busy, or check network access and proxy settings", + assertEquals("The Aviator request timed out; try again later" + + " (the server may be busy, or network/proxy delays may be involved)", AviatorConnectivityFailureKind.DEADLINE_EXCEEDED.formatMessage(DIRECT)); assertEquals("Aviator server aviator.example.com:443 is unavailable or could not be reached; " + "try again later, or check the Aviator URL, network access, and proxy settings", 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 fbaf18df1a2..35be010dc76 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 @@ -261,6 +261,33 @@ public void listApplications(ApplicationByTenantName request, StreamObserver observer) { + observer.onError(Status.DEADLINE_EXCEEDED.withDescription("sentinel-server-deadline-details").asRuntimeException()); + } + }).build().start(); + testChannel = NettyChannelBuilder.forAddress("127.0.0.1", grpcServer.getPort()) + .proxyDetector(address -> null).usePlaintext().build(); + var context = new AviatorConnectionContext(new AviatorEndpoint("127.0.0.1", grpcServer.getPort()), null); + try ( var client = new AviatorGrpcClient(testChannel, 30, null, 30, AviatorRequestPolicy.FAIL_FAST, context) ) { + var e = assertThrows(AviatorConnectivityException.class, + () -> client.listApplication("tenant", "signature", "message")); + + assertTrue(e.getMessage().contains("request timed out")); + assertFalse(e.getMessage().contains("30 seconds")); + assertFalse(e.getMessage().contains("did not respond")); + assertFalse(e.getMessage().contains("Timed out connecting")); + assertFalse(e.getMessage().contains("refused")); + assertFalse(e.getMessage().contains("sentinel-server-deadline-details")); + assertNull(e.getCause()); + } + } + private static AviatorConnectivityException callAndExpectConnectivityFailure(AviatorConnectionPlan plan) { try ( var client = AviatorGrpcClientHelper.createClient(plan, null, 30, AviatorRequestPolicy.FAIL_FAST) ) { long start = System.nanoTime(); From 1dbb5b897cf55be558a5230c3b6c42bb136862df Mon Sep 17 00:00:00 2001 From: Kireeti Varma <81861747+kireetivar@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:45:43 +0530 Subject: [PATCH 20/25] chore: Remove unrelated Aviator request call wrapping --- .../cli/aviator/grpc/AviatorGrpcClient.java | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) 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 6e99f9b1f67..0d4f2770f65 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 @@ -205,32 +205,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 GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::createApplication, request, Constants.OP_CREATE_APP); } public Application updateApplication(String projectId, String newName, String signature, String message, String tenantName) { UpdateApplicationRequest request = UpdateApplicationRequest.newBuilder().setId(Long.parseLong(projectId)).setName(newName).setTenantName(tenantName).setSignature(signature).setMessage(message).build(); - return GrpcUtil.executeGrpcCall(blockingStub, - ApplicationServiceGrpc.ApplicationServiceBlockingStub::updateApplication, request, Constants.OP_UPDATE_APP); + return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::updateApplication, request, Constants.OP_UPDATE_APP); } public Application addEntitlement(String projectId, String signature, String message, String tenantName) { ApplicationById request = ApplicationById.newBuilder().setId(Long.parseLong(projectId)).setSignature(signature).setMessage(message).setTenantName(tenantName).build(); - return GrpcUtil.executeGrpcCall(blockingStub, - ApplicationServiceGrpc.ApplicationServiceBlockingStub::addEntitlement, request, Constants.OP_ADD_APP_ENTITLEMENT); + return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::addEntitlement, request, Constants.OP_ADD_APP_ENTITLEMENT); } public ApplicationResponseMessage deleteApplication(String projectId, String signature, String message, String tenantName) { ApplicationById request = ApplicationById.newBuilder().setId(Long.parseLong(projectId)).setSignature(signature).setMessage(message).setTenantName(tenantName).build(); - return GrpcUtil.executeGrpcCall(blockingStub, - ApplicationServiceGrpc.ApplicationServiceBlockingStub::deleteApplication, request, Constants.OP_DELETE_APP); + return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::deleteApplication, request, Constants.OP_DELETE_APP); } public Application getApplication(String projectId, String signature, String message, String tenantName) { ApplicationById request = ApplicationById.newBuilder().setId(Long.parseLong(projectId)).setSignature(signature).setMessage(message).setTenantName(tenantName).build(); - return GrpcUtil.executeGrpcCall(blockingStub, - ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplication, request, Constants.OP_GET_APP); + return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplication, request, Constants.OP_GET_APP); } public Application getApplicationByToken(String token, String appName) { @@ -238,8 +233,7 @@ public Application getApplicationByToken(String token, String appName) { .setToken(token) .setAppName(appName) .build(); - return GrpcUtil.executeGrpcCall(blockingStub, - ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplicationByToken, request, Constants.OP_GET_APP_BY_TOKEN); + return GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::getApplicationByToken, request, Constants.OP_GET_APP_BY_TOKEN); } public long getDefaultQuota(String token) { @@ -272,21 +266,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 = GrpcUtil.executeGrpcCall(blockingStub, ApplicationServiceGrpc.ApplicationServiceBlockingStub::listApplications, request, Constants.OP_LIST_APPS); return applicationList.getApplicationsList(); } public TokenGenerationResponse generateToken(String email, String tokenName, String signature, String message, String tenantName, String endDate) { TokenGenerationRequest request = TokenGenerationRequest.newBuilder().setEmail(email != null ? email : "").setCustomTokenName(tokenName != null ? tokenName : "").setRequestSignature(signature).setEndDate(com.fortify.cli.aviator.util.StringUtil.isEmpty(endDate) ? "" : endDate).setMessage(message).setTenantName(tenantName).build(); - return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, - TokenServiceGrpc.TokenServiceBlockingStub::generateToken, request, Constants.OP_GENERATE_TOKEN); + return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::generateToken, request, Constants.OP_GENERATE_TOKEN); } public ListTokensResponse listTokens(String email, String tenantName, String signature, String message) { ListTokensRequest request = ListTokensRequest.newBuilder().setRequestSignature(signature).setMessage(message).setTenantName(tenantName).setIgnorePagination(true).build(); - return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, - TokenServiceGrpc.TokenServiceBlockingStub::listTokens, request, Constants.OP_LIST_TOKENS); + return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::listTokens, request, Constants.OP_LIST_TOKENS); } public ListTokensResponse listTokensByDeveloper(String tenantName, String developerEmail, String signature, String message) { @@ -295,46 +286,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 GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::listTokensByDeveloper, request, Constants.OP_LIST_TOKENS_BY_DEVELOPER); } public RevokeTokenResponse revokeToken(String token, String email, String tenantName, String signature, String message) { RevokeTokenRequest request = RevokeTokenRequest.newBuilder().setToken(token).setTenantName(tenantName).setRequestSignature(signature).setMessage(message).build(); - return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, - TokenServiceGrpc.TokenServiceBlockingStub::revokeToken, request, Constants.OP_REVOKE_TOKEN); + return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::revokeToken, request, Constants.OP_REVOKE_TOKEN); } public DeleteTokenResponse deleteToken(String token, String email, String tenantName, String signature, String message) { DeleteTokenRequest request = DeleteTokenRequest.newBuilder().setToken(token).setTenantName(tenantName).setRequestSignature(signature).setMessage(message).build(); - return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, - TokenServiceGrpc.TokenServiceBlockingStub::deleteToken, request, Constants.OP_DELETE_TOKEN); + return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::deleteToken, request, Constants.OP_DELETE_TOKEN); } public TokenValidationResponse validateToken(String token, String tenantName, String signature, String message) { TokenValidationRequest request = TokenValidationRequest.newBuilder().setToken(token).setTenantName(tenantName).setRequestSignature(signature).setMessage(message).build(); - return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, - TokenServiceGrpc.TokenServiceBlockingStub::validateToken, request, Constants.OP_VALIDATE_TOKEN); + return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::validateToken, request, Constants.OP_VALIDATE_TOKEN); } public TokenValidationResponse validateUserToken(String token, String tenantName) { ValidateUserTokenRequest request = ValidateUserTokenRequest.newBuilder().setToken(token).setTenantName(tenantName != null ? tenantName : "").build(); - return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, - TokenServiceGrpc.TokenServiceBlockingStub::validateUserToken, request, Constants.OP_VALIDATE_USER_TOKEN); + return GrpcUtil.executeGrpcCall(tokenServiceBlockingStub, TokenServiceGrpc.TokenServiceBlockingStub::validateUserToken, request, Constants.OP_VALIDATE_USER_TOKEN); } public List listEntitlements(String tenantName, String signature, String message) { ListEntitlementsByTenantRequest request = ListEntitlementsByTenantRequest.newBuilder().setTenantName(tenantName).setSignature(signature).setMessage(message).build(); - ListEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(entitlementServiceBlockingStub, - EntitlementServiceGrpc.EntitlementServiceBlockingStub::listEntitlementsByTenant, request, Constants.OP_LIST_ENTITLEMENTS); + ListEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(entitlementServiceBlockingStub, EntitlementServiceGrpc.EntitlementServiceBlockingStub::listEntitlementsByTenant, request, Constants.OP_LIST_ENTITLEMENTS); return response.getEntitlementsList(); } public List listDastEntitlements(String tenantName, String signature, String message) { ListDastEntitlementsByTenantRequest request = ListDastEntitlementsByTenantRequest.newBuilder().setTenantName(tenantName).setSignature(signature).setMessage(message).build(); - ListDastEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(dastEntitlementServiceBlockingStub, - DastEntitlementServiceGrpc.DastEntitlementServiceBlockingStub::listDastEntitlementsByTenant, - request, Constants.OP_LIST_DAST_ENTITLEMENTS); + ListDastEntitlementsByTenantResponse response = GrpcUtil.executeGrpcCall(dastEntitlementServiceBlockingStub, DastEntitlementServiceGrpc.DastEntitlementServiceBlockingStub::listDastEntitlementsByTenant, request, Constants.OP_LIST_DAST_ENTITLEMENTS); return response.getEntitlementsList(); } From 4febc648b135da19082c6a4965ce6ac29098acd1 Mon Sep 17 00:00:00 2001 From: Kireeti Varma <81861747+kireetivar@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:46:09 +0530 Subject: [PATCH 21/25] docs: Remove misplaced Aviator repository usage section --- doc-resources/repo-usage.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/doc-resources/repo-usage.md b/doc-resources/repo-usage.md index 1de942e5389..ffe8c0a0049 100644 --- a/doc-resources/repo-usage.md +++ b/doc-resources/repo-usage.md @@ -10,15 +10,3 @@ Docker images for fcli are built and published in the separate [fortify/fcli-doc Published images are available at [Docker Hub: fortifydocker/fcli](https://hub.docker.com/r/fortifydocker/fcli). For more information about available Docker images and usage instructions, see the [fcli-docker repository](https://github.com/fortify/fcli-docker). - -## Aviator Connectivity - -Ordinary Aviator admin commands and credential validation calls do not wait for a failed connection to recover. -A confirmed connection failure produces an actionable message. -An unavailable response can indicate a connection problem or a temporary server problem. -A deadline can also indicate a busy server. -The request deadline remains 30 seconds. - -Audit, streaming, correlation, and connection diagnostics keep their existing wait behavior. -Failed user-login or admin-config validation preserves the existing session or configuration. -This protection applies to validation before saving, not failures after saving. From 2c141129f5623fa3115ed5afe4b91640e2852008 Mon Sep 17 00:00:00 2001 From: Kireeti Varma <81861747+kireetivar@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:46:39 +0530 Subject: [PATCH 22/25] refactor: Require an explicit Aviator unary request policy Remove policy-free factories and unused constructors. Audit and diagnostic callers explicitly select WAIT_FOR_READY; ordinary commands retain FAIL_FAST. Streaming configuration is unchanged. Test both policies through both public factory signatures. --- .../cli/aviator/audit/IssueAuditor.java | 3 +- .../AviatorDefaultDiagnosticProbe.java | 3 +- .../cli/aviator/grpc/AviatorGrpcClient.java | 16 ----------- .../aviator/grpc/AviatorGrpcClientHelper.java | 21 +++++++------- .../AviatorGrpcClientConnectivityTest.java | 28 ++++++++++--------- ...tractAviatorAdminSessionOutputCommand.java | 2 +- .../AviatorSSCCorrelateSastDastCommand.java | 3 +- .../cli/cmd/AviatorSSCDastAuditCommand.java | 3 +- .../ssc/helper/AviatorSSCAuditHelper.java | 5 ++-- 9 files changed, 37 insertions(+), 47 deletions(-) diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java index 79e400cd0f1..04b9913b22a 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java @@ -56,6 +56,7 @@ import com.fortify.cli.aviator.fpr.processor.AuditProcessor; import com.fortify.cli.aviator.grpc.AviatorGrpcClient; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; +import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.aviator.util.ResourceUtil; import com.fortify.cli.aviator.util.StringUtil; @@ -169,7 +170,7 @@ public AuditOutcome performAudit(Map auditResponses) { if (promptsToAudit.isEmpty()) { options.getLogger().progress("Audit skipped - no issues to process after filtering."); } else { - try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(options.getUrl(), options.getLogger(), DEFAULT_PING_INTERVAL_SECONDS)) { + try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(options.getUrl(), options.getLogger(), DEFAULT_PING_INTERVAL_SECONDS, AviatorRequestPolicy.WAIT_FOR_READY)) { CompletableFuture> future = client.processBatchRequests(promptsToAudit, projectName, fprInfo.getBuildId(), SSCApplicationName, SSCApplicationVersion, options.getToken(), options.getFprHandle(), diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDefaultDiagnosticProbe.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDefaultDiagnosticProbe.java index 96ee570dfb7..cf75d1ae425 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDefaultDiagnosticProbe.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/diagnose/AviatorDefaultDiagnosticProbe.java @@ -36,6 +36,7 @@ import com.fortify.cli.aviator._common.exception.AviatorBugException; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan; +import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.common.http.ssl.trust.FcliTrustManager; import io.grpc.Status; @@ -140,7 +141,7 @@ private static AviatorTunnelResult.TlsSucceeded toTlsSucceeded(SSLSocket sslSock @Override public AviatorGrpcReachabilityResult probeGrpc(String url, int timeoutSeconds) throws Exception { - try (var client = AviatorGrpcClientHelper.createClient(url)) { + try (var client = AviatorGrpcClientHelper.createClient(url, AviatorRequestPolicy.WAIT_FOR_READY)) { client.probeGetDefaultQuota(timeoutSeconds); return AviatorGrpcReachabilityResult.responseReceived("OK", "Default quota response received"); } catch (StatusRuntimeException e) { 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 0d4f2770f65..b5ab582fe80 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 @@ -72,10 +72,7 @@ import com.fortify.grpc.token.TokenValidationResponse; import com.fortify.grpc.token.ValidateUserTokenRequest; -import io.grpc.CompressorRegistry; -import io.grpc.DecompressorRegistry; import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; public class AviatorGrpcClient implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(AviatorGrpcClient.class); @@ -95,10 +92,6 @@ public class AviatorGrpcClient implements AutoCloseable { private final java.util.concurrent.ScheduledExecutorService pingScheduler; final AtomicBoolean isShutdown = new AtomicBoolean(false); - public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) { - this(channel, defaultTimeoutSeconds, logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY, null); - } - /** Fail-fast clients require a connection context for error messages. */ AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds, AviatorRequestPolicy requestPolicy, AviatorConnectionContext connectionContext) { @@ -139,15 +132,6 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv }); } - public AviatorGrpcClient(String host, int port, long defaultTimeoutSeconds, IAviatorLogger logger, long pingIntervalSeconds) { - this(ManagedChannelBuilder.forAddress(host, port).useTransportSecurity().maxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).keepAliveTime(30, TimeUnit.SECONDS).keepAliveTimeout(10, TimeUnit.SECONDS).keepAliveWithoutCalls(true).enableRetry().compressorRegistry(CompressorRegistry.getDefaultInstance()).decompressorRegistry(DecompressorRegistry.getDefaultInstance()).build(), defaultTimeoutSeconds, logger, pingIntervalSeconds); - LOG.debug("Initialized AviatorGrpcClient - Host: {}, Port: {}", host, port); - } - - public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAviatorLogger logger) { - this(channel, defaultTimeoutSeconds, logger, 30); - } - public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle, List customPriorityOrder, ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) { 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 3d8ea4ac92c..bace76628ea 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 @@ -15,6 +15,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -45,16 +46,19 @@ public class AviatorGrpcClientHelper { private static final String AVIATOR_MODULE = "aviator"; /** - * Keeps wait-for-ready for audit and streaming callers. - * Ordinary unary callers must select {@link AviatorRequestPolicy#FAIL_FAST} explicitly. + * Creates a client with an explicit unary request policy. + * Streaming calls retain their own recovery behavior regardless of this policy. */ - public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, long pingIntervalSeconds) throws AviatorSimpleException { - return createClient(createConnectionPlan(url), logger, pingIntervalSeconds, AviatorRequestPolicy.WAIT_FOR_READY); + public static AviatorGrpcClient createClient(String url, IAviatorLogger logger, long pingIntervalSeconds, + AviatorRequestPolicy policy) throws AviatorSimpleException { + Objects.requireNonNull(policy, "policy"); + return createClient(createConnectionPlan(url), logger, pingIntervalSeconds, policy); } /** Accepts an explicit route so callers need not resolve proxy settings again. */ static AviatorGrpcClient createClient(AviatorConnectionPlan connectionPlan, IAviatorLogger logger, long pingIntervalSeconds, AviatorRequestPolicy policy) { + Objects.requireNonNull(policy, "policy"); var proxyDescriptor = connectionPlan.proxyDescriptor(); var target = connectionPlan.target(); var builder = target.port() == null @@ -201,15 +205,10 @@ static ProxiedSocketAddress toProxiedSocketAddress(SocketAddress targetAddress, } /** - * Keeps wait-for-ready for existing audit and diagnostic callers. - * Ordinary unary callers must select {@link AviatorRequestPolicy#FAIL_FAST} explicitly. + * Creates a client with the default ping interval and an explicit unary request policy. */ - public static AviatorGrpcClient createClient(String url) throws AviatorSimpleException { - return createClient(url, AviatorRequestPolicy.WAIT_FOR_READY); - } - public static AviatorGrpcClient createClient(String url, AviatorRequestPolicy policy) throws AviatorSimpleException { - return createClient(createConnectionPlan(url), null, Constants.DEFAULT_PING_INTERVAL_SECONDS, policy); + return createClient(url, null, Constants.DEFAULT_PING_INTERVAL_SECONDS, policy); } public record AviatorConnectionPlan( 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 35be010dc76..47b5599425b 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 @@ -42,7 +42,6 @@ import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.ValueSource; import com.fortify.aviator.application.ApplicationByTenantName; import com.fortify.aviator.application.ApplicationList; @@ -120,18 +119,21 @@ void directRefusalFailsFastWithDirectRefusedMessage() throws Exception { } @ParameterizedTest - @ValueSource(booleans = {false, true}) - void publicClientFactoriesKeepWaitForReady(boolean withLoggerAndPing) throws Exception { - try ( var client = withLoggerAndPing - ? AviatorGrpcClientHelper.createClient("127.0.0.1:1", null, 30) - : AviatorGrpcClientHelper.createClient("127.0.0.1:1") ) { - for ( var name : List.of("blockingStub", "tokenServiceBlockingStub", - "entitlementServiceBlockingStub", "dastEntitlementServiceBlockingStub") ) { - var field = AviatorGrpcClient.class.getDeclaredField(name); - field.setAccessible(true); - var stub = assertInstanceOf(AbstractBlockingStub.class, field.get(client)); - assertTrue(stub.getCallOptions().isWaitForReady(), name); - assertNull(stub.getCallOptions().getOption(AviatorConnectionContext.CALL_OPTION), name); + @EnumSource(AviatorRequestPolicy.class) + void publicClientFactoriesUseExplicitPolicy(AviatorRequestPolicy policy) throws Exception { + for ( boolean withLoggerAndPing : new boolean[] {false, true} ) { + try ( var client = withLoggerAndPing + ? AviatorGrpcClientHelper.createClient("127.0.0.1:1", null, 30, policy) + : AviatorGrpcClientHelper.createClient("127.0.0.1:1", policy) ) { + for ( var name : List.of("blockingStub", "tokenServiceBlockingStub", + "entitlementServiceBlockingStub", "dastEntitlementServiceBlockingStub") ) { + var field = AviatorGrpcClient.class.getDeclaredField(name); + field.setAccessible(true); + var stub = assertInstanceOf(AbstractBlockingStub.class, field.get(client)); + assertEquals(policy.isWaitForReady(), stub.getCallOptions().isWaitForReady(), name); + assertEquals(policy == AviatorRequestPolicy.FAIL_FAST, + stub.getCallOptions().getOption(AviatorConnectionContext.CALL_OPTION) != null, name); + } } } } 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 3742f89aea5..d198b37574e 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,7 +37,7 @@ public final JsonNode getJsonNode() { public abstract boolean isSingular(); - /** The caller must close the client. */ + /** Creates a fail-fast client for ordinary admin commands. The caller must close the client. */ protected final AviatorGrpcClient createGrpcClient(AviatorAdminConfigDescriptor configDescriptor) { return AviatorGrpcClientHelper.createClient(configDescriptor.getAviatorUrl(), AviatorRequestPolicy.FAIL_FAST); } diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java index acf07548bcd..d0ee2910495 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java @@ -37,6 +37,7 @@ import com.fortify.cli.aviator.fpr.Vulnerability; import com.fortify.cli.aviator.grpc.AviatorGrpcClient; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; +import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.aviator.grpc.CorrelationResult; import com.fortify.cli.aviator.grpc.CorrelationStreamConfig; import com.fortify.cli.aviator.grpc.CorrelationStreamProcessor; @@ -278,7 +279,7 @@ private CorrelationResult correlateViaGrpc(List mixedBuckets, av.getApplicationName(), av.getVersionName(), sastResult.buildId); CorrelationResult result; - try (var grpcClient = AviatorGrpcClientHelper.createClient(sessionDescriptor.getAviatorUrl(), logger, 30)) { + try (var grpcClient = AviatorGrpcClientHelper.createClient(sessionDescriptor.getAviatorUrl(), logger, 30, AviatorRequestPolicy.WAIT_FOR_READY)) { result = performCorrelation(grpcClient, config, bucketData, sastResult.scanGuid, alreadyTriedKeys); } diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java index 7f01f08488f..ca3bc5f7be8 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java @@ -34,6 +34,7 @@ import com.fortify.cli.aviator.config.IAviatorLogger; import com.fortify.cli.aviator.config.TagMappingConfig; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; +import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.aviator.grpc.DastAuditStreamConfig; import com.fortify.cli.aviator.grpc.DastAuditStreamProcessor; import com.fortify.cli.aviator.ssc.helper.AviatorSSCAttributeHelper; @@ -133,7 +134,7 @@ private DastAuditFprResult auditFpr( .sscApplicationName(appVersion.getApplicationName()) .sscApplicationVersion(appVersion.getVersionName()) .build(); - try (var grpcClient = AviatorGrpcClientHelper.createClient(session.getAviatorUrl(), logger, 30); + try (var grpcClient = AviatorGrpcClientHelper.createClient(session.getAviatorUrl(), logger, 30, AviatorRequestPolicy.WAIT_FOR_READY); var streamProcessor = new DastAuditStreamProcessor( logger, grpcClient.getDastAuditAsyncStub(), grpcClient.getPingScheduler(), grpcClient.getPingIntervalSeconds()); diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java index f52033ea53b..e4be2b669b7 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java @@ -33,6 +33,7 @@ import com.fortify.cli.aviator.config.AviatorLoggerImpl; import com.fortify.cli.aviator.grpc.AviatorGrpcClient; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; +import com.fortify.cli.aviator.grpc.AviatorRequestPolicy; import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.common.exception.FcliSimpleException; import com.fortify.cli.common.json.JsonHelper; @@ -717,7 +718,7 @@ private static String getFolderFilter(boolean noFilterSet, SSCIssueFilterSetDesc public static long getAvailableQuota(String aviatorUrl, String aviatorToken, String appName, AviatorLoggerImpl logger) { try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(aviatorUrl, logger, - Constants.DEFAULT_PING_INTERVAL_SECONDS)) { + Constants.DEFAULT_PING_INTERVAL_SECONDS, AviatorRequestPolicy.WAIT_FOR_READY)) { Application app = client.getApplicationByToken(aviatorToken, appName); long quota = app.getQuota(); logger.progress("Status: Available Aviator quota for app '%s': %s", appName, @@ -745,7 +746,7 @@ public static long getAvailableQuota(String aviatorUrl, String aviatorToken, Str public static long getDefaultQuota(String aviatorUrl, String aviatorToken, AviatorLoggerImpl logger) { try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(aviatorUrl, logger, - Constants.DEFAULT_PING_INTERVAL_SECONDS)) { + Constants.DEFAULT_PING_INTERVAL_SECONDS, AviatorRequestPolicy.WAIT_FOR_READY)) { long quota = client.getDefaultQuota(aviatorToken); logger.progress("Status: Default Aviator quota for new applications: %s", quota < 0 ? "unlimited" : String.valueOf(quota)); From 27db5676f4df4dd1fc93671bcc09fea31ba237f7 Mon Sep 17 00:00:00 2001 From: Kireeti Varma <81861747+kireetivar@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:24:49 +0530 Subject: [PATCH 23/25] fix: Bound Aviator stream initialization to 30 seconds Cancel pending RPCs and release SAST initialization waiters on timeout. Disarm the initialization timer when the stream initializes, preserving established-stream processing and existing recovery behavior. Leave total operation timeouts and SSC upload handling unchanged. Add regression coverage for silent initialization, cancellation, late responses, and successful initialization. Both Aviator test suites and formatting checks pass (686 passed, 2 skipped). --- .../aviator/grpc/AviatorStreamProcessor.java | 38 ++- .../grpc/CorrelationStreamProcessor.java | 15 ++ .../grpc/DastAuditStreamProcessor.java | 11 +- .../grpc/StreamInitializationTimeout.java | 52 ++++ .../grpc/StreamInitializationTimeoutTest.java | 227 ++++++++++++++++++ 5 files changed, 337 insertions(+), 6 deletions(-) create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java index 959c68a53f2..7fe77d1686b 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java @@ -87,6 +87,7 @@ class AviatorStreamProcessor implements AutoCloseable { private RequestHandler requestHandler; private ScheduledFuture pingTask; + private StreamInitializationTimeout initializationTimeout; private final AtomicBoolean isPinging = new AtomicBoolean(false); private final ConcurrentLinkedDeque processingQueue = new ConcurrentLinkedDeque<>(); @@ -191,8 +192,23 @@ private void startStreamWithRetry(Map responses, AtomicIn this.streamLatch = new CountDownLatch(1); CountDownLatch initLatch = new CountDownLatch(1); + var attemptLatch = this.streamLatch; + var attemptHandler = requestHandler; + var initializationTimeout = new StreamInitializationTimeout(pingScheduler, () -> { + var error = new AviatorSimpleException( + "Audit initialization timed out. Check the Aviator URL, proxy settings, and server availability."); + initLatch.countDown(); + attemptLatch.countDown(); + attemptHandler.sendError(error); + resultFuture.completeExceptionally(error); + }); + this.initializationTimeout = initializationTimeout; + resultFuture.whenComplete((result, error) -> { + initializationTimeout.close(); + initLatch.countDown(); + }); ClientResponseObserver responseObserver = createResponseObserver( - responses, processedRequests, resultFuture, this.streamLatch, initLatch); + responses, processedRequests, resultFuture, attemptLatch, initLatch, initializationTimeout); try { asyncStub.processStream(responseObserver); @@ -208,9 +224,8 @@ private void startStreamWithRetry(Map responses, AtomicIn processingExecutor.submit(() -> { try { // Outer try to propagate to future - if (!initLatch.await(30, TimeUnit.SECONDS)) { - throw new AviatorTechnicalException("Stream initialization timed out"); - } + initLatch.await(); + if (resultFuture.isDone() || !currentStreamState.isStreamInitialized) return; if (currentStreamState.streamRetryCount > 1) { reQueueUnprocessedRequests(); @@ -302,7 +317,7 @@ private void startStreamWithRetry(Map responses, AtomicIn private ClientResponseObserver createResponseObserver( Map responses, AtomicInteger processedRequests, CompletableFuture> resultFuture, - CountDownLatch streamLatch, CountDownLatch initLatch) { + CountDownLatch streamLatch, CountDownLatch initLatch, StreamInitializationTimeout initializationTimeout) { return new ClientResponseObserver() { private final AtomicBoolean isInitialized = new AtomicBoolean(false); @@ -314,6 +329,7 @@ public void beforeStart(ClientCallStreamObserver requestStrea @Override public void onNext(AuditorResponse response) { + if (resultFuture.isDone()) return; logger.info("Received response - Status: " + response.getStatus() + ", RequestId: " + response.getRequestId()); if ("PONG".equals(response.getStatus())) { @@ -361,6 +377,7 @@ public void onNext(AuditorResponse response) { if (completedWrapper == null) { if (!isInitialized.get()) { if ("SUCCESS".equals(response.getStatus())) { + if (!initializationTimeout.initialized()) return; if (response.hasQuota()) { currentStreamState.quota = response.getQuota(); if (response.hasQuotaLastUpdated()) { @@ -428,7 +445,14 @@ public void onNext(AuditorResponse response) { @Override public void onError(Throwable t) { + if (initializationTimeout.isExpired()) return; + initializationTimeout.close(); + initLatch.countDown(); stopPingPong(); + if (resultFuture.isDone()) { + streamLatch.countDown(); + return; + } if (isRetryableError(t)) { boolean infinite = isInfiniteRetryError(t); @@ -512,6 +536,9 @@ public void onError(Throwable t) { @Override public void onCompleted() { + if (initializationTimeout.isExpired()) return; + initializationTimeout.close(); + initLatch.countDown(); stopPingPong(); logger.progress("Stream completed by server"); if (!resultFuture.isDone()) { @@ -1068,6 +1095,7 @@ private void sendPing() { @Override public void close() { + if (initializationTimeout != null) initializationTimeout.close(); stopPingPong(); if (requestHandler != null && !requestHandler.isCompleted()) { requestHandler.complete(); // Flush remaining requests diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java index 4b2b27fb3e5..2f013c69e46 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java @@ -98,6 +98,7 @@ public class CorrelationStreamProcessor implements AutoCloseable { private final Set pendingCorrelationRequestIds = ConcurrentHashMap.newKeySet(); private final ConcurrentHashMap validationRequestToDastId = new ConcurrentHashMap<>(); private volatile CompletableFuture resultFuture; + private StreamInitializationTimeout initializationTimeout; /** * Keys of SAST–DAST pairs that were confirmed or rejected in a previous run. @@ -164,6 +165,15 @@ public CompletableFuture processCorrelation( this.resultFuture = resultFuture; this.streamLatch = new CountDownLatch(1); + initializationTimeout = new StreamInitializationTimeout(pingScheduler, () -> { + var error = new AviatorSimpleException( + "Correlation initialization timed out. Check the Aviator URL, proxy settings, and server availability."); + stopPingPong(); + streamLatch.countDown(); + if (requestHandler != null) requestHandler.sendError(error); + resultFuture.completeExceptionally(error); + }); + resultFuture.whenComplete((result, error) -> initializationTimeout.close()); startStream(resultFuture, scanGuid); return resultFuture; @@ -291,6 +301,7 @@ public void beforeStart(ClientCallStreamObserver reque @Override public void onNext(CorrelationServerMessage message) { + if (resultFuture.isDone()) return; try { switch (message.getResponseTypeCase()) { case INIT -> handleInitResponse(message.getInit()); @@ -307,6 +318,7 @@ public void onNext(CorrelationServerMessage message) { @Override public void onError(Throwable t) { + if (initializationTimeout.isExpired()) return; stopPingPong(); Status status = Status.fromThrowable(t); LOG.error("Correlation stream error: {} - {}", status.getCode(), status.getDescription(), t); @@ -318,6 +330,7 @@ public void onError(Throwable t) { @Override public void onCompleted() { + if (initializationTimeout.isExpired()) return; stopPingPong(); state.currentPhase = CorrelationStreamState.Phase.COMPLETE; logger.info("Correlation stream completed — " + state.confirmedPairs.size() + " confirmed pairs"); @@ -331,6 +344,7 @@ public void onCompleted() { // ─── Response handlers ───────────────────────────────────────────── private void handleInitResponse(CorrelationInitResponse resp) { + if (!initializationTimeout.initialized()) return; LOG.info("Correlation stream initialized. Server stream ID: {}, Reserved quota: {}, Status: {}", resp.getServerStreamId(), resp.getReservedQuota(), resp.getStatus()); @@ -717,6 +731,7 @@ public record CorrelationBucketData( @Override public void close() { + if (initializationTimeout != null) initializationTimeout.close(); stopPingPong(); if (requestHandler != null && !requestHandler.isCompleted()) { requestHandler.complete(); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java index c830ce7949c..c4839018c03 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java @@ -84,6 +84,7 @@ public class DastAuditStreamProcessor implements AutoCloseable { private final Map requestIdsByIssue = new ConcurrentHashMap<>(); private final Set completedRequestIds = ConcurrentHashMap.newKeySet(); private final Set pendingIssueIds = ConcurrentHashMap.newKeySet(); + private StreamInitializationTimeout initializationTimeout; private int reservedQuota; private int exceededCount; private boolean unlimitedQuota; @@ -118,6 +119,12 @@ public CompletableFuture process( this.isClosed.set(false); this.quotaMetadataInitialized = false; initializeWorkItems(workItems); + initializationTimeout = new StreamInitializationTimeout(pingScheduler, () -> { + completeExceptionally(new AviatorSimpleException( + "DAST audit initialization timed out. Check the Aviator URL, proxy settings, and server availability.")); + close(); + }); + resultFuture.whenComplete((result, error) -> initializationTimeout.close()); startStream(); return resultFuture; } @@ -166,6 +173,7 @@ private void sendInit() { } private void handleInit(DastAuditInitResponse response) { + if (!initializationTimeout.initialized()) return; if (!isSuccess(response.getStatus())) { fail(new AviatorSimpleException("DAST audit initialization failed: " + response.getStatusMessage())); return; @@ -294,7 +302,7 @@ public void beforeStart(ClientCallStreamObserver request @Override public void onNext(DastAuditServerMessage message) { - if (!isCurrent()) return; + if (!isCurrent() || resultFuture.isDone()) return; switch (message.getResponseTypeCase()) { case INIT -> handleInit(message.getInit()); case AUDIT -> handleAudit(message.getAudit()); @@ -479,6 +487,7 @@ private String value(String value) { @Override public void close() { isClosed.set(true); + if (initializationTimeout != null) initializationTimeout.close(); stopPingPong(); if (retryTask != null) retryTask.cancel(false); if (!isRpcCompleted.get() && activeRequestStream != null) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java new file mode 100644 index 00000000000..451f86b8603 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java @@ -0,0 +1,52 @@ +/* + * 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.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** Bounds initialization without imposing a deadline on an established stream. */ +final class StreamInitializationTimeout implements AutoCloseable { + private enum State { WAITING, INITIALIZED, EXPIRED, CLOSED } + + private final AtomicReference state = new AtomicReference<>(State.WAITING); + private final ScheduledFuture timeout; + + StreamInitializationTimeout(ScheduledExecutorService scheduler, Runnable onTimeout) { + timeout = scheduler.schedule(() -> { + if (state.compareAndSet(State.WAITING, State.EXPIRED)) { + onTimeout.run(); + } + }, 30, TimeUnit.SECONDS); + } + + /** Returns false if termination won the race with the initialization response. */ + boolean initialized() { + if (state.compareAndSet(State.WAITING, State.INITIALIZED)) { + timeout.cancel(false); + } + return state.get() == State.INITIALIZED; + } + + boolean isExpired() { + return state.get() == State.EXPIRED; + } + + @Override + public void close() { + state.compareAndSet(State.WAITING, State.CLOSED); + timeout.cancel(false); + } +} diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java new file mode 100644 index 00000000000..6cb4b9713eb --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java @@ -0,0 +1,227 @@ +/* + * 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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.fortify.aviator.dastaudit.DastAuditInitResponse; +import com.fortify.aviator.dastaudit.DastAuditServerMessage; +import com.fortify.aviator.dastaudit.DastAuditServiceGrpc; +import com.fortify.aviator.grpc.AuditorServiceGrpc; +import com.fortify.aviator.grpc.CorrelationInitResponse; +import com.fortify.aviator.grpc.CorrelationServerMessage; +import com.fortify.aviator.grpc.CorrelationServiceGrpc; +import com.fortify.cli.aviator.audit.model.IssueData; +import com.fortify.cli.aviator.audit.model.UserPrompt; +import com.fortify.cli.aviator.config.IAviatorLogger; +import com.fortify.cli.aviator.dast.DastIssue; +import com.fortify.cli.aviator.dast.DastSession; +import com.fortify.cli.aviator.fpr.Vulnerability; +import com.fortify.cli.aviator.fpr.utils.SourceDecoders; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; + +class StreamInitializationTimeoutTest { + private static final IAviatorLogger LOGGER = new IAviatorLogger() { + public void progress(String format, Object... args) {} + public void info(String format, Object... args) {} + public void warn(String format, Object... args) {} + public void error(String format, Object... args) {} + }; + + @Test + void initializationWinsAgainstAnAlreadyQueuedTimeout() { + try (var scheduler = new ManualScheduler()) { + var expired = new AtomicBoolean(); + var timeout = new StreamInitializationTimeout(scheduler, () -> expired.set(true)); + assertTrue(timeout.initialized()); + scheduler.fire(); + assertFalse(expired.get()); + } + } + + @Test + void expiredInitializationRejectsLateResponses() { + try (var scheduler = new ManualScheduler()) { + var expired = new AtomicBoolean(); + var timeout = new StreamInitializationTimeout(scheduler, () -> expired.set(true)); + scheduler.fire(); + assertTrue(expired.get()); + assertFalse(timeout.initialized()); + } + } + + @Test + void closingCancelsInitializationTimeout() { + try (var scheduler = new ManualScheduler()) { + var expired = new AtomicBoolean(); + var timeout = new StreamInitializationTimeout(scheduler, () -> expired.set(true)); + timeout.close(); + scheduler.fire(); + assertFalse(expired.get()); + assertFalse(timeout.initialized()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void dastTimeoutOnlyAppliesBeforeInitialization(boolean initialize) { + try (var scheduler = new ManualScheduler()) { + var channel = new SilentChannel(); + try (var processor = new DastAuditStreamProcessor(LOGGER, + DastAuditServiceGrpc.newStub(channel), scheduler, 0)) { + var issue = new DastIssue(); + issue.setId("DAST-1"); + var result = processor.process(DastAuditStreamConfig.builder() + .token("token").applicationName("app").build(), + List.of(new DastAuditWorkItem(new DastSession(), issue)), 1); + if (initialize) channel.respond(DastAuditServerMessage.newBuilder().setInit( + DastAuditInitResponse.newBuilder().setStatus("SUCCESS")).build()); + scheduler.fire(); + if (initialize) { + assertFalse(result.isDone()); + assertFalse(channel.cancelled); + } else { + assertThrows(CompletionException.class, result::join); + assertTrue(channel.cancelled); + int sent = channel.messages; + channel.respond(DastAuditServerMessage.newBuilder().setInit( + DastAuditInitResponse.newBuilder().setStatus("SUCCESS")).build()); + assertEquals(sent, channel.messages, "Late initialization must not submit findings"); + } + } + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void correlationTimeoutOnlyAppliesBeforeInitialization(boolean initialize) { + try (var scheduler = new ManualScheduler()) { + var channel = new SilentChannel(); + try (var processor = new CorrelationStreamProcessor(null, LOGGER, + CorrelationServiceGrpc.newStub(channel), scheduler, 0, 30)) { + var issue = new DastIssue(); + issue.setId("DAST-1"); + issue.setSessionUrl("https://example.com"); + var bucket = new CorrelationStreamProcessor.CorrelationBucketData("SQL Injection", + List.of(Vulnerability.builder().instanceID("SAST-1").build()), List.of(issue)); + var result = processor.processCorrelation(new CorrelationStreamConfig("token", "app", null, null, null), List.of(bucket), "scan", Set.of()); + if (initialize) channel.respond(CorrelationServerMessage.newBuilder().setInit( + CorrelationInitResponse.newBuilder().setStatus("SUCCESS")).build()); + scheduler.fire(); + if (initialize) { + assertFalse(result.isDone()); + assertFalse(channel.cancelled); + } else { + assertThrows(CompletionException.class, result::join); + assertTrue(channel.cancelled); + int sent = channel.messages; + channel.respond(CorrelationServerMessage.newBuilder().setInit( + CorrelationInitResponse.newBuilder().setStatus("SUCCESS")).build()); + assertEquals(sent, channel.messages, "Late initialization must not submit findings"); + } + } + } + } + + @Test + void sastInitializationTimeoutReleasesCallingThreadAndCancelsRpc() throws Exception { + var workers = Executors.newFixedThreadPool(2); + var caller = Executors.newSingleThreadExecutor(); + try (var scheduler = new ManualScheduler(); + var client = AviatorGrpcClientHelper.createClient("127.0.0.1:1", AviatorRequestPolicy.WAIT_FOR_READY)) { + var channel = new SilentChannel(); + try (var processor = new AviatorStreamProcessor(client, LOGGER, + AuditorServiceGrpc.newStub(channel), workers, scheduler, 30, 30, + null, SourceDecoders.defaults(), null)) { + var requests = new ArrayDeque(); + requests.add(UserPrompt.builder().issueData(IssueData.builder().instanceID("SAST-1").build()).build()); + var invocation = caller.submit(() -> processor.processBatchRequests( + requests, "app", "build", "ssc-app", "version", "token", List.of())); + assertTrue(channel.sent.await(5, TimeUnit.SECONDS)); + assertFalse(invocation.isDone(), "The caller waits for initialization"); + scheduler.fire(); + var result = invocation.get(5, TimeUnit.SECONDS); + assertThrows(CompletionException.class, result::join); + assertTrue(channel.cancelled); + } + } finally { + caller.shutdownNow(); + workers.shutdownNow(); + } + } + + private static final class ManualScheduler extends ScheduledThreadPoolExecutor implements AutoCloseable { + private volatile Runnable task; + ManualScheduler() { super(1); } + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + assertEquals(30, unit.toSeconds(delay)); + task = command; + return super.schedule(command, 1, TimeUnit.DAYS); + } + void fire() { task.run(); } + @Override + public void close() { shutdownNow(); } + } + + private static final class SilentChannel extends Channel { + private ClientCall.Listener listener; + private volatile boolean cancelled; + private final CountDownLatch sent = new CountDownLatch(1); + private int messages; + @Override + public String authority() { return "test"; } + @Override + @SuppressWarnings("unchecked") + public ClientCall newCall( + MethodDescriptor method, CallOptions options) { + return new ClientCall<>() { + public void start(Listener responseListener, Metadata headers) { + listener = (Listener) responseListener; + } + public void request(int count) {} + public void cancel(String message, Throwable cause) { + cancelled = true; + listener.onClose(Status.CANCELLED.withCause(cause), new Metadata()); + } + public void halfClose() {} + public void sendMessage(ReqT message) { messages++; sent.countDown(); } + }; + } + void respond(Object response) { listener.onMessage(response); } + } +} From 10cadf615d555cbac0de605a23718224aed8a8c2 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Wed, 28 Oct 2026 10:58:45 +0530 Subject: [PATCH 24/25] fix(aviator): add stream initialization timeout that cancels the RPC and releases the blocked caller --- .../aviator/grpc/AviatorStreamProcessor.java | 23 +++--- .../grpc/CorrelationStreamProcessor.java | 21 +++--- .../grpc/DastAuditStreamProcessor.java | 35 ++++++--- .../grpc/StreamInitializationTimeout.java | 66 ++++++++++++----- .../grpc/StreamInitializationTimeoutTest.java | 72 +++++++++++++++++++ 5 files changed, 163 insertions(+), 54 deletions(-) diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java index 7fe77d1686b..8482bb39788 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java @@ -87,7 +87,7 @@ class AviatorStreamProcessor implements AutoCloseable { private RequestHandler requestHandler; private ScheduledFuture pingTask; - private StreamInitializationTimeout initializationTimeout; + private volatile StreamInitializationTimeout initializationTimeout; private final AtomicBoolean isPinging = new AtomicBoolean(false); private final ConcurrentLinkedDeque processingQueue = new ConcurrentLinkedDeque<>(); @@ -192,29 +192,23 @@ private void startStreamWithRetry(Map responses, AtomicIn this.streamLatch = new CountDownLatch(1); CountDownLatch initLatch = new CountDownLatch(1); - var attemptLatch = this.streamLatch; var attemptHandler = requestHandler; - var initializationTimeout = new StreamInitializationTimeout(pingScheduler, () -> { - var error = new AviatorSimpleException( - "Audit initialization timed out. Check the Aviator URL, proxy settings, and server availability."); - initLatch.countDown(); - attemptLatch.countDown(); - attemptHandler.sendError(error); - resultFuture.completeExceptionally(error); - }); + var initializationTimeout = new StreamInitializationTimeout(pingScheduler, + () -> attemptHandler.sendError(StreamInitializationTimeout.expiredError("Audit"))); this.initializationTimeout = initializationTimeout; resultFuture.whenComplete((result, error) -> { initializationTimeout.close(); initLatch.countDown(); }); ClientResponseObserver responseObserver = createResponseObserver( - responses, processedRequests, resultFuture, attemptLatch, initLatch, initializationTimeout); + responses, processedRequests, resultFuture, this.streamLatch, initLatch, initializationTimeout); try { asyncStub.processStream(responseObserver); LOG.info("Sending initialization request for stream retry {}", currentStreamState.streamRetryCount); sendInitRequest(); + initializationTimeout.arm(); if (currentStreamState.streamRetryCount > 1) { requestSemaphore.drainPermits(); @@ -329,7 +323,6 @@ public void beforeStart(ClientCallStreamObserver requestStrea @Override public void onNext(AuditorResponse response) { - if (resultFuture.isDone()) return; logger.info("Received response - Status: " + response.getStatus() + ", RequestId: " + response.getRequestId()); if ("PONG".equals(response.getStatus())) { @@ -445,11 +438,12 @@ public void onNext(AuditorResponse response) { @Override public void onError(Throwable t) { - if (initializationTimeout.isExpired()) return; + boolean initializationExpired = initializationTimeout.isExpired(); initializationTimeout.close(); initLatch.countDown(); stopPingPong(); - if (resultFuture.isDone()) { + if (initializationExpired) { + resultFuture.completeExceptionally(StreamInitializationTimeout.expiredError("Audit")); streamLatch.countDown(); return; } @@ -536,7 +530,6 @@ public void onError(Throwable t) { @Override public void onCompleted() { - if (initializationTimeout.isExpired()) return; initializationTimeout.close(); initLatch.countDown(); stopPingPong(); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java index 2f013c69e46..008b414cdd4 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/CorrelationStreamProcessor.java @@ -98,7 +98,7 @@ public class CorrelationStreamProcessor implements AutoCloseable { private final Set pendingCorrelationRequestIds = ConcurrentHashMap.newKeySet(); private final ConcurrentHashMap validationRequestToDastId = new ConcurrentHashMap<>(); private volatile CompletableFuture resultFuture; - private StreamInitializationTimeout initializationTimeout; + private volatile StreamInitializationTimeout initializationTimeout; /** * Keys of SAST–DAST pairs that were confirmed or rejected in a previous run. @@ -165,14 +165,8 @@ public CompletableFuture processCorrelation( this.resultFuture = resultFuture; this.streamLatch = new CountDownLatch(1); - initializationTimeout = new StreamInitializationTimeout(pingScheduler, () -> { - var error = new AviatorSimpleException( - "Correlation initialization timed out. Check the Aviator URL, proxy settings, and server availability."); - stopPingPong(); - streamLatch.countDown(); - if (requestHandler != null) requestHandler.sendError(error); - resultFuture.completeExceptionally(error); - }); + initializationTimeout = new StreamInitializationTimeout(pingScheduler, + () -> requestHandler.sendError(StreamInitializationTimeout.expiredError("Correlation"))); resultFuture.whenComplete((result, error) -> initializationTimeout.close()); startStream(resultFuture, scanGuid); @@ -233,6 +227,7 @@ private void startStream(CompletableFuture resultFuture, Stri requestHandler.sendRequest( CorrelationClientMessage.newBuilder().setInit(initReq.build()).build() ); + initializationTimeout.arm(); } // ─── Phase 1: Send correlation requests ──────────────────────────── @@ -301,7 +296,6 @@ public void beforeStart(ClientCallStreamObserver reque @Override public void onNext(CorrelationServerMessage message) { - if (resultFuture.isDone()) return; try { switch (message.getResponseTypeCase()) { case INIT -> handleInitResponse(message.getInit()); @@ -318,8 +312,12 @@ public void onNext(CorrelationServerMessage message) { @Override public void onError(Throwable t) { - if (initializationTimeout.isExpired()) return; stopPingPong(); + if (initializationTimeout.isExpired()) { + resultFuture.completeExceptionally(StreamInitializationTimeout.expiredError("Correlation")); + streamLatch.countDown(); + return; + } Status status = Status.fromThrowable(t); LOG.error("Correlation stream error: {} - {}", status.getCode(), status.getDescription(), t); resultFuture.completeExceptionally( @@ -330,7 +328,6 @@ public void onError(Throwable t) { @Override public void onCompleted() { - if (initializationTimeout.isExpired()) return; stopPingPong(); state.currentPhase = CorrelationStreamState.Phase.COMPLETE; logger.info("Correlation stream completed — " + state.confirmedPairs.size() + " confirmed pairs"); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java index c4839018c03..e328483bfb9 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java @@ -84,7 +84,7 @@ public class DastAuditStreamProcessor implements AutoCloseable { private final Map requestIdsByIssue = new ConcurrentHashMap<>(); private final Set completedRequestIds = ConcurrentHashMap.newKeySet(); private final Set pendingIssueIds = ConcurrentHashMap.newKeySet(); - private StreamInitializationTimeout initializationTimeout; + private volatile StreamInitializationTimeout initializationTimeout; private int reservedQuota; private int exceededCount; private boolean unlimitedQuota; @@ -119,12 +119,7 @@ public CompletableFuture process( this.isClosed.set(false); this.quotaMetadataInitialized = false; initializeWorkItems(workItems); - initializationTimeout = new StreamInitializationTimeout(pingScheduler, () -> { - completeExceptionally(new AviatorSimpleException( - "DAST audit initialization timed out. Check the Aviator URL, proxy settings, and server availability.")); - close(); - }); - resultFuture.whenComplete((result, error) -> initializationTimeout.close()); + resultFuture.whenComplete((result, error) -> closeInitializationTimeout()); startStream(); return resultFuture; } @@ -148,11 +143,16 @@ private void startStream() { streamId, pendingIssueIds.size(), totalReportedIssues); try { + var attemptHandler = requestHandler; + var attemptTimeout = new StreamInitializationTimeout(pingScheduler, + () -> attemptHandler.sendError(StreamInitializationTimeout.expiredError("DAST audit"))); + initializationTimeout = attemptTimeout; StreamObserver requestObserver = - asyncStub.processDastAuditStream(new ResponseObserver(generation)); + asyncStub.processDastAuditStream(new ResponseObserver(generation, attemptTimeout)); requestHandler.initialize(requestObserver); startPingPong(); sendInit(); + attemptTimeout.arm(); } catch (RuntimeException exception) { handleStreamError(exception); } @@ -286,9 +286,11 @@ private void completeRequestsIfDone() { private class ResponseObserver implements ClientResponseObserver { private final long generation; + private final StreamInitializationTimeout attemptTimeout; - private ResponseObserver(long generation) { + private ResponseObserver(long generation, StreamInitializationTimeout attemptTimeout) { this.generation = generation; + this.attemptTimeout = attemptTimeout; } private boolean isCurrent() { @@ -302,7 +304,7 @@ public void beforeStart(ClientCallStreamObserver request @Override public void onNext(DastAuditServerMessage message) { - if (!isCurrent() || resultFuture.isDone()) return; + if (!isCurrent()) return; switch (message.getResponseTypeCase()) { case INIT -> handleInit(message.getInit()); case AUDIT -> handleAudit(message.getAudit()); @@ -316,6 +318,11 @@ public void onNext(DastAuditServerMessage message) { public void onError(Throwable throwable) { if (!isCurrent()) return; isRpcCompleted.set(true); + if (attemptTimeout.isExpired()) { + stopPingPong(); + completeExceptionally(StreamInitializationTimeout.expiredError("DAST audit")); + return; + } handleStreamError(throwable); } @@ -357,6 +364,7 @@ private void startPingPong() { } private void handleStreamError(Throwable throwable) { + closeInitializationTimeout(); stopPingPong(); if (resultFuture.isDone() || isClosed.get() || isRetryScheduled.get()) return; @@ -476,6 +484,11 @@ private void stopPingPong() { if (pingTask != null) pingTask.cancel(false); } + private void closeInitializationTimeout() { + var timeout = initializationTimeout; + if (timeout != null) timeout.close(); + } + private boolean isSuccess(String status) { return "SUCCESS".equalsIgnoreCase(status) || "OK".equalsIgnoreCase(status); } @@ -487,7 +500,7 @@ private String value(String value) { @Override public void close() { isClosed.set(true); - if (initializationTimeout != null) initializationTimeout.close(); + closeInitializationTimeout(); stopPingPong(); if (retryTask != null) retryTask.cancel(false); if (!isRpcCompleted.get() && activeRequestStream != null) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java index 451f86b8603..e72f3a34ddc 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeout.java @@ -17,25 +17,47 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -/** Bounds initialization without imposing a deadline on an established stream. */ +import com.fortify.cli.aviator._common.exception.AviatorSimpleException; + +/** + * Bounds the wait for one stream attempt's initialization response. The expiry action must only + * cancel the RPC; the processor's own onError path then runs its normal terminal handling. + * + *

TODO: Nothing bounds silence after initialization (pongs are only logged). Add an idle/no-progress + * timeout for the SAST, DAST and correlation streams. + */ final class StreamInitializationTimeout implements AutoCloseable { - private enum State { WAITING, INITIALIZED, EXPIRED, CLOSED } + private static final long TIMEOUT_SECONDS = 30; + + private enum State { IDLE, WAITING, INITIALIZED, EXPIRED, CLOSED } + + private final ScheduledExecutorService scheduler; + private final Runnable cancelRpc; + private final AtomicReference state = new AtomicReference<>(State.IDLE); + private volatile ScheduledFuture timeout; + + StreamInitializationTimeout(ScheduledExecutorService scheduler, Runnable cancelRpc) { + this.scheduler = scheduler; + this.cancelRpc = cancelRpc; + } - private final AtomicReference state = new AtomicReference<>(State.WAITING); - private final ScheduledFuture timeout; + static AviatorSimpleException expiredError(String operation) { + return new AviatorSimpleException(operation + + " initialization timed out. Check the Aviator URL, proxy settings, and server availability."); + } - StreamInitializationTimeout(ScheduledExecutorService scheduler, Runnable onTimeout) { - timeout = scheduler.schedule(() -> { - if (state.compareAndSet(State.WAITING, State.EXPIRED)) { - onTimeout.run(); - } - }, 30, TimeUnit.SECONDS); + /** Starts the countdown after the init request is queued. Does nothing without a scheduler. */ + void arm() { + if (scheduler == null || !state.compareAndSet(State.IDLE, State.WAITING)) return; + timeout = scheduler.schedule(this::expire, TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (state.get() != State.WAITING) timeout.cancel(false); } - /** Returns false if termination won the race with the initialization response. */ + /** Returns false if the timeout or a close won the race with the initialization response. */ boolean initialized() { - if (state.compareAndSet(State.WAITING, State.INITIALIZED)) { - timeout.cancel(false); + if (state.compareAndSet(State.WAITING, State.INITIALIZED) + || state.compareAndSet(State.IDLE, State.INITIALIZED)) { + cancelTimer(); } return state.get() == State.INITIALIZED; } @@ -46,7 +68,19 @@ boolean isExpired() { @Override public void close() { - state.compareAndSet(State.WAITING, State.CLOSED); - timeout.cancel(false); + if (state.compareAndSet(State.WAITING, State.CLOSED) || state.compareAndSet(State.IDLE, State.CLOSED)) { + cancelTimer(); + } + } + + private void expire() { + if (state.compareAndSet(State.WAITING, State.EXPIRED)) { + cancelRpc.run(); + } + } + + private void cancelTimer() { + var scheduled = timeout; + if (scheduled != null) scheduled.cancel(false); } -} +} \ No newline at end of file diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java index 6cb4b9713eb..cb472d81366 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/StreamInitializationTimeoutTest.java @@ -17,6 +17,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.InetAddress; +import java.net.ServerSocket; import java.util.ArrayDeque; import java.util.List; import java.util.Set; @@ -53,6 +55,7 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; class StreamInitializationTimeoutTest { private static final IAviatorLogger LOGGER = new IAviatorLogger() { @@ -67,6 +70,7 @@ void initializationWinsAgainstAnAlreadyQueuedTimeout() { try (var scheduler = new ManualScheduler()) { var expired = new AtomicBoolean(); var timeout = new StreamInitializationTimeout(scheduler, () -> expired.set(true)); + timeout.arm(); assertTrue(timeout.initialized()); scheduler.fire(); assertFalse(expired.get()); @@ -78,8 +82,10 @@ void expiredInitializationRejectsLateResponses() { try (var scheduler = new ManualScheduler()) { var expired = new AtomicBoolean(); var timeout = new StreamInitializationTimeout(scheduler, () -> expired.set(true)); + timeout.arm(); scheduler.fire(); assertTrue(expired.get()); + assertTrue(timeout.isExpired()); assertFalse(timeout.initialized()); } } @@ -89,6 +95,7 @@ void closingCancelsInitializationTimeout() { try (var scheduler = new ManualScheduler()) { var expired = new AtomicBoolean(); var timeout = new StreamInitializationTimeout(scheduler, () -> expired.set(true)); + timeout.arm(); timeout.close(); scheduler.fire(); assertFalse(expired.get()); @@ -96,6 +103,34 @@ void closingCancelsInitializationTimeout() { } } + @Test + void timeoutIsNotScheduledUntilArmed() { + try (var scheduler = new ManualScheduler()) { + var timeout = new StreamInitializationTimeout(scheduler, () -> {}); + assertFalse(scheduler.scheduled()); + timeout.arm(); + assertTrue(scheduler.scheduled()); + } + } + + @Test + void responseBeforeArmingDisablesTheTimeout() { + try (var scheduler = new ManualScheduler()) { + var timeout = new StreamInitializationTimeout(scheduler, () -> {}); + assertTrue(timeout.initialized()); + timeout.arm(); + assertFalse(scheduler.scheduled()); + } + } + + @Test + void missingSchedulerDisablesTheTimeoutWithoutFailing() { + var timeout = new StreamInitializationTimeout(null, () -> {}); + timeout.arm(); + assertFalse(timeout.isExpired()); + assertTrue(timeout.initialized()); + } + @ParameterizedTest @ValueSource(booleans = {false, true}) void dastTimeoutOnlyAppliesBeforeInitialization(boolean initialize) { @@ -172,6 +207,7 @@ void sastInitializationTimeoutReleasesCallingThreadAndCancelsRpc() throws Except var invocation = caller.submit(() -> processor.processBatchRequests( requests, "app", "build", "ssc-app", "version", "token", List.of())); assertTrue(channel.sent.await(5, TimeUnit.SECONDS)); + assertTrue(scheduler.armed.await(5, TimeUnit.SECONDS)); assertFalse(invocation.isDone(), "The caller waits for initialization"); scheduler.fire(); var result = invocation.get(5, TimeUnit.SECONDS); @@ -184,16 +220,52 @@ void sastInitializationTimeoutReleasesCallingThreadAndCancelsRpc() throws Except } } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void sastInitializationTimeoutReleasesCallerOnRealChannel(boolean silentServer) throws Exception { + var workers = Executors.newFixedThreadPool(2); + var caller = Executors.newSingleThreadExecutor(); + try (var scheduler = new ManualScheduler(); + var silent = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); + var client = AviatorGrpcClientHelper.createClient("127.0.0.1:1", AviatorRequestPolicy.WAIT_FOR_READY)) { + // Unreachable port: wait-for-ready keeps the call queued. Silent socket: connects, never answers. + int port = silentServer ? silent.getLocalPort() : 1; + var channel = NettyChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); + try (var processor = new AviatorStreamProcessor(client, LOGGER, + AuditorServiceGrpc.newStub(channel).withWaitForReady(), workers, scheduler, 30, 30, + null, SourceDecoders.defaults(), null)) { + var requests = new ArrayDeque(); + requests.add(UserPrompt.builder().issueData(IssueData.builder().instanceID("SAST-1").build()).build()); + var invocation = caller.submit(() -> processor.processBatchRequests( + requests, "app", "build", "ssc-app", "version", "token", List.of())); + assertTrue(scheduler.armed.await(5, TimeUnit.SECONDS)); + assertFalse(invocation.isDone(), "The caller waits for initialization"); + scheduler.fire(); + var result = invocation.get(10, TimeUnit.SECONDS); + var failure = assertThrows(CompletionException.class, result::join); + assertTrue(failure.getCause().getMessage().contains("initialization timed out")); + } finally { + channel.shutdownNow(); + } + } finally { + caller.shutdownNow(); + workers.shutdownNow(); + } + } + private static final class ManualScheduler extends ScheduledThreadPoolExecutor implements AutoCloseable { private volatile Runnable task; + private final CountDownLatch armed = new CountDownLatch(1); ManualScheduler() { super(1); } @Override public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { assertEquals(30, unit.toSeconds(delay)); task = command; + armed.countDown(); return super.schedule(command, 1, TimeUnit.DAYS); } void fire() { task.run(); } + boolean scheduled() { return task != null; } @Override public void close() { shutdownNow(); } } From 1e28582644501f10f71cfc271f79a58fa8823e31 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Wed, 28 Oct 2026 19:58:21 +0530 Subject: [PATCH 25/25] docs: Restore trailing blank line in repo-usage.md --- doc-resources/repo-usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc-resources/repo-usage.md b/doc-resources/repo-usage.md index ffe8c0a0049..9117e3cd8ef 100644 --- a/doc-resources/repo-usage.md +++ b/doc-resources/repo-usage.md @@ -10,3 +10,4 @@ Docker images for fcli are built and published in the separate [fortify/fcli-doc Published images are available at [Docker Hub: fortifydocker/fcli](https://hub.docker.com/r/fortifydocker/fcli). For more information about available Docker images and usage instructions, see the [fcli-docker repository](https://github.com/fortify/fcli-docker). +