diff --git a/build.gradle b/build.gradle index e8eb874ccb..c88fe4b4fe 100644 --- a/build.gradle +++ b/build.gradle @@ -183,6 +183,7 @@ ext.javaLibraries = [ 'apiml-security-common', 'apiml-tomcat-common', 'certificate-analyser', + 'pre-flight-check', 'common-service-core', 'security-service-client-spring', 'apiml-sample-extension', diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 607f189068..8f481a156d 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -11,6 +11,7 @@ ext.javaLibraries = [ 'apiml-security-common', 'apiml-tomcat-common', 'certificate-analyser', + 'pre-flight-check', 'common-service-core', 'security-service-client-spring', 'apiml-sample-extension', diff --git a/pre-flight-check/README.md b/pre-flight-check/README.md new file mode 100644 index 0000000000..b84b00b87c --- /dev/null +++ b/pre-flight-check/README.md @@ -0,0 +1,331 @@ +# Pre-Flight Check Tool + +A Java utility that verifies connectivity to the z/OSMF JWK endpoint **before/after** starting the Zowe API Mediation Layer. This tool helps diagnose configuration issues early such as incorrect hostnames, unreachable ports, missing certificates, or misconfigured z/OSMF by performing a lightweight HTTP(S) call to the z/OSMF JWK endpoint at `/jwt/ibm/api/zOSMFBuilder/jwk`. + +## Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Building](#building) +- [Usage](#usage) +- [CLI Flags Reference](#cli-flags-reference) +- [Certificate Verification Modes](#certificate-verification-modes) +- [Exit Codes](#exit-codes) +- [Response Interpretation](#response-interpretation) +- [Testing Scenarios](#testing-scenarios) + - [1. Quick Test — DISABLED Mode](#1-quick-test--disabled-mode-no-truststore-needed) + - [2. STRICT Mode — Full Verification](#2-strict-mode--full-certificate-and-hostname-verification) + - [3. NONSTRICT Mode — Skip Hostname Check](#3-nonstrict-mode--certificate-chain-verified-hostname-check-skipped) + - [4. HTTP Mode (No SSL)](#4-http-mode-no-ssl) + - [5. Validation Error Tests](#5-validation-error-tests) +- [SAF Keyrings](#saf-keyrings) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +When Zowe API ML starts, it attempts to reach z/OSMF to obtain public keys for JWT token validation. If z/OSMF is unreachable or misconfigured, the startup fails with errors that can be difficult to diagnose. This pre-flight check tool isolates that connectivity test into a simple, standalone JAR that can be run before Zowe startup. + +**What it checks:** + +- TCP connectivity to the z/OSMF host and port +- SSL/TLS handshake (when using HTTPS) +- Certificate trust chain validation (STRICT/NONSTRICT modes) +- Hostname verification (STRICT mode) +- HTTP response from the JWK endpoint (`/jwt/ibm/api/zOSMFBuilder/jwk`) + +## Prerequisites + +- **Java 17 or higher** (Java 17, 21, or any later version) +- Network access to the z/OSMF server +- A truststore containing the z/OSMF server's CA certificate (required for STRICT and NONSTRICT modes) + +## Building + +From the root of the `api-layer` repository: + +```bash +./gradlew :pre-flight-check:build +``` + +On Windows: + +```powershell +.\gradlew :pre-flight-check:build +``` + +The fat JAR (with all dependencies bundled) will be generated at: + +``` +pre-flight-check/build/libs/pre-flight-check-.jar +``` + +For example: `pre-flight-check/build/libs/pre-flight-check-3.5.12-SNAPSHOT.jar` + +## Usage + +```bash +java -jar pre-flight-check-.jar --zosmf-host --zosmf-port [options] +``` + +**Minimal example (DISABLED mode — quickest way to test):** + +```bash +java -jar pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --verify-certificates DISABLED +``` + +**Full example (STRICT mode with truststore):** + +```bash +java -jar pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore /path/to/truststore.p12 \ + --truststore-password changeit +``` + +**Display help:** + +```bash +java -jar pre-flight-check-.jar --help +``` + +## CLI Flags Reference + +### Required Flags + +| Flag | Description | Example | +|------|-------------|---------| +| `--zosmf-host` | Hostname or IP address of the z/OSMF server | `--zosmf-host myzosmf.example.com` | +| `--zosmf-port` | Port number of the z/OSMF server | `--zosmf-port 11443` | + +> **Note:** If `--zosmf-host` or `--zosmf-port` are omitted, picocli will display: +> `Missing required option: '--zosmf-host='` + +### Conditionally Required Flags + +These flags are required when `--scheme=https` (the default) and `--verify-certificates` is **not** `DISABLED`: + +| Flag | Description | Error when missing | +|------|-------------|-------------------| +| `--truststore` | Path to the truststore file containing the z/OSMF CA certificate | `ERROR: --truststore is required when --scheme=https and verification is not DISABLED.` | +| `--truststore-password` | Password for the truststore. If specified without a value, you will be prompted interactively. | `ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED.` | + +### Optional Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--scheme` | `https` | Protocol to use: `http` or `https` | +| `--verify-certificates` | `STRICT` | Certificate verification mode: `STRICT`, `NONSTRICT`, or `DISABLED` | +| `--truststore-type` | `PKCS12` | Format of the truststore file (e.g., `PKCS12`, `JKS`, `JCERACFKS`) | +| `--keystore` | *(none)* | Path to keystore file (only needed for mutual TLS / client certificate authentication) | +| `--keystore-password` | *(none)* | Password for the keystore. If specified without a value, you will be prompted interactively. | +| `--keystore-type` | `PKCS12` | Format of the keystore file | +| `-h`, `--help` | | Display usage help and exit | + +## Certificate Verification Modes + +The `--verify-certificates` flag controls how SSL/TLS certificates are validated when connecting over HTTPS. This mirrors the `zowe.verifyCertificates` setting in the Zowe configuration (`zowe.yaml`). + +### STRICT (Default) + +```bash +--verify-certificates STRICT +``` + +- **Certificate chain**: Fully validated against the truststore +- **Hostname verification**: The server certificate's CN/SAN must match the `--zosmf-host` value +- **Truststore**: Required +- **Use case**: Production environments — maximum security + +### NONSTRICT + +```bash +--verify-certificates NONSTRICT +``` + +- **Certificate chain**: Fully validated against the truststore +- **Hostname verification**: Skipped — the server certificate does not need to match the hostname +- **Truststore**: Required +- **Use case**: Environments where the z/OSMF certificate is issued for a different hostname (e.g., accessing via IP address when the cert has a DNS name) + +### DISABLED + +```bash +--verify-certificates DISABLED +``` + +- **Certificate chain**: Not validated — all certificates are trusted +- **Hostname verification**: Skipped +- **Truststore**: Not required +- **Use case**: Development/testing environments, or initial connectivity debugging +- **Warning**: Prints `WARNING: SSL certificate verification is DISABLED. All certificates will be trusted.` + +> **Security Note:** `DISABLED` mode should **never** be used in production. It is vulnerable to man-in-the-middle attacks. + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | **Success** — z/OSMF JWK endpoint is reachable and responding | +| `4` | **Failure** — connection failed, SSL error, endpoint not found, or configuration error | +| `8` | **Help** — help/version was displayed; no check was performed | + +## Response Interpretation + +The tool interprets HTTP response codes from the z/OSMF JWK endpoint as follows: + +| HTTP Code | Result | Message | +|-----------|--------|---------| +| 200-299 | **SUCCESS** | `z/OSMF JWK endpoint is reachable and responding. HTTP ` | +| 401 | **SUCCESS** | `z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401` | +| 404 | **FAILURE** | `z/OSMF JWK endpoint not found. HTTP 404` — Consider configuring `jwtAutoConfiguration` to LTPA | +| 4xx (other) | **FAILURE** | `z/OSMF JWK endpoint returned unexpected client error. HTTP ` | +| 5xx | **FAILURE** | `z/OSMF JWK endpoint returned server error. HTTP ` | + +**Note:** A `401 Unauthorized` is treated as **success** because the tool does not send authentication credentials. A 401 confirms the endpoint exists and z/OSMF is processing requests. + +### Connection-Level Errors + +| Error | Message | +|-------|---------| +| SSL handshake failure | `FAILURE: SSL handshake failed. Verify that the truststore contains the z/OSMF server certificate.` | +| Connection refused | `FAILURE: Cannot connect to :. Verify the host and port are correct and z/OSMF is running.` | +| Connection timeout | `FAILURE: Connection timed out to :.` | + +## Testing Scenarios + +Below are step-by-step commands for testing all modes. Replace `` with your actual JAR version (e.g., `3.5.12-SNAPSHOT`) and adjust the host/port for your environment. + +### 1. Quick Test — DISABLED Mode (No Truststore Needed) + +The fastest way to verify basic TCP + HTTP connectivity: + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --verify-certificates DISABLED +``` + +**Expected output (success):** + +``` +WARNING: SSL certificate verification is DISABLED. All certificates will be trusted. +Checking z/OSMF JWK endpoint: https://myzosmf.example.com:11443/jwt/ibm/api/zOSMFBuilder/jwk +SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401 +``` + +### 2. STRICT Mode — Full Certificate and Hostname Verification + +Requires a truststore containing the z/OSMF server's CA certificate (see [Creating a Truststore](#creating-a-truststore)): + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore /path/to/zosmf-truststore.p12 \ + --truststore-password password +``` + +**Expected output (success):** + +``` +Checking z/OSMF JWK endpoint: https://myzosmf.example.com:11443/jwt/ibm/api/zOSMFBuilder/jwk +SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401 +``` + +**Expected output (SSL failure — wrong truststore):** + +``` +FAILURE: SSL handshake failed when connecting to https://myzosmf.example.com:11443/jwt/ibm/api/zOSMFBuilder/jwk. +Verify that the truststore contains the z/OSMF server certificate. +Details: PKIX path building failed: ...unable to find valid certification path to requested target +``` + +### 3. NONSTRICT Mode — Certificate Chain Verified, Hostname Check Skipped + +Useful when connecting via IP address but the certificate has a DNS name: + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host 10.0.0.50 \ + --zosmf-port 11443 \ + --truststore /path/to/zosmf-truststore.p12 \ + --truststore-password password \ + --verify-certificates NONSTRICT +``` + +**Expected output (success):** + +``` +INFO: Hostname verification is disabled (NONSTRICT mode). +Checking z/OSMF JWK endpoint: https://10.0.0.50:11443/jwt/ibm/api/zOSMFBuilder/jwk +SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401 +``` + +### 4. HTTP Mode (No SSL) + +For z/OSMF instances running on plain HTTP (uncommon): + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 80 \ + --scheme http +``` + +### 5. Validation Error Tests + +**Missing required flags:** + +```bash +# No arguments at all +java -jar pre-flight-check-.jar +# Output: Missing required options: '--zosmf-host=', '--zosmf-port=' + +# Missing truststore in STRICT mode +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 +# Output: ERROR: --truststore is required when --scheme=https and verification is not DISABLED. + +# Missing truststore password +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --truststore my.p12 +# Output: ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED. +``` + +**Invalid values:** + +```bash +# Invalid scheme +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --scheme ftp +# Output: ERROR: --scheme must be 'http' or 'https', got: ftp + +# Invalid verify mode +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --verify-certificates INVALID +# Output: ERROR: --verify-certificates must be STRICT, NONSTRICT, or DISABLED, got: INVALID +``` + +**Unreachable host:** + +```bash +java -jar pre-flight-check-.jar --zosmf-host nonexistent.host --zosmf-port 443 --verify-certificates DISABLED +# Output: FAILURE: Cannot connect to nonexistent.host:443. +``` + +## SAF Keyrings + +On z/OS, if you are using SAF keyrings instead of file-based keystores/truststores, provide the keyring path in the `safkeyring://` format and add the JVM protocol handler: + +```bash +java -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -jar pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore safkeyring://IZUSVR/ZoweKeyring \ + --truststore-password password \ + --truststore-type JCERACFKS +``` diff --git a/pre-flight-check/build.gradle b/pre-flight-check/build.gradle new file mode 100644 index 0000000000..ca98a4c3d4 --- /dev/null +++ b/pre-flight-check/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' +} + +dependencies { + implementation libs.picocli + annotationProcessor libs.picocli.codegen + + testImplementation libs.mockito.core + testImplementation libs.hamcrest +} + +compileJava { + options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] +} + +jar { + manifest { + attributes( + 'Main-Class': 'org.zowe.apiml.PreFlightCheck' + ) + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java new file mode 100644 index 0000000000..df3fa0d5e5 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java @@ -0,0 +1,72 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; + +@SuppressWarnings("squid:S106") +public class HttpClientWrapper { + + private static final int CONNECT_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 5000; + + private final SSLContext sslContext; + private final boolean useHttps; + private final HostnameVerifier hostnameVerifier; + + public HttpClientWrapper(SSLContext sslContext, HostnameVerifier hostnameVerifier) { + this.sslContext = sslContext; + this.useHttps = true; + this.hostnameVerifier = hostnameVerifier; + } + + public HttpClientWrapper() { + this.sslContext = null; + this.useHttps = false; + this.hostnameVerifier = null; + } + + public int executeCall(URL url, Map headers) throws IOException { + HttpURLConnection con; + if (useHttps) { + HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); + httpsCon.setSSLSocketFactory(sslContext.getSocketFactory()); + if (hostnameVerifier != null) { + httpsCon.setHostnameVerifier(hostnameVerifier); + } + con = httpsCon; + } else { + con = (HttpURLConnection) url.openConnection(); + } + + con.setRequestMethod("GET"); + con.setConnectTimeout(CONNECT_TIMEOUT); + con.setReadTimeout(READ_TIMEOUT); + + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + + try { + return con.getResponseCode(); + } finally { + con.disconnect(); + } + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java b/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java new file mode 100644 index 0000000000..c9301b2084 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java @@ -0,0 +1,100 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import javax.net.ssl.SSLHandshakeException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("squid:S106") +public class JwkEndpointChecker { + + static final String JWK_ENDPOINT_PATH = "/jwt/ibm/api/zOSMFBuilder/jwk"; + private static final String ZOSMF_CSRF_HEADER = "X-CSRF-ZOSMF-HEADER"; + + private final HttpClientWrapper httpClient; + private final PreFlightCheckConfig conf; + + public JwkEndpointChecker(HttpClientWrapper httpClient, PreFlightCheckConfig conf) { + this.httpClient = httpClient; + this.conf = conf; + } + + public boolean check() { + String urlString = conf.getScheme() + "://" + conf.getZosmfHost() + ":" + conf.getZosmfPort() + JWK_ENDPOINT_PATH; + + Map headers = new HashMap<>(); + headers.put(ZOSMF_CSRF_HEADER, ""); + + try { + URL url = new URL(urlString); + System.out.println("Checking z/OSMF JWK endpoint: " + urlString); + + int responseCode = httpClient.executeCall(url, headers); + return evaluateResponseCode(responseCode, urlString); + } catch (SSLHandshakeException e) { + System.err.println("FAILURE: SSL handshake failed when connecting to " + urlString + "."); + System.err.println("Verify that the truststore contains the z/OSMF server certificate."); + System.err.println("Details: " + e.getMessage()); + return false; + } catch (ConnectException e) { + System.err.println("FAILURE: Cannot connect to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); + System.err.println("Verify the host and port are correct and z/OSMF is running."); + System.err.println("Details: " + e.getMessage()); + return false; + } catch (SocketTimeoutException e) { + System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); + System.err.println("Details: " + e.getMessage()); + return false; + } catch (Exception e) { + System.err.println("FAILURE: Unexpected error when calling " + urlString + "."); + System.err.println("Details: " + e.getMessage()); + return false; + } + } + + private boolean evaluateResponseCode(int responseCode, String urlString) { + if (responseCode >= 200 && responseCode < 300) { + System.out.println("SUCCESS: z/OSMF JWK endpoint is reachable and responding. HTTP " + responseCode); + return true; + } + + if (responseCode == 401) { + System.out.println("SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401"); + return true; + } + + if (responseCode == 404) { + System.err.println("FAILURE: z/OSMF JWK endpoint not found. HTTP 404"); + System.err.println("Try configuring the jwtAutoConfiguration to LTPA"); + return false; + } + + if (responseCode >= 400 && responseCode < 500) { + System.err.println("FAILURE: z/OSMF JWK endpoint returned unexpected client error. HTTP " + responseCode); + System.err.println("URL: " + urlString); + return false; + } + + if (responseCode >= 500) { + System.err.println("FAILURE: z/OSMF JWK endpoint returned server error. HTTP " + responseCode); + System.err.println("URL: " + urlString); + return false; + } + + System.err.println("FAILURE: z/OSMF JWK endpoint returned unexpected response code. HTTP " + responseCode); + System.err.println("URL: " + urlString); + return false; + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java new file mode 100644 index 0000000000..034860b9f1 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java @@ -0,0 +1,97 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import picocli.CommandLine; + +import javax.net.ssl.HostnameVerifier; + +@SuppressWarnings("squid:S106") +public class PreFlightCheck { + + static final String VERIFY_STRICT = "STRICT"; + static final String VERIFY_NONSTRICT = "NONSTRICT"; + static final String VERIFY_DISABLED = "DISABLED"; + + public static int mainWithExitCode(String[] args) { + try { + PreFlightCheckConf conf = new PreFlightCheckConf(); + CommandLine cmd = new CommandLine(conf); + cmd.parseArgs(args); + + if (conf.isHelpRequested()) { + cmd.printVersionHelp(System.out); + CommandLine.usage(new PreFlightCheckConf(), System.out); + return 8; + } + + validateConfig(conf); + + HttpClientWrapper httpClient; + if ("https".equalsIgnoreCase(conf.getScheme())) { + String verifyMode = conf.getVerifyCertificates().toUpperCase(); + + if (VERIFY_DISABLED.equals(verifyMode)) { + SSLContextFactory sslContextFactory = SSLContextFactory.initTrustAllSSLContext(); + HostnameVerifier noopVerifier = (hostname, session) -> true; + httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), noopVerifier); + } else { + Stores stores = new Stores(conf); + SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); + + HostnameVerifier hostnameVerifier; + if (VERIFY_NONSTRICT.equals(verifyMode)) { + hostnameVerifier = (hostname, session) -> true; + System.out.println("INFO: Hostname verification is disabled (NONSTRICT mode)."); + } else { + hostnameVerifier = null; // use default JDK hostname verifier + } + httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), hostnameVerifier); + } + } else { + httpClient = new HttpClientWrapper(); + } + + JwkEndpointChecker checker = new JwkEndpointChecker(httpClient, conf); + boolean success = checker.check(); + return success ? 0 : 4; + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + return 4; + } + } + + static void validateConfig(PreFlightCheckConf conf) { + String scheme = conf.getScheme(); + if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { + throw new IllegalArgumentException("--scheme must be 'http' or 'https', got: " + scheme); + } + + String verifyMode = conf.getVerifyCertificates().toUpperCase(); + if (!VERIFY_STRICT.equals(verifyMode) && !VERIFY_NONSTRICT.equals(verifyMode) && !VERIFY_DISABLED.equals(verifyMode)) { + throw new IllegalArgumentException("--verify-certificates must be STRICT, NONSTRICT, or DISABLED, got: " + conf.getVerifyCertificates()); + } + + if ("https".equalsIgnoreCase(scheme) && !VERIFY_DISABLED.equals(verifyMode)) { + if (conf.getTrustStore() == null) { + throw new IllegalArgumentException("--truststore is required when --scheme=https and verification is not DISABLED. " + + "Provide the path to the truststore containing the z/OSMF server certificate."); + } + if (conf.getTrustStorePassword() == null) { + throw new IllegalArgumentException("--truststore-password is required when --scheme=https and verification is not DISABLED."); + } + } + } + + public static void main(String[] args) { + System.exit(mainWithExitCode(args)); + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java new file mode 100644 index 0000000000..106c4c664c --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java @@ -0,0 +1,114 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import picocli.CommandLine; +import picocli.CommandLine.Option; + +@CommandLine.Command( + name = "pre-flight-check", + version = { + "Pre-Flight Check 1.0", + "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", + "OS: ${os.name} ${os.version} ${os.arch}" + }, + description = "Performs a pre-flight connectivity check against the z/OSMF JWK endpoint." +) +public class PreFlightCheckConf implements PreFlightCheckConfig { + + @Option(names = {"--zosmf-host"}, required = true, description = "Hostname or IP of the z/OSMF server") + private String zosmfHost; + + @Option(names = {"--zosmf-port"}, required = true, description = "Port of the z/OSMF server") + private int zosmfPort; + + @Option(names = {"--scheme"}, description = "http or https (default: ${DEFAULT-VALUE})") + private String scheme = "https"; + + @Option(names = {"--keystore"}, description = "Path to the keystore file (for HTTPS mutual TLS)") + private String keyStore; + + @Option(names = {"--keystore-password"}, arity = "0..1", interactive = true, description = "Password for the keystore") + private String keyStorePassword; + + @Option(names = {"--keystore-type"}, description = "Type of keystore (default: ${DEFAULT-VALUE})") + private String keyStoreType = "PKCS12"; + + @Option(names = {"--truststore"}, description = "Path to the truststore file (for HTTPS)") + private String trustStore; + + @Option(names = {"--truststore-password"}, arity = "0..1", interactive = true, description = "Password for the truststore") + private String trustStorePassword; + + @Option(names = {"--truststore-type"}, description = "Type of truststore (default: ${DEFAULT-VALUE})") + private String trustStoreType = "PKCS12"; + + @Option(names = {"--verify-certificates"}, description = "Certificate verification mode: STRICT, NONSTRICT, or DISABLED (default: ${DEFAULT-VALUE})") + private String verifyCertificates = "STRICT"; + + @Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message") + private boolean helpRequested = false; + + @Override + public String getZosmfHost() { + return zosmfHost; + } + + @Override + public int getZosmfPort() { + return zosmfPort; + } + + @Override + public String getScheme() { + return scheme; + } + + @Override + public String getKeyStore() { + return keyStore; + } + + @Override + public String getKeyStorePassword() { + return keyStorePassword; + } + + @Override + public String getKeyStoreType() { + return keyStoreType; + } + + @Override + public String getTrustStore() { + return trustStore; + } + + @Override + public String getTrustStorePassword() { + return trustStorePassword; + } + + @Override + public String getTrustStoreType() { + return trustStoreType; + } + + @Override + public String getVerifyCertificates() { + return verifyCertificates; + } + + @Override + public boolean isHelpRequested() { + return helpRequested; + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java new file mode 100644 index 0000000000..0f0bb1e215 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java @@ -0,0 +1,36 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +public interface PreFlightCheckConfig { + + String getZosmfHost(); + + int getZosmfPort(); + + String getScheme(); + + String getKeyStore(); + + String getKeyStorePassword(); + + String getKeyStoreType(); + + String getTrustStore(); + + String getTrustStorePassword(); + + String getTrustStoreType(); + + String getVerifyCertificates(); + + boolean isHelpRequested(); +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java new file mode 100644 index 0000000000..480d6f03e8 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -0,0 +1,86 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +@SuppressWarnings("squid:S106") +public class SSLContextFactory { + + private final Stores stores; + private SSLContext sslContext; + + private SSLContextFactory(Stores stores) { + this.stores = stores; + } + + public SSLContext getSslContext() { + return sslContext; + } + + public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { + SSLContextFactory factory = new SSLContextFactory(stores); + + TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustFactory.init(stores.getTrustStore()); + + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (stores.getKeyStore() != null) { + keyFactory.init(stores.getKeyStore(), stores.getConf().getKeyStorePassword().toCharArray()); + } else { + KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); + emptyKeystore.load(null, null); + keyFactory.init(emptyKeystore, null); + } + + factory.sslContext = SSLContext.getInstance("TLSv1.2"); + factory.sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom()); + return factory; + } + + public static SSLContextFactory initTrustAllSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException { + SSLContextFactory factory = new SSLContextFactory(null); + + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // trust all + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // trust all + } + } + }; + + KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); + emptyKeystore.load(null, null); + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyFactory.init(emptyKeystore, null); + + factory.sslContext = SSLContext.getInstance("TLSv1.2"); + factory.sslContext.init(keyFactory.getKeyManagers(), trustAllCerts, new SecureRandom()); + System.out.println("WARNING: SSL certificate verification is DISABLED. All certificates will be trusted."); + return factory; + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java b/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java new file mode 100644 index 0000000000..04041403fb --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java @@ -0,0 +1,128 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings("squid:S106") +public class Stores { + + private static final Pattern KEYRING_PATTERN = Pattern.compile("^(safkeyring[^:]*):/{2,4}([^/]+)/([^/]+)$"); + + private KeyStore keyStore; + private KeyStore trustStore; + private final PreFlightCheckConfig conf; + + public Stores(PreFlightCheckConfig conf) { + this.conf = conf; + init(); + } + + public static boolean isKeyring(String input) { + if (input == null) return false; + Matcher matcher = KEYRING_PATTERN.matcher(input); + return matcher.matches(); + } + + public static String formatKeyringUrl(String input) { + if (input == null) return null; + Matcher matcher = KEYRING_PATTERN.matcher(input); + if (matcher.matches()) { + return matcher.group(1) + "://" + matcher.group(2) + "/" + matcher.group(3); + } + return input; + } + + void init() { + try { + initKeystore(); + if (trustStore == null) { + initTruststore(); + } + } catch (FileNotFoundException e) { + throw new StoresNotInitializeException("Error while loading keystore file. Error message: " + e.getMessage() + "\n" + + "Possible solution: Verify correct path to the keystore. Change owner or permission to the keystore file."); + } catch (Exception e) { + throw new StoresNotInitializeException(e.getMessage()); + } + } + + private void initTruststore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + if (conf.getTrustStore() == null) { + System.out.println("No truststore specified, will use empty."); + try { + this.trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + this.trustStore.load(null, null); + } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { + System.err.println(e.getMessage()); + } + return; + } + try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { + this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + } + } + + private void initKeystore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + if (conf.getKeyStore() == null) { + return; + } + if (isKeyring(conf.getKeyStore())) { + try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { + this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + this.trustStore = this.keyStore; + } catch (Exception e) { + throw new StoresNotInitializeException(e.getMessage()); + } + } else { + try (InputStream keyStoreIStream = new FileInputStream(conf.getKeyStore())) { + this.keyStore = readKeyStore(keyStoreIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + } + } + } + + public static KeyStore readKeyStore(InputStream is, char[] pass, String type) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + KeyStore keyStore = KeyStore.getInstance(type); + keyStore.load(is, pass); + return keyStore; + } + + public KeyStore getKeyStore() { + return keyStore; + } + + public KeyStore getTrustStore() { + return trustStore; + } + + public PreFlightCheckConfig getConf() { + return conf; + } + + public static URL keyRingUrl(String uri) throws MalformedURLException { + if (!isKeyring(uri)) { + throw new StoresNotInitializeException("Incorrect key ring format: " + uri + + ". Make sure you use format safkeyring://userId/keyRing"); + } + return new URL(formatKeyringUrl(uri)); + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java b/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java new file mode 100644 index 0000000000..e5c77f773c --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java @@ -0,0 +1,18 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +public class StoresNotInitializeException extends RuntimeException { + + public StoresNotInitializeException(String message) { + super(message); + } +} diff --git a/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java b/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java new file mode 100644 index 0000000000..642f4d507c --- /dev/null +++ b/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java @@ -0,0 +1,148 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLHandshakeException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.net.ConnectException; +import java.net.SocketTimeoutException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JwkEndpointCheckerTest { + + private final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errStream = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + private HttpClientWrapper mockClient; + private PreFlightCheckConfig mockConf; + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outStream)); + System.setErr(new PrintStream(errStream)); + + mockClient = mock(HttpClientWrapper.class); + mockConf = mock(PreFlightCheckConfig.class); + when(mockConf.getScheme()).thenReturn("https"); + when(mockConf.getZosmfHost()).thenReturn("zosmf.example.com"); + when(mockConf.getZosmfPort()).thenReturn(443); + } + + @AfterEach + void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Nested + class SuccessResponses { + + @Test + void response200IsSuccess() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(200); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertTrue(checker.check()); + assertTrue(outStream.toString().contains("SUCCESS")); + assertTrue(outStream.toString().contains("200")); + } + + @Test + void response401IsSuccess() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(401); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertTrue(checker.check()); + assertTrue(outStream.toString().contains("SUCCESS")); + assertTrue(outStream.toString().contains("401")); + } + } + + @Nested + class FailureResponses { + + @Test + void response404IsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(404); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("FAILURE")); + assertTrue(errStream.toString().contains("404")); + } + + @Test + void response500IsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(500); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("FAILURE")); + assertTrue(errStream.toString().contains("server error")); + } + + @Test + void response403IsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(403); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("FAILURE")); + assertTrue(errStream.toString().contains("client error")); + } + } + + @Nested + class ExceptionHandling { + + @Test + void sslHandshakeExceptionReportsCertificateError() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new SSLHandshakeException("certificate unknown")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("SSL handshake failed")); + assertTrue(errStream.toString().contains("truststore")); + } + + @Test + void connectExceptionReportsUnreachable() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new ConnectException("Connection refused")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("Cannot connect")); + } + + @Test + void socketTimeoutExceptionReportsTimeout() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new SocketTimeoutException("Read timed out")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("timed out")); + } + + @Test + void unexpectedExceptionIsHandled() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new IOException("unexpected")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("Unexpected error")); + } + } +} diff --git a/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java b/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java new file mode 100644 index 0000000000..762a5c4a5d --- /dev/null +++ b/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java @@ -0,0 +1,118 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.*; + +class PreFlightCheckTest { + + private final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errStream = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + @BeforeEach + void setupStreams() { + System.setOut(new PrintStream(outStream)); + System.setErr(new PrintStream(errStream)); + } + + @AfterEach + void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + void helpFlagReturnsExitCode8() { + String[] args = {"--help"}; + assertEquals(8, PreFlightCheck.mainWithExitCode(args)); + assertTrue(outStream.toString().contains("Pre-Flight Check")); + } + + @Test + void missingRequiredArgsReturnsExitCode4() { + String[] args = {}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + } + + @Nested + class ValidationTests { + + @Test + void invalidSchemeIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "ftp"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--scheme must be 'http' or 'https'")); + } + + @Test + void invalidVerifyCertificatesIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", + "--truststore", "some/path.p12", "--truststore-password", "pass", + "--verify-certificates", "INVALID"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--verify-certificates must be STRICT, NONSTRICT, or DISABLED")); + } + + @Test + void httpsStrictWithoutTruststoreIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--truststore is required")); + } + + @Test + void httpsNonstrictWithoutTruststoreIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", + "--verify-certificates", "NONSTRICT"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--truststore is required")); + } + + @Test + void httpsWithoutTruststorePasswordIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", + "--truststore", "some/path.p12"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--truststore-password is required")); + } + + @Test + void httpsDisabledDoesNotRequireTruststore() { + // DISABLED mode skips certificate verification entirely — no truststore needed + // Will fail to connect to a non-existent server, but should pass validation + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "https", + "--verify-certificates", "DISABLED"}; + int exitCode = PreFlightCheck.mainWithExitCode(args); + assertEquals(4, exitCode); + assertFalse(errStream.toString().contains("--truststore is required")); + } + + @Test + void httpDoesNotRequireTruststore() { + // This will fail to connect but should not fail validation + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "http"}; + int exitCode = PreFlightCheck.mainWithExitCode(args); + // Should be 4 (connection failure) not a validation error + assertEquals(4, exitCode); + assertFalse(errStream.toString().contains("--truststore is required")); + } + } +} diff --git a/settings.gradle b/settings.gradle index 2fadb0f914..8c4a32051f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -54,6 +54,7 @@ include 'onboarding-enabler-python' include 'zaas-client' include 'mock-services' include 'certificate-analyser' +include 'pre-flight-check' include 'apiml-tomcat-common' include 'apiml-sample-extension' include 'apiml-sample-extension-package'