diff --git a/configuration/esapi/ESAPI.properties b/configuration/esapi/ESAPI.properties index d489cdce8..955e49fa2 100644 --- a/configuration/esapi/ESAPI.properties +++ b/configuration/esapi/ESAPI.properties @@ -407,6 +407,10 @@ Logger.UserInfo=true # Determines whether ESAPI should log the session id and client IP. Logger.ClientInfo=true +# Determines whether ESAPI should log the prefix of [EVENT_TYPE - APPLICATION NAME]. +# If all above Logger entries are set to false and LogIgnorePrefix is true, then the output would be the same like if no ESAPI was used +Logger.LogIgnorePrefix=true + #=========================================================================== # ESAPI Intrusion Detection # diff --git a/src/main/java/org/owasp/esapi/PropNames.java b/src/main/java/org/owasp/esapi/PropNames.java index 2f3f8ee49..57fa255a3 100644 --- a/src/main/java/org/owasp/esapi/PropNames.java +++ b/src/main/java/org/owasp/esapi/PropNames.java @@ -111,6 +111,7 @@ public final class PropNames { public static final String LOG_ENCODING_REQUIRED = "Logger.LogEncodingRequired"; public static final String LOG_APPLICATION_NAME = "Logger.LogApplicationName"; public static final String LOG_SERVER_IP = "Logger.LogServerIP"; + public static final String LOG_IGNORE_PREFIX = "Logger.LogIgnorePrefix"; public static final String VALIDATION_PROPERTIES = "Validator.ConfigurationFile"; public static final String VALIDATION_PROPERTIES_MULTIVALUED = "Validator.ConfigurationFile.MultiValued"; diff --git a/src/main/java/org/owasp/esapi/logging/appender/EventTypeLogSupplier.java b/src/main/java/org/owasp/esapi/logging/appender/EventTypeLogSupplier.java index 681839af5..9251678d2 100644 --- a/src/main/java/org/owasp/esapi/logging/appender/EventTypeLogSupplier.java +++ b/src/main/java/org/owasp/esapi/logging/appender/EventTypeLogSupplier.java @@ -30,18 +30,27 @@ public class EventTypeLogSupplier // implements Supplier { /** EventType reference to supply log representation of. */ private final EventType eventType; + /** Whether to log or not the event type */ + private boolean ignoreLogEventType = false; /** * Ctr * - * @param evtyp EventType reference to supply log representation for + * @param eventType EventType reference to supply log representation for */ - public EventTypeLogSupplier(EventType evtyp) { - this.eventType = evtyp == null ? Logger.EVENT_UNSPECIFIED : evtyp; + public EventTypeLogSupplier(EventType eventType) { + this.eventType = eventType == null ? Logger.EVENT_UNSPECIFIED : eventType; } // @Override -- Uncomment when we switch to Java 8 as minimal baseline. public String get() { + if (this.ignoreLogEventType) { + return ""; + } return eventType.toString(); } + + public void setIgnoreLogEventType(boolean ignoreLogEventType) { + this.ignoreLogEventType = ignoreLogEventType; + } } diff --git a/src/main/java/org/owasp/esapi/logging/appender/LogPrefixAppender.java b/src/main/java/org/owasp/esapi/logging/appender/LogPrefixAppender.java index 20f692ebf..fb6b07f08 100644 --- a/src/main/java/org/owasp/esapi/logging/appender/LogPrefixAppender.java +++ b/src/main/java/org/owasp/esapi/logging/appender/LogPrefixAppender.java @@ -35,6 +35,8 @@ public class LogPrefixAppender implements LogAppender { private final boolean logApplicationName; /** Application Name to record. */ private final String appName; + /** Whether or not to print the prefix. */ + private final boolean ignoreLogPrefix; /** * Ctr. @@ -51,11 +53,32 @@ public LogPrefixAppender(boolean logUserInfo, boolean logClientInfo, boolean log this.logServerIp = logServerIp; this.logApplicationName = logApplicationName; this.appName = appName; + this.ignoreLogPrefix = false; + } + + /** + * Ctr. + * + * @param logUserInfo Whether or not to record user information + * @param logClientInfo Whether or not to record client information + * @param logServerIp Whether or not to record server ip information + * @param logApplicationName Whether or not to record application name + * @param appName Application Name to record. + * @param ignoreLogPrefix Whether or not to print the prefix + */ + public LogPrefixAppender(boolean logUserInfo, boolean logClientInfo, boolean logServerIp, boolean logApplicationName, String appName, boolean ignoreLogPrefix) { + this.logUserInfo = logUserInfo; + this.logClientInfo = logClientInfo; + this.logServerIp = logServerIp; + this.logApplicationName = logApplicationName; + this.appName = appName; + this.ignoreLogPrefix = ignoreLogPrefix; } @Override public String appendTo(String logName, EventType eventType, String message) { EventTypeLogSupplier eventTypeSupplier = new EventTypeLogSupplier(eventType); + eventTypeSupplier.setIgnoreLogEventType(this.ignoreLogPrefix); UserInfoSupplier userInfoSupplier = new UserInfoSupplier(); userInfoSupplier.setLogUserInfo(logUserInfo); @@ -66,6 +89,7 @@ public String appendTo(String logName, EventType eventType, String message) { ServerInfoSupplier serverInfoSupplier = new ServerInfoSupplier(logName); serverInfoSupplier.setLogServerIp(logServerIp); serverInfoSupplier.setLogApplicationName(logApplicationName, appName); + serverInfoSupplier.setIgnoreLogName(ignoreLogPrefix); String eventTypeMsg = eventTypeSupplier.get().trim(); String userInfoMsg = userInfoSupplier.get().trim(); @@ -81,8 +105,10 @@ public String appendTo(String logName, EventType eventType, String message) { String[] optionalPrefixContent = new String[] {userInfoMsg + clientInfoMsg, serverInfoMsg}; StringBuilder logPrefix = new StringBuilder(); - //EventType is always appended - logPrefix.append(eventTypeMsg); + //EventType is always appended (unless we specifically asked not to Log Prefix) + if (!this.ignoreLogPrefix) { + logPrefix.append(eventTypeMsg); + } for (String element : optionalPrefixContent) { if (!element.isEmpty()) { @@ -91,6 +117,9 @@ public String appendTo(String logName, EventType eventType, String message) { } } + if (logPrefix.toString().isEmpty()) { + return message; + } return String.format(RESULT_FORMAT, logPrefix.toString(), message); } } diff --git a/src/main/java/org/owasp/esapi/logging/appender/ServerInfoSupplier.java b/src/main/java/org/owasp/esapi/logging/appender/ServerInfoSupplier.java index 45fb4da55..934142f2d 100644 --- a/src/main/java/org/owasp/esapi/logging/appender/ServerInfoSupplier.java +++ b/src/main/java/org/owasp/esapi/logging/appender/ServerInfoSupplier.java @@ -34,7 +34,8 @@ public class ServerInfoSupplier // implements Supplier private boolean logAppName = true; /** The application name to log. */ private String applicationName = ""; - + /** Whether to log the Name */ + private boolean ignoreLogName = false; /** Reference to the associated logname/module name. */ private final String logName; @@ -57,10 +58,19 @@ public String get() { appInfo.append(request.getLocalAddr()).append(":").append(request.getLocalPort()); } } - if (logAppName) { - appInfo.append("/").append(applicationName); + + if (this.logAppName) { + if (this.applicationName != null && !this.applicationName.isEmpty()) { + appInfo.append("/").append(this.applicationName); + } + else if (this.applicationName == null) { + appInfo.append("/").append(this.applicationName); + } + } + + if (!this.ignoreLogName) { + appInfo.append("/").append(logName); } - appInfo.append("/").append(logName); return appInfo.toString(); } @@ -74,6 +84,15 @@ public void setLogServerIp(boolean log) { this.logServerIP = log; } + /** + * Specify whether the instance should record the prefix. + * + * @param ignoreLogName {@code true} to record + */ + public void setIgnoreLogName(boolean ignoreLogName) { + this.ignoreLogName = ignoreLogName; + } + /** * Specify whether the instance should record the application name * diff --git a/src/main/java/org/owasp/esapi/logging/java/JavaLogFactory.java b/src/main/java/org/owasp/esapi/logging/java/JavaLogFactory.java index 9ebd52d92..3cac1e1f8 100644 --- a/src/main/java/org/owasp/esapi/logging/java/JavaLogFactory.java +++ b/src/main/java/org/owasp/esapi/logging/java/JavaLogFactory.java @@ -20,6 +20,7 @@ import static org.owasp.esapi.PropNames.LOG_ENCODING_REQUIRED; import static org.owasp.esapi.PropNames.LOG_SERVER_IP; import static org.owasp.esapi.PropNames.LOG_USER_INFO; +import static org.owasp.esapi.PropNames.LOG_IGNORE_PREFIX; import java.io.IOException; import java.io.InputStream; @@ -79,7 +80,8 @@ public class JavaLogFactory implements LogFactory { boolean logApplicationName = ESAPI.securityConfiguration().getBooleanProp(LOG_APPLICATION_NAME); String appName = ESAPI.securityConfiguration().getStringProp(APPLICATION_NAME); boolean logServerIp = ESAPI.securityConfiguration().getBooleanProp(LOG_SERVER_IP); - JAVA_LOG_APPENDER = createLogAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName); + boolean logIgnorePrefix = ESAPI.securityConfiguration().getBooleanProp(LOG_IGNORE_PREFIX); + JAVA_LOG_APPENDER = createLogAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName, logIgnorePrefix); Map levelLookup = new HashMap<>(); levelLookup.put(Logger.ALL, JavaLogLevelHandlers.ALWAYS); @@ -144,6 +146,20 @@ public class JavaLogFactory implements LogFactory { return new LogPrefixAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName); } + /** + * Populates the default log appender for use in factory-created loggers. + * @param appName + * @param logApplicationName + * @param logServerIp + * @param logClientInfo + * @param logIgnorePrefix + * + * @return LogAppender instance. + */ + /*package*/ static LogAppender createLogAppender(boolean logUserInfo, boolean logClientInfo, boolean logServerIp, boolean logApplicationName, String appName, boolean logIgnorePrefix) { + return new LogPrefixAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName, logIgnorePrefix); + } + @Override public Logger getLogger(String moduleName) { diff --git a/src/main/java/org/owasp/esapi/logging/slf4j/Slf4JLogFactory.java b/src/main/java/org/owasp/esapi/logging/slf4j/Slf4JLogFactory.java index af113b80c..387672116 100644 --- a/src/main/java/org/owasp/esapi/logging/slf4j/Slf4JLogFactory.java +++ b/src/main/java/org/owasp/esapi/logging/slf4j/Slf4JLogFactory.java @@ -36,6 +36,7 @@ import static org.owasp.esapi.PropNames.LOG_APPLICATION_NAME; import static org.owasp.esapi.PropNames.APPLICATION_NAME; import static org.owasp.esapi.PropNames.LOG_SERVER_IP; +import static org.owasp.esapi.PropNames.LOG_IGNORE_PREFIX; import org.slf4j.LoggerFactory; /** * LogFactory implementation which creates SLF4J supporting Loggers. @@ -69,7 +70,8 @@ public class Slf4JLogFactory implements LogFactory { boolean logApplicationName = ESAPI.securityConfiguration().getBooleanProp(LOG_APPLICATION_NAME); String appName = ESAPI.securityConfiguration().getStringProp(APPLICATION_NAME); boolean logServerIp = ESAPI.securityConfiguration().getBooleanProp(LOG_SERVER_IP); - SLF4J_LOG_APPENDER = createLogAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName); + boolean logIgnorePrefix = ESAPI.securityConfiguration().getBooleanProp(LOG_IGNORE_PREFIX); + SLF4J_LOG_APPENDER = createLogAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName, logIgnorePrefix); Map levelLookup = new HashMap<>(); levelLookup.put(Logger.ALL, Slf4JLogLevelHandlers.TRACE); @@ -114,6 +116,19 @@ public class Slf4JLogFactory implements LogFactory { return new LogPrefixAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName); } + /** + * Populates the default log appender for use in factory-created loggers. + * @param appName + * @param logApplicationName + * @param logServerIp + * @param logClientInfo + * @param logIgnorePrefix + * + * @return LogAppender instance. + */ + /*package*/ static LogAppender createLogAppender(boolean logUserInfo, boolean logClientInfo, boolean logServerIp, boolean logApplicationName, String appName, boolean logIgnorePrefix) { + return new LogPrefixAppender(logUserInfo, logClientInfo, logServerIp, logApplicationName, appName, logIgnorePrefix); + } @Override public Logger getLogger(String moduleName) { diff --git a/src/test/java/org/owasp/esapi/logging/appender/EventTypeLogSupplierIgnoreEventTypeTest.java b/src/test/java/org/owasp/esapi/logging/appender/EventTypeLogSupplierIgnoreEventTypeTest.java new file mode 100644 index 000000000..b5e15a15e --- /dev/null +++ b/src/test/java/org/owasp/esapi/logging/appender/EventTypeLogSupplierIgnoreEventTypeTest.java @@ -0,0 +1,44 @@ +package org.owasp.esapi.logging.appender; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.owasp.esapi.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@RunWith(Parameterized.class) +public class EventTypeLogSupplierIgnoreEventTypeTest { + + @Parameterized.Parameters (name="{0} -> {1}") + public static Collection assembleTests() { + List paramSets = new ArrayList<>(); + paramSets.add(new Object[] {Logger.EVENT_FAILURE,""}); + paramSets.add(new Object[] {Logger.EVENT_SUCCESS,""}); + paramSets.add(new Object[] {Logger.EVENT_UNSPECIFIED,""}); + paramSets.add(new Object[] {Logger.SECURITY_AUDIT,""}); + paramSets.add(new Object[] {Logger.SECURITY_FAILURE,""}); + paramSets.add(new Object[] {Logger.SECURITY_SUCCESS,""}); + paramSets.add(new Object[] {null, ""}); + + return paramSets; + } + + private final Logger.EventType eventType; + private final String expectedResult; + + public EventTypeLogSupplierIgnoreEventTypeTest(Logger.EventType eventType, String result) { + this.eventType = eventType; + this.expectedResult = result; + } + @Test + public void testEventTypeLogIgnoreEventType() { + EventTypeLogSupplier supplier = new EventTypeLogSupplier(eventType); + supplier.setIgnoreLogEventType(true); + assertEquals(expectedResult, supplier.get()); + } +} diff --git a/src/test/java/org/owasp/esapi/logging/appender/LogPrefixAppenderTest.java b/src/test/java/org/owasp/esapi/logging/appender/LogPrefixAppenderTest.java index bc733ec2e..ad3422c97 100644 --- a/src/test/java/org/owasp/esapi/logging/appender/LogPrefixAppenderTest.java +++ b/src/test/java/org/owasp/esapi/logging/appender/LogPrefixAppenderTest.java @@ -34,6 +34,7 @@ public class LogPrefixAppenderTest { private String testLogMessage = testName.getMethodName() + "-MESSAGE"; private String testApplicationName = testName.getMethodName() + "-APPLICATION_NAME"; private EventType testEventType = Logger.EVENT_UNSPECIFIED; + private boolean testIgnorePrefix = true; private EventTypeLogSupplier etlsSpy; private ClientInfoSupplier cisSpy; @@ -145,7 +146,6 @@ public void testLogContentWhenUserInfoEmptyAndClientInfoEmptyAndServerInfoEmpty( runTest(ETL_RESULT, EMPTY_RESULT, EMPTY_RESULT, EMPTY_RESULT, "[EVENT_TYPE]"); } - private void runTest(String typeResult, String userResult, String clientResult, String serverResult, String exResult) throws Exception{ when(etlsSpy.get()).thenReturn(typeResult); when(uisSpy.get()).thenReturn(userResult); @@ -163,4 +163,57 @@ private void runTest(String typeResult, String userResult, String clientResult, assertEquals(exResult + " " + testName.getMethodName() + "-MESSAGE", result); } + + @Test + public void testLogContentWhenServerInfoEmptyAndIgnoreLogPrefix() throws Exception { + runTestWithLogPrefixIgnore(ETL_RESULT, UIS_RESULT, CIS_RESULT, EMPTY_RESULT, true, "[ USER_INFO:CLIENT_INFO]"); + } + + @Test + public void testLogContentWhenUserInfoEmptyAndServerInfoEmptyAndIgnoreLogPrefix() throws Exception { + runTestWithLogPrefixIgnore(ETL_RESULT, EMPTY_RESULT, CIS_RESULT, EMPTY_RESULT, true, "[ CLIENT_INFO]"); + } + + @Test + public void testLogContentWhenUserInfoEmptyAndClientInfoEmptyAndIgnoreLogPrefix() throws Exception { + runTestWithLogPrefixIgnore(ETL_RESULT, EMPTY_RESULT, EMPTY_RESULT, SIS_RESULT, true, "[ -> SERVER_INFO]"); + } + + @Test + public void testLogContentWhenClientInfoEmptyAndServerInfoEmptyAndIgnoreLogPrefix() throws Exception { + runTestWithLogPrefixIgnore(ETL_RESULT, UIS_RESULT, EMPTY_RESULT, EMPTY_RESULT, true, "[ USER_INFO]"); + } + + @Test + public void testLogContentWhenUserInfoEmptyAndClientInfoEmptyAndServerInfoEmptyAndIgnoreLogPrefix() throws Exception { + runTestWithLogPrefixIgnore(ETL_RESULT, EMPTY_RESULT, EMPTY_RESULT, EMPTY_RESULT, true, ""); + } + + private void runTestWithLogPrefixIgnore(String typeResult, String userResult, String clientResult, String serverResult, boolean ignoreLogPrefix, String exResult) throws Exception{ + etlsSpy.setIgnoreLogEventType(ignoreLogPrefix); + when(etlsSpy.get()).thenReturn(typeResult); + + when(uisSpy.get()).thenReturn(userResult); + when(cisSpy.get()).thenReturn(clientResult); + + sisSpy.setIgnoreLogName(ignoreLogPrefix); + when(sisSpy.get()).thenReturn(serverResult); + + whenNew(EventTypeLogSupplier.class).withArguments(testEventType).thenReturn(etlsSpy); + whenNew(UserInfoSupplier.class).withNoArguments().thenReturn(uisSpy); + whenNew(ClientInfoSupplier.class).withNoArguments().thenReturn(cisSpy); + whenNew(ServerInfoSupplier.class).withArguments(testLoggerName).thenReturn(sisSpy); + + //Since everything is mocked these booleans don't much matter aside from the later verifies + LogPrefixAppender lpa = new LogPrefixAppender(false, false, false, false, null, true); + String result = lpa.appendTo(testLoggerName, testEventType, testLogMessage); + + if (exResult.isEmpty()) { + assertEquals( testName.getMethodName() + "-MESSAGE", result); + } + else { + assertEquals(exResult + " " + testName.getMethodName() + "-MESSAGE", result); + } + } + } diff --git a/src/test/java/org/owasp/esapi/logging/appender/ServerInfoSupplierIgnoreLogNameTest.java b/src/test/java/org/owasp/esapi/logging/appender/ServerInfoSupplierIgnoreLogNameTest.java new file mode 100644 index 000000000..b8822a967 --- /dev/null +++ b/src/test/java/org/owasp/esapi/logging/appender/ServerInfoSupplierIgnoreLogNameTest.java @@ -0,0 +1,116 @@ +package org.owasp.esapi.logging.appender; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.owasp.esapi.ESAPI; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ ESAPI.class }) +public class ServerInfoSupplierIgnoreLogNameTest { + @Rule + public TestName testName = new TestName(); + + private HttpServletRequest request; + + @Before + public void buildStaticMocks() { + request = mock(HttpServletRequest.class); + mockStatic(ESAPI.class); + } + + @Test + public void verifyFullOutputIgnoreLogName() throws Exception { + when(ESAPI.class, "currentRequest").thenReturn(request); + when(request.getLocalAddr()).thenReturn("LOCAL_ADDR"); + when(request.getLocalPort()).thenReturn(99999); + + ServerInfoSupplier sis = new ServerInfoSupplier(testName.getMethodName()); + sis.setLogApplicationName(true, testName.getMethodName() + "-APPLICATION"); + sis.setLogServerIp(true); + sis.setIgnoreLogName(true); + + String result = sis.get(); + assertEquals("LOCAL_ADDR:99999/" + testName.getMethodName() + "-APPLICATION", + result); + } + + @Test + public void verifyOutputNullRequestIgnoreLogName() throws Exception { + when(ESAPI.class, "currentRequest").thenReturn(null); + ServerInfoSupplier sis = new ServerInfoSupplier(testName.getMethodName()); + sis.setLogApplicationName(true, testName.getMethodName() + "-APPLICATION"); + sis.setLogServerIp(true); + sis.setIgnoreLogName(true); + + String result = sis.get(); + assertEquals("/" + testName.getMethodName() + "-APPLICATION", result); + } + + @Test + public void verifyOutputNoAppNameIgnoreLogName() throws Exception { + when(ESAPI.class, "currentRequest").thenReturn(request); + when(request.getLocalAddr()).thenReturn("LOCAL_ADDR"); + when(request.getLocalPort()).thenReturn(99999); + + ServerInfoSupplier sis = new ServerInfoSupplier(testName.getMethodName()); + sis.setLogApplicationName(false, null); + sis.setLogServerIp(true); + sis.setIgnoreLogName(true); + + String result = sis.get(); + assertEquals("LOCAL_ADDR:99999", result); + } + + @Test + public void verifyOutputNullAppNameIgnoreLogName() throws Exception { + when(ESAPI.class, "currentRequest").thenReturn(request); + when(request.getLocalAddr()).thenReturn("LOCAL_ADDR"); + when(request.getLocalPort()).thenReturn(99999); + + ServerInfoSupplier sis = new ServerInfoSupplier(testName.getMethodName()); + sis.setLogApplicationName(true, null); + sis.setLogServerIp(true); + sis.setIgnoreLogName(true); + + String result = sis.get(); + assertEquals("LOCAL_ADDR:99999/null", result); + } + + @Test + public void verifyOutputNoServerIpIgnoreLogName() { + ServerInfoSupplier sis = new ServerInfoSupplier(testName.getMethodName()); + sis.setLogApplicationName(true, testName.getMethodName() + "-APPLICATION"); + sis.setLogServerIp(false); + sis.setIgnoreLogName(true); + + String result = sis.get(); + assertEquals("/" + testName.getMethodName() + "-APPLICATION", result); + } + + @Test + public void verifyOutputNullRequestNoServerIpNullAppNameIgnoreLogName() throws Exception { + when(ESAPI.class, "currentRequest").thenReturn(null); + ServerInfoSupplier sis = new ServerInfoSupplier(testName.getMethodName()); + sis.setLogApplicationName(false, null); + sis.setLogServerIp(false); + sis.setIgnoreLogName(true); + + String result = sis.get(); + assertEquals("", result); + } + + +} + diff --git a/src/test/resources/esapi/ESAPI.properties b/src/test/resources/esapi/ESAPI.properties index c967bad33..85351dcf0 100644 --- a/src/test/resources/esapi/ESAPI.properties +++ b/src/test/resources/esapi/ESAPI.properties @@ -439,6 +439,10 @@ Logger.UserInfo=true # Determines whether ESAPI should log the session id and client IP. Logger.ClientInfo=true +# Determines whether ESAPI should log the prefix of [EVENT_TYPE - APPLICATION NAME]. +# If all above Logger entries are set to false and LogIgnorePrefix is true, then the output would be the same like if no ESAPI was used +Logger.LogIgnorePrefix=true + #=========================================================================== # ESAPI Intrusion Detection #