diff --git a/.classpath b/.classpath index e944152..b50339b 100644 --- a/.classpath +++ b/.classpath @@ -11,6 +11,7 @@ + @@ -22,6 +23,7 @@ + diff --git a/pom.xml b/pom.xml index 7e97c67..6a40781 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.4 + 3.3.5 @@ -16,9 +16,9 @@ release branch is labeled ..-izgw-core-SNAPSHOT main branch is labeled ..-izgw-core-RELEASE --> - 2.1.3-izgw-core-SNAPSHOT + 2.1.6-izgw-core-SNAPSHOT jar - IZ Gateway Core 2.0 + IZ Gateway Core 2.1.6 IZ Gateway Core contains the core code for the IZ Gateway Hub and Transformation services @@ -35,9 +35,10 @@ ${project.artifactId}-${project.version}-${timestamp} UTF-8 ${project.groupId} - - ${maven.build.timestamp} - false + 6.3.4 + 6.2.0 + 6.2.0 + 4.1.115.Final @@ -147,17 +148,17 @@ org.bouncycastle bc-fips - 1.0.2.5 + 2.0.0 org.bouncycastle bcpkix-fips - 1.0.7 + 2.0.7 org.bouncycastle bctls-fips - 1.0.19 + 2.0.19 org.codehaus.janino @@ -175,12 +176,12 @@ org.springdoc springdoc-openapi-starter-webflux-ui - [2.2.0,) + 2.6.0 org.springdoc springdoc-openapi-starter-webmvc-ui - [2.2.0,) + 2.6.0 org.springframework diff --git a/src/main/java/gov/cdc/izgateway/common/HealthService.java b/src/main/java/gov/cdc/izgateway/common/HealthService.java index 16b97c0..5bd7780 100644 --- a/src/main/java/gov/cdc/izgateway/common/HealthService.java +++ b/src/main/java/gov/cdc/izgateway/common/HealthService.java @@ -37,4 +37,12 @@ public static void setServerName(String serverName) { public static void setBuildName(String build) { health.setBuildName(build); } + + public static void setDatabase(String url) { + if (health.getDatabase() == null) { + health.setDatabase(url); + } else { + health.setDatabase(health.getDatabase() + ", " + url); + } + } } \ No newline at end of file diff --git a/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java b/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java index 5c2820a..227a503 100644 --- a/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java +++ b/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java @@ -21,6 +21,11 @@ public class AppProperties { @Getter @Value("${server.mode:prod}") private String serverMode; + + @Getter + @Value("${spring.database:jpa}") + private String databaseType; + @Getter private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "Background-Scheduler")); diff --git a/src/main/java/gov/cdc/izgateway/logging/event/Health.java b/src/main/java/gov/cdc/izgateway/logging/event/Health.java index 123f557..c880e32 100644 --- a/src/main/java/gov/cdc/izgateway/logging/event/Health.java +++ b/src/main/java/gov/cdc/izgateway/logging/event/Health.java @@ -85,7 +85,11 @@ public class Health { @JsonProperty @Schema(description="Host name as known by the operating system") private String hostname; - + + @JsonProperty + @Schema(description="The database in use") + private String database; + public Health() { started = new Date(ManagementFactory.getRuntimeMXBean().getStartTime()); environment = SystemUtils.getDestTypeAsString(); @@ -113,6 +117,7 @@ private Health(Health that) { this.requestVolume = that.requestVolume; this.successVolume = that.successVolume; this.hostname = that.hostname; + this.database = that.database; } public Health copy() { diff --git a/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java b/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java index 3be36a2..c7856af 100644 --- a/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java +++ b/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java @@ -30,7 +30,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; - import java.util.Date; import java.util.Map; import java.util.TreeMap; @@ -653,12 +652,13 @@ private String getFirstFieldComponent(String[] parts, int index) { } public void setProcessError(String summary, String detail) { - processErrorSummary = summary; - processErrorDetail = detail; + processErrorSummary = HL7Utils.maskSegments(summary); + processErrorDetail = HL7Utils.maskSegments(detail); hasProcessError = !StringUtils.isAllEmpty(summary, detail); } - public void setProcessError(Exception fault) { + + public void setProcessError(Exception fault) { FaultSupport s = null; if (fault instanceof UnsupportedOperationFault f) { setMessageType(MessageType.INVALID_REQUEST); diff --git a/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java b/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java index e8f2939..20a2eee 100644 --- a/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java +++ b/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java @@ -5,6 +5,7 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.sql.Timestamp; +import java.util.Date; import java.util.ServiceConfigurationError; import jakarta.xml.bind.DatatypeConverter; @@ -23,13 +24,13 @@ public interface ICertificateStatus { void setCertSerialNumber(String certificateSerialNumber); - Timestamp getLastCheckedTimeStamp(); + Date getLastCheckedTimeStamp(); - void setLastCheckedTimeStamp(Timestamp lastCheckedTimeStamp); + void setLastCheckedTimeStamp(Date lastCheckedTimeStamp); - Timestamp getNextCheckTimeStamp(); + Date getNextCheckTimeStamp(); - void setNextCheckTimeStamp(Timestamp nextCheckTimeStamp); + void setNextCheckTimeStamp(Date nextCheckTimeStamp); String getLastCheckStatus(); diff --git a/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java b/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java index d688300..17d6109 100644 --- a/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java +++ b/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java @@ -25,6 +25,8 @@ public interface IEndpointStatus extends IEndpoint { int getStatusId(); void setDestId(String destId); + + int getDestTypeId(); void setDestUri(String destUri); diff --git a/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java b/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java index 165273d..d3fa67a 100644 --- a/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java +++ b/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java @@ -24,7 +24,7 @@ public enum RetryStrategy { * The IIS is not responsive for some reason. This may be due to networking infrastructure (Internet) * failures between the IZ Gateway and the IIS, or it may be related to routine or emergency IIS * maintenance. Check the IIS Status before attempting a retry. Some errors (e.g. DNS not found, - * expired certificates) will not disappear without human intervention. + * expired certificates, or invalid response data) will not disappear without human intervention. */ CHECK_IIS_STATUS("Check IIS Status before Retry", HttpStatus.BAD_GATEWAY), diff --git a/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java b/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java index 3d9afe8..846c3de 100644 --- a/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java +++ b/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java @@ -12,13 +12,14 @@ public interface EndpointStatusRepository { public static final String[] INCLUDE_ALL = new String[0]; - List findAll(); + List findAll(); IEndpointStatus findById(String id); IEndpointStatus saveAndFlush(IEndpointStatus status); boolean removeById(String id); - List find(int maxQuarterHours, String[] include); + List find(int maxQuarterHours, String[] include); boolean refresh(); + void resetCircuitBreakers(); IEndpointStatus newEndpointStatus(); IEndpointStatus newEndpointStatus(IDestination dest); } diff --git a/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java b/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java index 77065e5..01f69fe 100644 --- a/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java +++ b/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java @@ -122,7 +122,7 @@ public boolean accessAllowed(HttpServletRequest req, HttpServletResponse resp) { if (Boolean.FALSE.equals(check)) { // NOSONAR Null is still possible here, SONAR flags it as always true log.error("Access denied to protected URL {} address by {} at {}", path, user, host); resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return true; + return false; } if (isSwagger(path, user)) { diff --git a/src/main/java/gov/cdc/izgateway/security/AccessController.java b/src/main/java/gov/cdc/izgateway/security/AccessController.java index 52d5dfa..cefe9b9 100644 --- a/src/main/java/gov/cdc/izgateway/security/AccessController.java +++ b/src/main/java/gov/cdc/izgateway/security/AccessController.java @@ -91,15 +91,13 @@ public Set getUsersInGroup(@PathVariable String group) { @Operation(summary="Add a user to blacklist", description="Add the specified user to the blacklist") @PostMapping("/access/blacklist") - public IAccessControl addUserToBlackList(@RequestParam String user - ) { + public IAccessControl addUserToBlackList(@RequestParam String user) { return service.addUserToBlacklist(user); } @Operation(summary="Delete a user from the blacklist", description="Delete the specified user from the blacklist") @DeleteMapping("/access/blacklist") - public IAccessControl removeUserFromBlackList(@RequestParam String user - ) { + public IAccessControl removeUserFromBlackList(@RequestParam String user) { return service.removeUserFromBlacklist(user); } diff --git a/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java b/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java index 666fef5..095420e 100644 --- a/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java +++ b/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java @@ -29,10 +29,10 @@ public List getHosts() { return l; } - public List findAll() { + public List findAll() { return endpointStatusRepository.find(1, EndpointStatusRepository.INCLUDE_ALL); } - public List find(int count, String[] include) { + public List find(int count, String[] include) { return endpointStatusRepository.find(count, include); } @@ -62,4 +62,8 @@ public boolean refresh() { public boolean removeById(String id) { return endpointStatusRepository.removeById(id); } + + public void resetCircuitBreakers() { + endpointStatusRepository.resetCircuitBreakers(); + } } diff --git a/src/main/java/gov/cdc/izgateway/soap/MockMessage.java b/src/main/java/gov/cdc/izgateway/soap/MockMessage.java index 18280eb..9ff1a7d 100644 --- a/src/main/java/gov/cdc/izgateway/soap/MockMessage.java +++ b/src/main/java/gov/cdc/izgateway/soap/MockMessage.java @@ -153,9 +153,14 @@ public enum MockMessage { HttpStatus.BAD_REQUEST), TC_24H(MediaType.APPLICATION_XML, MockMessageText.TC_24H_TEXT, - HttpStatus.BAD_REQUEST), TC_UNKF(MockMessage::simulateFault, + HttpStatus.BAD_REQUEST), + + TC_24I(MockMessageText.TC_24I_TEXT), + + TC_UNKF(MockMessage::simulateFault, MockMessageText.TC_UNKF_TEXT); + public static final MockMessage TC_FORCE_TIMEOUT = TC_13C; private static int retryableRequestCount = 0; /** @@ -596,19 +601,21 @@ private MockMessageText() { + "soap:Receiver" + "Invalid Username, Password or FacilityID" + "" - + "401" + "Security" // Change - // from - // required - // "Security" - // to - // "Security - // Fault" + + "401Security" + "Invalid Username, Password or FacilityID" + "" + ""; static final String TC_24H_TEXT = "This is not a SOAP Fault nor is it XML"; + static final String TC_24I_TEXT = "MSH|^~\\&|IRIS IIS|IRIS||IZG|20220205||RSP^K11^RSP_K11|20210330093013AZQ231|P|2.5.1|||||||||Z32^CDCPHINVS\r" + + "MSA|AA|20210330093013AZQ231||0||0^Message Accepted^HL70357" + Character.valueOf((char)0x13) + + "QAK|20210330093013AZQ231|NF|Z34^Request Complete Immunization history^CDCPHINVS\r" + + "QPD|Z34^Request Immunization History^CDCPHINVS|20210330093013IA231|112258-9^^^IA^MR|" + + "JohnsonIZG^JamesIZG^AndrewIZG^^^^L|LeungIZG^SarahIZG^^^^^M|20160414|M|" + + "Main Street&&123^^Adel^IA^50003^^L|^PRN^PH^^^555^5551111|Y|1\r"; + + static final String TC_UNKF_TEXT = "Unknown Exception"; static final String TC_22B_TEXT = ENVELOPE + "" + "" diff --git a/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java b/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java index 05d7c79..8485c12 100644 --- a/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java +++ b/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java @@ -580,6 +580,7 @@ protected ResponseEntity handleFault(Fault fault) { logFault(fault); FaultMessage faultMessage = new FaultMessage(fault, messageNamespace); faultMessage.updateAction(isHub()); + logResponseMessage(faultMessage); return new ResponseEntity<>(faultMessage, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java b/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java index ae93c4f..a3fba07 100644 --- a/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java +++ b/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java @@ -24,6 +24,11 @@ import java.util.List; import java.util.Map; +/** + * The HubClientFault class is used to report errors when the client reports a SOAP Fault or other connection error. + * + * @author Audacious Inquiry + */ public class HubClientFault extends Fault implements HasDestinationUri { private static final long serialVersionUID = 1L; @@ -61,7 +66,7 @@ public class HubClientFault extends Fault implements HasDestinationUri { new MessageSupport(FAULT_NAME, "227", "Destination Threw Fault", null, "The destination returned a generic fault. See the fault details.", RetryStrategy.CONTACT_SUPPORT), new MessageSupport(FAULT_NAME, "228", "Destination Returned Invalid Response", null, - "The destination returned an invalid response to the message", RetryStrategy.CHECK_IIS_STATUS), + "The destination returned an invalid response to the message. Please contact the destination IIS.", RetryStrategy.CHECK_IIS_STATUS), new MessageSupport(FAULT_NAME, "201", "HTTP Bad Request Error", null, "The Destination sent a 'Bad Request' HTTP Error code in response to the request. " @@ -92,9 +97,9 @@ public class HubClientFault extends Fault implements HasDestinationUri { RetryStrategy.CHECK_IIS_STATUS, Arrays.asList(HttpStatus.INTERNAL_SERVER_ERROR)), new MessageSupport(FAULT_NAME, "206", "HTTP Gateway Error", null, "The Destination sent an internal infrastructure HTTP Error code in response to the request. " - + "This error code usually indicates the destination is offline for maintenance.", + + "This error code usually indicates that a destination component is offline for maintenance.", RetryStrategy.CHECK_IIS_STATUS, - Arrays.asList(HttpStatus.BAD_GATEWAY, HttpStatus.SERVICE_UNAVAILABLE, HttpStatus.GATEWAY_TIMEOUT)), + Arrays.asList(HttpStatus.BAD_GATEWAY, HttpStatus.SERVICE_UNAVAILABLE, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.INTERNAL_SERVER_ERROR)), new MessageSupport(FAULT_NAME, "207", "HTTP Unknown Error", null, "The Destination sent an unexpected HTTP Error code. " + "This error code do not make sense in the context of the IZ Gateway integration, and may be a result of " @@ -136,17 +141,40 @@ private HubClientFault(MessageSupport messageSupport, IDestination destination, this.statusCode = statusCode; } - // Client returned something, but it didn't parse, go figure it out - public static HubClientFault invalidMessage(Throwable rootCause, IDestination dest, int statusCode, InputStream body, - SoapMessage result) { + /** + * Client returned something, but it didn't parse, go figure it out + * @param rootCause The root cause of the error + * @param dest The destination + * @param statusCode The status code of the response + * @param body The message body + * @return The hub client fault + */ + public static HubClientFault invalidMessage(Throwable rootCause, IDestination dest, int statusCode, InputStream body) { String bodyString = XmlUtils.toString(body); if (statusCode != 500 && statusCode != 200) { - return new HubClientFault(getHttpMessageSupport(statusCode), dest, rootCause, statusCode, bodyString, result); + return new HubClientFault(getHttpMessageSupport(statusCode), dest, rootCause, statusCode, bodyString, null); + } + if (statusCode == 200) { + // These are dangerous. The client thought it had successfully shipped something, but it wasn't valid. + // DO NOT REPORT the original response as it may have a corrupted HL7 message containing PHI. + bodyString = null; + } + while (rootCause.getCause() != null) { + rootCause = rootCause.getCause(); } - return new HubClientFault(MESSAGE_TEMPLATES[8], dest, rootCause, statusCode, bodyString, result); + return new HubClientFault(MESSAGE_TEMPLATES[8].setDetail(rootCause.getMessage()), dest, rootCause, statusCode, bodyString, null); } // Client returns 500 (or 400) with a fault message + /** + * Construct a HubClientFault + * @param rootCause The root cause of the fault + * @param dest The destination throwing the fault + * @param statusCode The status code returned + * @param body The body of the fault content + * @param result The resulting soap message + * @return The hub client fault + */ public static HubClientFault clientThrewFault(Throwable rootCause, IDestination dest, int statusCode, InputStream body, SoapMessage result) { String bodyString = XmlUtils.toString(body); @@ -167,17 +195,33 @@ public static HubClientFault clientThrewFault(Throwable rootCause, IDestination + "" + ""; + /** + * Construct a developer selected fault for error response testing + * @param dest The destination throwing the fault + * @return The Hub Client Fault + */ public static HubClientFault devAction(IDestination dest) { return clientThrewFault(UnsupportedOperationFault.devAction(), dest, 420, new ByteArrayInputStream(FAULT_XML.getBytes(StandardCharsets.UTF_8)), new SoapMessage()); } - // Client returned an Http status code without a valid fault message. + /** + * Construct a HubClientFault when the Client returns an Http status code without a valid fault message. + * @param dest The destination + * @param statusCode The status code + * @param error The error message + * @return The hub client fault + */ public static HubClientFault httpError(IDestination dest, int statusCode, String error) { return new HubClientFault(getHttpMessageSupport(statusCode), dest, null, statusCode, error, null); } + /** + * Create a generic simulated fault + * @param dest The destination + * @return The simulated fault + */ public static HubClientFault devHttpAction(IDestination dest) { return httpError(dest, 420, "This is a simulated fault"); } diff --git a/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java b/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java index bc986ec..17da14a 100644 --- a/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java +++ b/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java @@ -243,10 +243,10 @@ public String findTestCaseIdentifier() { */ public int length() { if (this instanceof HasEchoBack echo) { - return echo.getEchoBack().length(); + return echo.getEchoBack() == null ? 0 : echo.getEchoBack().length(); } if (this instanceof HasHL7Message hl7) { - return hl7.getHl7Message().length(); + return hl7.getHl7Message() == null ? 0 : hl7.getHl7Message().length(); } return 0; } diff --git a/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java b/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java index 82898a7..991ffae 100644 --- a/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java +++ b/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java @@ -59,6 +59,7 @@ import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLPeerUnverifiedException; +import javax.xml.stream.XMLStreamException; /** * MessageSender is responsible for sending messages to a given destination, @@ -183,7 +184,7 @@ public SubmitSingleMessageResponse sendSubmitSingleMessage( private void checkRetries(IDestination dest, IEndpointStatus status, int retryCount, Fault f) throws Fault { - if (f.getRetry() != RetryStrategy.CHECK_IIS_STATUS) { + if (f.getRetry() != RetryStrategy.CHECK_IIS_STATUS || f.getCause() instanceof XMLStreamException) { // This is not a retry-able failure. RequestContext.getTransactionData().setRetries(retryCount); throw f; @@ -377,7 +378,6 @@ private HttpURLConnection setupConnection( private T readResult(Class clazz, IDestination dest, HttpURLConnection con, long started) throws Fault { - SoapMessage result = null; int statusCode = -1; try { statusCode = con.getResponseCode(); @@ -394,6 +394,7 @@ private T readResult(Class clazz, IDestination dest, InputStream body = null; Exception savedEx; try { + SoapMessage result = null; // Mark the buffer so we can reread on error. m = new HttpUrlConnectionInputMessage(con, clientConfig.getMaxBufferSize()); statusCode = m.getStatusCode(); @@ -425,7 +426,8 @@ private T readResult(Class clazz, IDestination dest, if (m != null) { m.reset(); } - throw HubClientFault.invalidMessage(savedEx, dest, statusCode, body, result); + // There can be no result here. + throw HubClientFault.invalidMessage(savedEx, dest, statusCode, body); } private HubClientFault processHttpError(IDestination dest, int statusCode, InputStream err) { diff --git a/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java b/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java index bb9535b..f5d7928 100644 --- a/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java +++ b/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java @@ -89,14 +89,35 @@ public class SoapMessageReader { /** You can set a writer to copy events to on the reader. */ private XMLStreamWriter writer; + /** + * Wraps exceptions found during parsing + * + * @author Audacious Inquiry + */ public static class SoapParseException extends XMLStreamException { private static final long serialVersionUID = 1L; @Getter private final String exchange; - SoapParseException(String msg, Location location, Throwable th, String exchange) { - super(msg, location == null ? XMLStreamLocation2.NOT_AVAILABLE : location, th); + SoapParseException(Throwable th, String exchange) { + super(getMessage(th), + th instanceof XMLStreamException xex ? xex.getLocation() : XMLStreamLocation2.NOT_AVAILABLE, th); this.exchange = exchange; } + /** + * Remove extra text in the produced message. + * @param th The original exception. + * @return Just the message without location and Message: prefix + */ + private static String getMessage(Throwable th) { + if (th instanceof XMLStreamException) { + String msg = StringUtils.substringAfter(th.getMessage(), "Message: "); + if (StringUtils.isBlank(msg)) { + msg = th.getMessage(); + } + return msg; + } + return th.getMessage(); + } } /** * Read a soap message @@ -172,15 +193,15 @@ public SoapMessage read() throws SecurityFault, XMLStreamException { } } catch (XMLStreamException ex) { // NOSONAR Exception handling is OK log.error(Markers2.append(ex), "XMLStreamException parsing SOAP Message"); - throw new SoapParseException(ex.getMessage(), ex.getLocation(), ex, req == null ? null : req.getClass().getSimpleName()); + throw new SoapParseException(ex, req == null ? null : req.getClass().getSimpleName()); } catch (BadRequestException ex) { // NOSONAR Exception handling is OK log.error(Markers2.append(ex), "Invalid XML input in SOAP Message"); - throw new SoapParseException(ex.getMessage(), null, ex, req == null ? null : req.getClass().getSimpleName()); + throw new SoapParseException(ex, req == null ? null : req.getClass().getSimpleName()); } catch (SecurityFault sf) { throw sf; } catch (Exception ex) { // NOSONAR Exception handling is OK log.error(Markers2.append(ex), "Unexpected exception parsing SOAP Message"); - throw new SoapParseException(ex.getMessage(), null, ex, req == null ? null : req.getClass().getSimpleName()); + throw new SoapParseException(ex, req == null ? null : req.getClass().getSimpleName()); } req.updateAction(isHub()); return req; diff --git a/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java b/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java index 003dc54..4e012c0 100644 --- a/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java +++ b/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.function.UnaryOperator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; @@ -24,8 +26,25 @@ public class HL7Utils { static { DEFAULT_ALLOWED_SEGMENTS.put("MSH", Arrays.asList(1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 21)); DEFAULT_ALLOWED_SEGMENTS.put("MSA", Arrays.asList(1, 2, 3)); + DEFAULT_ALLOWED_SEGMENTS.put("QAK", Arrays.asList(1, 2, 3)); DEFAULT_ALLOWED_SEGMENTS.put("ERR", Arrays.asList(1, 2, -3, 4)); } + + /** + * Remove any potential PHI from the text of the message + * This function ensures that HL7 message content potentially containing PHI is removed from the message. + * Sadly, some systems report detailed message content in fault responses. + * + * MSH|^~\\&|WebIZ.1.0.9035.29972|PW0000|AS0000|AS0000|20241116123011.989+0900||RSP^K11^RSP_K11|PW000020241116301198|P|2.5.1|||NE|NE|||||Z33^CDCPHINVS\r + * MSA|AE|AS000020241115301090\rERR||MSH^1^6^1^1|999^ApplicationError^HL70357|E|4^Invalid value^HL70533^WEBIZ-AUTH-625^Facility is inactive or suspended^L||, + * Next Diagnostic: |Facility is inactive or suspended., Next Message: One or more errors/warnings occured that may effect query results.\r + * QAK|755718|AE|Z34^Request Immunization History^CDCPHINVS\r + * QPD|Z34^Request Immunization History^CDCPHINVS|755718|84579^^^AS0000^MR~000000002^^^PW0000^SR|SIMPSON^BART^M^^^^L||19990101|M|2000 AS ST^^AENKAN^AS^96960^^P|^NET^X.400^BERSERY-KEMP@ENVISIONTECHNOLOGY.COM~^PRN^PH^^^864^1309701|N\r + * ZSA|AF^Application Fail - Message Failed to Execute. Generally this occurs for critical errors, which preve...^ENV0008|1853^^^PW0000^HL7LogIdIncomming~701265^^^PW0000^WebServiceLogIdIncomming~AS000020241115301090^^^AS0000^MessageControlId\r + * @param message The message text + * @return The masked message text + */ + public static final Pattern SEGMENT_STARTS = Pattern.compile("((^|[^\\w])(\\w{3})\\|[^\\r\\n]*([\\r\\n]|$))"); private HL7Utils() { // Do nothing @@ -218,4 +237,30 @@ public String getFirstSubFieldOf(int index) { return StringUtils.substringBefore(parts[index], "~"); } } + + /** + * Mash PHI in segments + * @param message The message that may contain an HL7 Segment containing PHI in it + * @return The masked message + */ + public static String maskSegments(String message) { + // Identify segment starts in text messages using the case sensitive pattern /[^a-zA-Z0-9][A-Z]{3}\|/ + // (a non-alphanumeric character, followed by three uppercase alphabetic characters followed by a vertical bar |). + // identify segment ends at either \r or \n or next segment start. + if (message == null) { + return null; + } + Matcher m = HL7Utils.SEGMENT_STARTS.matcher(message); + StringBuffer b = new StringBuffer(); + for (boolean result = m.find(); result; result = m.find()) { + String seg = m.group(3); + if (!DEFAULT_ALLOWED_SEGMENTS.containsKey(seg)) { + m.appendReplacement(b, "$2$3|..."); + } else { + m.appendReplacement(b, "$0"); + } + } + m.appendTail(b); + return b.toString(); + } } diff --git a/src/test/java/test/gov/cdc/izgateway/utils/TestHL7Utils.java b/src/test/java/test/gov/cdc/izgateway/utils/TestHL7Utils.java new file mode 100644 index 0000000..50a464e --- /dev/null +++ b/src/test/java/test/gov/cdc/izgateway/utils/TestHL7Utils.java @@ -0,0 +1,61 @@ +package test.gov.cdc.izgateway.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.apache.commons.text.StringEscapeUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import gov.cdc.izgateway.utils.HL7Utils; + +class TestHL7Utils { + + @ParameterizedTest + @MethodSource + void testMaskSegments(String message, String expected) { + String actual = HL7Utils.maskSegments(message); + assertEquals(expected, actual); + } + + public static String[][] testMaskSegments() { + return new String[][] { + { "MSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + "MSH|this is an msh\r followed by some stuff followed by a PID|..." + }, + { " MSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge\n and some more stuff", + " MSH|this is an msh\r followed by some stuff followed by a PID|... and some more stuff" + }, + { "AMSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + "AMSH|this is an msh\r followed by some stuff followed by a PID|..." + }, + { "1MSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge\r", + "1MSH|this is an msh\r followed by some stuff followed by a PID|..." + }, + { "QPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge and \r some more stuff", + "QPD|... followed by some stuff followed by a PID|... some more stuff" + }, + { " QPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + " QPD|... followed by some stuff followed by a PID|..." + }, + { "AQPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge \r and some more stuff", + "AQPD|this is an msh\r followed by some stuff followed by a PID|... and some more stuff" + }, + { "1QPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + "1QPD|this is an msh\r followed by some stuff followed by a PID|..." + }, + { "ERR|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + "ERR|this is an msh\r followed by some stuff followed by a PID|..." + }, + { "MSA|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + "MSA|this is an msh\r followed by some stuff followed by a PID|..." + }, + { "QAK|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + "QAK|this is an msh\r followed by some stuff followed by a PID|..." + }, + { "ZA1|this is an msh\r followed by some stuff followed by a PID|withsomegunge", + "ZA1|... followed by some stuff followed by a PID|..." + } + }; + } +}